# EventMap – Gesamtplan Dieses Dokument fasst den vom architect-Agenten erarbeiteten Plan zusammen und wird phasenweise fortgeschrieben. Aktuell umgesetzt: **Phase 0 (Scaffold)** und **Phase 1 (Datenmodell + Karte + Liste, read-only mit Beispieldaten)**. ## Tech-Stack - Vanilla HTML5/CSS3/JavaScript, klassische Scripts (kein `type="module"`); Module-Ersatz über einen globalen Namespace `window.EventMap` + IIFE-Kapselung pro Datei, siehe `docs/adr/0001-vanilla-esm.md`. Grund: die App wird per Doppelklick direkt aus `file://` geöffnet, native ES-Module würden dort per CORS-Policy blockiert. - Kein Build-Tool, kein Bundler, kein Framework - Karte: [Leaflet](https://leafletjs.com/) 1.9.4 + [Leaflet.markercluster](https://github.com/Leaflet/Leaflet.markercluster) 1.5.3, lokal vendored unter `vendor/` - Kartengrundlage: OpenStreetMap-Daten, ausgeliefert über CARTO-Basemap-Tiles (`basemaps.cartocdn.com`, Stil "light_all") statt des offiziellen `tile.openstreetmap.org`. Grund: dessen Tile Usage Policy verlangt seit 2024 einen gültigen HTTP-Referer, den `file://`-Seiten (Doppelklick-Start) grundsätzlich nicht mitsenden können ("Access blocked: Referer is required"). CARTO liefert dieselben OSM-Daten kartographisch auf, ohne diese Einschränkung; Attribution zeigt weiterhin OpenStreetMap + CARTO. - Datenhaltung: LocalStorage (Offline-first), vorbereitet für spätere Migration auf Backend/API ## Ordnerstruktur ``` index.html css/ reset, variables (Design-Tokens), layout, components, responsive js/config/ constants.js, categories.js js/store/ store.js (Pub/Sub-State), storage.js (LocalStorage-IO), migrations.js js/models/ place.js (Normalisierung), schema.js (Feldstruktur/Defaults) js/map/ mapController.js, markers.js, geolocation.js js/ui/ list.js (Ergebnisliste; weitere UI-Module folgen in späteren Phasen) js/utils/ dom.js, geo.js, id.js vendor/ lokal vendorte Leaflet-Bibliotheken (JS/CSS/Bildassets) docs/ dieser Plan, ADRs ``` ## Datenmodell (Ausflugsziel / Place) Das interne Place-Objekt orientiert sich eng am verbindlichen KI-Import-JSON-Format aus `anforderung.md` (siehe dortiger Abschnitt "Vorgabe für das KI-JSON"): - Basisfelder: `name`, `category`, `shortDescription`, `description`, `latitude`, `longitude`, `address`, `city`, `region` - `ageRecommendation: { min, max }` - `cost: { type: 'free'|'paid'|'mixed'|'unknown', description }` - `openingHours`, `durationMinutes` - `facilities: { strollerAccessible, wheelchairAccessible, toilets, parking, restaurant }` - `environment: 'indoor'|'outdoor'|'mixed'` - `website`, `image`, `rating`, `tags[]`, `source`, `sourceVerified` - Intern generiert: `id` (UUID), `createdAt`, `updatedAt` (ISO-Strings) `js/models/place.js` normalisiert rohe Objekte (aus den per `EventMap.config.SEED_DATA_URL` nachgeladenen Beispieldaten oder späteren Importen) zu diesem vollständigen internen Format; die Kategorie wird dabei immer auf eine gültige Kategorie-`id` (Slug, siehe `js/config/categories.js`) aufgelöst – entweder direkt (falls schon ein Slug vorliegt) oder über das Label (z.B. `"Spielplatz"` → `spielplatz`), wie es die KI liefern würde. ### Kategorien 14 Kategorien aus der Anforderung + "Sonstige" als generischer Fallback (insgesamt 15), jeweils mit `id` (Slug), `label`, `color` (Hex) und `icon` (Emoji-Glyph) – siehe `js/config/categories.js`. ## Store-Architektur `js/store/store.js` ist ein zentraler Pub/Sub-Store: ```js state = { places, filters: null, userLocation: null, selectedPlaceId: null } ``` - `subscribe(fn)` registriert Listener, `setState(patch)` merged den State und benachrichtigt alle Subscriber. - `initStore()` (async) lädt beim Start persistierte Daten aus LocalStorage (`js/store/storage.js`). Ist noch nichts gespeichert, werden die Beispieldaten per `fetch(EventMap.config.SEED_DATA_URL)` nachgeladen, normalisiert und einmalig persistiert. Schlägt der Download fehl (offline, CORS, Server nicht erreichbar), startet die App mit einer leeren Places-Liste und zeigt einen Hinweis im Status-Banner an, statt die Initialisierung abzubrechen. - Einstellungen (`lastFilters`, `lastMapView`, `userConsentGeo`, `defaultCenter`) werden getrennt über `getSettings()`/`updateSettings()` verwaltet und im Key `eventmap.settings.v1` persistiert. ### LocalStorage-Schema - `eventmap.data.v1` → `{ schemaVersion: 1, places: [...], updatedAt }` - `eventmap.settings.v1` → `{ lastFilters, lastMapView, userConsentGeo, defaultCenter }` `js/store/migrations.js` stellt eine schlanke, sequenziell anwendbare Migrations-Infrastruktur bereit (`migrate(data)`). Aktuell existiert nur Schema-Version 1, das Migrationsarray ist entsprechend leer. `defaultCenter` (frei wählbares Fallback-Kartenzentrum) ist als Datenfeld vorbereitet, aber in Phase 1 noch nicht über die UI einstellbar. Solange kein `defaultCenter` gesetzt ist, verwendet die Karte den hartcodierten Default aus `js/config/constants.js` (geografische Mitte Deutschlands, lat 51.1657, lng 10.4515, zoom 6). ## Karte - `js/map/mapController.js`: initialisiert Leaflet-Karte + OSM-Tile-Layer (inkl. korrekter Attribution), verwaltet ein `L.markerClusterGroup`, reagiert per `subscribe()` auf Änderungen an `state.places` (Marker neu rendern) und `state.selectedPlaceId` (Karte zentrieren + Popup öffnen). `fitToPlaces()` passt die Ansicht auf alle vorhandenen Places an, mit Fallback auf das Default-Center falls keine Places mit gültigen Koordinaten vorhanden sind. - `js/map/markers.js`: erzeugt je Kategorie ein `L.divIcon` (Farbe + Emoji, kein PNG-Sprite nötig) und den Popup-Inhalt (Name, Kategorie, Kurzbeschreibung) als DOM-Node (nicht als HTML-String, siehe Sicherheitshinweis unten). - `js/map/geolocation.js`: fragt den Standort nur auf explizite Nutzeraktion (Button "Meinen Standort verwenden") ab, setzt bei Erfolg `userLocation` im Store und zentriert die Karte; bei Ablehnung/Fehler bleibt das Default-Center erhalten und ein Hinweis wird angezeigt. ## Ergebnisliste `js/ui/list.js` rendert `state.places` als Cards (Name, Kategorie mit Farbe/Icon, Kurzbeschreibung). Ein Klick auf eine Card setzt `selectedPlaceId` im Store; `mapController.js` reagiert darauf und zentriert die Karte auf den zugehörigen Marker. Filterung (Phase 4) ist hier noch nicht implementiert – aktuell werden immer alle Places gezeigt. **Sicherheitshinweis:** Da künftig Fremddaten per KI-Import in die App gelangen, wird an keiner Stelle `innerHTML` mit Place-Daten befüllt. `js/utils/dom.js` stellt dafür `el()` (DOM-Erzeugung über `textContent`/ Properties) sowie `escapeHtml()` als zusätzliches Sicherheitsnetz bereit. ## Layout - Header mit Titel "EventMap". - Mobile/Tablet (< 1024px): Karte oben (fixe Höhe ca. 50vh), Liste darunter, normal scrollbar. - Desktop (≥ 1024px): Grid mit Karte (~65%) und Liste-Sidebar (~35%) nebeneinander, siehe `css/responsive.css`. ## Kein Dev-Server nötig Die App verwendet klassische `