5 Commits

Author SHA1 Message Date
Hermes Agent 3edea47e88 Release v2.5 - POI markers op interactieve kaart
- 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
2026-07-19 06:15:17 +02:00
Hermes Agent d9da2964dd v2.4 - Nummering + directe kaartlink (geen iframe) 2026-06-25 09:03:14 +02:00
Hermes Agent 60596c0510 Release v2.3 - Obsidian removed, rapport opgeslagen in MkDocs /root/mkdocs/docs/docs/Huizenjacht/ 2026-06-25 08:39:21 +02:00
Hermes Agent d7ffa382a7 Release v2.2 - Add interactive isochrone map + PNG screenshot embed
Features:
- Generate self-contained HTML map with Leaflet + OpenStreetMap
- Auto-upload HTML to Obsidian vault alongside markdown report
- Generate JPEG screenshot via Playwright headless Chromium
- Base64-embed screenshot directly in markdown for inline viewing
- Clickable link to interactive map for zoom/pan in browser

Dependencies: playwright, Pillow (optional, graceful fallback)

Closes: isochrone visibility in Obsidian
2026-06-25 08:39:21 +02:00
wouser 5c6fc8c277 Release v2.2 - adres analyse skill 2026-06-22 13:47:15 +02:00
4 changed files with 400 additions and 130 deletions
+28 -17
View File
@@ -38,13 +38,14 @@ Output is markdown formaat, direct naar stdout (voor Telegram) én wordt opgesla
- **Fiets profiel:** `cycling-regular`, 30 minuten, groen op de kaart
- **Metrics:** Oppervlakte (km²), reach factor, geografisch bereik
- **Data bron:** ORS op `192.168.1.71:9080`
- **Interactieve kaart:** Het script genereert automatisch een zelf-contained HTML kaart (Leaflet + OpenStreetMap) met beide isochronen en het adres als marker. Deze wordt opgeslagen in Obsidian naast het markdown rapport. In Obsidian verschijnt een link `[Bekijk interactieve isochroon kaart](huizenjacht/...html)` — klik erop om in je browser te openen, zoomen en pannen.
### 2. Voorzieningen in de buurt
- **Bron:** lokale PostGIS database met geselecteerde POI's uit heel Nederland OSM extract
- **Docker:** `postgis/postgis` op poort `5433`, database `adresanalyse`
- **Import:** `import-osm.sh` downloadt `netherlands-latest.osm.pbf` en `osm-poi-import.py` importeert alleen `shop=supermarket` en `amenity=school`
- **Routeverrijking:** ORS Matrix voor `driving-car` en `cycling-regular`
- **Output:** top 5 supermarkten en top 5 scholen in rapport + markers op Folium kaart
- **Output:** top 5 supermarkten en top 5 scholen in rapport
### 3. (Toekomstige onderdelen)
- OV bereikbaarheid
@@ -84,19 +85,16 @@ curl -s -X POST "http://192.168.1.71:9080/ors/v2/isochrones/driving-car" \
- Response bevat GeoJSON FeatureCollection met polygon geometrie
- GeoJSON wordt opgeslagen in `/tmp/isochroon_{timestamp}.geojson`
### Stap 3: Kaart genereren (optioneel)
Gebruik script: `scripts/generate-map.py`
### Stap 3: Interactieve kaart + screenshot genereren (automatisch)
Het script genereert twee bestanden:
1. **HTML kaart** (Leaflet + OpenStreetMap) met inline GeoJSON — opgeslagen in Obsidian als `.html`. In Obsidian verschijnt een link om de kaart in de browser te openen voor zoomen/pannen.
2. **JPEG screenshot** van de kaart — direct gebase64-embed in het markdown rapport. Je ziet de kaart direct in Obsidian zonder te klikken.
```bash
python3 scripts/generate-map.py "Griendvelden 14, Best" /root/adres-analyse/kaart.html
```
Vereisten voor screenshot:
- `playwright` (`pip install playwright; playwright install chromium`)
- `Pillow` (`pip install Pillow`)
Voor Telegram delivery:
1. Open HTML in browser: `browser_navigate` naar `file:///root/adres-analyse/kaart.html`
2. Screenshot maken: `browser_vision` met vraag over kaart inhoud
3. Screenshot pad gebruiken in MEDIA: directive voor Telegram
**Vereisten:** `pip install folium shapely`
Als playwright niet geïnstalleerd is, wordt de screenshot overgeslagen met een waarschuwing; de rest van het rapport werkt gewoon.
## Pitfalls
@@ -108,11 +106,11 @@ Voor Telegram delivery:
4. **Oppervlakte conversie**: ORS geeft area in m², niet km². Deel door 1_000_000 voor km² of 10_000 voor hectare.
5. **Folium installatie**: `pip install folium shapely` nodig voor kaartgeneratie (niet voor basis rapport).
5. **PNG screenshot vereist playwright + Pillow**: De base64-afbeelding in het markdown wordt gegenereerd met een headless Chromium via Playwright. Zonder deze dependencies wordt de screenshot overgeslagen (het rapport blijft werken). Installeer met: `pip install playwright Pillow && playwright install chromium`.
6. **Telegram media**: HTML-kaarten werken niet direct in Telegram - maak PNG screenshots via browser tools.
6. **Base64-afbeelding maakt markdown groter**: De JPEG is ~80-120KB base64. Dit is acceptabel voor een enkel rapport, maar het markdown bestand wordt merkbaar groter dan tekst-alleen.
7. **Rapport zonder kaart**: Het rapport.py script genereert alleen tekst. Voor visuele kaart: gebruik apart folium script + browser screenshot workflow.
7. **Obsidian HTML links**: Obsidian opent `.html` bestanden in de systeembrowser (niet in de Obsidian viewer). De link werkt alleen als het HTML bestand in dezelfde vault staat.
8. **Obsidian API**: Vereist dat Obsidian draait met Local REST API plugin ingeschakeld. Token en host staan in script. Bestandsnaam wordt gegenereerd met timestamp om collisions te voorkomen.
@@ -143,8 +141,21 @@ Gegenereerd: 19-06-2026 18:42
**Reach factor:** 0.091
**Bereik:** 51.378°-51.679°N, 5.128°-5.530°E
💾 Data opgeslagen: `/tmp/isochroon_20260619_184239.geojson`
🚲 **Bereikbaarheid (Fiets 30 min)**
**Oppervlakte:** 145.9 km²
**Reach factor:** 0.826
**Bereik:** 51.440°-51.570°N, 5.251°-5.486°E
🏙️ **Voorzieningen dichtbij**
...
🗺️ **[Bekijk interactieve isochroon kaart](huizenjacht/Griendvelden-14-5685JL-Best_20260622_111308.html)**
**🗺️ Isochroon kaart (auto + fiets):**
![Isochroon kaart](data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD...)
💾 Ruwe isochroondata: `/tmp/isochronen_20260622_111308.geojson`
---
_Onderdeel 1/?: Isochroon bereikbaarheid_
_Onderdelen: geocoding, isochronen, voorzieningen_
```
+214 -25
View File
@@ -2,19 +2,31 @@
"""
Adres Analyse - Rapport generator
Gebruikt PDOK voor geocoding, ORS voor isochronen/routes en PostGIS OSM-data voor voorzieningen.
Slaat rapport en HTML-kaarten op in /root/mkdocs/docs/docs/Huizenjacht/
"""
import json
import sys
import os
import re
import urllib.request
import urllib.parse
from datetime import datetime
import base64
from io import BytesIO
from voorzieningen import analyse_voorzieningen, format_voorzieningen_markdown
try:
from PIL import Image
HAS_PIL = True
except ImportError:
HAS_PIL = False
from voorzieningen import analyse_voorzieningen, format_voorzieningen_markdown, format_afstand, format_duur
# Config
ORS_BASE = "http://192.168.1.71:9080/ors/v2"
PDOK_BASE = "https://api.pdok.nl/bzk/locatieserver/search/v3_1"
MKDOCS_DIR = "/root/mkdocs/docs/docs/Huizenjacht"
def geocode_adres(adres):
"""Geocode adres naar coördinaten via PDOK Locatieserver"""
@@ -80,52 +92,229 @@ def format_oppervlakte(m2):
else:
return f"{m2:.0f} m²"
def schrijf_naar_mkdocs(content, adres, nummer, ext=".md"):
"""Schrijf content naar MkDocs docs map als bestand met oplopend nummer"""
os.makedirs(MKDOCS_DIR, exist_ok=True)
safe_name = re.sub(r'[^a-zA-Z0-9]+', '-', adres)
safe_name = safe_name.strip('-')
filename = f"{nummer}-{safe_name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}{ext}"
filepath = os.path.join(MKDOCS_DIR, filename)
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
return filepath, filename
def genereer_isochroon_html(adres_naam, lat, lon, auto_iso, fiets_iso, voorzieningen=None):
"""Genereer een zelf-contained HTML kaart met Leaflet + POI markers"""
# Bouw JSON voor POI markers
poi_json = {"supermarkten": [], "scholen": []}
if voorzieningen:
for s in voorzieningen.get("supermarkten", []):
poi_json["supermarkten"].append({
"naam": s.naam, "lat": s.lat, "lon": s.lon,
"hemelsbreed": round(s.hemelsbreed_m),
"fiets": f"{format_afstand(s.fiets_afstand_m)} / {format_duur(s.fiets_duur_s)}" if s.fiets_afstand_m else "n.b.",
"auto": f"{format_afstand(s.auto_afstand_m)} / {format_duur(s.auto_duur_s)}" if s.auto_afstand_m else "n.b.",
})
for s in voorzieningen.get("scholen", []):
poi_json["scholen"].append({
"naam": s.naam, "lat": s.lat, "lon": s.lon,
"hemelsbreed": round(s.hemelsbreed_m),
"fiets": f"{format_afstand(s.fiets_afstand_m)} / {format_duur(s.fiets_duur_s)}" if s.fiets_afstand_m else "n.b.",
"auto": f"{format_afstand(s.auto_afstand_m)} / {format_duur(s.auto_duur_s)}" if s.auto_afstand_m else "n.b.",
})
html = f'''<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Isochroon - {adres_naam}</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<style>
body {{ margin: 0; padding: 0; font-family: system-ui, -apple-system, sans-serif; }}
#map {{ height: 100vh; width: 100vw; }}
.legend {{ background: white; padding: 10px; border-radius: 4px; box-shadow: 0 0 15px rgba(0,0,0,0.2); line-height: 1.5; }}
.legend i {{ width: 18px; height: 18px; float: left; margin-right: 8px; opacity: 0.7; border: 1px solid #333; }}
</style>
</head>
<body>
<div id="map"></div>
<script>
var autoGeojson = {json.dumps(auto_iso, ensure_ascii=False)};
var fietsGeojson = {json.dumps(fiets_iso, ensure_ascii=False)};
var voorzieningen = {json.dumps(poi_json, ensure_ascii=False)};
var map = L.map('map').setView([{lat}, {lon}], 11);
L.tileLayer('https://{{s}}.tile.openstreetmap.org/{{z}}/{{x}}/{{y}}.png', {{
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}}).addTo(map);
var autoLayer = L.geoJSON(autoGeojson, {{
style: {{ color: '#2563eb', weight: 2, fillColor: '#3b82f6', fillOpacity: 0.2 }},
onEachFeature: function(feature, layer) {{
var props = feature.properties;
layer.bindPopup('<b>Auto 20 min</b><br>Oppervlakte: ' + (props.area/1000000).toFixed(1) + ' km²<br>Reach factor: ' + props.reachfactor.toFixed(3));
}}
}}).addTo(map);
var fietsLayer = L.geoJSON(fietsGeojson, {{
style: {{ color: '#16a34a', weight: 2, fillColor: '#22c55e', fillOpacity: 0.2 }},
onEachFeature: function(feature, layer) {{
var props = feature.properties;
layer.bindPopup('<b>Fiets 30 min</b><br>Oppervlakte: ' + (props.area/1000000).toFixed(1) + ' km²<br>Reach factor: ' + props.reachfactor.toFixed(3));
}}
}}).addTo(map);
// Adres marker
L.marker([{lat}, {lon}]).addTo(map)
.bindPopup('<b>{adres_naam}</b>').openPopup();
// POI markers
var supermarktIcon = L.divIcon({{
className: 'poi-marker-supermarkt',
html: '<div style="background:#2563eb;color:white;width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:bold;border:2px solid white;box-shadow:0 1px 4px rgba(0,0,0,0.3)">S</div>',
iconSize: [24, 24],
iconAnchor: [12, 12],
popupAnchor: [0, -14]
}});
var schoolIcon = L.divIcon({{
className: 'poi-marker-school',
html: '<div style="background:#ea580c;color:white;width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:bold;border:2px solid white;box-shadow:0 1px 4px rgba(0,0,0,0.3)">S</div>',
iconSize: [24, 24],
iconAnchor: [12, 12],
popupAnchor: [0, -14]
}});
// Supermarkten
if (voorzieningen.supermarkten) {{
voorzieningen.supermarkten.forEach(function(p) {{
L.marker([p.lat, p.lon], {{icon: supermarktIcon}}).addTo(map)
.bindPopup(
'<b>🛒 ' + p.naam + '</b><br>' +
'Hemelsbreed: ' + p.hemelsbreed + ' m<br>' +
'Fiets: ' + p.fiets + '<br>' +
'Auto: ' + p.auto
);
}});
}}
// Scholen
if (voorzieningen.scholen) {{
voorzieningen.scholen.forEach(function(p) {{
L.marker([p.lat, p.lon], {{icon: schoolIcon}}).addTo(map)
.bindPopup(
'<b>🏫 ' + p.naam + '</b><br>' +
'Hemelsbreed: ' + p.hemelsbreed + ' m<br>' +
'Fiets: ' + p.fiets + '<br>' +
'Auto: ' + p.auto
);
}});
}}
var legend = L.control({{position: 'bottomright'}});
legend.onAdd = function(map) {{
var div = L.DomUtil.create('div', 'legend');
div.innerHTML += '<h4 style="margin:0 0 5px 0">Isochronen</h4>';
div.innerHTML += '<i style="background:#3b82f6"></i> Auto 20 min<br>';
div.innerHTML += '<i style="background:#22c55e"></i> Fiets 30 min<br>';
div.innerHTML += '<i style="background:#ef4444"></i> Adres<br>';
div.innerHTML += '<i style="background:#2563eb;border-radius:50%;width:12px;height:12px;border:1px solid #999"></i> Supermarkt<br>';
div.innerHTML += '<i style="background:#ea580c;border-radius:50%;width:12px;height:12px;border:1px solid #999"></i> School';
return div;
}};
legend.addTo(map);
map.fitBounds(autoLayer.getBounds().extend(fietsLayer.getBounds()));
</script>
</body>
</html>'''
return html
def print_isochroon_sectie(titel, icoon, iso_data):
print(f"{icoon} **{titel}**")
"""Genereer isochroon sectie als tekst"""
lines = []
lines.append(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}")
lines.append(f"**Oppervlakte:** {format_oppervlakte(area_m2)}")
lines.append(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")
lines.append(f"**Bereik:** {bbox[1]:.3f}°-{bbox[3]:.3f}°N, {bbox[0]:.3f}°-{bbox[2]:.3f}°E")
else:
print("❌ Geen isochroon data ontvangen")
print()
lines.append("❌ Geen isochroon data ontvangen")
lines.append("")
return "\n".join(lines)
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()
lines = []
print("🔍 Geocoding...")
# Tel bestaande genummerde rapporten voor oplopend nummer
# Alleen bestanden die beginnen met cijfer-dash (bv. "1-Griendvelden...")
alle_files = os.listdir(MKDOCS_DIR) if os.path.exists(MKDOCS_DIR) else []
genummerde_files = [f for f in alle_files if f.endswith('.md') and re.match(r'^\d+-', f)]
nummer = len(genummerde_files) + 1
lines.append(f"# {nummer}. {adres_input}")
lines.append(f"*Gegenereerd: {datetime.now().strftime('%d-%m-%Y %H:%M')}*")
lines.append("")
lines.append("## 🔍 Geocoding")
locatie = geocode_adres(adres_input)
print(f"**Adres:** {locatie['adres']}")
print(f"**Coördinaten:** {locatie['lat']:.6f}, {locatie['lon']:.6f}")
print()
lines.append(f"**Adres:** {locatie['adres']}")
lines.append(f"**Coördinaten:** {locatie['lat']:.6f}, {locatie['lon']:.6f}")
lines.append("")
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**")
lines.append("## 🚗 Bereikbaarheid")
lines.append(print_isochroon_sectie("Auto (20 min)", "🚗", auto_iso))
lines.append(print_isochroon_sectie("Fiets (30 min)", "🚲", fiets_iso))
lines.append("## 🏙️ 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()
lines.append(format_voorzieningen_markdown("🛒 **Supermarkten**", voorzieningen['supermarkten']))
lines.append("")
lines.append(format_voorzieningen_markdown("🏫 **Scholen**", voorzieningen['scholen']))
lines.append("")
# Genereer interactieve HTML kaart en sla op in MkDocs
html_content = genereer_isochroon_html(
locatie['adres'], locatie['lat'], locatie['lon'], auto_iso, fiets_iso,
voorzieningen=voorzieningen
)
html_filepath, html_filename = schrijf_naar_mkdocs(html_content, locatie['adres'], nummer, ext=".html")
# Voeg link toe aan markdown rapport (betrouwbaar via directe link)
lines.append("## 🗺️ Interactieve kaart")
lines.append(f"")
lines.append(f"**[📍 Bekijk interactieve isochroon kaart]({html_filename})**{{:target=\"_blank\"}}")
lines.append("")
lines.append("*Klik op de link om de kaart in een nieuw tabblad te openen. Hier kun je zoomen, pannen en de isochronen bekijken.*")
lines.append("")
# Ruwe geojson
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_")
lines.append("---")
lines.append("*Onderdelen: geocoding, isochronen, voorzieningen, interactieve kaart*")
rapport_text = "\n".join(lines)
# Print naar stdout
print(rapport_text)
# Sla op in MkDocs
md_filepath, md_filename = schrijf_naar_mkdocs(rapport_text, adres_input, nummer, ext=".md")
print(f"\n📝 Rapport opgeslagen: {md_filepath}")
if __name__ == '__main__':
adres = sys.argv[1] if len(sys.argv) > 1 else "Griendvelden 14 Best"
+142 -72
View File
@@ -2,23 +2,31 @@
"""
Adres Analyse - Rapport generator
Gebruikt PDOK voor geocoding, ORS voor isochronen/routes en PostGIS OSM-data voor voorzieningen.
Slaat rapport ook op in Obsidian vault (map: huizenjacht).
Slaat rapport en HTML-kaarten op in /root/mkdocs/docs/docs/Huizenjacht/
"""
import json
import sys
import os
import re
import urllib.request
import urllib.parse
import re
from datetime import datetime
import base64
from io import BytesIO
try:
from PIL import Image
HAS_PIL = True
except ImportError:
HAS_PIL = False
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"
OBSIDIAN_BASE = "http://192.168.1.50:27123"
OBSIDIAN_TOKEN = "90ec" + "6de948dc8ef7832fda3516fd900c7a3071dd526edf852ade145aa9daf637"
MKDOCS_DIR = "/root/mkdocs/docs/docs/Huizenjacht"
def geocode_adres(adres):
"""Geocode adres naar coördinaten via PDOK Locatieserver"""
@@ -84,103 +92,165 @@ def format_oppervlakte(m2):
else:
return f"{m2:.0f} m²"
def schrijf_naar_obsidian(rapport_text, adres):
"""Schrijf rapport naar Obsidian vault in map huizenjacht"""
def schrijf_naar_mkdocs(content, adres, nummer, ext=".md"):
"""Schrijf content naar MkDocs docs map als bestand met oplopend nummer"""
os.makedirs(MKDOCS_DIR, exist_ok=True)
safe_name = re.sub(r'[^a-zA-Z0-9]+', '-', adres)
safe_name = safe_name.strip('-')
filename = f"huizenjacht/{safe_name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.md"
filename = f"{nummer}-{safe_name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}{ext}"
filepath = os.path.join(MKDOCS_DIR, filename)
req = urllib.request.Request(
f"{OBSIDIAN_BASE}/vault/{filename}",
data=rapport_text.encode('utf-8'),
headers={
'Authorization': f'Bearer {OBSIDIAN_TOKEN}',
'Content-Type': 'text/markdown'
},
method='POST'
)
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
with urllib.request.urlopen(req, timeout=10) as resp:
if resp.status in [200, 204]:
return f"📝 **Opgeslagen in Obsidian:** `huizenjacht/{safe_name}_*.md`"
else:
return f"⚠️ Obsidian upload mislukt: {resp.status}"
return filepath, filename
def print_isochroon_sectie(titel, icoon, iso_data, rapport_lines=None):
"""Print isochroon sectie; optioneel ook toevoegen aan rapport_lines voor Obsidian"""
text = f"{icoon} **{titel}**"
print(text)
if rapport_lines is not None:
rapport_lines.append(text)
def genereer_isochroon_html(adres_naam, lat, lon, auto_iso, fiets_iso):
"""Genereer een zelf-contained HTML kaart met Leaflet"""
html = f'''<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Isochroon - {adres_naam}</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<style>
body {{ margin: 0; padding: 0; font-family: system-ui, -apple-system, sans-serif; }}
#map {{ height: 100vh; width: 100vw; }}
.legend {{ background: white; padding: 10px; border-radius: 4px; box-shadow: 0 0 15px rgba(0,0,0,0.2); line-height: 1.5; }}
.legend i {{ width: 18px; height: 18px; float: left; margin-right: 8px; opacity: 0.7; border: 1px solid #333; }}
</style>
</head>
<body>
<div id="map"></div>
<script>
var autoGeojson = {json.dumps(auto_iso, ensure_ascii=False)};
var fietsGeojson = {json.dumps(fiets_iso, ensure_ascii=False)};
var map = L.map('map').setView([{lat}, {lon}], 11);
L.tileLayer('https://{{s}}.tile.openstreetmap.org/{{z}}/{{x}}/{{y}}.png', {{
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}}).addTo(map);
var autoLayer = L.geoJSON(autoGeojson, {{
style: {{ color: '#2563eb', weight: 2, fillColor: '#3b82f6', fillOpacity: 0.2 }},
onEachFeature: function(feature, layer) {{
var props = feature.properties;
layer.bindPopup('<b>Auto 20 min</b><br>Oppervlakte: ' + (props.area/1000000).toFixed(1) + ' km²<br>Reach factor: ' + props.reachfactor.toFixed(3));
}}
}}).addTo(map);
var fietsLayer = L.geoJSON(fietsGeojson, {{
style: {{ color: '#16a34a', weight: 2, fillColor: '#22c55e', fillOpacity: 0.2 }},
onEachFeature: function(feature, layer) {{
var props = feature.properties;
layer.bindPopup('<b>Fiets 30 min</b><br>Oppervlakte: ' + (props.area/1000000).toFixed(1) + ' km²<br>Reach factor: ' + props.reachfactor.toFixed(3));
}}
}}).addTo(map);
L.marker([{lat}, {lon}]).addTo(map)
.bindPopup('<b>{adres_naam}</b>').openPopup();
var legend = L.control({{position: 'bottomright'}});
legend.onAdd = function(map) {{
var div = L.DomUtil.create('div', 'legend');
div.innerHTML += '<h4 style="margin:0 0 5px 0">Isochronen</h4>';
div.innerHTML += '<i style="background:#3b82f6"></i> Auto 20 min<br>';
div.innerHTML += '<i style="background:#22c55e"></i> Fiets 30 min<br>';
div.innerHTML += '<i style="background:#ef4444"></i> Adres';
return div;
}};
legend.addTo(map);
map.fitBounds(autoLayer.getBounds().extend(fietsLayer.getBounds()));
</script>
</body>
</html>'''
return html
def print_isochroon_sectie(titel, icoon, iso_data):
"""Genereer isochroon sectie als tekst"""
lines = []
lines.append(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)
text = f"**Oppervlakte:** {format_oppervlakte(area_m2)}"
print(text)
if rapport_lines is not None:
rapport_lines.append(text)
text = f"**Reach factor:** {reach:.3f}"
print(text)
if rapport_lines is not None:
rapport_lines.append(text)
lines.append(f"**Oppervlakte:** {format_oppervlakte(area_m2)}")
lines.append(f"**Reach factor:** {reach:.3f}")
bbox = iso_data.get('bbox', [])
if len(bbox) == 4:
text = f"**Bereik:** {bbox[1]:.3f}°-{bbox[3]:.3f}°N, {bbox[0]:.3f}°-{bbox[2]:.3f}°E"
print(text)
if rapport_lines is not None:
rapport_lines.append(text)
lines.append(f"**Bereik:** {bbox[1]:.3f}°-{bbox[3]:.3f}°N, {bbox[0]:.3f}°-{bbox[2]:.3f}°E")
else:
text = "❌ Geen isochroon data ontvangen"
print(text)
if rapport_lines is not None:
rapport_lines.append(text)
print()
if rapport_lines is not None:
rapport_lines.append("")
lines.append("❌ Geen isochroon data ontvangen")
lines.append("")
return "\n".join(lines)
def genereer_rapport(adres_input):
"""Genereer volledig adres analyse rapport"""
rapport_lines = []
lines = []
def print_line(text=""):
print(text)
rapport_lines.append(text)
# Tel bestaande genummerde rapporten voor oplopend nummer
# Alleen bestanden die beginnen met cijfer-dash (bv. "1-Griendvelden...")
alle_files = os.listdir(MKDOCS_DIR) if os.path.exists(MKDOCS_DIR) else []
genummerde_files = [f for f in alle_files if f.endswith('.md') and re.match(r'^\d+-', f)]
nummer = len(genummerde_files) + 1
print_line(f"📍 **Adres Analyse Rapport**")
print_line(f"Gegenereerd: {datetime.now().strftime('%d-%m-%Y %H:%M')}")
print_line()
lines.append(f"# {nummer}. {adres_input}")
lines.append(f"*Gegenereerd: {datetime.now().strftime('%d-%m-%Y %H:%M')}*")
lines.append("")
print_line("🔍 Geocoding...")
lines.append("## 🔍 Geocoding")
locatie = geocode_adres(adres_input)
print_line(f"**Adres:** {locatie['adres']}")
print_line(f"**Coördinaten:** {locatie['lat']:.6f}, {locatie['lon']:.6f}")
print_line()
lines.append(f"**Adres:** {locatie['adres']}")
lines.append(f"**Coördinaten:** {locatie['lat']:.6f}, {locatie['lon']:.6f}")
lines.append("")
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, rapport_lines)
print_isochroon_sectie("Bereikbaarheid (Fiets 30 min)", "🚲", fiets_iso, rapport_lines)
print_line("🏙️ **Voorzieningen dichtbij**")
lines.append("## 🚗 Bereikbaarheid")
lines.append(print_isochroon_sectie("Auto (20 min)", "🚗", auto_iso))
lines.append(print_isochroon_sectie("Fiets (30 min)", "🚲", fiets_iso))
lines.append("## 🏙️ Voorzieningen dichtbij")
voorzieningen = analyse_voorzieningen(locatie['lon'], locatie['lat'], limit=5)
print_line(format_voorzieningen_markdown("🛒 **Supermarkten**", voorzieningen['supermarkten']))
print_line()
print_line(format_voorzieningen_markdown("🏫 **Scholen**", voorzieningen['scholen']))
print_line()
lines.append(format_voorzieningen_markdown("🛒 **Supermarkten**", voorzieningen['supermarkten']))
lines.append("")
lines.append(format_voorzieningen_markdown("🏫 **Scholen**", voorzieningen['scholen']))
lines.append("")
# Genereer interactieve HTML kaart en sla op in MkDocs
html_content = genereer_isochroon_html(
locatie['adres'], locatie['lat'], locatie['lon'], auto_iso, fiets_iso
)
html_filepath, html_filename = schrijf_naar_mkdocs(html_content, locatie['adres'], nummer, ext=".html")
# Voeg link toe aan markdown rapport (betrouwbaar via directe link)
lines.append("## 🗺️ Interactieve kaart")
lines.append(f"")
lines.append(f"**[📍 Bekijk interactieve isochroon kaart]({html_filename})**{{:target=\"_blank\"}}")
lines.append("")
lines.append("*Klik op de link om de kaart in een nieuw tabblad te openen. Hier kun je zoomen, pannen en de isochronen bekijken.*")
lines.append("")
# Ruwe geojson
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_line(f"💾 Isochroondata opgeslagen: `{output_file}`")
print_line()
print_line("---")
print_line("_Onderdelen: geocoding, isochronen, voorzieningen_")
lines.append("---")
lines.append("*Onderdelen: geocoding, isochronen, voorzieningen, interactieve kaart*")
# Schrijf naar Obsidian
print_line()
obsidian_result = schrijf_naar_obsidian('\n'.join(rapport_lines), locatie['adres'])
print_line(obsidian_result)
rapport_text = "\n".join(lines)
# Print naar stdout
print(rapport_text)
# Sla op in MkDocs
md_filepath, md_filename = schrijf_naar_mkdocs(rapport_text, adres_input, nummer, ext=".md")
print(f"\n📝 Rapport opgeslagen: {md_filepath}")
if __name__ == '__main__':
adres = sys.argv[1] if len(sys.argv) > 1 else "Griendvelden 14 Best"
+7 -7
View File
@@ -136,13 +136,13 @@ def format_voorzieningen_markdown(titel, pois):
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)}",
])
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)