3edea47e88
- Supermarkt en school markers als gekleurde cirkels op de Leaflet kaart - Popups met hemelsbreed/fiets/auto afstanden per voorziening - Legenda uitgebreid met POI categorieën - voorzieningen.py: format_afstand/format_duur exports toegevoegd
158 lines
4.8 KiB
Python
158 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Voorzieningenanalyse op basis van lokale PostGIS OSM-data + ORS Matrix."""
|
|
|
|
import json
|
|
import math
|
|
import os
|
|
import urllib.request
|
|
from dataclasses import dataclass, asdict
|
|
|
|
import psycopg2
|
|
import psycopg2.extras
|
|
|
|
DB_DSN = os.environ.get(
|
|
"ADRES_ANALYSE_DB_DSN",
|
|
"host=127.0.0.1 port=5433 dbname=adresanalyse user=adres password=adres",
|
|
)
|
|
ORS_BASE = os.environ.get("ORS_BASE", "http://192.168.1.71:9080/ors/v2")
|
|
|
|
|
|
@dataclass
|
|
class Voorziening:
|
|
categorie: str
|
|
naam: str
|
|
lat: float
|
|
lon: float
|
|
hemelsbreed_m: float
|
|
fiets_afstand_m: float | None = None
|
|
fiets_duur_s: float | None = None
|
|
auto_afstand_m: float | None = None
|
|
auto_duur_s: float | None = None
|
|
bron: str = "OSM/PostGIS"
|
|
|
|
def to_dict(self):
|
|
return asdict(self)
|
|
|
|
|
|
def _matrix(lon, lat, pois, profiel):
|
|
if not pois:
|
|
return []
|
|
locations = [[lon, lat]] + [[p.lon, p.lat] for p in pois]
|
|
payload = json.dumps({
|
|
"locations": locations,
|
|
"sources": [0],
|
|
"destinations": list(range(1, len(locations))),
|
|
"metrics": ["distance", "duration"],
|
|
}).encode()
|
|
req = urllib.request.Request(
|
|
f"{ORS_BASE}/matrix/{profiel}",
|
|
data=payload,
|
|
headers={"Content-Type": "application/json"},
|
|
method="POST",
|
|
)
|
|
with urllib.request.urlopen(req, timeout=60) as resp:
|
|
data = json.loads(resp.read())
|
|
distances = data.get("distances", [[]])[0]
|
|
durations = data.get("durations", [[]])[0]
|
|
return list(zip(distances, durations))
|
|
|
|
|
|
def verrijk_met_routes(lon, lat, pois):
|
|
fiets = _matrix(lon, lat, pois, "cycling-regular")
|
|
auto = _matrix(lon, lat, pois, "driving-car")
|
|
for i, poi in enumerate(pois):
|
|
if i < len(fiets):
|
|
poi.fiets_afstand_m, poi.fiets_duur_s = fiets[i]
|
|
if i < len(auto):
|
|
poi.auto_afstand_m, poi.auto_duur_s = auto[i]
|
|
return pois
|
|
|
|
|
|
def zoek_voorzieningen(lon, lat, categorie, limit=5, radius_m=10000):
|
|
if categorie == "supermarkt":
|
|
view = "poi_supermarkten"
|
|
elif categorie == "school":
|
|
view = "poi_scholen"
|
|
else:
|
|
raise ValueError(f"Onbekende categorie: {categorie}")
|
|
|
|
sql = f"""
|
|
WITH origin AS (
|
|
SELECT ST_SetSRID(ST_MakePoint(%s, %s), 4326)::geography AS geog
|
|
)
|
|
SELECT DISTINCT ON (lower(name))
|
|
name,
|
|
ST_Y(geom) AS lat,
|
|
ST_X(geom) AS lon,
|
|
ST_Distance(geom::geography, origin.geog) AS hemelsbreed_m
|
|
FROM {view}, origin
|
|
WHERE geom IS NOT NULL
|
|
AND ST_DWithin(geom::geography, origin.geog, %s)
|
|
ORDER BY lower(name), hemelsbreed_m ASC
|
|
"""
|
|
# DISTINCT ON kiest dichtstbijzijnde per naam; buitenquery sorteert weer op afstand.
|
|
wrapped = f"SELECT * FROM ({sql}) q ORDER BY hemelsbreed_m ASC LIMIT %s"
|
|
with psycopg2.connect(DB_DSN) as conn:
|
|
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
|
|
cur.execute(wrapped, (lon, lat, radius_m, limit))
|
|
rows = cur.fetchall()
|
|
return [
|
|
Voorziening(
|
|
categorie=categorie,
|
|
naam=row["name"],
|
|
lat=float(row["lat"]),
|
|
lon=float(row["lon"]),
|
|
hemelsbreed_m=float(row["hemelsbreed_m"]),
|
|
)
|
|
for row in rows
|
|
]
|
|
|
|
|
|
def analyse_voorzieningen(lon, lat, limit=5):
|
|
supermarkten = zoek_voorzieningen(lon, lat, "supermarkt", limit=limit, radius_m=10000)
|
|
scholen = zoek_voorzieningen(lon, lat, "school", limit=limit, radius_m=15000)
|
|
verrijk_met_routes(lon, lat, supermarkten)
|
|
verrijk_met_routes(lon, lat, scholen)
|
|
return {"supermarkten": supermarkten, "scholen": scholen}
|
|
|
|
|
|
def format_afstand(m):
|
|
if m is None:
|
|
return "n.b."
|
|
if m < 1000:
|
|
return f"{m:.0f} m"
|
|
return f"{m/1000:.1f} km".replace(".", ",")
|
|
|
|
|
|
def format_duur(s):
|
|
if s is None:
|
|
return "n.b."
|
|
minutes = max(1, round(s / 60))
|
|
return f"{minutes} min"
|
|
|
|
|
|
def format_voorzieningen_markdown(titel, pois):
|
|
lines = [titel]
|
|
if not pois:
|
|
lines.append("Geen voorzieningen gevonden in zoekradius.")
|
|
return "\n".join(lines)
|
|
for p in pois:
|
|
lines.append(
|
|
f"• **{p.naam}** — "
|
|
f"Hemelsbreed: {format_afstand(p.hemelsbreed_m)} | "
|
|
f"Fiets: {format_afstand(p.fiets_afstand_m)} / {format_duur(p.fiets_duur_s)} | "
|
|
f"Auto: {format_afstand(p.auto_afstand_m)} / {format_duur(p.auto_duur_s)}"
|
|
)
|
|
return "\n".join(lines)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import argparse
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("lon", type=float)
|
|
parser.add_argument("lat", type=float)
|
|
parser.add_argument("--limit", type=int, default=5)
|
|
args = parser.parse_args()
|
|
result = analyse_voorzieningen(args.lon, args.lat, args.limit)
|
|
print(json.dumps({k: [p.to_dict() for p in v] for k, v in result.items()}, indent=2, ensure_ascii=False))
|