Compare commits
1 Commits
8f2613087c
...
v2.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 5c6fc8c277 |
@@ -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):**
|
||||||
|

|
||||||
|
|
||||||
|
💾 Ruwe isochroondata: `/tmp/isochronen_20260622_111308.geojson`
|
||||||
|
|
||||||
---
|
---
|
||||||
_Onderdeel 1/?: Isochroon bereikbaarheid_
|
_Onderdelen: geocoding, isochronen, voorzieningen_
|
||||||
```
|
```
|
||||||
|
|||||||
+158
-10
@@ -10,7 +10,21 @@ import sys
|
|||||||
import urllib.request
|
import urllib.request
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import re
|
import re
|
||||||
|
import base64
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
try:
|
||||||
|
from playwright.sync_api import sync_playwright
|
||||||
|
HAS_PLAYWRIGHT = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_PLAYWRIGHT = False
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
@@ -84,27 +98,137 @@ 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_obsidian(rapport_text, adres, subpath="huizenjacht", ext=".md"):
|
||||||
"""Schrijf rapport naar Obsidian vault in map huizenjacht"""
|
"""Schrijf rapport naar Obsidian vault"""
|
||||||
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"{subpath}/{safe_name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}{ext}"
|
||||||
|
|
||||||
|
content_type = "text/html" if ext == ".html" else "text/markdown"
|
||||||
req = urllib.request.Request(
|
req = urllib.request.Request(
|
||||||
f"{OBSIDIAN_BASE}/vault/{filename}",
|
f"{OBSIDIAN_BASE}/vault/{filename}",
|
||||||
data=rapport_text.encode('utf-8'),
|
data=rapport_text.encode('utf-8'),
|
||||||
headers={
|
headers={
|
||||||
'Authorization': f'Bearer {OBSIDIAN_TOKEN}',
|
'Authorization': f'Bearer {OBSIDIAN_TOKEN}',
|
||||||
'Content-Type': 'text/markdown'
|
'Content-Type': content_type
|
||||||
},
|
},
|
||||||
method='POST'
|
method='POST'
|
||||||
)
|
)
|
||||||
|
|
||||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||||
if resp.status in [200, 204]:
|
if resp.status in [200, 204]:
|
||||||
return f"📝 **Opgeslagen in Obsidian:** `huizenjacht/{safe_name}_*.md`"
|
return filename
|
||||||
else:
|
else:
|
||||||
return f"⚠️ Obsidian upload mislukt: {resp.status}"
|
return None
|
||||||
|
|
||||||
|
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"
|
||||||
|
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||||
|
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></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: '© <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\u00b2<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\u00b2<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 maak_isochroon_screenshot(html_content, width=1280, height=900, resize_width=800, quality=70):
|
||||||
|
"""Genereer een JPEG screenshot van de HTML kaart, retourneer base64 string"""
|
||||||
|
if not HAS_PLAYWRIGHT:
|
||||||
|
return None, "Playwright niet geïnstalleerd"
|
||||||
|
if not HAS_PIL:
|
||||||
|
return None, "Pillow niet geïnstalleerd"
|
||||||
|
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Write HTML to temp file
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False, encoding='utf-8') as f:
|
||||||
|
f.write(html_content)
|
||||||
|
html_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
with sync_playwright() as p:
|
||||||
|
browser = p.chromium.launch(headless=True)
|
||||||
|
page = browser.new_page(viewport={'width': width, 'height': height})
|
||||||
|
page.goto(f'file://{html_path}')
|
||||||
|
page.wait_for_timeout(2500) # Wait for tiles
|
||||||
|
png_bytes = page.screenshot(full_page=False)
|
||||||
|
browser.close()
|
||||||
|
except Exception as e:
|
||||||
|
os.unlink(html_path)
|
||||||
|
return None, f"Screenshot fout: {e}"
|
||||||
|
finally:
|
||||||
|
os.unlink(html_path)
|
||||||
|
|
||||||
|
# Compress to JPEG
|
||||||
|
img = Image.open(BytesIO(png_bytes))
|
||||||
|
ratio = resize_width / img.width
|
||||||
|
resize_height = int(img.height * ratio)
|
||||||
|
img = img.resize((resize_width, resize_height), Image.Resampling.LANCZOS)
|
||||||
|
|
||||||
|
buf = BytesIO()
|
||||||
|
img.save(buf, format='JPEG', quality=quality, optimize=True)
|
||||||
|
jpeg_bytes = buf.getvalue()
|
||||||
|
|
||||||
|
b64 = base64.b64encode(jpeg_bytes).decode('ascii')
|
||||||
|
return b64, None
|
||||||
|
|
||||||
def print_isochroon_sectie(titel, icoon, iso_data, rapport_lines=None):
|
def print_isochroon_sectie(titel, icoon, iso_data, rapport_lines=None):
|
||||||
"""Print isochroon sectie; optioneel ook toevoegen aan rapport_lines voor Obsidian"""
|
"""Print isochroon sectie; optioneel ook toevoegen aan rapport_lines voor Obsidian"""
|
||||||
@@ -169,18 +293,42 @@ def genereer_rapport(adres_input):
|
|||||||
print_line(format_voorzieningen_markdown("🏫 **Scholen**", voorzieningen['scholen']))
|
print_line(format_voorzieningen_markdown("🏫 **Scholen**", voorzieningen['scholen']))
|
||||||
print_line()
|
print_line()
|
||||||
|
|
||||||
|
# Genereer interactieve HTML kaart en upload naar Obsidian
|
||||||
|
html_content = genereer_isochroon_html(
|
||||||
|
locatie['adres'], locatie['lat'], locatie['lon'], auto_iso, fiets_iso
|
||||||
|
)
|
||||||
|
html_path = schrijf_naar_obsidian(html_content, locatie['adres'], subpath="huizenjacht", ext=".html")
|
||||||
|
if html_path:
|
||||||
|
print_line(f"🗺️ **[Bekijk interactieve isochroon kaart]({html_path})**")
|
||||||
|
else:
|
||||||
|
print_line("⚠️ Kon interactieve kaart niet opslaan in Obsidian")
|
||||||
|
|
||||||
|
# Genereer screenshot en embed in markdown
|
||||||
|
b64_img, err = maak_isochroon_screenshot(html_content)
|
||||||
|
if b64_img:
|
||||||
|
print_line("")
|
||||||
|
print_line("**🗺️ Isochroon kaart (auto + fiets):**")
|
||||||
|
img_md = f""
|
||||||
|
print_line(img_md)
|
||||||
|
else:
|
||||||
|
print_line(f"⚠️ Kon geen screenshot maken: {err}")
|
||||||
|
|
||||||
|
# Sla ook ruwe geojson op (lokaal, niet in Obsidian)
|
||||||
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}`")
|
print_line(f"💾 Ruwe isochroondata: `{output_file}`")
|
||||||
print_line()
|
print_line()
|
||||||
print_line("---")
|
print_line("---")
|
||||||
print_line("_Onderdelen: geocoding, isochronen, voorzieningen_")
|
print_line("_Onderdelen: geocoding, isochronen, voorzieningen_")
|
||||||
|
|
||||||
# Schrijf naar Obsidian
|
# Schrijf rapport naar Obsidian
|
||||||
print_line()
|
print_line()
|
||||||
obsidian_result = schrijf_naar_obsidian('\n'.join(rapport_lines), locatie['adres'])
|
md_path = schrijf_naar_obsidian('\n'.join(rapport_lines), locatie['adres'])
|
||||||
print_line(obsidian_result)
|
if md_path:
|
||||||
|
print_line(f"📝 **Opgeslagen in Obsidian:** `{md_path}`")
|
||||||
|
else:
|
||||||
|
print_line("⚠️ Obsidian upload mislukt")
|
||||||
|
|
||||||
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"
|
||||||
|
|||||||
Reference in New Issue
Block a user