1 Commits

Author SHA1 Message Date
Hermes Agent c254463001 Release v2.0 - add Netherlands POI analysis 2026-06-21 14:55:44 +02:00
9 changed files with 651 additions and 35 deletions
+2
View File
@@ -4,3 +4,5 @@
__pycache__/
*.pyc
.pytest_cache/
data/
*.osm.pbf
+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
+35 -33
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""
Adres Analyse - Rapport generator
Gebruikt PDOK voor geocoding en ORS voor isochronen
Gebruikt PDOK voor geocoding, ORS voor isochronen/routes en PostGIS OSM-data voor voorzieningen.
"""
import json
@@ -10,13 +10,14 @@ 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"""
# Gebruik suggest endpoint voor betere adres matching
params = urllib.parse.urlencode({
'q': adres,
'rows': 5,
@@ -30,7 +31,6 @@ def geocode_adres(adres):
if not data['response']['docs']:
raise ValueError(f"Adres niet gevonden: {adres}")
# Zoek naar exact adres match (type=adres)
doc = None
for d in data['response']['docs']:
if d.get('type') == 'adres':
@@ -38,9 +38,8 @@ def geocode_adres(adres):
break
if not doc:
doc = data['response']['docs'][0] # Fallback naar eerste resultaat
doc = data['response']['docs'][0]
# Parse POINT(lon lat) format
point = doc['centroide_ll']
coords = point.replace('POINT(', '').replace(')', '').split()
lon, lat = float(coords[0]), float(coords[1])
@@ -58,7 +57,7 @@ def haal_isochroon(lon, lat, profiel='driving-car', minuten=20):
url = f"{ORS_BASE}/isochrones/{profiel}"
payload = {
"locations": [[lon, lat]],
"range": [minuten * 60], # seconden
"range": [minuten * 60],
"attributes": ["area", "reachfactor", "total_pop"]
}
@@ -81,49 +80,52 @@ def format_oppervlakte(m2):
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()
# Geocoding
print("🔍 Geocoding...")
locatie = geocode_adres(adres_input)
print(f"**Adres:** {locatie['adres']}")
print(f"**Coördinaten:** {locatie['lat']:.6f}, {locatie['lon']:.6f}")
print()
# Isochroon
print("🚗 **Bereikbaarheid (Auto 20 min)**")
iso_data = haal_isochroon(locatie['lon'], locatie['lat'], 'driving-car', 20)
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)
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}")
print()
# Bereken bbox
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")
print()
# Save GeoJSON voor kaart generatie
output_file = f"/tmp/isochroon_{datetime.now().strftime('%Y%m%d_%H%M%S')}.geojson"
with open(output_file, 'w') as f:
json.dump(iso_data, f)
print(f"💾 Data opgeslagen: `{output_file}`")
else:
print("❌ Geen isochroon data ontvangen")
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("_Onderdeel 1/?: Isochroon bereikbaarheid_")
print("_Onderdelen: geocoding, isochronen, voorzieningen_")
if __name__ == '__main__':
adres = sys.argv[1] if len(sys.argv) > 1 else "Griendvelden 14 Best"
+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))