#!/usr/bin/env catnip
# Zone de tranquillité autour d'une école : buffer, intersections et surfaces.
#
# La norme impose 300 m de calme autour de l'école. Catnip porte le cadastre
# (parcelles typées) et les verdicts ; Shapely calcule le buffer, les
# intersections et les aires. Complémentaire des exemples « hand-rolled » de
# la catégorie (hulls, point-in-polygon) : ici le moteur géométrique est DE9-IM
# via GEOS — voir Wikipedia « DE-9IM ».
#
# Contrôles : symétrie des parcelles miroir, parc entièrement couvert,
# parcelle lointaine intacte, conservation des surfaces.
#
# DEPS: shapely
# OFFLINE: géométries embarquées
import('shapely.geometry', 'box')
import('shapely', 'union_all')
union Kind {
school; house; park
label(self): str => {
match self {
Kind.school => { "école" }
Kind.house => { "habitation" }
Kind.park => { "parc" }
}
}
}
struct Parcel {
name: str; kind: Kind; geom
area(self): float => { self.geom.area }
}
# Coordonnées en mètres, école centrée à l'origine.
parcels = list(
Parcel('école', Kind.school, box(-20.0, -20.0, 20.0, 20.0)),
Parcel('parc municipal', Kind.park, box(-100.0, -100.0, 100.0, 100.0)),
Parcel('villa est', Kind.house, box(200.0, -50.0, 260.0, 50.0)),
Parcel('villa ouest', Kind.house, box(-260.0, -50.0, -200.0, 50.0)),
Parcel('ferme nord', Kind.house, box(400.0, 300.0, 480.0, 380.0)),
)
BUFFER = 300.0
zone = parcels[0].geom.buffer(BUFFER)
print(f"⇒ Zone de tranquillité : buffer {BUFFER} m autour de l'école")
print(f" École : {round(parcels[0].area(), 1)} m², zone : {round(zone.area, 1)} m²")
print()
print("⇒ Parcelles touchées")
struct Impact { parcel: Parcel; touched: float }
impacts = list()
for p in parcels[1:] {
touched = p.geom.intersection(zone).area
impacts.append(Impact(p, touched))
pct = round(100.0 * touched / p.area(), 1)
print(f" {p.name:<14} {p.kind.label():<10} {round(p.area(), 1):>9} m² touchée {round(touched, 1):>9} m² ({pct} %)")
}
# Empreinte bâtie touchée : union des habitations intersectées, puis reste
# public = zone moins cadastre.
houses = list()
for p in parcels[1:] {
if p.kind == Kind.house { houses.append(p.geom) }
}
housing = union_all(houses)
housing_touched = housing.intersection(zone).area
public_touched = zone.difference(union_all(list(housing, parcels[1].geom))).area
print()
print(f"⇒ Empreinte habitation touchée : {round(housing_touched, 1)} m²")
print(f"⇒ Zone hors parcelles cadastrées : {round(public_touched, 1)} m²")
# Oracles : symétrie, couverture totale du parc, isolement de la ferme,
# conservation (somme des intersections = intersection de l'union).
symmetric = abs(impacts[1].touched - impacts[2].touched) < 1e-9
park_covered = abs(impacts[0].touched - impacts[0].parcel.area()) < 1.0
farm_untouched = impacts[3].touched == 0.0
sum_parts = 0.0
for p in parcels[1:] {
sum_parts = sum_parts + p.geom.intersection(zone).area
}
cadastre = union_all(parcels[1:].[(p) => { p.geom }])
conserved = abs(sum_parts - cadastre.intersection(zone).area) < 1e-6
oracle_ok = symmetric and park_covered and farm_untouched and conserved
print()
print(f"⇒ Oracle (symétrie + parc couvert + ferme intacte + conservation) : {oracle_ok}")