4 Commits

Author SHA1 Message Date
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
3 changed files with 328 additions and 122 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 - **Fiets profiel:** `cycling-regular`, 30 minuten, groen op de kaart
- **Metrics:** Oppervlakte (km²), reach factor, geografisch bereik - **Metrics:** Oppervlakte (km²), reach factor, geografisch bereik
- **Data bron:** ORS op `192.168.1.71:9080` - **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 ### 2. Voorzieningen in de buurt
- **Bron:** lokale PostGIS database met geselecteerde POI's uit heel Nederland OSM extract - **Bron:** lokale PostGIS database met geselecteerde POI's uit heel Nederland OSM extract
- **Docker:** `postgis/postgis` op poort `5433`, database `adresanalyse` - **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` - **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` - **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) ### 3. (Toekomstige onderdelen)
- OV bereikbaarheid - 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 - Response bevat GeoJSON FeatureCollection met polygon geometrie
- GeoJSON wordt opgeslagen in `/tmp/isochroon_{timestamp}.geojson` - GeoJSON wordt opgeslagen in `/tmp/isochroon_{timestamp}.geojson`
### Stap 3: Kaart genereren (optioneel) ### Stap 3: Interactieve kaart + screenshot genereren (automatisch)
Gebruik script: `scripts/generate-map.py` 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 Vereisten voor screenshot:
python3 scripts/generate-map.py "Griendvelden 14, Best" /root/adres-analyse/kaart.html - `playwright` (`pip install playwright; playwright install chromium`)
``` - `Pillow` (`pip install Pillow`)
Voor Telegram delivery: Als playwright niet geïnstalleerd is, wordt de screenshot overgeslagen met een waarschuwing; de rest van het rapport werkt gewoon.
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`
## Pitfalls ## 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. 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. 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 **Reach factor:** 0.091
**Bereik:** 51.378°-51.679°N, 5.128°-5.530°E **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_
``` ```
+149 -24
View File
@@ -2,19 +2,31 @@
""" """
Adres Analyse - Rapport generator Adres Analyse - Rapport generator
Gebruikt PDOK voor geocoding, ORS voor isochronen/routes en PostGIS OSM-data voor voorzieningen. 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 json
import sys import sys
import os
import re
import urllib.request import urllib.request
import urllib.parse import urllib.parse
from datetime import datetime 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 from voorzieningen import analyse_voorzieningen, format_voorzieningen_markdown
# Config # Config
ORS_BASE = "http://192.168.1.71:9080/ors/v2" ORS_BASE = "http://192.168.1.71:9080/ors/v2"
PDOK_BASE = "https://api.pdok.nl/bzk/locatieserver/search/v3_1" PDOK_BASE = "https://api.pdok.nl/bzk/locatieserver/search/v3_1"
MKDOCS_DIR = "/root/mkdocs/docs/docs/Huizenjacht"
def geocode_adres(adres): def geocode_adres(adres):
"""Geocode adres naar coördinaten via PDOK Locatieserver""" """Geocode adres naar coördinaten via PDOK Locatieserver"""
@@ -80,52 +92,165 @@ def format_oppervlakte(m2):
else: else:
return f"{m2:.0f} m²" 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):
"""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): 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']: if 'features' in iso_data and iso_data['features']:
props = iso_data['features'][0]['properties'] props = iso_data['features'][0]['properties']
area_m2 = props.get('area', 0) area_m2 = props.get('area', 0)
reach = props.get('reachfactor', 0) reach = props.get('reachfactor', 0)
print(f"**Oppervlakte:** {format_oppervlakte(area_m2)}") lines.append(f"**Oppervlakte:** {format_oppervlakte(area_m2)}")
print(f"**Reach factor:** {reach:.3f}") lines.append(f"**Reach factor:** {reach:.3f}")
bbox = iso_data.get('bbox', []) bbox = iso_data.get('bbox', [])
if len(bbox) == 4: 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: else:
print("❌ Geen isochroon data ontvangen") lines.append("❌ Geen isochroon data ontvangen")
print() lines.append("")
return "\n".join(lines)
def genereer_rapport(adres_input): def genereer_rapport(adres_input):
"""Genereer volledig adres analyse rapport""" """Genereer volledig adres analyse rapport"""
print(f"📍 **Adres Analyse Rapport**") lines = []
print(f"Gegenereerd: {datetime.now().strftime('%d-%m-%Y %H:%M')}")
print()
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) locatie = geocode_adres(adres_input)
print(f"**Adres:** {locatie['adres']}") lines.append(f"**Adres:** {locatie['adres']}")
print(f"**Coördinaten:** {locatie['lat']:.6f}, {locatie['lon']:.6f}") lines.append(f"**Coördinaten:** {locatie['lat']:.6f}, {locatie['lon']:.6f}")
print() lines.append("")
auto_iso = 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) 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) voorzieningen = analyse_voorzieningen(locatie['lon'], locatie['lat'], limit=5)
print(format_voorzieningen_markdown("🛒 **Supermarkten**", voorzieningen['supermarkten'])) lines.append(format_voorzieningen_markdown("🛒 **Supermarkten**", voorzieningen['supermarkten']))
print() lines.append("")
print(format_voorzieningen_markdown("🏫 **Scholen**", voorzieningen['scholen'])) lines.append(format_voorzieningen_markdown("🏫 **Scholen**", voorzieningen['scholen']))
print() 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" output_file = f"/tmp/isochronen_{datetime.now().strftime('%Y%m%d_%H%M%S')}.geojson"
with open(output_file, 'w') as f: with open(output_file, 'w') as f:
json.dump({'auto_20_min': auto_iso, 'fiets_30_min': fiets_iso}, f) json.dump({'auto_20_min': auto_iso, 'fiets_30_min': fiets_iso}, f)
print(f"💾 Isochroondata opgeslagen: `{output_file}`") lines.append("---")
print() lines.append("*Onderdelen: geocoding, isochronen, voorzieningen, interactieve kaart*")
print("---")
print("_Onderdelen: geocoding, isochronen, voorzieningen_") 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__': if __name__ == '__main__':
adres = sys.argv[1] if len(sys.argv) > 1 else "Griendvelden 14 Best" 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 Adres Analyse - Rapport generator
Gebruikt PDOK voor geocoding, ORS voor isochronen/routes en PostGIS OSM-data voor voorzieningen. 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 json
import sys import sys
import os
import re
import urllib.request import urllib.request
import urllib.parse import urllib.parse
import re
from datetime import datetime 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 from voorzieningen import analyse_voorzieningen, format_voorzieningen_markdown
# Config # Config
ORS_BASE = "http://192.168.1.71:9080/ors/v2" ORS_BASE = "http://192.168.1.71:9080/ors/v2"
PDOK_BASE = "https://api.pdok.nl/bzk/locatieserver/search/v3_1" PDOK_BASE = "https://api.pdok.nl/bzk/locatieserver/search/v3_1"
OBSIDIAN_BASE = "http://192.168.1.50:27123" MKDOCS_DIR = "/root/mkdocs/docs/docs/Huizenjacht"
OBSIDIAN_TOKEN = "90ec" + "6de948dc8ef7832fda3516fd900c7a3071dd526edf852ade145aa9daf637"
def geocode_adres(adres): def geocode_adres(adres):
"""Geocode adres naar coördinaten via PDOK Locatieserver""" """Geocode adres naar coördinaten via PDOK Locatieserver"""
@@ -84,103 +92,165 @@ def format_oppervlakte(m2):
else: else:
return f"{m2:.0f} m²" return f"{m2:.0f} m²"
def schrijf_naar_obsidian(rapport_text, adres): def schrijf_naar_mkdocs(content, adres, nummer, ext=".md"):
"""Schrijf rapport naar Obsidian vault in map huizenjacht""" """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 = re.sub(r'[^a-zA-Z0-9]+', '-', adres)
safe_name = safe_name.strip('-') 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( with open(filepath, 'w', encoding='utf-8') as f:
f"{OBSIDIAN_BASE}/vault/{filename}", f.write(content)
data=rapport_text.encode('utf-8'),
headers={
'Authorization': f'Bearer {OBSIDIAN_TOKEN}',
'Content-Type': 'text/markdown'
},
method='POST'
)
with urllib.request.urlopen(req, timeout=10) as resp: return filepath, filename
if resp.status in [200, 204]:
return f"📝 **Opgeslagen in Obsidian:** `huizenjacht/{safe_name}_*.md`"
else:
return f"⚠️ Obsidian upload mislukt: {resp.status}"
def print_isochroon_sectie(titel, icoon, iso_data, rapport_lines=None): def genereer_isochroon_html(adres_naam, lat, lon, auto_iso, fiets_iso):
"""Print isochroon sectie; optioneel ook toevoegen aan rapport_lines voor Obsidian""" """Genereer een zelf-contained HTML kaart met Leaflet"""
text = f"{icoon} **{titel}**" html = f'''<!DOCTYPE html>
print(text) <html lang="nl">
if rapport_lines is not None: <head>
rapport_lines.append(text) <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']: if 'features' in iso_data and iso_data['features']:
props = iso_data['features'][0]['properties'] props = iso_data['features'][0]['properties']
area_m2 = props.get('area', 0) area_m2 = props.get('area', 0)
reach = props.get('reachfactor', 0) reach = props.get('reachfactor', 0)
text = f"**Oppervlakte:** {format_oppervlakte(area_m2)}" lines.append(f"**Oppervlakte:** {format_oppervlakte(area_m2)}")
print(text) lines.append(f"**Reach factor:** {reach:.3f}")
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)
bbox = iso_data.get('bbox', []) bbox = iso_data.get('bbox', [])
if len(bbox) == 4: if len(bbox) == 4:
text = 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")
print(text)
if rapport_lines is not None:
rapport_lines.append(text)
else: else:
text = "❌ Geen isochroon data ontvangen" lines.append("❌ Geen isochroon data ontvangen")
print(text) lines.append("")
if rapport_lines is not None: return "\n".join(lines)
rapport_lines.append(text)
print()
if rapport_lines is not None:
rapport_lines.append("")
def genereer_rapport(adres_input): def genereer_rapport(adres_input):
"""Genereer volledig adres analyse rapport""" """Genereer volledig adres analyse rapport"""
rapport_lines = [] lines = []
def print_line(text=""): # Tel bestaande genummerde rapporten voor oplopend nummer
print(text) # Alleen bestanden die beginnen met cijfer-dash (bv. "1-Griendvelden...")
rapport_lines.append(text) 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**") lines.append(f"# {nummer}. {adres_input}")
print_line(f"Gegenereerd: {datetime.now().strftime('%d-%m-%Y %H:%M')}") lines.append(f"*Gegenereerd: {datetime.now().strftime('%d-%m-%Y %H:%M')}*")
print_line() lines.append("")
print_line("🔍 Geocoding...") lines.append("## 🔍 Geocoding")
locatie = geocode_adres(adres_input) locatie = geocode_adres(adres_input)
print_line(f"**Adres:** {locatie['adres']}") lines.append(f"**Adres:** {locatie['adres']}")
print_line(f"**Coördinaten:** {locatie['lat']:.6f}, {locatie['lon']:.6f}") lines.append(f"**Coördinaten:** {locatie['lat']:.6f}, {locatie['lon']:.6f}")
print_line() lines.append("")
auto_iso = 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) 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) voorzieningen = analyse_voorzieningen(locatie['lon'], locatie['lat'], limit=5)
print_line(format_voorzieningen_markdown("🛒 **Supermarkten**", voorzieningen['supermarkten'])) lines.append(format_voorzieningen_markdown("🛒 **Supermarkten**", voorzieningen['supermarkten']))
print_line() lines.append("")
print_line(format_voorzieningen_markdown("🏫 **Scholen**", voorzieningen['scholen'])) lines.append(format_voorzieningen_markdown("🏫 **Scholen**", voorzieningen['scholen']))
print_line() 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" output_file = f"/tmp/isochronen_{datetime.now().strftime('%Y%m%d_%H%M%S')}.geojson"
with open(output_file, 'w') as f: with open(output_file, 'w') as f:
json.dump({'auto_20_min': auto_iso, 'fiets_30_min': fiets_iso}, f) json.dump({'auto_20_min': auto_iso, 'fiets_30_min': fiets_iso}, f)
print_line(f"💾 Isochroondata opgeslagen: `{output_file}`") lines.append("---")
print_line() lines.append("*Onderdelen: geocoding, isochronen, voorzieningen, interactieve kaart*")
print_line("---")
print_line("_Onderdelen: geocoding, isochronen, voorzieningen_")
# Schrijf naar Obsidian rapport_text = "\n".join(lines)
print_line()
obsidian_result = schrijf_naar_obsidian('\n'.join(rapport_lines), locatie['adres']) # Print naar stdout
print_line(obsidian_result) 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__': if __name__ == '__main__':
adres = sys.argv[1] if len(sys.argv) > 1 else "Griendvelden 14 Best" adres = sys.argv[1] if len(sys.argv) > 1 else "Griendvelden 14 Best"