From 5c6fc8c27725f6bb6bea463abf9b2e3ff998ed6b Mon Sep 17 00:00:00 2001 From: wouser Date: Mon, 22 Jun 2026 13:47:15 +0200 Subject: [PATCH] Release v2.2 - adres analyse skill --- SKILL.md | 45 +++++++----- scripts/rapport.py | 168 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 186 insertions(+), 27 deletions(-) diff --git a/SKILL.md b/SKILL.md index 68c83f3..ec01b53 100644 --- a/SKILL.md +++ b/SKILL.md @@ -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_ ``` diff --git a/scripts/rapport.py b/scripts/rapport.py index 9a05db4..18162b3 100644 --- a/scripts/rapport.py +++ b/scripts/rapport.py @@ -10,7 +10,21 @@ import sys import urllib.request import urllib.parse import re +import base64 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 @@ -84,27 +98,137 @@ 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_obsidian(rapport_text, adres, subpath="huizenjacht", ext=".md"): + """Schrijf rapport naar Obsidian vault""" 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"{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( f"{OBSIDIAN_BASE}/vault/{filename}", data=rapport_text.encode('utf-8'), headers={ 'Authorization': f'Bearer {OBSIDIAN_TOKEN}', - 'Content-Type': 'text/markdown' + 'Content-Type': content_type }, method='POST' ) with urllib.request.urlopen(req, timeout=10) as resp: if resp.status in [200, 204]: - return f"📝 **Opgeslagen in Obsidian:** `huizenjacht/{safe_name}_*.md`" + return filename 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''' + + + + +Isochroon - {adres_naam} + + + + + +
+ + +''' + 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): """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() + # 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"![Isochroon kaart](data:image/jpeg;base64,{b64_img})" + 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" 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(f"💾 Ruwe isochroondata: `{output_file}`") print_line() print_line("---") print_line("_Onderdelen: geocoding, isochronen, voorzieningen_") - # Schrijf naar Obsidian + # Schrijf rapport naar Obsidian print_line() - obsidian_result = schrijf_naar_obsidian('\n'.join(rapport_lines), locatie['adres']) - print_line(obsidian_result) + md_path = schrijf_naar_obsidian('\n'.join(rapport_lines), locatie['adres']) + if md_path: + print_line(f"📝 **Opgeslagen in Obsidian:** `{md_path}`") + else: + print_line("⚠️ Obsidian upload mislukt") if __name__ == '__main__': adres = sys.argv[1] if len(sys.argv) > 1 else "Griendvelden 14 Best"