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

188 lines
9.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
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:
```js
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`.