Files
eventmap/docs/plan.md
T
dapelza dac952fbf9 Phase 4: Filter- und Suchfunktion
Neues Modul js/filters/ mit reiner Filterlogik (filterEngine.js:
applyFilters(), getDistanceKm()) und UI (filterPanel.js) fuer alle Filter
aus anforderung.md Abschnitt 3: Kategorie, Entfernung (mit "Standort
verwenden"-Button), Alter der Kinder, Kosten, Indoor/Outdoor, 5
Ausstattungsmerkmale, Aufenthaltsdauer, Mindestbewertung sowie
Freitextsuche ueber Name/Ort/Region/Adresse. Trefferzaehler, "Filter
zuruecksetzen" und "Auf Ergebnisse zoomen" ergaenzt.

Store erweitert um updateFilters()/getFilteredPlaces() (Selector).
Karte und Liste rendern jetzt die gefilterte statt die volle
Ausflugsziel-Liste; die Karten-Rerender-Optimierung vergleicht dafuer
die Trefferliste per Id-Schluessel statt nur state.places, damit reine
Filter-/Standortaenderungen ohne Places-Aenderung erkannt werden.

Layout: Filter-Panel als aufklappbares <details> auf Mobile, eigener
Grid-Bereich neben der Karte auf Desktop. docs/plan.md entsprechend
fortgeschrieben (Phase 2-5 als umgesetzt markiert, Phase 5
Navigation war bereits Teil von Phase 2).
2026-07-24 12:21:07 +02:00

9.6 KiB
Raw Blame History

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 1.9.4 + 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
data/seed.js     Beispieldaten (10 Ausflugsziele, wird nur beim allerersten Start geladen)
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 data/seed.js 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:

state = { places, filters: null, userLocation: null, selectedPlaceId: null }
  • subscribe(fn) registriert Listener, setState(patch) merged den State und benachrichtigt alle Subscriber.
  • initStore() lädt beim Start persistierte Daten aus LocalStorage (js/store/storage.js). Ist noch nichts gespeichert, wird data/seed.js geladen, normalisiert und einmalig persistiert.
  • 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 <script>-Tags statt ES-Module (siehe docs/adr/0001-vanilla-esm.md) und lässt sich daher per Doppelklick auf index.html direkt aus dem lokal synchronisierten Ordner öffnen (file://...) ein lokaler HTTP-Server ist dafür nicht mehr nötig.

Die Beispieldaten liegen in data/seed.js als eingebettetes <script>-Objekt (EventMap.data.SEED_PLACES) statt als .json-Datei, da fetch() auf lokale Dateien über file:// in manchen Browsern (insbesondere Chrome/Chromium) blockiert wird. initStore() liest die Seed-Daten dadurch synchron direkt aus dem globalen Namespace, ganz ohne fetch().

Phasenübersicht

Phase Inhalt Status
0 Scaffold (Ordnerstruktur, vendored Leaflet, Grundgerüst) umgesetzt
1 Datenmodell, Karte, Liste (read-only, Seed-Daten) umgesetzt
2 Detailansicht (Titelbild, vollständige Beschreibung, Mini-Karte, Navigation-Button, Bearbeiten/Löschen) umgesetzt
3 Formular zum manuellen Anlegen/Bearbeiten (Validierung, Koordinatenprüfung) umgesetzt
4 Filter- und Suchfunktion (Kategorie, Entfernung, Alter, Kosten, Indoor/Outdoor, Ausstattung, Bewertung, Region) inkl. Anwendung auf Karte + Liste umgesetzt
5 Route/Navigation-Integration bereits in Phase 2 erledigt (OSM-Directions-Button in der Detailansicht)
6 KI-Prompt-Generator (Eingabeformular, Promptgenerierung, Copy/Download) offen
7 KI-JSON-Import (Validierung, Vorschau, Übernahme ins LocalStorage) + Backup-Export/Import + Politur offen

Filter- und Suchfunktion (Phase 4)

  • js/filters/filterEngine.js: reine Funktion applyFilters(places, filters, userLocation) plus getDistanceKm(). Leerer/null/[]-Wert je Filterfeld = kein Filter aktiv. Places ohne Bewertung/Dauer werden bei aktivem Bewertungs-/Dauer-Filter nicht automatisch ausgeschlossen; Places ohne gültige Koordinaten werden bei aktivem Entfernungsfilter (mit vorhandenem Standort) ausgeschlossen, da ihre Entfernung nicht verifizierbar ist. Fehlt der Nutzerstandort komplett, wird der Entfernungsfilter ignoriert statt die Liste leerzuräumen.
  • js/filters/filterPanel.js: UI für alle Filter aus anforderung.md Abschnitt 3 (Kategorie, Entfernung inkl. "Standort verwenden"-Button, Alter, Kosten, Indoor/Outdoor, 5 Ausstattungsmerkmale, Aufenthaltsdauer, Mindestbewertung, Freitextsuche über Name/Ort/Region/Adresse), Trefferzähler "X von Y", "Filter zurücksetzen" und "Auf Ergebnisse zoomen".
  • Store: EventMap.store.updateFilters(patch) und EventMap.store.getFilteredPlaces() (Selector). js/ui/list.js und js/map/mapController.js rendern seitdem die gefilterte statt die volle Liste.
  • Layout: Mobile (< 1024px) als <details>/<summary> einklappbar oberhalb der Liste; Desktop (≥ 1024px) als eigener Grid-Bereich neben der Karte, über der Liste (css/responsive.css).
  • Nicht umgesetzt (bewusst kein Muss laut Plan): Persistierung der Filter in settings.lastFilters.