3 Commits

Author SHA1 Message Date
wouser 8f2613087c Merge branch 'main' of https://git.wouterverduin.nl/wouser/adres-analyse 2026-06-22 07:05:44 +02:00
Hermes Agent c254463001 Release v2.0 - add Netherlands POI analysis 2026-06-21 14:55:44 +02:00
Hermes Agent c0c9441930 Initial release v1.0: Adres analyse met isochroon bereikbaarheid
- Geocoding via PDOK Locatieserver
- Isochroon (auto 20 min) via OpenRouteService
- Rapport generatie in markdown formaat
- Voorbeeld adres: Griendvelden 14, Best
2026-06-19 18:53:39 +02:00
10 changed files with 825 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
*.json
*.html
*.png
__pycache__/
*.pyc
.pytest_cache/
data/
*.osm.pbf
+73
View File
@@ -0,0 +1,73 @@
# Adres Analyse
Genereert modulaire rapporten voor Nederlandse adressen met bereikbaarheid, voorzieningen en locatie kenmerken.
## Installatie
```bash
git clone https://git.wouterverduin.nl/wouser/adres-analyse.git
cd adres-analyse
pip install folium shapely
```
## Gebruik
```bash
python3 rapport.py "Griendvelden 14 Best"
```
Output is markdown formaat naar stdout.
## Features
### Huidig (v1.0)
- **Geocoding** via PDOK Locatieserver (gratis, geen API key)
- **Isochroon bereikbaarheid** (auto, 20 min) via OpenRouteService
- Oppervlakte bereikbaar gebied (km²)
- Reach factor en geografisch bereik
### Gepland
- Cycling isochroon (fiets)
- OV bereikbaarheid
- Voorzieningen in de buurt (OSM)
- Luchtkwaliteit (RIVM)
- Geluidsniveaus
- Overstromingsrisico
- Demografie (CBS)
- Woningwaarde (Kadaster/WOZ)
## Vereisten
- **OpenRouteService**: Draaiend op `192.168.1.71:9080` met `driving-car` profiel
- **PDOK Locatieserver**: Openbaar, geen setup nodig
## Configuratie
Pas aan in `rapport.py`:
```python
ORS_BASE = "http://192.168.1.71:9080/ors/v2"
PDOK_BASE = "https://api.pdok.nl/bzk/locatieserver/search/v3_1"
```
## Voorbeeld output
```
📍 **Adres Analyse Rapport**
Gegenereerd: 19-06-2026 18:49
🔍 Geocoding...
**Adres:** Griendvelden 14, 5685JL Best
**Coördinaten:** 51.505547, 5.370553
🚗 **Bereikbaarheid (Auto 20 min)**
**Oppervlakte:** 316.8 km²
**Reach factor:** 0.091
**Bereik:** 51.378°-51.679°N, 5.128°-5.530°E
---
_Onderdeel 1/?: Isochroon bereikbaarheid_
```
## Licentie
MIT
+48
View File
@@ -0,0 +1,48 @@
services:
postgis:
image: postgis/postgis:16-3.4
container_name: adres-analyse-postgis
environment:
POSTGRES_DB: adresanalyse
POSTGRES_USER: adres
POSTGRES_PASSWORD: adres
TZ: Europe/Amsterdam
ports:
- "5433:5432"
volumes:
- ./data/postgis:/var/lib/postgresql/data
shm_size: 1g
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U adres -d adresanalyse"]
interval: 10s
timeout: 5s
retries: 10
osm2pgsql:
image: iboates/osm2pgsql:1.11.0
container_name: adres-analyse-osm2pgsql
profiles: ["import"]
depends_on:
postgis:
condition: service_healthy
volumes:
- ./data/osm:/data/osm:ro
- ./osm2pgsql.style:/data/osm2pgsql.style:ro
environment:
PGPASSWORD: adres
command: >
osm2pgsql
--create
--slim
--drop
--cache 2000
--number-processes 4
--hstore
--latlong
--style /data/osm2pgsql.style
-d adresanalyse
-U adres
-H postgis
-P 5432
/data/osm/netherlands-latest.osm.pbf
+174
View File
@@ -0,0 +1,174 @@
#!/usr/bin/env python3
"""
Genereer interactieve kaart voor adres analyse.
Toont isochronen plus dichtstbijzijnde supermarkten en scholen uit lokale PostGIS OSM-data.
"""
import json
import sys
import urllib.parse
import urllib.request
import folium
from folium.plugins import MarkerCluster
from voorzieningen import analyse_voorzieningen, format_afstand, format_duur
ORS_BASE = "http://192.168.1.71:9080/ors/v2"
PDOK_BASE = "https://api.pdok.nl/bzk/locatieserver/search/v3_1"
def geocode_adres(adres):
params = urllib.parse.urlencode({
"q": adres,
"rows": 5,
"fl": "centroide_ll,weergavenaam,postcode,woonplaatsnaam,type,huisnummer",
})
url = f"{PDOK_BASE}/free?{params}"
with urllib.request.urlopen(url, timeout=10) as resp:
data = json.loads(resp.read())
docs = data.get("response", {}).get("docs", [])
if not docs:
raise ValueError(f"Adres niet gevonden: {adres}")
doc = next((d for d in docs if d.get("type") == "adres"), docs[0])
point = doc["centroide_ll"].replace("POINT(", "").replace(")", "").split()
lon, lat = float(point[0]), float(point[1])
return {
"adres": doc.get("weergavenaam", adres),
"postcode": doc.get("postcode", ""),
"woonplaats": doc.get("woonplaatsnaam", ""),
"lat": lat,
"lon": lon,
}
def haal_isochroon(lon, lat, profiel, minuten):
payload = json.dumps({
"locations": [[lon, lat]],
"range": [minuten * 60],
"attributes": ["area", "reachfactor", "total_pop"],
}).encode()
req = urllib.request.Request(
f"{ORS_BASE}/isochrones/{profiel}",
data=payload,
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=60) as resp:
return json.loads(resp.read())
def polygon_bounds(feature_collection):
bbox = feature_collection.get("bbox", [])
if len(bbox) == 4:
return [[bbox[1], bbox[0]], [bbox[3], bbox[2]]]
bounds = []
for feature in feature_collection.get("features", []):
coords = feature.get("geometry", {}).get("coordinates", [[]])[0]
bounds.extend([[lat, lon] for lon, lat in coords])
return bounds
def voeg_isochroon_toe(group, iso, label, kleur, tooltip):
for feature in iso.get("features", []):
coords = feature["geometry"]["coordinates"][0]
props = feature.get("properties", {})
area_km2 = props.get("area", 0) / 1_000_000
reach = props.get("reachfactor", 0)
folium.Polygon(
locations=[[lat, lon] for lon, lat in coords],
color=kleur,
weight=3,
fill=True,
fill_color=kleur,
fill_opacity=0.18,
popup=(
f"<b>{label}</b><br>"
f"Oppervlakte: {area_km2:.1f} km²<br>"
f"Reach factor: {reach:.3f}"
),
tooltip=tooltip,
).add_to(group)
def voeg_pois_toe(group, pois, kleur, icon):
for poi in pois:
popup = (
f"<b>{poi.naam}</b><br>"
f"Categorie: {poi.categorie}<br>"
f"Hemelsbreed: {format_afstand(poi.hemelsbreed_m)}<br>"
f"Fiets: {format_afstand(poi.fiets_afstand_m)} / {format_duur(poi.fiets_duur_s)}<br>"
f"Auto: {format_afstand(poi.auto_afstand_m)} / {format_duur(poi.auto_duur_s)}<br>"
f"Bron: {poi.bron}"
)
folium.Marker(
[poi.lat, poi.lon],
popup=popup,
tooltip=f"{poi.categorie}: {poi.naam}",
icon=folium.Icon(color=kleur, icon=icon, prefix="fa"),
).add_to(group)
def genereer_kaart(adres, output_file="/root/adres-analyse/kaart.html"):
locatie = geocode_adres(adres)
lat, lon = locatie["lat"], locatie["lon"]
auto_iso = haal_isochroon(lon, lat, "driving-car", 20)
fiets_iso = haal_isochroon(lon, lat, "cycling-regular", 30)
voorzieningen = analyse_voorzieningen(lon, lat, limit=5)
m = folium.Map(location=[lat, lon], zoom_start=11, tiles="OpenStreetMap")
folium.Marker(
[lat, lon],
popup=locatie["adres"],
tooltip=locatie["adres"],
icon=folium.Icon(color="red", icon="home"),
).add_to(m)
auto_group = folium.FeatureGroup(name="Isochroon: 20 min auto", show=True)
fiets_group = folium.FeatureGroup(name="Isochroon: 30 min fiets", show=True)
super_group = MarkerCluster(name="Supermarkten", show=True)
school_group = MarkerCluster(name="Scholen", show=True)
voeg_isochroon_toe(auto_group, auto_iso, "Auto 20 min", "#2196F3", "Bereikbaar in 20 min (auto)")
voeg_isochroon_toe(fiets_group, fiets_iso, "Fiets 30 min", "#2E7D32", "Bereikbaar in 30 min (fiets)")
voeg_pois_toe(super_group, voorzieningen["supermarkten"], "orange", "shopping-cart")
voeg_pois_toe(school_group, voorzieningen["scholen"], "purple", "graduation-cap")
auto_group.add_to(m)
fiets_group.add_to(m)
super_group.add_to(m)
school_group.add_to(m)
folium.LayerControl(collapsed=False).add_to(m)
all_bounds = polygon_bounds(auto_iso) + polygon_bounds(fiets_iso)
all_bounds += [[p.lat, p.lon] for groep in voorzieningen.values() for p in groep]
if all_bounds:
m.fit_bounds(all_bounds)
title_html = f'''
<div style="position:fixed;top:10px;left:60px;z-index:1000;background:white;padding:10px 15px;border-radius:8px;box-shadow:0 2px 6px rgba(0,0,0,0.3);font-family:sans-serif;font-size:14px;line-height:1.35;">
<b>📍 Adres Analyse</b><br>
{locatie["adres"]}<br>
<span style="color:#2196F3">■</span> 20 min autorijden<br>
<span style="color:#2E7D32">■</span> 30 min fietsen<br>
🛒 Supermarkten · 🏫 Scholen
</div>
'''
m.get_root().html.add_child(folium.Element(title_html))
m.save(output_file)
print(f"Kaart gegenereerd: {output_file}")
print(f"Adres: {locatie['adres']}")
print(f"Coördinaten: {lat}, {lon}")
print(f"Supermarkten: {len(voorzieningen['supermarkten'])}")
print(f"Scholen: {len(voorzieningen['scholen'])}")
if __name__ == "__main__":
adres = sys.argv[1] if len(sys.argv) > 1 else "Griendvelden 14 Best"
output = sys.argv[2] if len(sys.argv) > 2 else "/root/adres-analyse/kaart.html"
genereer_kaart(adres, output)
Executable
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
mkdir -p data/osm data/postgis
PBF="data/osm/netherlands-latest.osm.pbf"
URL="https://download.geofabrik.de/europe/netherlands-latest.osm.pbf"
if [[ ! -s "$PBF" ]]; then
echo "Downloading Netherlands OSM extract..."
curl -L --fail --continue-at - -o "$PBF" "$URL"
else
echo "Using existing $PBF"
fi
echo "Starting PostGIS..."
docker compose up -d postgis
echo "Waiting for PostGIS healthcheck..."
until docker compose exec -T postgis pg_isready -U adres -d adresanalyse >/dev/null 2>&1; do
sleep 2
done
echo "Importing supermarkets and schools from full Netherlands OSM extract..."
python3 osm-poi-import.py "$PBF"
echo "Done."
+136
View File
@@ -0,0 +1,136 @@
#!/usr/bin/env python3
"""Importeer alleen benodigde POI's uit een OSM PBF naar PostGIS.
Veel lichter dan volledige osm2pgsql-import voor heel Nederland.
"""
import argparse
import os
import osmium
import psycopg2
from psycopg2.extras import execute_values
DB_DSN = os.environ.get(
"ADRES_ANALYSE_DB_DSN",
"host=127.0.0.1 port=5433 dbname=adresanalyse user=adres password=adres",
)
SCHEMA_SQL = """
CREATE EXTENSION IF NOT EXISTS postgis;
DROP TABLE IF EXISTS osm_pois;
CREATE TABLE osm_pois (
id bigserial PRIMARY KEY,
osm_type text NOT NULL,
osm_id bigint NOT NULL,
category text NOT NULL,
name text NOT NULL,
brand text,
operator text,
lon double precision NOT NULL,
lat double precision NOT NULL,
geom geometry(Point, 4326) NOT NULL
);
CREATE INDEX osm_pois_category_geom_gix ON osm_pois USING GIST (geom);
CREATE INDEX osm_pois_category_idx ON osm_pois (category);
CREATE INDEX osm_pois_name_idx ON osm_pois (lower(name));
CREATE OR REPLACE VIEW poi_supermarkten AS
SELECT osm_id, name, 'supermarkt'::text AS category, 'supermarket'::text AS shop, NULL::text AS amenity, brand, operator, geom
FROM osm_pois WHERE category = 'supermarkt';
CREATE OR REPLACE VIEW poi_scholen AS
SELECT osm_id, name, 'school'::text AS category, NULL::text AS shop, 'school'::text AS amenity, brand, operator, geom
FROM osm_pois WHERE category = 'school';
"""
def poi_from_tags(tags):
if tags.get("shop") == "supermarket":
return "supermarkt", tags.get("name") or tags.get("brand") or "Supermarkt"
if tags.get("amenity") == "school":
return "school", tags.get("name") or "School"
return None, None
class PoiHandler(osmium.SimpleHandler):
def __init__(self, conn, batch_size=1000):
super().__init__()
self.conn = conn
self.batch_size = batch_size
self.rows = []
self.count = 0
self.factory = osmium.geom.WKBFactory()
def flush(self):
if not self.rows:
return
with self.conn.cursor() as cur:
execute_values(
cur,
"""
INSERT INTO osm_pois (osm_type, osm_id, category, name, brand, operator, lon, lat, geom)
VALUES %s
""",
self.rows,
)
self.conn.commit()
self.rows.clear()
def add_row(self, osm_type, osm_id, tags, lon, lat):
category, name = poi_from_tags(tags)
if not category:
return
self.rows.append((
osm_type,
int(osm_id),
category,
name,
tags.get("brand"),
tags.get("operator"),
float(lon),
float(lat),
f"SRID=4326;POINT({float(lon)} {float(lat)})",
))
self.count += 1
if len(self.rows) >= self.batch_size:
self.flush()
def node(self, n):
if not n.location.valid():
return
self.add_row("node", n.id, n.tags, n.location.lon, n.location.lat)
def area(self, a):
category, _ = poi_from_tags(a.tags)
if not category:
return
try:
wkb = self.factory.create_multipolygon(a)
with self.conn.cursor() as cur:
cur.execute("SELECT ST_X(p), ST_Y(p) FROM (SELECT ST_PointOnSurface(ST_GeomFromWKB(%s, 4326)) p) s", (psycopg2.Binary(wkb),))
lon, lat = cur.fetchone()
self.add_row("area", a.orig_id(), a.tags, lon, lat)
except Exception:
return
def main():
parser = argparse.ArgumentParser()
parser.add_argument("pbf")
args = parser.parse_args()
with psycopg2.connect(DB_DSN) as conn:
with conn.cursor() as cur:
cur.execute(SCHEMA_SQL)
conn.commit()
handler = PoiHandler(conn)
handler.apply_file(args.pbf, locations=True)
handler.flush()
with conn.cursor() as cur:
cur.execute("ANALYZE osm_pois")
conn.commit()
print(f"Imported {handler.count} POIs into osm_pois")
if __name__ == "__main__":
main()
+12
View File
@@ -0,0 +1,12 @@
# OsmType Tag DataType Flags
node,way name text linear
node,way amenity text polygon
node,way shop text polygon
node,way brand text linear
node,way operator text linear
node,way addr:street text linear
node,way addr:housenumber text linear
node,way addr:postcode text linear
node,way addr:city text linear
node,way wheelchair text linear
node,way opening_hours text linear
+132
View File
@@ -0,0 +1,132 @@
#!/usr/bin/env python3
"""
Adres Analyse - Rapport generator
Gebruikt PDOK voor geocoding, ORS voor isochronen/routes en PostGIS OSM-data voor voorzieningen.
"""
import json
import sys
import urllib.request
import urllib.parse
from datetime import datetime
from voorzieningen import analyse_voorzieningen, format_voorzieningen_markdown
# Config
ORS_BASE = "http://192.168.1.71:9080/ors/v2"
PDOK_BASE = "https://api.pdok.nl/bzk/locatieserver/search/v3_1"
def geocode_adres(adres):
"""Geocode adres naar coördinaten via PDOK Locatieserver"""
params = urllib.parse.urlencode({
'q': adres,
'rows': 5,
'fl': 'centroide_ll,weergavenaam,postcode,woonplaatsnaam,type,huisnummer'
})
url = f"{PDOK_BASE}/free?{params}"
with urllib.request.urlopen(url, timeout=10) as resp:
data = json.loads(resp.read())
if not data['response']['docs']:
raise ValueError(f"Adres niet gevonden: {adres}")
doc = None
for d in data['response']['docs']:
if d.get('type') == 'adres':
doc = d
break
if not doc:
doc = data['response']['docs'][0]
point = doc['centroide_ll']
coords = point.replace('POINT(', '').replace(')', '').split()
lon, lat = float(coords[0]), float(coords[1])
return {
'adres': doc.get('weergavenaam', adres),
'postcode': doc.get('postcode', ''),
'woonplaats': doc.get('woonplaatsnaam', ''),
'lat': lat,
'lon': lon
}
def haal_isochroon(lon, lat, profiel='driving-car', minuten=20):
"""Haal isochroon op van ORS"""
url = f"{ORS_BASE}/isochrones/{profiel}"
payload = {
"locations": [[lon, lat]],
"range": [minuten * 60],
"attributes": ["area", "reachfactor", "total_pop"]
}
req = urllib.request.Request(
url,
data=json.dumps(payload).encode(),
headers={'Content-Type': 'application/json'},
method='POST'
)
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read())
def format_oppervlakte(m2):
"""Converteer m² naar leesbaar formaat"""
if m2 >= 1_000_000:
return f"{m2/1_000_000:.1f} km²"
elif m2 >= 10_000:
return f"{m2/10_000:.1f} ha"
else:
return f"{m2:.0f} m²"
def print_isochroon_sectie(titel, icoon, iso_data):
print(f"{icoon} **{titel}**")
if 'features' in iso_data and iso_data['features']:
props = iso_data['features'][0]['properties']
area_m2 = props.get('area', 0)
reach = props.get('reachfactor', 0)
print(f"**Oppervlakte:** {format_oppervlakte(area_m2)}")
print(f"**Reach factor:** {reach:.3f}")
bbox = iso_data.get('bbox', [])
if len(bbox) == 4:
print(f"**Bereik:** {bbox[1]:.3f}°-{bbox[3]:.3f}°N, {bbox[0]:.3f}°-{bbox[2]:.3f}°E")
else:
print("❌ Geen isochroon data ontvangen")
print()
def genereer_rapport(adres_input):
"""Genereer volledig adres analyse rapport"""
print(f"📍 **Adres Analyse Rapport**")
print(f"Gegenereerd: {datetime.now().strftime('%d-%m-%Y %H:%M')}")
print()
print("🔍 Geocoding...")
locatie = geocode_adres(adres_input)
print(f"**Adres:** {locatie['adres']}")
print(f"**Coördinaten:** {locatie['lat']:.6f}, {locatie['lon']:.6f}")
print()
auto_iso = haal_isochroon(locatie['lon'], locatie['lat'], 'driving-car', 20)
fiets_iso = haal_isochroon(locatie['lon'], locatie['lat'], 'cycling-regular', 30)
print_isochroon_sectie("Bereikbaarheid (Auto 20 min)", "🚗", auto_iso)
print_isochroon_sectie("Bereikbaarheid (Fiets 30 min)", "🚲", fiets_iso)
print("🏙️ **Voorzieningen dichtbij**")
voorzieningen = analyse_voorzieningen(locatie['lon'], locatie['lat'], limit=5)
print(format_voorzieningen_markdown("🛒 **Supermarkten**", voorzieningen['supermarkten']))
print()
print(format_voorzieningen_markdown("🏫 **Scholen**", voorzieningen['scholen']))
print()
output_file = f"/tmp/isochronen_{datetime.now().strftime('%Y%m%d_%H%M%S')}.geojson"
with open(output_file, 'w') as f:
json.dump({'auto_20_min': auto_iso, 'fiets_30_min': fiets_iso}, f)
print(f"💾 Isochroondata opgeslagen: `{output_file}`")
print()
print("---")
print("_Onderdelen: geocoding, isochronen, voorzieningen_")
if __name__ == '__main__':
adres = sys.argv[1] if len(sys.argv) > 1 else "Griendvelden 14 Best"
genereer_rapport(adres)
+57
View File
@@ -0,0 +1,57 @@
CREATE EXTENSION IF NOT EXISTS postgis;
CREATE EXTENSION IF NOT EXISTS hstore;
CREATE OR REPLACE VIEW poi_supermarkten AS
SELECT
osm_id,
COALESCE(NULLIF(name, ''), NULLIF(brand, ''), 'Supermarkt') AS name,
'supermarkt'::text AS category,
shop,
amenity,
brand,
operator,
way AS geom
FROM planet_osm_point
WHERE shop = 'supermarket'
UNION ALL
SELECT
osm_id,
COALESCE(NULLIF(name, ''), NULLIF(brand, ''), 'Supermarkt') AS name,
'supermarkt'::text AS category,
shop,
amenity,
brand,
operator,
ST_PointOnSurface(way) AS geom
FROM planet_osm_polygon
WHERE shop = 'supermarket';
CREATE OR REPLACE VIEW poi_scholen AS
SELECT
osm_id,
COALESCE(NULLIF(name, ''), 'School') AS name,
'school'::text AS category,
shop,
amenity,
brand,
operator,
way AS geom
FROM planet_osm_point
WHERE amenity = 'school'
UNION ALL
SELECT
osm_id,
COALESCE(NULLIF(name, ''), 'School') AS name,
'school'::text AS category,
shop,
amenity,
brand,
operator,
ST_PointOnSurface(way) AS geom
FROM planet_osm_polygon
WHERE amenity = 'school';
CREATE INDEX IF NOT EXISTS planet_osm_point_way_gix ON planet_osm_point USING GIST (way);
CREATE INDEX IF NOT EXISTS planet_osm_polygon_way_gix ON planet_osm_polygon USING GIST (way);
ANALYZE planet_osm_point;
ANALYZE planet_osm_polygon;
+157
View File
@@ -0,0 +1,157 @@
#!/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 i, p in enumerate(pois, 1):
lines.extend([
f"{i}. **{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))