From ee55fb1cf9cd14258fb1ed46f0512760e0afca6b Mon Sep 17 00:00:00 2001 From: Hermann Date: Fri, 24 Jul 2026 11:24:47 +0200 Subject: [PATCH] Phase 0+1: Scaffold, Datenmodell, Karte und Liste Reines HTML5/CSS3/JavaScript ohne Build-Tooling, lauffaehig direkt per Doppelklick auf index.html (file://, kein Server noetig): - Datenmodell fuer Ausflugsziele mit allen Feldern aus der Anforderung, 15 feste Kategorien inkl. Farbe/Icon - Zentraler Pub/Sub-Store mit LocalStorage-Persistenz und Migrations-Grundgeruest - Interaktive Karte (Leaflet + Leaflet.markercluster, lokal vendored) mit kategoriefarbenen Markern, Clustering, Popups, Geolocation-Button - Kartengrundlage ueber CARTO-Basemaps statt tile.openstreetmap.org, da dessen Referer-Pflicht file://-Aufrufe blockiert - Seed-Daten als eingebettetes Script (data/seed.js) statt JSON-Datei, da fetch() auf lokale Dateien unter file:// nicht zuverlaessig funktioniert - Ergebnisliste mit 10 Beispiel-Ausflugszielen, sicher gerendert ueber textContent/DOM-APIs statt innerHTML (Vorbereitung fuer spaeteren KI-JSON-Import mit Fremddaten) - Responsives Layout (Desktop nebeneinander, Mobile gestapelt) --- css/components.css | 129 ++++ css/layout.css | 59 ++ css/reset.css | 47 ++ css/responsive.css | 20 + css/variables.css | 46 ++ data/seed.js | 298 ++++++++ docs/adr/0001-vanilla-esm.md | 98 +++ docs/plan.md | 168 +++++ index.html | 70 ++ js/config/categories.js | 47 ++ js/config/constants.js | 23 + js/main.js | 53 ++ js/map/geolocation.js | 39 ++ js/map/mapController.js | 130 ++++ js/map/markers.js | 64 ++ js/models/place.js | 56 ++ js/models/schema.js | 50 ++ js/store/migrations.js | 37 + js/store/storage.js | 63 ++ js/store/store.js | 88 +++ js/ui/list.js | 77 ++ js/utils/dom.js | 95 +++ js/utils/geo.js | 44 ++ js/utils/id.js | 13 + .../MarkerCluster.Default.css | 60 ++ .../leaflet.markercluster/MarkerCluster.css | 14 + .../leaflet.markercluster.js | 2 + vendor/leaflet/images/marker-icon-2x.png | Bin 0 -> 2464 bytes vendor/leaflet/images/marker-icon.png | Bin 0 -> 1466 bytes vendor/leaflet/images/marker-shadow.png | Bin 0 -> 618 bytes vendor/leaflet/leaflet.css | 661 ++++++++++++++++++ vendor/leaflet/leaflet.js | 6 + 32 files changed, 2557 insertions(+) create mode 100644 css/components.css create mode 100644 css/layout.css create mode 100644 css/reset.css create mode 100644 css/responsive.css create mode 100644 css/variables.css create mode 100644 data/seed.js create mode 100644 docs/adr/0001-vanilla-esm.md create mode 100644 docs/plan.md create mode 100644 index.html create mode 100644 js/config/categories.js create mode 100644 js/config/constants.js create mode 100644 js/main.js create mode 100644 js/map/geolocation.js create mode 100644 js/map/mapController.js create mode 100644 js/map/markers.js create mode 100644 js/models/place.js create mode 100644 js/models/schema.js create mode 100644 js/store/migrations.js create mode 100644 js/store/storage.js create mode 100644 js/store/store.js create mode 100644 js/ui/list.js create mode 100644 js/utils/dom.js create mode 100644 js/utils/geo.js create mode 100644 js/utils/id.js create mode 100644 vendor/leaflet.markercluster/MarkerCluster.Default.css create mode 100644 vendor/leaflet.markercluster/MarkerCluster.css create mode 100644 vendor/leaflet.markercluster/leaflet.markercluster.js create mode 100644 vendor/leaflet/images/marker-icon-2x.png create mode 100644 vendor/leaflet/images/marker-icon.png create mode 100644 vendor/leaflet/images/marker-shadow.png create mode 100644 vendor/leaflet/leaflet.css create mode 100644 vendor/leaflet/leaflet.js diff --git a/css/components.css b/css/components.css new file mode 100644 index 0000000..210de1d --- /dev/null +++ b/css/components.css @@ -0,0 +1,129 @@ +/* Wiederverwendbare Komponenten: Status-Banner, Buttons, Ergebnis-Cards, + Karten-Marker/Popups. */ + +.app-status { + background: #fdecea; + color: var(--color-danger); + padding: var(--space-sm) var(--space-lg); + font-size: 0.9rem; + border-bottom: 1px solid var(--color-danger); +} + +.btn { + border: none; + border-radius: var(--radius-sm); + padding: var(--space-sm) var(--space-md); + font-size: 0.85rem; + font-weight: 600; + box-shadow: var(--shadow-card); +} + +.btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.btn--secondary { + background: var(--color-surface); + color: var(--color-primary-dark); +} + +.map-panel__loading { + padding: var(--space-md); + color: var(--color-text-muted); +} + +/* Ergebnisliste */ + +.place-list { + display: flex; + flex-direction: column; + gap: var(--space-md); +} + +.list-empty { + color: var(--color-text-muted); + padding: var(--space-md) 0; +} + +.place-card { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + padding: var(--space-md); + display: flex; + flex-direction: column; + gap: var(--space-xs); + cursor: pointer; + transition: box-shadow 0.15s ease, transform 0.15s ease; +} + +.place-card:hover, +.place-card:focus-visible { + box-shadow: var(--shadow-card); + transform: translateY(-1px); + outline: none; +} + +.place-card__name { + font-size: 1rem; + font-weight: 700; +} + +.place-card__category { + align-self: flex-start; + color: #fff; + font-size: 0.75rem; + font-weight: 600; + padding: 2px var(--space-sm); + border-radius: 999px; +} + +.place-card__desc { + color: var(--color-text-muted); + font-size: 0.875rem; + line-height: 1.4; +} + +/* Karten-Marker (L.divIcon) */ + +.eventmap-marker { + background: transparent; + border: none; +} + +.eventmap-marker__glyph { + display: flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + border-radius: 50%; + box-shadow: var(--shadow-card); + font-size: 1rem; + line-height: 1; + border: 2px solid #fff; +} + +/* Karten-Popup */ + +.eventmap-popup { + display: flex; + flex-direction: column; + gap: 4px; + max-width: 220px; +} + +.eventmap-popup__title { + font-size: 0.95rem; +} + +.eventmap-popup__category { + font-size: 0.8rem; + font-weight: 600; +} + +.eventmap-popup__desc { + font-size: 0.85rem; + color: var(--color-text-muted); +} diff --git a/css/layout.css b/css/layout.css new file mode 100644 index 0000000..844c451 --- /dev/null +++ b/css/layout.css @@ -0,0 +1,59 @@ +/* Grundlegendes App-Layout: Header + Karte/Liste. Mobile-first; + Desktop-Anpassungen (>=1024px) siehe css/responsive.css. */ + +.app-header { + height: var(--header-height); + display: flex; + flex-direction: column; + justify-content: center; + padding: 0 var(--space-lg); + background: var(--color-primary); + color: #fff; +} + +.app-header__title { + font-size: 1.4rem; + font-weight: 700; +} + +.app-header__subtitle { + font-size: 0.85rem; + opacity: 0.9; +} + +.app-layout { + display: flex; + flex-direction: column; +} + +.map-panel { + position: relative; + height: 50vh; + min-height: 320px; + display: flex; + flex-direction: column; +} + +.map-panel__toolbar { + position: absolute; + top: var(--space-sm); + left: var(--space-sm); + z-index: 1000; +} + +.map-panel__map { + flex: 1; + width: 100%; + height: 100%; + background: var(--color-border); +} + +.list-panel { + padding: var(--space-md); + overflow-y: auto; +} + +.list-panel__title { + font-size: 1.1rem; + margin-bottom: var(--space-md); +} diff --git a/css/reset.css b/css/reset.css new file mode 100644 index 0000000..4a3e6dd --- /dev/null +++ b/css/reset.css @@ -0,0 +1,47 @@ +/* Schlankes Reset - kein komplettes Normalize.css nötig für dieses Projekt. */ + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body { + height: 100%; +} + +body { + margin: 0; + font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + color: var(--color-text); + background: var(--color-background); +} + +h1, +h2, +h3, +p { + margin: 0; +} + +ul, +ol { + margin: 0; + padding: 0; + list-style: none; +} + +button { + font: inherit; + cursor: pointer; +} + +img { + max-width: 100%; + display: block; +} + +a { + color: inherit; +} diff --git a/css/responsive.css b/css/responsive.css new file mode 100644 index 0000000..dbb77e0 --- /dev/null +++ b/css/responsive.css @@ -0,0 +1,20 @@ +/* Desktop-Layout ab 1024px: Karte und Liste nebeneinander (Grid). */ + +@media (min-width: 1024px) { + .app-layout { + display: grid; + grid-template-columns: 65% 35%; + height: calc(100vh - var(--header-height)); + } + + .map-panel { + height: 100%; + min-height: 0; + } + + .list-panel { + height: 100%; + overflow-y: auto; + border-left: 1px solid var(--color-border); + } +} diff --git a/css/variables.css b/css/variables.css new file mode 100644 index 0000000..efc9caf --- /dev/null +++ b/css/variables.css @@ -0,0 +1,46 @@ +/* Design-Tokens der Anwendung. */ + +:root { + /* Basisfarben */ + --color-primary: #2e7d32; + --color-primary-dark: #1b5e20; + --color-text: #1a1a1a; + --color-text-muted: #5f6368; + --color-background: #f5f6f8; + --color-surface: #ffffff; + --color-border: #e0e0e0; + --color-danger: #c62828; + + /* Kategorie-Farben (siehe js/config/categories.js) */ + --category-naturpark: #2e7d32; + --category-wald-wanderweg: #1b5e20; + --category-see-badestelle: #0288d1; + --category-erlebnispark: #f57c00; + --category-freizeitpark: #e64a19; + --category-tierpark-zoo: #ff8f00; + --category-spielplatz: #d81b60; + --category-museum-kinder: #7b1fa2; + --category-indoor-spielplatz: #5e35b1; + --category-bauernhof: #6d4c41; + --category-kletterpark: #00897b; + --category-aussichtspunkt: #455a64; + --category-cafe-restaurant: #c62828; + --category-sonstige-aktivitaet: #00acc1; + --category-sonstige: #616161; + + /* Spacing */ + --space-xs: 4px; + --space-sm: 8px; + --space-md: 16px; + --space-lg: 24px; + --space-xl: 32px; + + /* Radius/Shadow */ + --radius-sm: 4px; + --radius-md: 8px; + --shadow-card: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.08); + + /* Layout */ + --breakpoint-desktop: 1024px; + --header-height: 64px; +} diff --git a/data/seed.js b/data/seed.js new file mode 100644 index 0000000..6338a53 --- /dev/null +++ b/data/seed.js @@ -0,0 +1,298 @@ +// Beispiel-Ausflugsziele fuer den allerersten App-Start (leerer LocalStorage). +// Als eingebettetes Script statt JSON-Datei, da fetch() lokaler Dateien unter +// file:// (insbesondere in Chrome) von der Same-Origin-Policy blockiert wird. +window.EventMap = window.EventMap || {}; +EventMap.data = EventMap.data || {}; + +EventMap.data.SEED_PLACES = [ + { + "name": "Tierpark Berlin", + "category": "Tierpark/Zoo", + "shortDescription": "Weitläufiger Tierpark im Grünen mit über 7.000 Tieren.", + "description": "Der Tierpark Berlin ist einer der größten Landschaftstierparks Europas. Auf großzügigen Freianlagen können Familien zahlreiche Tierarten beobachten, darunter Elefanten, Löwen und Pandas. Weitläufige Wege eignen sich gut für Kinderwagen, mehrere Spielplätze laden zwischendurch zum Toben ein.", + "latitude": 52.5063, + "longitude": 13.4996, + "address": "Am Tierpark 125", + "city": "Berlin", + "region": "Berlin", + "ageRecommendation": { "min": 0, "max": 14 }, + "cost": { "type": "paid", "description": "Erwachsene ca. 16€, Kinder ca. 8€" }, + "openingHours": "Täglich ab 9:00 Uhr, saisonal wechselnde Schließzeiten", + "durationMinutes": 240, + "facilities": { + "strollerAccessible": true, + "wheelchairAccessible": true, + "toilets": true, + "parking": true, + "restaurant": true + }, + "environment": "outdoor", + "website": "https://www.tierpark-berlin.de", + "image": "", + "rating": 4.6, + "tags": ["Tiere", "familienfreundlich", "Kinderwagen geeignet"], + "source": "Seed-Daten", + "sourceVerified": true + }, + { + "name": "Spielplatz Westpark München", + "category": "Spielplatz", + "shortDescription": "Großer Spielplatz mit Klettergerüsten, Rutschen und Wasserspielbereich.", + "description": "Im Westpark München gibt es mehrere gut ausgestattete Spielplätze für unterschiedliche Altersgruppen, inklusive Wasserspielbereich im Sommer, Sandkästen und Klettergerüsten. Der Park bietet zudem viel Platz für Picknicks und lange Spaziergänge.", + "latitude": 48.1231, + "longitude": 11.5310, + "address": "Westendstraße 224", + "city": "München", + "region": "Bayern", + "ageRecommendation": { "min": 1, "max": 10 }, + "cost": { "type": "free", "description": "Kostenlos" }, + "openingHours": "Durchgehend geöffnet", + "durationMinutes": 90, + "facilities": { + "strollerAccessible": true, + "wheelchairAccessible": true, + "toilets": true, + "parking": false, + "restaurant": false + }, + "environment": "outdoor", + "website": "", + "image": "", + "rating": 4.3, + "tags": ["kostenlos", "Spielplatz", "Wasser"], + "source": "Seed-Daten", + "sourceVerified": true + }, + { + "name": "Kletterwald Schluchsee", + "category": "Kletterpark", + "shortDescription": "Hochseilgarten mit Parcours für Kinder und Erwachsene im Schwarzwald.", + "description": "Der Kletterwald Schluchsee bietet verschiedene Parcours mit unterschiedlichen Schwierigkeitsgraden, darunter spezielle Kinderparcours ab 4 Jahren. Umgeben von dichtem Nadelwald direkt am Schluchsee gelegen.", + "latitude": 47.8206, + "longitude": 8.1653, + "address": "Kaiserhaustraße 1", + "city": "Schluchsee", + "region": "Baden-Württemberg", + "ageRecommendation": { "min": 4, "max": 99 }, + "cost": { "type": "paid", "description": "Ab ca. 14€ pro Person, je nach Parcours" }, + "openingHours": "April bis Oktober, 9:30 bis 18:00 Uhr (witterungsabhängig)", + "durationMinutes": 180, + "facilities": { + "strollerAccessible": false, + "wheelchairAccessible": false, + "toilets": true, + "parking": true, + "restaurant": true + }, + "environment": "outdoor", + "website": "https://www.kletterwald-schluchsee.de", + "image": "", + "rating": 4.7, + "tags": ["Klettern", "Wald", "Tagesausflug"], + "source": "Seed-Daten", + "sourceVerified": true + }, + { + "name": "Europa-Park Rust", + "category": "Freizeitpark", + "shortDescription": "Großer Freizeitpark mit Themenbereichen, Achterbahnen und Kinderfahrgeschäften.", + "description": "Der Europa-Park ist einer der größten Freizeitparks Europas mit europäisch thematisierten Bereichen. Neben actionreichen Achterbahnen gibt es zahlreiche ruhigere Fahrgeschäfte und einen eigenen Kinderbereich für die jüngsten Besucher.", + "latitude": 48.2669, + "longitude": 7.7220, + "address": "Europa-Park-Straße 2", + "city": "Rust", + "region": "Baden-Württemberg", + "ageRecommendation": { "min": 2, "max": 99 }, + "cost": { "type": "paid", "description": "Tagesticket ab ca. 60€" }, + "openingHours": "Saisonal geöffnet, i.d.R. 9:00 bis 18:00 Uhr", + "durationMinutes": 480, + "facilities": { + "strollerAccessible": true, + "wheelchairAccessible": true, + "toilets": true, + "parking": true, + "restaurant": true + }, + "environment": "mixed", + "website": "https://www.europapark.de", + "image": "", + "rating": 4.8, + "tags": ["Freizeitpark", "Tagesausflug", "Achterbahn"], + "source": "Seed-Daten", + "sourceVerified": true + }, + { + "name": "Naturpark Schönbuch", + "category": "Naturpark", + "shortDescription": "Ausgedehntes Waldgebiet mit markierten Wanderwegen und Wildgehegen.", + "description": "Der Naturpark Schönbuch bei Tübingen bietet zahlreiche familienfreundliche Wanderwege, Waldlehrpfade und ein Wildgehege mit heimischen Tierarten. Ideal für einen entspannten Tag in der Natur.", + "latitude": 48.5667, + "longitude": 9.0667, + "address": "Schönbuchturm", + "city": "Herrenberg", + "region": "Baden-Württemberg", + "ageRecommendation": { "min": 0, "max": 99 }, + "cost": { "type": "free", "description": "Kostenlos" }, + "openingHours": "Durchgehend geöffnet", + "durationMinutes": 150, + "facilities": { + "strollerAccessible": true, + "wheelchairAccessible": false, + "toilets": false, + "parking": true, + "restaurant": false + }, + "environment": "outdoor", + "website": "", + "image": "", + "rating": 4.5, + "tags": ["Natur", "Wandern", "kostenlos", "Tiere"], + "source": "Seed-Daten", + "sourceVerified": false + }, + { + "name": "Partnachklamm Wanderweg", + "category": "Wald/Wanderweg", + "shortDescription": "Spektakuläre Felsschlucht bei Garmisch-Partenkirchen, gut begehbarer Weg.", + "description": "Der Weg durch die Partnachklamm führt über gut gesicherte Wege und teilweise durch Tunnel entlang der tosenden Partnach. Für Kinder ab dem Schulalter ein beeindruckendes Naturerlebnis, festes Schuhwerk empfohlen.", + "latitude": 47.4931, + "longitude": 11.0817, + "address": "Am Kurpark", + "city": "Garmisch-Partenkirchen", + "region": "Bayern", + "ageRecommendation": { "min": 6, "max": 99 }, + "cost": { "type": "paid", "description": "Eintritt ca. 6€, Kinder ermäßigt" }, + "openingHours": "Ganzjährig, witterungsabhängig gesperrt", + "durationMinutes": 90, + "facilities": { + "strollerAccessible": false, + "wheelchairAccessible": false, + "toilets": true, + "parking": true, + "restaurant": true + }, + "environment": "outdoor", + "website": "", + "image": "", + "rating": 4.7, + "tags": ["Wandern", "Natur", "Schlucht"], + "source": "Seed-Daten", + "sourceVerified": false + }, + { + "name": "Strandbad Herrsching am Ammersee", + "category": "See/Badestelle", + "shortDescription": "Familienfreundliches Strandbad mit flachem Uferbereich am Ammersee.", + "description": "Das Strandbad Herrsching bietet einen flach abfallenden Badebereich, eine große Liegewiese und einen Spielplatz direkt am See. Ideal für einen entspannten Tag am Wasser mit kleinen Kindern.", + "latitude": 48.0000, + "longitude": 11.1667, + "address": "Seepromenade", + "city": "Herrsching am Ammersee", + "region": "Bayern", + "ageRecommendation": { "min": 0, "max": 99 }, + "cost": { "type": "free", "description": "Kostenlos, Liegestuhlverleih gegen Gebühr" }, + "openingHours": "Durchgehend zugänglich", + "durationMinutes": 180, + "facilities": { + "strollerAccessible": true, + "wheelchairAccessible": true, + "toilets": true, + "parking": true, + "restaurant": true + }, + "environment": "outdoor", + "website": "", + "image": "", + "rating": 4.4, + "tags": ["Baden", "See", "kostenlos"], + "source": "Seed-Daten", + "sourceVerified": false + }, + { + "name": "Junges Schloss Stuttgart", + "category": "Museum für Kinder", + "shortDescription": "Kindermuseum im Alten Schloss mit interaktiven Mitmachstationen.", + "description": "Das Junge Schloss ist das Landesmuseum für Kinder in Stuttgart und vermittelt Geschichte spielerisch über zahlreiche Mitmach-Stationen, Verkleidungen und Rätsel. Besonders für Grundschulkinder geeignet.", + "latitude": 48.7791, + "longitude": 9.1804, + "address": "Schillerplatz 6", + "city": "Stuttgart", + "region": "Baden-Württemberg", + "ageRecommendation": { "min": 5, "max": 12 }, + "cost": { "type": "paid", "description": "Eintritt ca. 4€, Kinder unter 6 Jahren frei" }, + "openingHours": "Di–So 10:00–17:00 Uhr, montags geschlossen", + "durationMinutes": 120, + "facilities": { + "strollerAccessible": true, + "wheelchairAccessible": true, + "toilets": true, + "parking": false, + "restaurant": false + }, + "environment": "indoor", + "website": "https://www.jungesschloss.de", + "image": "", + "rating": 4.5, + "tags": ["Museum", "Geschichte", "Indoor"], + "source": "Seed-Daten", + "sourceVerified": true + }, + { + "name": "Jump House Hamburg", + "category": "Indoor-Spielplatz", + "shortDescription": "Große Trampolinhalle mit Bereichen für kleine und große Kinder.", + "description": "Im Jump House Hamburg können Kinder auf zahlreichen vernetzten Trampolinen springen und toben. Ein separater Bereich für Kleinkinder sorgt für sicheren Spaß auch bei den Jüngsten - ideal bei schlechtem Wetter.", + "latitude": 53.5511, + "longitude": 9.9937, + "address": "Amsinckstraße 57", + "city": "Hamburg", + "region": "Hamburg", + "ageRecommendation": { "min": 2, "max": 16 }, + "cost": { "type": "paid", "description": "Ab ca. 10€ pro Stunde" }, + "openingHours": "Täglich 10:00–20:00 Uhr", + "durationMinutes": 90, + "facilities": { + "strollerAccessible": true, + "wheelchairAccessible": true, + "toilets": true, + "parking": true, + "restaurant": true + }, + "environment": "indoor", + "website": "", + "image": "", + "rating": 4.2, + "tags": ["Indoor", "bei schlechtem Wetter geeignet", "Trampolin"], + "source": "Seed-Daten", + "sourceVerified": false + }, + { + "name": "Erlebnisbauernhof Gut Sonnenhof", + "category": "Bauernhof", + "shortDescription": "Bauernhof zum Anfassen mit Streichelzoo und Traktorfahrten.", + "description": "Auf dem Erlebnisbauernhof Gut Sonnenhof können Kinder Tiere füttern und streicheln, an Traktorfahrten teilnehmen und den Alltag auf einem Bauernhof hautnah erleben. Ein Café mit hofeigenen Produkten lädt zur Rast ein.", + "latitude": 52.5000, + "longitude": 9.5000, + "address": "Hofstraße 3", + "city": "Neustadt am Rübenberge", + "region": "Niedersachsen", + "ageRecommendation": { "min": 1, "max": 10 }, + "cost": { "type": "paid", "description": "Eintritt ca. 5€, Kinder unter 2 Jahren frei" }, + "openingHours": "Sa–So und Feiertage 10:00–17:00 Uhr", + "durationMinutes": 150, + "facilities": { + "strollerAccessible": true, + "wheelchairAccessible": false, + "toilets": true, + "parking": true, + "restaurant": true + }, + "environment": "outdoor", + "website": "", + "image": "", + "rating": 4.4, + "tags": ["Tiere", "Bauernhof", "mit Tieren"], + "source": "Seed-Daten", + "sourceVerified": false + } +]; diff --git a/docs/adr/0001-vanilla-esm.md b/docs/adr/0001-vanilla-esm.md new file mode 100644 index 0000000..0a8d47e --- /dev/null +++ b/docs/adr/0001-vanilla-esm.md @@ -0,0 +1,98 @@ +# ADR 0001: Kein Build-Tooling, klassische Scripts mit globalem Namespace, Leaflet lokal vendored + +## Status + +Akzeptiert (überarbeitet – siehe "Revision" unten; ursprüngliche +Entscheidung für native ES-Module wurde revidiert) + +## Kontext + +`anforderung.md` fordert explizit "möglichst einfache und gut verständliche +Technologien" (HTML5, CSS3, JavaScript, eine Open-Source-Kartenbibliothek, +LocalStorage) und stellt keinen Backend-Zwang für die erste Version. Die +Anwendung soll außerdem vollständig responsiv sein und ohne größere +Infrastruktur betrieben und gewartet werden können. + +Die tatsächliche Zielumgebung des Nutzers wurde präzisiert: Die App wird +per **Doppelklick auf `index.html`** direkt aus einem lokal synchronisierten +Nextcloud-Ordner geöffnet (`file://...`). Es steht **kein lokaler +HTTP-Server** zur Verfügung und ein solcher soll vom Endnutzer auch nicht +manuell gestartet werden müssen – Priorität hat maximale Einfachheit für den +Endnutzer, nicht Entwicklerkomfort. + +## Revision (2026-07-24) + +Die ursprüngliche Entscheidung, JavaScript als native ES-Module +(` + + + + + + + + + + + + + + + + + + + + + diff --git a/js/config/categories.js b/js/config/categories.js new file mode 100644 index 0000000..44a33e2 --- /dev/null +++ b/js/config/categories.js @@ -0,0 +1,47 @@ +// Feste Liste der Ausflugsziel-Kategorien (siehe anforderung.md). +// Jede Kategorie hat eine stabile id (slug), ein Label, eine Farbe (Hex) +// für Marker/Labels und ein Emoji-Icon als Glyph. + +(function () { + const CATEGORIES = [ + { id: 'naturpark', label: 'Naturpark', color: '#2e7d32', icon: '🌳' }, + { id: 'wald-wanderweg', label: 'Wald/Wanderweg', color: '#1b5e20', icon: '🌲' }, + { id: 'see-badestelle', label: 'See/Badestelle', color: '#0288d1', icon: '🏖️' }, + { id: 'erlebnispark', label: 'Erlebnispark', color: '#f57c00', icon: '🎢' }, + { id: 'freizeitpark', label: 'Freizeitpark', color: '#e64a19', icon: '🎡' }, + { id: 'tierpark-zoo', label: 'Tierpark/Zoo', color: '#ff8f00', icon: '🦁' }, + { id: 'spielplatz', label: 'Spielplatz', color: '#d81b60', icon: '🛝' }, + { id: 'museum-kinder', label: 'Museum für Kinder', color: '#7b1fa2', icon: '🏛️' }, + { id: 'indoor-spielplatz', label: 'Indoor-Spielplatz', color: '#5e35b1', icon: '🧸' }, + { id: 'bauernhof', label: 'Bauernhof', color: '#6d4c41', icon: '🐄' }, + { id: 'kletterpark', label: 'Kletterpark', color: '#00897b', icon: '🧗' }, + { id: 'aussichtspunkt', label: 'Aussichtspunkt', color: '#455a64', icon: '🔭' }, + { id: 'cafe-restaurant', label: 'Familienfreundliches Café/Restaurant', color: '#c62828', icon: '☕' }, + { id: 'sonstige-aktivitaet', label: 'Sonstige kinderfreundliche Aktivität', color: '#00acc1', icon: '🎈' }, + { id: 'sonstige', label: 'Sonstige', color: '#616161', icon: '📍' }, + ]; + + const CATEGORY_MAP = new Map(CATEGORIES.map((c) => [c.id, c])); + const CATEGORY_LABEL_MAP = new Map( + CATEGORIES.map((c) => [c.label.toLowerCase(), c]) + ); + + function getCategoryById(id) { + return CATEGORY_MAP.get(id) ?? CATEGORY_MAP.get('sonstige'); + } + + /** + * Findet eine Kategorie anhand ihres Labels (z.B. aus KI-Import-JSON). + * Fällt auf "Sonstige" zurück, falls kein Treffer gefunden wird. + */ + function getCategoryByLabel(label) { + if (typeof label !== 'string') { + return CATEGORY_MAP.get('sonstige'); + } + return CATEGORY_LABEL_MAP.get(label.toLowerCase()) ?? CATEGORY_MAP.get('sonstige'); + } + + EventMap.config.CATEGORIES = CATEGORIES; + EventMap.config.getCategoryById = getCategoryById; + EventMap.config.getCategoryByLabel = getCategoryByLabel; +})(); diff --git a/js/config/constants.js b/js/config/constants.js new file mode 100644 index 0000000..00bc4c5 --- /dev/null +++ b/js/config/constants.js @@ -0,0 +1,23 @@ +// Zentrale Konstanten der Anwendung. + +(function () { + const STORAGE_KEYS = { + DATA: 'eventmap.data.v1', + SETTINGS: 'eventmap.settings.v1', + }; + + const CURRENT_SCHEMA_VERSION = 1; + + // Geografische Mitte Deutschlands als Standard-Kartenzentrum, + // solange der Nutzer keinen eigenen Standort freigibt oder ein + // eigenes defaultCenter in den Settings hinterlegt hat. + const DEFAULT_MAP_CENTER = { + lat: 51.1657, + lng: 10.4515, + zoom: 6, + }; + + EventMap.config.STORAGE_KEYS = STORAGE_KEYS; + EventMap.config.CURRENT_SCHEMA_VERSION = CURRENT_SCHEMA_VERSION; + EventMap.config.DEFAULT_MAP_CENTER = DEFAULT_MAP_CENTER; +})(); diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..1f53d36 --- /dev/null +++ b/js/main.js @@ -0,0 +1,53 @@ +// Einstiegspunkt der Anwendung: initialisiert Store, Karte und Liste. + +(function () { + const { initStore } = EventMap.store; + const { initMap, centerOnCoords, requestUserLocation } = EventMap.map; + const { initList } = EventMap.ui; + + function showFatalError(message) { + const status = document.getElementById('app-status'); + if (!status) return; + status.textContent = message; + status.hidden = false; + } + + function setupGeolocationButton() { + const button = document.getElementById('locate-btn'); + if (!button) return; + + const defaultLabel = button.textContent; + + button.addEventListener('click', () => { + button.disabled = true; + button.textContent = 'Standort wird ermittelt…'; + + requestUserLocation({ + onSuccess: ({ lat, lng }) => { + centerOnCoords(lat, lng, 13); + button.disabled = false; + button.textContent = defaultLabel; + }, + onError: () => { + button.disabled = false; + button.textContent = defaultLabel; + showFatalError( + 'Standort konnte nicht ermittelt werden. Bitte Standortfreigabe im Browser prüfen.' + ); + }, + }); + }); + } + + async function bootstrap() { + await initStore(); + initMap('map'); + initList('place-list'); + setupGeolocationButton(); + } + + bootstrap().catch((err) => { + console.error('EventMap: Initialisierung fehlgeschlagen.', err); + showFatalError('Die Anwendung konnte nicht geladen werden. Bitte Seite neu laden.'); + }); +})(); diff --git a/js/map/geolocation.js b/js/map/geolocation.js new file mode 100644 index 0000000..0c3e0b0 --- /dev/null +++ b/js/map/geolocation.js @@ -0,0 +1,39 @@ +// Optionale Standortabfrage via navigator.geolocation. Wird nur auf +// explizite Nutzeraktion (Button) angefragt, nie automatisch beim Start. + +(function () { + const { setState, updateSettings } = EventMap.store; + + /** + * Fragt den aktuellen Standort des Nutzers an. + * @param {{onSuccess?: (loc:{lat:number,lng:number}) => void, onError?: (err:Error) => void}} handlers + */ + function requestUserLocation({ onSuccess, onError } = {}) { + if (!('geolocation' in navigator)) { + const err = new Error('Geolocation wird von diesem Browser nicht unterstützt.'); + console.warn('EventMap:', err.message); + if (onError) onError(err); + return; + } + + navigator.geolocation.getCurrentPosition( + (position) => { + const userLocation = { + lat: position.coords.latitude, + lng: position.coords.longitude, + }; + setState({ userLocation }); + updateSettings({ userConsentGeo: true }); + if (onSuccess) onSuccess(userLocation); + }, + (error) => { + console.warn('EventMap: Standort konnte nicht ermittelt werden.', error.message); + updateSettings({ userConsentGeo: false }); + if (onError) onError(error); + }, + { enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 } + ); + } + + EventMap.map.requestUserLocation = requestUserLocation; +})(); diff --git a/js/map/mapController.js b/js/map/mapController.js new file mode 100644 index 0000000..964f507 --- /dev/null +++ b/js/map/mapController.js @@ -0,0 +1,130 @@ +/* global L */ +// Initialisiert die Leaflet-Karte, verwaltet Marker-Clustering und +// reagiert auf Store-Änderungen (places, selectedPlaceId). + +(function () { + const { DEFAULT_MAP_CENTER } = EventMap.config; + const { subscribe, getState, setState } = EventMap.store; + const { getPlaceLatLng } = EventMap.models; + const { createMarkerForPlace } = EventMap.map; + + let map = null; + let clusterGroup = null; + const markersByPlaceId = new Map(); + + let lastPlacesRef = null; + let lastSelectedPlaceId = null; + + function hasValidCoords(place) { + const { lat, lng } = getPlaceLatLng(place); + return typeof lat === 'number' && typeof lng === 'number' && !Number.isNaN(lat) && !Number.isNaN(lng); + } + + function renderMarkers(places) { + clusterGroup.clearLayers(); + markersByPlaceId.clear(); + + for (const place of places) { + if (!hasValidCoords(place)) continue; + + const marker = createMarkerForPlace(place, { + onSelect: (placeId) => setState({ selectedPlaceId: placeId }), + }); + markersByPlaceId.set(place.id, marker); + clusterGroup.addLayer(marker); + } + } + + /** Passt den Kartenausschnitt an alle übergebenen Places an (Fallback: Default-Center). */ + function fitToPlaces(places) { + const valid = places.filter(hasValidCoords); + + if (valid.length === 0) { + map.setView([DEFAULT_MAP_CENTER.lat, DEFAULT_MAP_CENTER.lng], DEFAULT_MAP_CENTER.zoom); + return; + } + + const bounds = L.latLngBounds( + valid.map((place) => { + const { lat, lng } = getPlaceLatLng(place); + return [lat, lng]; + }) + ); + map.fitBounds(bounds, { padding: [32, 32], maxZoom: 14 }); + } + + /** Zentriert die Karte auf einen bestimmten Place und öffnet dessen Popup. */ + function centerOnPlace(placeId) { + const marker = markersByPlaceId.get(placeId); + if (!marker || !map) return; + map.setView(marker.getLatLng(), Math.max(map.getZoom(), 14)); + marker.openPopup(); + } + + /** Zentriert die Karte auf beliebige Koordinaten (z.B. Nutzerstandort). */ + function centerOnCoords(lat, lng, zoom = 13) { + if (!map) return; + map.setView([lat, lng], zoom); + } + + function getMapInstance() { + return map; + } + + /** Initialisiert die Karte im Element mit der übergebenen id. */ + function initMap(containerId) { + // Leaflet leert den Container beim Initialisieren nicht selbst - den + // statischen "Karte wird geladen…"-Platzhalter aus index.html entfernen. + const container = document.getElementById(containerId); + if (container) { + container.textContent = ''; + } + + map = L.map(containerId, { + center: [DEFAULT_MAP_CENTER.lat, DEFAULT_MAP_CENTER.lng], + zoom: DEFAULT_MAP_CENTER.zoom, + }); + + // tile.openstreetmap.org verlangt seit 2024 einen gültigen HTTP-Referer + // (Tile Usage Policy). Seiten, die per file:// geöffnet werden, senden + // browserseitig grundsätzlich keinen Referer mit ("Access blocked: Referer + // is required"). Daher CARTO-Basemaps (kartographisch auf OSM-Daten + // basierend, korrekt attributiert) statt des offiziellen OSM-Tile-Servers. + L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { + maxZoom: 19, + subdomains: 'abcd', + attribution: + '© OpenStreetMap-Mitwirkende ' + + '© CARTO', + }).addTo(map); + + clusterGroup = L.markerClusterGroup(); + map.addLayer(clusterGroup); + + const initialState = getState(); + renderMarkers(initialState.places); + fitToPlaces(initialState.places); + lastPlacesRef = initialState.places; + + subscribe((state) => { + if (state.places !== lastPlacesRef) { + lastPlacesRef = state.places; + renderMarkers(state.places); + } + if (state.selectedPlaceId !== lastSelectedPlaceId) { + lastSelectedPlaceId = state.selectedPlaceId; + if (state.selectedPlaceId) { + centerOnPlace(state.selectedPlaceId); + } + } + }); + + return map; + } + + EventMap.map.fitToPlaces = fitToPlaces; + EventMap.map.centerOnPlace = centerOnPlace; + EventMap.map.centerOnCoords = centerOnCoords; + EventMap.map.getMapInstance = getMapInstance; + EventMap.map.initMap = initMap; +})(); diff --git a/js/map/markers.js b/js/map/markers.js new file mode 100644 index 0000000..13a8397 --- /dev/null +++ b/js/map/markers.js @@ -0,0 +1,64 @@ +/* global L */ +// Erzeugt Leaflet-Marker für Places inkl. Kategorie-Icon (L.divIcon) und +// Popup-Inhalt. Popup-Inhalte werden als DOM-Nodes (nicht als HTML-String) +// erzeugt, damit Place-Daten niemals ungesichert als innerHTML landen. + +(function () { + const { getCategoryById } = EventMap.config; + const { getPlaceCategory, getPlaceLatLng } = EventMap.models; + const { el } = EventMap.utils; + + const iconCache = new Map(); + + function buildDivIcon(category) { + return L.divIcon({ + className: 'eventmap-marker', + html: `${category.icon}`, + iconSize: [32, 32], + iconAnchor: [16, 16], + popupAnchor: [0, -16], + }); + } + + function getIconForCategory(categoryId) { + if (!iconCache.has(categoryId)) { + iconCache.set(categoryId, buildDivIcon(getCategoryById(categoryId))); + } + return iconCache.get(categoryId); + } + + function buildPopupContent(place) { + const category = getPlaceCategory(place); + + return el('div', { className: 'eventmap-popup' }, [ + el('strong', { className: 'eventmap-popup__title', text: place.name }), + el('div', { + className: 'eventmap-popup__category', + style: { color: category.color }, + text: `${category.icon} ${category.label}`, + }), + el('p', { className: 'eventmap-popup__desc', text: place.shortDescription }), + ]); + } + + /** + * Erzeugt einen Leaflet-Marker für ein Place-Objekt. + * @param {object} place + * @param {{onSelect?: (placeId: string) => void}} [handlers] + */ + function createMarkerForPlace(place, { onSelect } = {}) { + const { lat, lng } = getPlaceLatLng(place); + const marker = L.marker([lat, lng], { icon: getIconForCategory(place.category) }); + + marker.bindPopup(buildPopupContent(place)); + marker.on('click', () => { + if (onSelect) { + onSelect(place.id); + } + }); + + return marker; + } + + EventMap.map.createMarkerForPlace = createMarkerForPlace; +})(); diff --git a/js/models/place.js b/js/models/place.js new file mode 100644 index 0000000..cd58502 --- /dev/null +++ b/js/models/place.js @@ -0,0 +1,56 @@ +// Internes Place-Objekt: bleibt nah am KI-Import-JSON-Format aus +// anforderung.md, ergänzt um intern generierte Felder (id, createdAt, +// updatedAt). Die Kategorie wird beim Normalisieren immer auf eine +// gültige Kategorie-id (Slug) aufgelöst, damit Styling (Farbe/Icon) +// überall per getCategoryById() funktioniert. + +(function () { + const { generateId } = EventMap.utils; + const { CATEGORIES, getCategoryById, getCategoryByLabel } = EventMap.config; + const { createDefaultPlaceFields } = EventMap.models; + + const CATEGORY_IDS = new Set(CATEGORIES.map((c) => c.id)); + + function resolveCategory(rawCategory) { + if (typeof rawCategory === 'string' && CATEGORY_IDS.has(rawCategory)) { + return rawCategory; + } + return getCategoryByLabel(rawCategory).id; + } + + /** + * Normalisiert ein rohes Place-Objekt (z.B. aus data/seed.js oder einem + * späteren KI-Import) zu einem vollständigen internen Place-Objekt. + * Fehlende Felder werden mit Defaults aufgefüllt, id/createdAt/updatedAt + * werden übernommen falls vorhanden, sonst neu generiert. + */ + function normalizePlace(raw = {}) { + const defaults = createDefaultPlaceFields(); + const now = new Date().toISOString(); + + return { + id: raw.id || generateId(), + ...defaults, + ...raw, + category: resolveCategory(raw.category), + ageRecommendation: { ...defaults.ageRecommendation, ...(raw.ageRecommendation || {}) }, + cost: { ...defaults.cost, ...(raw.cost || {}) }, + facilities: { ...defaults.facilities, ...(raw.facilities || {}) }, + tags: Array.isArray(raw.tags) ? [...raw.tags] : [], + createdAt: raw.createdAt || now, + updatedAt: raw.updatedAt || now, + }; + } + + function getPlaceCategory(place) { + return getCategoryById(place.category); + } + + function getPlaceLatLng(place) { + return { lat: place.latitude, lng: place.longitude }; + } + + EventMap.models.normalizePlace = normalizePlace; + EventMap.models.getPlaceCategory = getPlaceCategory; + EventMap.models.getPlaceLatLng = getPlaceLatLng; +})(); diff --git a/js/models/schema.js b/js/models/schema.js new file mode 100644 index 0000000..44ece54 --- /dev/null +++ b/js/models/schema.js @@ -0,0 +1,50 @@ +// Beschreibt die Feldstruktur eines Ausflugsziels (~27 Felder, siehe +// anforderung.md) und liefert Default-Werte. Dient als einzige Quelle der +// Wahrheit für die Form eines Place-Objekts; Validierungslogik für +// Formulare folgt in einer späteren Phase. + +(function () { + /** + * Liefert ein leeres/defaultes Place-Objekt ohne id/createdAt/updatedAt + * (diese werden von place.js ergänzt). + */ + function createDefaultPlaceFields() { + return { + name: '', + category: 'sonstige', + shortDescription: '', + description: '', + latitude: null, + longitude: null, + address: '', + city: '', + region: '', + ageRecommendation: { min: null, max: null }, + cost: { type: 'unknown', description: '' }, + openingHours: '', + durationMinutes: null, + facilities: { + strollerAccessible: false, + wheelchairAccessible: false, + toilets: false, + parking: false, + restaurant: false, + }, + environment: 'outdoor', + website: '', + image: '', + rating: null, + tags: [], + source: '', + sourceVerified: false, + }; + } + + // Gültige Werte für die Enums des Datenmodells. + const COST_TYPES = ['free', 'paid', 'mixed', 'unknown']; + const ENVIRONMENT_TYPES = ['indoor', 'outdoor', 'mixed']; + + EventMap.models.createDefaultPlaceFields = createDefaultPlaceFields; + EventMap.models.COST_TYPES = COST_TYPES; + EventMap.models.ENVIRONMENT_TYPES = ENVIRONMENT_TYPES; +})(); diff --git a/js/store/migrations.js b/js/store/migrations.js new file mode 100644 index 0000000..4424112 --- /dev/null +++ b/js/store/migrations.js @@ -0,0 +1,37 @@ +// Schlanke Migrations-Infrastruktur für das LocalStorage-Datenschema. +// Aktuell existiert nur Schema-Version 1, daher ist MIGRATIONS leer. +// Neue Migrationen werden als { from, to, migrate(data) } ergänzt und +// von migrate() sequenziell angewendet. + +(function () { + const { CURRENT_SCHEMA_VERSION } = EventMap.config; + + const MIGRATIONS = [ + // Beispiel für zukünftige Migrationen: + // { from: 1, to: 2, migrate: (data) => ({ ...data, neuesFeld: 'default' }) }, + ]; + + /** + * Wendet alle nötigen Migrationsschritte an, bis schemaVersion der + * aktuellen CURRENT_SCHEMA_VERSION entspricht. + */ + function migrate(data) { + let current = data ?? {}; + let version = current.schemaVersion ?? 1; + + while (version < CURRENT_SCHEMA_VERSION) { + const step = MIGRATIONS.find((m) => m.from === version); + if (!step) { + // Keine passende Migration gefunden: auf aktuelle Version springen, + // um die App nicht zu blockieren. + break; + } + current = step.migrate(current); + version = step.to; + } + + return { ...current, schemaVersion: CURRENT_SCHEMA_VERSION }; + } + + EventMap.store.migrate = migrate; +})(); diff --git a/js/store/storage.js b/js/store/storage.js new file mode 100644 index 0000000..ec924d7 --- /dev/null +++ b/js/store/storage.js @@ -0,0 +1,63 @@ +// Persistenzschicht: liest/schreibt die beiden LocalStorage-Keys +// eventmap.data.v1 und eventmap.settings.v1. + +(function () { + const { STORAGE_KEYS, CURRENT_SCHEMA_VERSION } = EventMap.config; + const { migrate } = EventMap.store; + + function createDefaultSettings() { + return { + lastFilters: null, + lastMapView: null, + userConsentGeo: null, + defaultCenter: null, + }; + } + + /** Liest die persistierten Places. Gibt null zurück, falls noch nichts gespeichert ist. */ + function loadData() { + const raw = window.localStorage.getItem(STORAGE_KEYS.DATA); + if (!raw) { + return null; + } + try { + const parsed = JSON.parse(raw); + return migrate(parsed); + } catch (err) { + console.warn('EventMap: Gespeicherte Daten sind beschädigt und werden verworfen.', err); + return null; + } + } + + function saveData(places) { + const payload = { + schemaVersion: CURRENT_SCHEMA_VERSION, + places, + updatedAt: new Date().toISOString(), + }; + window.localStorage.setItem(STORAGE_KEYS.DATA, JSON.stringify(payload)); + } + + function loadSettings() { + const raw = window.localStorage.getItem(STORAGE_KEYS.SETTINGS); + if (!raw) { + return createDefaultSettings(); + } + try { + return { ...createDefaultSettings(), ...JSON.parse(raw) }; + } catch (err) { + console.warn('EventMap: Gespeicherte Einstellungen sind beschädigt, Defaults werden verwendet.', err); + return createDefaultSettings(); + } + } + + function saveSettings(settings) { + window.localStorage.setItem(STORAGE_KEYS.SETTINGS, JSON.stringify(settings)); + } + + EventMap.store.createDefaultSettings = createDefaultSettings; + EventMap.store.loadData = loadData; + EventMap.store.saveData = saveData; + EventMap.store.loadSettings = loadSettings; + EventMap.store.saveSettings = saveSettings; +})(); diff --git a/js/store/store.js b/js/store/store.js new file mode 100644 index 0000000..7426eb5 --- /dev/null +++ b/js/store/store.js @@ -0,0 +1,88 @@ +// Zentraler Pub/Sub-Store der Anwendung. Hält den Laufzeitzustand +// (places, filters, userLocation, selectedPlaceId) und benachrichtigt +// Subscriber bei Änderungen. Lädt beim Start aus LocalStorage bzw. beim +// allerersten Start aus data/seed.js und persistiert Änderungen zurück. + +(function () { + const { loadData, saveData, loadSettings, saveSettings } = EventMap.store; + const { normalizePlace } = EventMap.models; + + let state = { + places: [], + filters: null, + userLocation: null, + selectedPlaceId: null, + }; + + let settings = loadSettings(); + + const subscribers = new Set(); + + function notify() { + for (const fn of subscribers) { + fn(state); + } + } + + /** Registriert einen Listener, der bei jeder Zustandsänderung aufgerufen wird. */ + function subscribe(fn) { + subscribers.add(fn); + return () => subscribers.delete(fn); + } + + function getState() { + return state; + } + + function setState(patch) { + state = { ...state, ...patch }; + notify(); + } + + function getSettings() { + return settings; + } + + function updateSettings(patch) { + settings = { ...settings, ...patch }; + saveSettings(settings); + } + + function loadSeedPlaces() { + const raw = (EventMap.data && EventMap.data.SEED_PLACES) || []; + return raw.map((item) => normalizePlace(item)); + } + + /** + * Initialisiert den Store: lädt persistierte Places aus LocalStorage oder, + * falls noch keine vorhanden sind, die eingebetteten Beispieldaten + * (data/seed.js). Gibt den finalen State zurück. + */ + function initStore() { + const persisted = loadData(); + let places = []; + + if (persisted && Array.isArray(persisted.places) && persisted.places.length > 0) { + places = persisted.places.map((p) => normalizePlace(p)); + } else { + places = loadSeedPlaces(); + saveData(places); + } + + setState({ places }); + return state; + } + + /** Persistiert state.places nach Änderungen (Erstellen/Bearbeiten/Löschen). */ + function persistPlaces() { + saveData(state.places); + } + + EventMap.store.subscribe = subscribe; + EventMap.store.getState = getState; + EventMap.store.setState = setState; + EventMap.store.getSettings = getSettings; + EventMap.store.updateSettings = updateSettings; + EventMap.store.initStore = initStore; + EventMap.store.persistPlaces = persistPlaces; +})(); diff --git a/js/ui/list.js b/js/ui/list.js new file mode 100644 index 0000000..d9253fb --- /dev/null +++ b/js/ui/list.js @@ -0,0 +1,77 @@ +// Ergebnisliste (Cards), rendert aus store.state.places. Filterung folgt +// in einer späteren Phase - aktuell werden alle Places angezeigt. +// Rendering ausschließlich über textContent/DOM-APIs (el()), niemals +// innerHTML mit Place-Daten. + +(function () { + const { subscribe, getState, setState } = EventMap.store; + const { getCategoryById } = EventMap.config; + const { el, clearChildren } = EventMap.utils; + + function createCard(place) { + const category = getCategoryById(place.category); + + const select = () => setState({ selectedPlaceId: place.id }); + + return el( + 'li', + { + className: 'place-card', + attrs: { tabindex: '0', role: 'button' }, + dataset: { placeId: place.id }, + on: { + click: select, + keydown: (event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + select(); + } + }, + }, + }, + [ + el('h3', { className: 'place-card__name', text: place.name }), + el('span', { + className: 'place-card__category', + style: { backgroundColor: category.color }, + text: `${category.icon} ${category.label}`, + }), + el('p', { className: 'place-card__desc', text: place.shortDescription }), + ] + ); + } + + function render(container, places) { + clearChildren(container); + + if (!places || places.length === 0) { + container.appendChild( + el('p', { className: 'list-empty', text: 'Keine Ausflugsziele vorhanden.' }) + ); + return; + } + + const list = el('ul', { className: 'place-list' }); + for (const place of places) { + list.appendChild(createCard(place)); + } + container.appendChild(list); + } + + /** Initialisiert die Liste im Element mit der übergebenen id und abonniert den Store. */ + function initList(containerId) { + const container = document.getElementById(containerId); + if (!container) { + console.error(`EventMap: List-Container #${containerId} nicht gefunden.`); + return; + } + + render(container, getState().places); + + subscribe((state) => { + render(container, state.places); + }); + } + + EventMap.ui.initList = initList; +})(); diff --git a/js/utils/dom.js b/js/utils/dom.js new file mode 100644 index 0000000..84c608d --- /dev/null +++ b/js/utils/dom.js @@ -0,0 +1,95 @@ +// Kleine DOM-Helfer. Wichtig: Daten aus Places werden NIE per innerHTML +// eingefügt, sondern immer über textContent bzw. DOM-APIs (el()). +// +// Klassisches Script (kein ES-Modul): Die App wird per file:// direkt aus +// einem synchronisierten Ordner geöffnet, daher können keine ES-Module +// (import/export) verwendet werden (Browser blockieren deren Nachladen +// unter file:// per CORS-Policy). Stattdessen hängen alle Dateien ihre +// öffentliche API an das globale Namespace-Objekt window.EventMap. +// Diese Datei wird als erste geladen und legt den Namespace an. + +window.EventMap = window.EventMap || {}; +window.EventMap.utils = window.EventMap.utils || {}; +window.EventMap.config = window.EventMap.config || {}; +window.EventMap.models = window.EventMap.models || {}; +window.EventMap.store = window.EventMap.store || {}; +window.EventMap.map = window.EventMap.map || {}; +window.EventMap.ui = window.EventMap.ui || {}; + +(function () { + /** + * Erzeugt ein DOM-Element. + * @param {string} tag Tag-Name, z.B. 'div' + * @param {object} [props] Eigenschaften: className, attrs{}, dataset{}, style{}, + * text (setzt textContent), on{ click: fn, ... } für Event-Listener + * @param {Array} [children] Kindknoten oder Strings (werden als Text angehängt) + */ + function el(tag, props = {}, children = []) { + const node = document.createElement(tag); + + if (props.className) { + node.className = props.className; + } + if (props.text !== undefined) { + node.textContent = props.text; + } + if (props.attrs) { + for (const [key, value] of Object.entries(props.attrs)) { + if (value !== undefined && value !== null) { + node.setAttribute(key, value); + } + } + } + if (props.dataset) { + for (const [key, value] of Object.entries(props.dataset)) { + node.dataset[key] = value; + } + } + if (props.style) { + Object.assign(node.style, props.style); + } + if (props.on) { + for (const [event, handler] of Object.entries(props.on)) { + node.addEventListener(event, handler); + } + } + + for (const child of children) { + if (child === null || child === undefined) continue; + if (typeof child === 'string') { + node.appendChild(document.createTextNode(child)); + } else { + node.appendChild(child); + } + } + + return node; + } + + /** + * Escaped HTML-Sonderzeichen. Nur als Sicherheitsnetz gedacht, falls an + * einzelnen Stellen doch einmal mit HTML-Strings gearbeitet werden muss + * (z.B. Leaflet-Popups, die intern innerHTML nutzen). Für normales + * Rendering bitte el()/textContent verwenden. + */ + function escapeHtml(value) { + const str = String(value ?? ''); + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + + /** Entfernt alle Kindknoten eines Elements. */ + function clearChildren(node) { + while (node.firstChild) { + node.removeChild(node.firstChild); + } + } + + EventMap.utils.el = el; + EventMap.utils.escapeHtml = escapeHtml; + EventMap.utils.clearChildren = clearChildren; +})(); diff --git a/js/utils/geo.js b/js/utils/geo.js new file mode 100644 index 0000000..cd651f0 --- /dev/null +++ b/js/utils/geo.js @@ -0,0 +1,44 @@ +// Geografische Hilfsfunktionen. + +(function () { + const EARTH_RADIUS_KM = 6371; + + function toRad(deg) { + return (deg * Math.PI) / 180; + } + + /** + * Entfernung zwischen zwei Koordinaten in Kilometern (Haversine-Formel). + * @param {{lat:number,lng:number}} a + * @param {{lat:number,lng:number}} b + */ + function haversineDistanceKm(a, b) { + const dLat = toRad(b.lat - a.lat); + const dLng = toRad(b.lng - a.lng); + const lat1 = toRad(a.lat); + const lat2 = toRad(b.lat); + + const h = + Math.sin(dLat / 2) ** 2 + + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLng / 2) ** 2; + const c = 2 * Math.atan2(Math.sqrt(h), Math.sqrt(1 - h)); + + return EARTH_RADIUS_KM * c; + } + + function isValidCoordinate(lat, lng) { + return ( + typeof lat === 'number' && + typeof lng === 'number' && + Number.isFinite(lat) && + Number.isFinite(lng) && + lat >= -90 && + lat <= 90 && + lng >= -180 && + lng <= 180 + ); + } + + EventMap.utils.haversineDistanceKm = haversineDistanceKm; + EventMap.utils.isValidCoordinate = isValidCoordinate; +})(); diff --git a/js/utils/id.js b/js/utils/id.js new file mode 100644 index 0000000..d3a1082 --- /dev/null +++ b/js/utils/id.js @@ -0,0 +1,13 @@ +// Erzeugt eindeutige IDs für Ausflugsziele. + +(function () { + function generateId() { + if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { + return crypto.randomUUID(); + } + // Fallback für Umgebungen ohne crypto.randomUUID (z.B. sehr alte Browser). + return 'id-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10); + } + + EventMap.utils.generateId = generateId; +})(); diff --git a/vendor/leaflet.markercluster/MarkerCluster.Default.css b/vendor/leaflet.markercluster/MarkerCluster.Default.css new file mode 100644 index 0000000..bbc8c9f --- /dev/null +++ b/vendor/leaflet.markercluster/MarkerCluster.Default.css @@ -0,0 +1,60 @@ +.marker-cluster-small { + background-color: rgba(181, 226, 140, 0.6); + } +.marker-cluster-small div { + background-color: rgba(110, 204, 57, 0.6); + } + +.marker-cluster-medium { + background-color: rgba(241, 211, 87, 0.6); + } +.marker-cluster-medium div { + background-color: rgba(240, 194, 12, 0.6); + } + +.marker-cluster-large { + background-color: rgba(253, 156, 115, 0.6); + } +.marker-cluster-large div { + background-color: rgba(241, 128, 23, 0.6); + } + + /* IE 6-8 fallback colors */ +.leaflet-oldie .marker-cluster-small { + background-color: rgb(181, 226, 140); + } +.leaflet-oldie .marker-cluster-small div { + background-color: rgb(110, 204, 57); + } + +.leaflet-oldie .marker-cluster-medium { + background-color: rgb(241, 211, 87); + } +.leaflet-oldie .marker-cluster-medium div { + background-color: rgb(240, 194, 12); + } + +.leaflet-oldie .marker-cluster-large { + background-color: rgb(253, 156, 115); + } +.leaflet-oldie .marker-cluster-large div { + background-color: rgb(241, 128, 23); +} + +.marker-cluster { + background-clip: padding-box; + border-radius: 20px; + } +.marker-cluster div { + width: 30px; + height: 30px; + margin-left: 5px; + margin-top: 5px; + + text-align: center; + border-radius: 15px; + font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif; + } +.marker-cluster span { + line-height: 30px; + } \ No newline at end of file diff --git a/vendor/leaflet.markercluster/MarkerCluster.css b/vendor/leaflet.markercluster/MarkerCluster.css new file mode 100644 index 0000000..c60d71b --- /dev/null +++ b/vendor/leaflet.markercluster/MarkerCluster.css @@ -0,0 +1,14 @@ +.leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow { + -webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in; + -moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in; + -o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in; + transition: transform 0.3s ease-out, opacity 0.3s ease-in; +} + +.leaflet-cluster-spider-leg { + /* stroke-dashoffset (duration and function) should match with leaflet-marker-icon transform in order to track it exactly */ + -webkit-transition: -webkit-stroke-dashoffset 0.3s ease-out, -webkit-stroke-opacity 0.3s ease-in; + -moz-transition: -moz-stroke-dashoffset 0.3s ease-out, -moz-stroke-opacity 0.3s ease-in; + -o-transition: -o-stroke-dashoffset 0.3s ease-out, -o-stroke-opacity 0.3s ease-in; + transition: stroke-dashoffset 0.3s ease-out, stroke-opacity 0.3s ease-in; +} diff --git a/vendor/leaflet.markercluster/leaflet.markercluster.js b/vendor/leaflet.markercluster/leaflet.markercluster.js new file mode 100644 index 0000000..66fe516 --- /dev/null +++ b/vendor/leaflet.markercluster/leaflet.markercluster.js @@ -0,0 +1,2 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t(((e=e||self).Leaflet=e.Leaflet||{},e.Leaflet.markercluster={}))}(this,function(e){"use strict";var t=L.MarkerClusterGroup=L.FeatureGroup.extend({options:{maxClusterRadius:80,iconCreateFunction:null,clusterPane:L.Marker.prototype.options.pane,spiderfyOnEveryZoom:!1,spiderfyOnMaxZoom:!0,showCoverageOnHover:!0,zoomToBoundsOnClick:!0,singleMarkerMode:!1,disableClusteringAtZoom:null,removeOutsideVisibleBounds:!0,animate:!0,animateAddingMarkers:!1,spiderfyShapePositions:null,spiderfyDistanceMultiplier:1,spiderLegPolylineOptions:{weight:1.5,color:"#222",opacity:.5},chunkedLoading:!1,chunkInterval:200,chunkDelay:50,chunkProgress:null,polygonOptions:{}},initialize:function(e){L.Util.setOptions(this,e),this.options.iconCreateFunction||(this.options.iconCreateFunction=this._defaultIconCreateFunction),this._featureGroup=L.featureGroup(),this._featureGroup.addEventParent(this),this._nonPointGroup=L.featureGroup(),this._nonPointGroup.addEventParent(this),this._inZoomAnimation=0,this._needsClustering=[],this._needsRemoving=[],this._currentShownBounds=null,this._queue=[],this._childMarkerEventHandlers={dragstart:this._childMarkerDragStart,move:this._childMarkerMoved,dragend:this._childMarkerDragEnd};var t=L.DomUtil.TRANSITION&&this.options.animate;L.extend(this,t?this._withAnimation:this._noAnimation),this._markerCluster=t?L.MarkerCluster:L.MarkerClusterNonAnimated},addLayer:function(e){if(e instanceof L.LayerGroup)return this.addLayers([e]);if(!e.getLatLng)return this._nonPointGroup.addLayer(e),this.fire("layeradd",{layer:e}),this;if(!this._map)return this._needsClustering.push(e),this.fire("layeradd",{layer:e}),this;if(this.hasLayer(e))return this;this._unspiderfy&&this._unspiderfy(),this._addLayer(e,this._maxZoom),this.fire("layeradd",{layer:e}),this._topClusterLevel._recalculateBounds(),this._refreshClustersIcons();var t=e,i=this._zoom;if(e.__parent)for(;t.__parent._zoom>=i;)t=t.__parent;return this._currentShownBounds.contains(t.getLatLng())&&(this.options.animateAddingMarkers?this._animationAddLayer(e,t):this._animationAddLayerNonAnimated(e,t)),this},removeLayer:function(e){return e instanceof L.LayerGroup?this.removeLayers([e]):(e.getLatLng?this._map?e.__parent&&(this._unspiderfy&&(this._unspiderfy(),this._unspiderfyLayer(e)),this._removeLayer(e,!0),this.fire("layerremove",{layer:e}),this._topClusterLevel._recalculateBounds(),this._refreshClustersIcons(),e.off(this._childMarkerEventHandlers,this),this._featureGroup.hasLayer(e)&&(this._featureGroup.removeLayer(e),e.clusterShow&&e.clusterShow())):(!this._arraySplice(this._needsClustering,e)&&this.hasLayer(e)&&this._needsRemoving.push({layer:e,latlng:e._latlng}),this.fire("layerremove",{layer:e})):(this._nonPointGroup.removeLayer(e),this.fire("layerremove",{layer:e})),this)},addLayers:function(n,s){if(!L.Util.isArray(n))return this.addLayer(n);var o,a=this._featureGroup,h=this._nonPointGroup,l=this.options.chunkedLoading,u=this.options.chunkInterval,_=this.options.chunkProgress,d=n.length,p=0,c=!0;if(this._map){var f=(new Date).getTime(),m=L.bind(function(){var e=(new Date).getTime();for(this._map&&this._unspiderfy&&this._unspiderfy();p"+t+"",className:"marker-cluster"+i,iconSize:new L.Point(40,40)})},_bindEvents:function(){var e=this._map,t=this.options.spiderfyOnMaxZoom,i=this.options.showCoverageOnHover,r=this.options.zoomToBoundsOnClick,n=this.options.spiderfyOnEveryZoom;(t||r||n)&&this.on("clusterclick clusterkeypress",this._zoomOrSpiderfy,this),i&&(this.on("clustermouseover",this._showCoverage,this),this.on("clustermouseout",this._hideCoverage,this),e.on("zoomend",this._hideCoverage,this))},_zoomOrSpiderfy:function(e){var t=e.layer,i=t;if("clusterkeypress"!==e.type||!e.originalEvent||13===e.originalEvent.keyCode){for(;1===i._childClusters.length;)i=i._childClusters[0];i._zoom===this._maxZoom&&i._childCount===t._childCount&&this.options.spiderfyOnMaxZoom?t.spiderfy():this.options.zoomToBoundsOnClick&&t.zoomToBounds(),this.options.spiderfyOnEveryZoom&&t.spiderfy(),e.originalEvent&&13===e.originalEvent.keyCode&&this._map._container.focus()}},_showCoverage:function(e){var t=this._map;this._inZoomAnimation||(this._shownPolygon&&t.removeLayer(this._shownPolygon),2h._zoom;r--)u=new this._markerCluster(this,r,u),n[r].addObject(u,this._map.project(a.getLatLng(),r));return h._addChild(u),void this._removeFromGridUnclustered(a,t)}s[t].addObject(e,i)}this._topClusterLevel._addChild(e),e.__parent=this._topClusterLevel},_refreshClustersIcons:function(){this._featureGroup.eachLayer(function(e){e instanceof L.MarkerCluster&&e._iconNeedsUpdate&&e._updateIcon()})},_enqueue:function(e){this._queue.push(e),this._queueTimeout||(this._queueTimeout=setTimeout(L.bind(this._processQueue,this),300))},_processQueue:function(){for(var e=0;ee?(this._animationStart(),this._animationZoomOut(this._zoom,e)):this._moveEnd()},_getExpandedVisibleBounds:function(){return this.options.removeOutsideVisibleBounds?L.Browser.mobile?this._checkBoundsMaxLat(this._map.getBounds()):this._checkBoundsMaxLat(this._map.getBounds().pad(1)):this._mapBoundsInfinite},_checkBoundsMaxLat:function(e){var t=this._maxLat;return void 0!==t&&(e.getNorth()>=t&&(e._northEast.lat=1/0),e.getSouth()<=-t&&(e._southWest.lat=-1/0)),e},_animationAddLayerNonAnimated:function(e,t){if(t===e)this._featureGroup.addLayer(e);else if(2===t._childCount){t._addToMap();var i=t.getAllChildMarkers();this._featureGroup.removeLayer(i[0]),this._featureGroup.removeLayer(i[1])}else t._updateIcon()},_extractNonGroupLayers:function(e,t){var i,r=e.getLayers(),n=0;for(t=t||[];ni)&&(i=(o=d).lat),(!1===r||d.latn)&&(n=(h=d).lng),(!1===s||d.lng=this._circleSpiralSwitchover?this._generatePointsSpiral(t.length,i):(i.y+=10,this._generatePointsCircle(t.length,i)),this._animationSpiderfy(t,e)}},unspiderfy:function(e){this._group._inZoomAnimation||(this._animationUnspiderfy(e),this._group._spiderfied=null)},_generatePointsCircle:function(e,t){var i,r,n=this._group.options.spiderfyDistanceMultiplier*this._circleFootSeparation*(2+e)/this._2PI,s=this._2PI/e,o=[];for(n=Math.max(n,35),o.length=e,i=0;iYnU^5s62$4H-fe}gSR(=wKRaTHh!@*b)YV6mo|a4Fn6Rgc&Rpk zvn_X|3VY?v=>nJ{slE^V1GaGWk}m@aIWGIpghbfPh8m@aIWEo_%AZI>==moIFVE^L=C zZJ91?mo03UEp3-BY?wBGur6$uD{Yr9Y?m%SHF8Fk1pc(Nva%QJ+{FLkalfypz3&M|||Fn`7|g3c~4(nXHKFmRnwn$J#_$xE8i z|Ns9!kC;(oC1qQk>LMp3_a2(odYyMT@>voX=UI)k>1cJdn;gjmJ-|6v4nb1Oryh)eQMwHP(i@!36%vGJyFK(JTj?Vb{{C=jx&)@1l zlFmnw%0`&bqruifkkHKC=vbiAM3&E`#Mv>2%tw;VK8?_|&E89cs{a1}$J*!f_xd-C z&F%B|oxRgPlh0F!txkxrQjNA`m9~?&&|jw4W0<`_iNHsX$VQXVK!B}Xkh4>av|f_8 zLY2?t?ejE=%(TnfV5iqOjm?d;&qI~ZGl|SzU77a)002XDQchC<95+*MjE@82?VLm= z3xf6%Vd@99z|q|-ua5l3kJxvZwan-8K1cPiwQAtlcNX~ZqLeoMB+a;7)WA|O#HOB% zg6SX;754xD1{Fy}K~#8Ntklac&zTpadXZ& zC*_=T&g7hfbI$R?v%9?sknIb97gJOJ=`-8YyS3ndqN+Jm+x33!p&Hc@@L$w))s2@N ztv~i}Emc?DykgwFWwma($8+~b>l?tqj$dh13R^nMZnva9 zn0Vflzv2Dvp`oVQw{Guby~i`JGbyBGTEC{y>yzCkg>K&CIeQ$u;lyQ+M{O~gEJ^)Z zrF3p)^>|uT;57}WY&IRwyOQ=dq%Az}_t=_hKowP!Z79q0;@Zu(SWEJJcHY+5T6I({ zw)wj*SNi4wrd+POUfZe4gF77vW?j zoFS}|r2n&$U9Y!S4VEOyN}OpZZi|?cr1VcE_tHsDQgp-ga(SwkBrkCm{|*-yb=}ZW zvcYvLvfA90TPn|!-TuYJV<6`}+RJeRgP3EA=qQcF9k0*#*{f&I_pjam%I6Dd#YE|G zqB!R}tW-K!wV1w+4JcFA_s6~=@9F&j8`u$-ifLN3vK;`lvaA-`jRn_}(8|)!3?-}I zvFi{H;@A$gEZYh?%|Qr_y#*UkOPjwiRCsJQ>mb6h5yGIk6C5_XA=8T?IBfm_?+P0; zhhUs)-(0R*H<&Kku(1>#cGtOpk&Z&kQcw&SJv-4VY<+;=8hYnoX zfNJMCa9)^5Z0;2dCUk;x-%#yS!I~Jr3pNuI!g_tHz!$hKwt1GL~sFvx)3u4TA zv>CLGdQtoZ7Du7ctJRfTqY;FPxs1G{ZJ?73D5J@OO{6BHcPbk{_mjg&p2QFeke%QI zlAJ-kvjuwy1<5D-6>su68A+i998aSZNnQX)+Q}6(GK-C%8G-!1bOJBONU{gT%IOOE z;Yk24YC@^lFW77>r6x7eS1Omc;8=GUp#&zLQ&L{ zv8$hGC`wp~$9pR>f%-_Ps3>YhzP(+vC(E*zr1CVO8ChN^MI-VGMX7+|(r!SGZ9gd5 zzO9sQd>sm|f1|X&oh=8lOzd6+ITvo zCXInR?>RZ#>Hb*PO=7dI!dZ(wY4O}ZGv zdfQFio7+0~PN*RFCZGM6@9-o~y*@?;k00NvOsw54t1^tt{*ATMs^2j}4Wp=4t3RH* z_+8b`F-{E=0sOgM<;VHTo!Ij3u zmmI`2?K7g(GOcGA)@h?$SW&pwHdtj1n57PLI8&6RHhx4R%Q7b z^JEqR)@06V!pbS*@D_ZyRMo_LlT}r{#sXOx4kM-V<_V{!5SSuM^SIVCA37|nY7LWQ zZA#B1h4l`6asz=Lvax_#GMRX|NF>=$=p{Qn0i@ExX1jGhy@B8a*_uR+ODEbVi8ObL zezG?azy>E~S~dl43&8<$(2H}P&*tuBdESUP83KQ?8B z?K(!uS>H1wlWQz;qOfB`T#TZ=EoSp~vZ5XtCvwm1h*Ex6mzTsn_y@_=xREIslV-%- zpdWkEzMjeNOGWrSM32gpBt27*O29NdhGzuDgYxcf`Jjjqw@B;Vmdb@fxdhCRi`Kg> zmUTr$=&@#i!%F4Q6mb&4QKfR^95KJ!<6~fqx-f^66AV!|ywG{6D^Vay-3b99>XOe# e-I|>x8~*?ZhF3snGbtJX0000cOl4 literal 0 HcmV?d00001 diff --git a/vendor/leaflet/images/marker-icon.png b/vendor/leaflet/images/marker-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..950edf24677ded147df13b26f91baa2b0fa70513 GIT binary patch literal 1466 zcmV;r1x5OaP)P001cn1^@s6z>|W`000GnNklGNuHDcIX17Zdjl&3`L?0sTjIws<{((Dh&g-s0<@jYQyl?D*X^?%13;ml^gy> ziMrY_^1WI=(g@LMizu=zCoA>C`6|QEq1eV92k*7m>G65*&@&6)aC&e}G zI)pf-Za|N`DT&Cn1J|o`19mumxW~hiKiKyc-P`S@q)rdTo84@QI@;0yXrG%9uhI>A zG5QHb6s4=<6xy{1 z@NMxEkryp{LS44%z$3lP^cX!9+2-;CTt3wM4(k*#C{aiIiLuB>jJj;KPhPzIC00bL zU3a#;aJld94lCW=`4&aAy8M7PY=HQ>O%$YEP4c4UY#CRxfgbE~(|uiI=YS8q;O9y6 zmIkXzR`}p7ti|PrM3a}WMnR=3NVnWdAAR>b9X@)DKL6=YsvmH%?I24wdq?Gh54_;# z$?_LvgjEdspdQlft#4CQ z`2Zyvy?*)N1Ftw|{_hakhG9WjS?Az@I@+IZ8JbWewR!XUK4&6346+d#~gsE0SY(LX8&JfY>Aj)RxGy96nwhs2rv zzW6pTnMpFkDSkT*a*6Dx|u@ds6ISVn0@^RmIsKZ5Y;bazbc;tTSq(kg(=481ODrPyNB6n z-$+U}(w$m6U6H$w17Bw+wDaFIe~GvNMYvnw31MpY0eQKT9l>SU``8k7w4)z!GZKMI z#_cEKq7k~i%nlK@6c-K?+R;B#5$?T#YpKD`t_4bAs^#E+@5QW$@OX3*`;(#{U^d-vY)&xEE>n5lYl&T?Amke9$Lam@{1K@O ze*LXqlKQHiv=gx+V^Cbb2?z@ISBQ*3amF;9UJ3SBg(N|710TLamQmYZ&Qjn2LuO<* zCZlB4n%@pc&7NNnY1}x+NWpHlq`OJEo|`aYN9<`RBUB+79g;>dgb6YlfN#kGL?lO_ z!6~M^7sOnbsUkKk<@Ysie&`G>ruxH&Mgy&8;i=A zB9OO!xR{AyODw>DS-q5YM{0ExFEAzt zm>RdS+ssW(-8|?xr0(?$vBVB*%(xDLtq3Hf0I5yFm<_g=W2`QWAax{1rWVH=I!VrP zs(rTFX@W#t$hXNvbgX`gK&^w_YD;CQ!B@e0QbLIWaKAXQe2-kkloo;{iF#6}z!4=W zi$giRj1{ zt;2w`VSCF#WE&*ev7jpsC=6175@(~nTE2;7M-L((0bH@yG}-TB$R~WXd?tA$s3|%y zA`9$sA(>F%J3ioz<-LJl*^o1|w84l>HBR`>3l9c8$5Xr@xCiIQ7{x$fMCzOk_-M=% z+{a_Q#;42`#KfUte@$NT77uaTz?b-fBe)1s5XE$yA79fm?KqM^VgLXD07*qoM6N<$ Ef<_J(9smFU literal 0 HcmV?d00001 diff --git a/vendor/leaflet/leaflet.css b/vendor/leaflet/leaflet.css new file mode 100644 index 0000000..2961b76 --- /dev/null +++ b/vendor/leaflet/leaflet.css @@ -0,0 +1,661 @@ +/* required styles */ + +.leaflet-pane, +.leaflet-tile, +.leaflet-marker-icon, +.leaflet-marker-shadow, +.leaflet-tile-container, +.leaflet-pane > svg, +.leaflet-pane > canvas, +.leaflet-zoom-box, +.leaflet-image-layer, +.leaflet-layer { + position: absolute; + left: 0; + top: 0; + } +.leaflet-container { + overflow: hidden; + } +.leaflet-tile, +.leaflet-marker-icon, +.leaflet-marker-shadow { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + -webkit-user-drag: none; + } +/* Prevents IE11 from highlighting tiles in blue */ +.leaflet-tile::selection { + background: transparent; +} +/* Safari renders non-retina tile on retina better with this, but Chrome is worse */ +.leaflet-safari .leaflet-tile { + image-rendering: -webkit-optimize-contrast; + } +/* hack that prevents hw layers "stretching" when loading new tiles */ +.leaflet-safari .leaflet-tile-container { + width: 1600px; + height: 1600px; + -webkit-transform-origin: 0 0; + } +.leaflet-marker-icon, +.leaflet-marker-shadow { + display: block; + } +/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */ +/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */ +.leaflet-container .leaflet-overlay-pane svg { + max-width: none !important; + max-height: none !important; + } +.leaflet-container .leaflet-marker-pane img, +.leaflet-container .leaflet-shadow-pane img, +.leaflet-container .leaflet-tile-pane img, +.leaflet-container img.leaflet-image-layer, +.leaflet-container .leaflet-tile { + max-width: none !important; + max-height: none !important; + width: auto; + padding: 0; + } + +.leaflet-container img.leaflet-tile { + /* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */ + mix-blend-mode: plus-lighter; +} + +.leaflet-container.leaflet-touch-zoom { + -ms-touch-action: pan-x pan-y; + touch-action: pan-x pan-y; + } +.leaflet-container.leaflet-touch-drag { + -ms-touch-action: pinch-zoom; + /* Fallback for FF which doesn't support pinch-zoom */ + touch-action: none; + touch-action: pinch-zoom; +} +.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom { + -ms-touch-action: none; + touch-action: none; +} +.leaflet-container { + -webkit-tap-highlight-color: transparent; +} +.leaflet-container a { + -webkit-tap-highlight-color: rgba(51, 181, 229, 0.4); +} +.leaflet-tile { + filter: inherit; + visibility: hidden; + } +.leaflet-tile-loaded { + visibility: inherit; + } +.leaflet-zoom-box { + width: 0; + height: 0; + -moz-box-sizing: border-box; + box-sizing: border-box; + z-index: 800; + } +/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */ +.leaflet-overlay-pane svg { + -moz-user-select: none; + } + +.leaflet-pane { z-index: 400; } + +.leaflet-tile-pane { z-index: 200; } +.leaflet-overlay-pane { z-index: 400; } +.leaflet-shadow-pane { z-index: 500; } +.leaflet-marker-pane { z-index: 600; } +.leaflet-tooltip-pane { z-index: 650; } +.leaflet-popup-pane { z-index: 700; } + +.leaflet-map-pane canvas { z-index: 100; } +.leaflet-map-pane svg { z-index: 200; } + +.leaflet-vml-shape { + width: 1px; + height: 1px; + } +.lvml { + behavior: url(#default#VML); + display: inline-block; + position: absolute; + } + + +/* control positioning */ + +.leaflet-control { + position: relative; + z-index: 800; + pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ + pointer-events: auto; + } +.leaflet-top, +.leaflet-bottom { + position: absolute; + z-index: 1000; + pointer-events: none; + } +.leaflet-top { + top: 0; + } +.leaflet-right { + right: 0; + } +.leaflet-bottom { + bottom: 0; + } +.leaflet-left { + left: 0; + } +.leaflet-control { + float: left; + clear: both; + } +.leaflet-right .leaflet-control { + float: right; + } +.leaflet-top .leaflet-control { + margin-top: 10px; + } +.leaflet-bottom .leaflet-control { + margin-bottom: 10px; + } +.leaflet-left .leaflet-control { + margin-left: 10px; + } +.leaflet-right .leaflet-control { + margin-right: 10px; + } + + +/* zoom and fade animations */ + +.leaflet-fade-anim .leaflet-popup { + opacity: 0; + -webkit-transition: opacity 0.2s linear; + -moz-transition: opacity 0.2s linear; + transition: opacity 0.2s linear; + } +.leaflet-fade-anim .leaflet-map-pane .leaflet-popup { + opacity: 1; + } +.leaflet-zoom-animated { + -webkit-transform-origin: 0 0; + -ms-transform-origin: 0 0; + transform-origin: 0 0; + } +svg.leaflet-zoom-animated { + will-change: transform; +} + +.leaflet-zoom-anim .leaflet-zoom-animated { + -webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1); + -moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1); + transition: transform 0.25s cubic-bezier(0,0,0.25,1); + } +.leaflet-zoom-anim .leaflet-tile, +.leaflet-pan-anim .leaflet-tile { + -webkit-transition: none; + -moz-transition: none; + transition: none; + } + +.leaflet-zoom-anim .leaflet-zoom-hide { + visibility: hidden; + } + + +/* cursors */ + +.leaflet-interactive { + cursor: pointer; + } +.leaflet-grab { + cursor: -webkit-grab; + cursor: -moz-grab; + cursor: grab; + } +.leaflet-crosshair, +.leaflet-crosshair .leaflet-interactive { + cursor: crosshair; + } +.leaflet-popup-pane, +.leaflet-control { + cursor: auto; + } +.leaflet-dragging .leaflet-grab, +.leaflet-dragging .leaflet-grab .leaflet-interactive, +.leaflet-dragging .leaflet-marker-draggable { + cursor: move; + cursor: -webkit-grabbing; + cursor: -moz-grabbing; + cursor: grabbing; + } + +/* marker & overlays interactivity */ +.leaflet-marker-icon, +.leaflet-marker-shadow, +.leaflet-image-layer, +.leaflet-pane > svg path, +.leaflet-tile-container { + pointer-events: none; + } + +.leaflet-marker-icon.leaflet-interactive, +.leaflet-image-layer.leaflet-interactive, +.leaflet-pane > svg path.leaflet-interactive, +svg.leaflet-image-layer.leaflet-interactive path { + pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ + pointer-events: auto; + } + +/* visual tweaks */ + +.leaflet-container { + background: #ddd; + outline-offset: 1px; + } +.leaflet-container a { + color: #0078A8; + } +.leaflet-zoom-box { + border: 2px dotted #38f; + background: rgba(255,255,255,0.5); + } + + +/* general typography */ +.leaflet-container { + font-family: "Helvetica Neue", Arial, Helvetica, sans-serif; + font-size: 12px; + font-size: 0.75rem; + line-height: 1.5; + } + + +/* general toolbar styles */ + +.leaflet-bar { + box-shadow: 0 1px 5px rgba(0,0,0,0.65); + border-radius: 4px; + } +.leaflet-bar a { + background-color: #fff; + border-bottom: 1px solid #ccc; + width: 26px; + height: 26px; + line-height: 26px; + display: block; + text-align: center; + text-decoration: none; + color: black; + } +.leaflet-bar a, +.leaflet-control-layers-toggle { + background-position: 50% 50%; + background-repeat: no-repeat; + display: block; + } +.leaflet-bar a:hover, +.leaflet-bar a:focus { + background-color: #f4f4f4; + } +.leaflet-bar a:first-child { + border-top-left-radius: 4px; + border-top-right-radius: 4px; + } +.leaflet-bar a:last-child { + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + border-bottom: none; + } +.leaflet-bar a.leaflet-disabled { + cursor: default; + background-color: #f4f4f4; + color: #bbb; + } + +.leaflet-touch .leaflet-bar a { + width: 30px; + height: 30px; + line-height: 30px; + } +.leaflet-touch .leaflet-bar a:first-child { + border-top-left-radius: 2px; + border-top-right-radius: 2px; + } +.leaflet-touch .leaflet-bar a:last-child { + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; + } + +/* zoom control */ + +.leaflet-control-zoom-in, +.leaflet-control-zoom-out { + font: bold 18px 'Lucida Console', Monaco, monospace; + text-indent: 1px; + } + +.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out { + font-size: 22px; + } + + +/* layers control */ + +.leaflet-control-layers { + box-shadow: 0 1px 5px rgba(0,0,0,0.4); + background: #fff; + border-radius: 5px; + } +.leaflet-control-layers-toggle { + background-image: url(images/layers.png); + width: 36px; + height: 36px; + } +.leaflet-retina .leaflet-control-layers-toggle { + background-image: url(images/layers-2x.png); + background-size: 26px 26px; + } +.leaflet-touch .leaflet-control-layers-toggle { + width: 44px; + height: 44px; + } +.leaflet-control-layers .leaflet-control-layers-list, +.leaflet-control-layers-expanded .leaflet-control-layers-toggle { + display: none; + } +.leaflet-control-layers-expanded .leaflet-control-layers-list { + display: block; + position: relative; + } +.leaflet-control-layers-expanded { + padding: 6px 10px 6px 6px; + color: #333; + background: #fff; + } +.leaflet-control-layers-scrollbar { + overflow-y: scroll; + overflow-x: hidden; + padding-right: 5px; + } +.leaflet-control-layers-selector { + margin-top: 2px; + position: relative; + top: 1px; + } +.leaflet-control-layers label { + display: block; + font-size: 13px; + font-size: 1.08333em; + } +.leaflet-control-layers-separator { + height: 0; + border-top: 1px solid #ddd; + margin: 5px -10px 5px -6px; + } + +/* Default icon URLs */ +.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */ + background-image: url(images/marker-icon.png); + } + + +/* attribution and scale controls */ + +.leaflet-container .leaflet-control-attribution { + background: #fff; + background: rgba(255, 255, 255, 0.8); + margin: 0; + } +.leaflet-control-attribution, +.leaflet-control-scale-line { + padding: 0 5px; + color: #333; + line-height: 1.4; + } +.leaflet-control-attribution a { + text-decoration: none; + } +.leaflet-control-attribution a:hover, +.leaflet-control-attribution a:focus { + text-decoration: underline; + } +.leaflet-attribution-flag { + display: inline !important; + vertical-align: baseline !important; + width: 1em; + height: 0.6669em; + } +.leaflet-left .leaflet-control-scale { + margin-left: 5px; + } +.leaflet-bottom .leaflet-control-scale { + margin-bottom: 5px; + } +.leaflet-control-scale-line { + border: 2px solid #777; + border-top: none; + line-height: 1.1; + padding: 2px 5px 1px; + white-space: nowrap; + -moz-box-sizing: border-box; + box-sizing: border-box; + background: rgba(255, 255, 255, 0.8); + text-shadow: 1px 1px #fff; + } +.leaflet-control-scale-line:not(:first-child) { + border-top: 2px solid #777; + border-bottom: none; + margin-top: -2px; + } +.leaflet-control-scale-line:not(:first-child):not(:last-child) { + border-bottom: 2px solid #777; + } + +.leaflet-touch .leaflet-control-attribution, +.leaflet-touch .leaflet-control-layers, +.leaflet-touch .leaflet-bar { + box-shadow: none; + } +.leaflet-touch .leaflet-control-layers, +.leaflet-touch .leaflet-bar { + border: 2px solid rgba(0,0,0,0.2); + background-clip: padding-box; + } + + +/* popup */ + +.leaflet-popup { + position: absolute; + text-align: center; + margin-bottom: 20px; + } +.leaflet-popup-content-wrapper { + padding: 1px; + text-align: left; + border-radius: 12px; + } +.leaflet-popup-content { + margin: 13px 24px 13px 20px; + line-height: 1.3; + font-size: 13px; + font-size: 1.08333em; + min-height: 1px; + } +.leaflet-popup-content p { + margin: 17px 0; + margin: 1.3em 0; + } +.leaflet-popup-tip-container { + width: 40px; + height: 20px; + position: absolute; + left: 50%; + margin-top: -1px; + margin-left: -20px; + overflow: hidden; + pointer-events: none; + } +.leaflet-popup-tip { + width: 17px; + height: 17px; + padding: 1px; + + margin: -10px auto 0; + pointer-events: auto; + + -webkit-transform: rotate(45deg); + -moz-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); + } +.leaflet-popup-content-wrapper, +.leaflet-popup-tip { + background: white; + color: #333; + box-shadow: 0 3px 14px rgba(0,0,0,0.4); + } +.leaflet-container a.leaflet-popup-close-button { + position: absolute; + top: 0; + right: 0; + border: none; + text-align: center; + width: 24px; + height: 24px; + font: 16px/24px Tahoma, Verdana, sans-serif; + color: #757575; + text-decoration: none; + background: transparent; + } +.leaflet-container a.leaflet-popup-close-button:hover, +.leaflet-container a.leaflet-popup-close-button:focus { + color: #585858; + } +.leaflet-popup-scrolled { + overflow: auto; + } + +.leaflet-oldie .leaflet-popup-content-wrapper { + -ms-zoom: 1; + } +.leaflet-oldie .leaflet-popup-tip { + width: 24px; + margin: 0 auto; + + -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; + filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); + } + +.leaflet-oldie .leaflet-control-zoom, +.leaflet-oldie .leaflet-control-layers, +.leaflet-oldie .leaflet-popup-content-wrapper, +.leaflet-oldie .leaflet-popup-tip { + border: 1px solid #999; + } + + +/* div icon */ + +.leaflet-div-icon { + background: #fff; + border: 1px solid #666; + } + + +/* Tooltip */ +/* Base styles for the element that has a tooltip */ +.leaflet-tooltip { + position: absolute; + padding: 6px; + background-color: #fff; + border: 1px solid #fff; + border-radius: 3px; + color: #222; + white-space: nowrap; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + pointer-events: none; + box-shadow: 0 1px 3px rgba(0,0,0,0.4); + } +.leaflet-tooltip.leaflet-interactive { + cursor: pointer; + pointer-events: auto; + } +.leaflet-tooltip-top:before, +.leaflet-tooltip-bottom:before, +.leaflet-tooltip-left:before, +.leaflet-tooltip-right:before { + position: absolute; + pointer-events: none; + border: 6px solid transparent; + background: transparent; + content: ""; + } + +/* Directions */ + +.leaflet-tooltip-bottom { + margin-top: 6px; +} +.leaflet-tooltip-top { + margin-top: -6px; +} +.leaflet-tooltip-bottom:before, +.leaflet-tooltip-top:before { + left: 50%; + margin-left: -6px; + } +.leaflet-tooltip-top:before { + bottom: 0; + margin-bottom: -12px; + border-top-color: #fff; + } +.leaflet-tooltip-bottom:before { + top: 0; + margin-top: -12px; + margin-left: -6px; + border-bottom-color: #fff; + } +.leaflet-tooltip-left { + margin-left: -6px; +} +.leaflet-tooltip-right { + margin-left: 6px; +} +.leaflet-tooltip-left:before, +.leaflet-tooltip-right:before { + top: 50%; + margin-top: -6px; + } +.leaflet-tooltip-left:before { + right: 0; + margin-right: -12px; + border-left-color: #fff; + } +.leaflet-tooltip-right:before { + left: 0; + margin-left: -12px; + border-right-color: #fff; + } + +/* Printing */ + +@media print { + /* Prevent printers from removing background-images of controls. */ + .leaflet-control { + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + } + } diff --git a/vendor/leaflet/leaflet.js b/vendor/leaflet/leaflet.js new file mode 100644 index 0000000..a3bf693 --- /dev/null +++ b/vendor/leaflet/leaflet.js @@ -0,0 +1,6 @@ +/* @preserve + * Leaflet 1.9.4, a JS library for interactive maps. https://leafletjs.com + * (c) 2010-2023 Vladimir Agafonkin, (c) 2010-2011 CloudMade + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).leaflet={})}(this,function(t){"use strict";function l(t){for(var e,i,n=1,o=arguments.length;n=this.min.x&&i.x<=this.max.x&&e.y>=this.min.y&&i.y<=this.max.y},intersects:function(t){t=_(t);var e=this.min,i=this.max,n=t.min,t=t.max,o=t.x>=e.x&&n.x<=i.x,t=t.y>=e.y&&n.y<=i.y;return o&&t},overlaps:function(t){t=_(t);var e=this.min,i=this.max,n=t.min,t=t.max,o=t.x>e.x&&n.xe.y&&n.y=n.lat&&i.lat<=o.lat&&e.lng>=n.lng&&i.lng<=o.lng},intersects:function(t){t=g(t);var e=this._southWest,i=this._northEast,n=t.getSouthWest(),t=t.getNorthEast(),o=t.lat>=e.lat&&n.lat<=i.lat,t=t.lng>=e.lng&&n.lng<=i.lng;return o&&t},overlaps:function(t){t=g(t);var e=this._southWest,i=this._northEast,n=t.getSouthWest(),t=t.getNorthEast(),o=t.lat>e.lat&&n.late.lng&&n.lng","http://www.w3.org/2000/svg"===(Wt.firstChild&&Wt.firstChild.namespaceURI));function y(t){return 0<=navigator.userAgent.toLowerCase().indexOf(t)}var b={ie:pt,ielt9:mt,edge:n,webkit:ft,android:gt,android23:vt,androidStock:yt,opera:xt,chrome:wt,gecko:bt,safari:Pt,phantom:Lt,opera12:o,win:Tt,ie3d:Mt,webkit3d:zt,gecko3d:_t,any3d:Ct,mobile:Zt,mobileWebkit:St,mobileWebkit3d:Et,msPointer:kt,pointer:Ot,touch:Bt,touchNative:At,mobileOpera:It,mobileGecko:Rt,retina:Nt,passiveEvents:Dt,canvas:jt,svg:Ht,vml:!Ht&&function(){try{var t=document.createElement("div"),e=(t.innerHTML='',t.firstChild);return e.style.behavior="url(#default#VML)",e&&"object"==typeof e.adj}catch(t){return!1}}(),inlineSvg:Wt,mac:0===navigator.platform.indexOf("Mac"),linux:0===navigator.platform.indexOf("Linux")},Ft=b.msPointer?"MSPointerDown":"pointerdown",Ut=b.msPointer?"MSPointerMove":"pointermove",Vt=b.msPointer?"MSPointerUp":"pointerup",qt=b.msPointer?"MSPointerCancel":"pointercancel",Gt={touchstart:Ft,touchmove:Ut,touchend:Vt,touchcancel:qt},Kt={touchstart:function(t,e){e.MSPOINTER_TYPE_TOUCH&&e.pointerType===e.MSPOINTER_TYPE_TOUCH&&O(e);ee(t,e)},touchmove:ee,touchend:ee,touchcancel:ee},Yt={},Xt=!1;function Jt(t,e,i){return"touchstart"!==e||Xt||(document.addEventListener(Ft,$t,!0),document.addEventListener(Ut,Qt,!0),document.addEventListener(Vt,te,!0),document.addEventListener(qt,te,!0),Xt=!0),Kt[e]?(i=Kt[e].bind(this,i),t.addEventListener(Gt[e],i,!1),i):(console.warn("wrong event specified:",e),u)}function $t(t){Yt[t.pointerId]=t}function Qt(t){Yt[t.pointerId]&&(Yt[t.pointerId]=t)}function te(t){delete Yt[t.pointerId]}function ee(t,e){if(e.pointerType!==(e.MSPOINTER_TYPE_MOUSE||"mouse")){for(var i in e.touches=[],Yt)e.touches.push(Yt[i]);e.changedTouches=[e],t(e)}}var ie=200;function ne(t,i){t.addEventListener("dblclick",i);var n,o=0;function e(t){var e;1!==t.detail?n=t.detail:"mouse"===t.pointerType||t.sourceCapabilities&&!t.sourceCapabilities.firesTouchEvents||((e=Ne(t)).some(function(t){return t instanceof HTMLLabelElement&&t.attributes.for})&&!e.some(function(t){return t instanceof HTMLInputElement||t instanceof HTMLSelectElement})||((e=Date.now())-o<=ie?2===++n&&i(function(t){var e,i,n={};for(i in t)e=t[i],n[i]=e&&e.bind?e.bind(t):e;return(t=n).type="dblclick",n.detail=2,n.isTrusted=!1,n._simulated=!0,n}(t)):n=1,o=e))}return t.addEventListener("click",e),{dblclick:i,simDblclick:e}}var oe,se,re,ae,he,le,ue=we(["transform","webkitTransform","OTransform","MozTransform","msTransform"]),ce=we(["webkitTransition","transition","OTransition","MozTransition","msTransition"]),de="webkitTransition"===ce||"OTransition"===ce?ce+"End":"transitionend";function _e(t){return"string"==typeof t?document.getElementById(t):t}function pe(t,e){var i=t.style[e]||t.currentStyle&&t.currentStyle[e];return"auto"===(i=i&&"auto"!==i||!document.defaultView?i:(t=document.defaultView.getComputedStyle(t,null))?t[e]:null)?null:i}function P(t,e,i){t=document.createElement(t);return t.className=e||"",i&&i.appendChild(t),t}function T(t){var e=t.parentNode;e&&e.removeChild(t)}function me(t){for(;t.firstChild;)t.removeChild(t.firstChild)}function fe(t){var e=t.parentNode;e&&e.lastChild!==t&&e.appendChild(t)}function ge(t){var e=t.parentNode;e&&e.firstChild!==t&&e.insertBefore(t,e.firstChild)}function ve(t,e){return void 0!==t.classList?t.classList.contains(e):0<(t=xe(t)).length&&new RegExp("(^|\\s)"+e+"(\\s|$)").test(t)}function M(t,e){var i;if(void 0!==t.classList)for(var n=F(e),o=0,s=n.length;othis.options.maxZoom)?this.setZoom(t):this},panInsideBounds:function(t,e){this._enforcingBounds=!0;var i=this.getCenter(),t=this._limitCenter(i,this._zoom,g(t));return i.equals(t)||this.panTo(t,e),this._enforcingBounds=!1,this},panInside:function(t,e){var i=m((e=e||{}).paddingTopLeft||e.padding||[0,0]),n=m(e.paddingBottomRight||e.padding||[0,0]),o=this.project(this.getCenter()),t=this.project(t),s=this.getPixelBounds(),i=_([s.min.add(i),s.max.subtract(n)]),s=i.getSize();return i.contains(t)||(this._enforcingBounds=!0,n=t.subtract(i.getCenter()),i=i.extend(t).getSize().subtract(s),o.x+=n.x<0?-i.x:i.x,o.y+=n.y<0?-i.y:i.y,this.panTo(this.unproject(o),e),this._enforcingBounds=!1),this},invalidateSize:function(t){if(!this._loaded)return this;t=l({animate:!1,pan:!0},!0===t?{animate:!0}:t);var e=this.getSize(),i=(this._sizeChanged=!0,this._lastCenter=null,this.getSize()),n=e.divideBy(2).round(),o=i.divideBy(2).round(),n=n.subtract(o);return n.x||n.y?(t.animate&&t.pan?this.panBy(n):(t.pan&&this._rawPanBy(n),this.fire("move"),t.debounceMoveend?(clearTimeout(this._sizeTimer),this._sizeTimer=setTimeout(a(this.fire,this,"moveend"),200)):this.fire("moveend")),this.fire("resize",{oldSize:e,newSize:i})):this},stop:function(){return this.setZoom(this._limitZoom(this._zoom)),this.options.zoomSnap||this.fire("viewreset"),this._stop()},locate:function(t){var e,i;return t=this._locateOptions=l({timeout:1e4,watch:!1},t),"geolocation"in navigator?(e=a(this._handleGeolocationResponse,this),i=a(this._handleGeolocationError,this),t.watch?this._locationWatchId=navigator.geolocation.watchPosition(e,i,t):navigator.geolocation.getCurrentPosition(e,i,t)):this._handleGeolocationError({code:0,message:"Geolocation not supported."}),this},stopLocate:function(){return navigator.geolocation&&navigator.geolocation.clearWatch&&navigator.geolocation.clearWatch(this._locationWatchId),this._locateOptions&&(this._locateOptions.setView=!1),this},_handleGeolocationError:function(t){var e;this._container._leaflet_id&&(e=t.code,t=t.message||(1===e?"permission denied":2===e?"position unavailable":"timeout"),this._locateOptions.setView&&!this._loaded&&this.fitWorld(),this.fire("locationerror",{code:e,message:"Geolocation error: "+t+"."}))},_handleGeolocationResponse:function(t){if(this._container._leaflet_id){var e,i,n=new v(t.coords.latitude,t.coords.longitude),o=n.toBounds(2*t.coords.accuracy),s=this._locateOptions,r=(s.setView&&(e=this.getBoundsZoom(o),this.setView(n,s.maxZoom?Math.min(e,s.maxZoom):e)),{latlng:n,bounds:o,timestamp:t.timestamp});for(i in t.coords)"number"==typeof t.coords[i]&&(r[i]=t.coords[i]);this.fire("locationfound",r)}},addHandler:function(t,e){return e&&(e=this[t]=new e(this),this._handlers.push(e),this.options[t]&&e.enable()),this},remove:function(){if(this._initEvents(!0),this.options.maxBounds&&this.off("moveend",this._panInsideMaxBounds),this._containerId!==this._container._leaflet_id)throw new Error("Map container is being reused by another instance");try{delete this._container._leaflet_id,delete this._containerId}catch(t){this._container._leaflet_id=void 0,this._containerId=void 0}for(var t in void 0!==this._locationWatchId&&this.stopLocate(),this._stop(),T(this._mapPane),this._clearControlPos&&this._clearControlPos(),this._resizeRequest&&(r(this._resizeRequest),this._resizeRequest=null),this._clearHandlers(),this._loaded&&this.fire("unload"),this._layers)this._layers[t].remove();for(t in this._panes)T(this._panes[t]);return this._layers=[],this._panes=[],delete this._mapPane,delete this._renderer,this},createPane:function(t,e){e=P("div","leaflet-pane"+(t?" leaflet-"+t.replace("Pane","")+"-pane":""),e||this._mapPane);return t&&(this._panes[t]=e),e},getCenter:function(){return this._checkIfLoaded(),this._lastCenter&&!this._moved()?this._lastCenter.clone():this.layerPointToLatLng(this._getCenterLayerPoint())},getZoom:function(){return this._zoom},getBounds:function(){var t=this.getPixelBounds();return new s(this.unproject(t.getBottomLeft()),this.unproject(t.getTopRight()))},getMinZoom:function(){return void 0===this.options.minZoom?this._layersMinZoom||0:this.options.minZoom},getMaxZoom:function(){return void 0===this.options.maxZoom?void 0===this._layersMaxZoom?1/0:this._layersMaxZoom:this.options.maxZoom},getBoundsZoom:function(t,e,i){t=g(t),i=m(i||[0,0]);var n=this.getZoom()||0,o=this.getMinZoom(),s=this.getMaxZoom(),r=t.getNorthWest(),t=t.getSouthEast(),i=this.getSize().subtract(i),t=_(this.project(t,n),this.project(r,n)).getSize(),r=b.any3d?this.options.zoomSnap:1,a=i.x/t.x,i=i.y/t.y,t=e?Math.max(a,i):Math.min(a,i),n=this.getScaleZoom(t,n);return r&&(n=Math.round(n/(r/100))*(r/100),n=e?Math.ceil(n/r)*r:Math.floor(n/r)*r),Math.max(o,Math.min(s,n))},getSize:function(){return this._size&&!this._sizeChanged||(this._size=new p(this._container.clientWidth||0,this._container.clientHeight||0),this._sizeChanged=!1),this._size.clone()},getPixelBounds:function(t,e){t=this._getTopLeftPoint(t,e);return new f(t,t.add(this.getSize()))},getPixelOrigin:function(){return this._checkIfLoaded(),this._pixelOrigin},getPixelWorldBounds:function(t){return this.options.crs.getProjectedBounds(void 0===t?this.getZoom():t)},getPane:function(t){return"string"==typeof t?this._panes[t]:t},getPanes:function(){return this._panes},getContainer:function(){return this._container},getZoomScale:function(t,e){var i=this.options.crs;return e=void 0===e?this._zoom:e,i.scale(t)/i.scale(e)},getScaleZoom:function(t,e){var i=this.options.crs,t=(e=void 0===e?this._zoom:e,i.zoom(t*i.scale(e)));return isNaN(t)?1/0:t},project:function(t,e){return e=void 0===e?this._zoom:e,this.options.crs.latLngToPoint(w(t),e)},unproject:function(t,e){return e=void 0===e?this._zoom:e,this.options.crs.pointToLatLng(m(t),e)},layerPointToLatLng:function(t){t=m(t).add(this.getPixelOrigin());return this.unproject(t)},latLngToLayerPoint:function(t){return this.project(w(t))._round()._subtract(this.getPixelOrigin())},wrapLatLng:function(t){return this.options.crs.wrapLatLng(w(t))},wrapLatLngBounds:function(t){return this.options.crs.wrapLatLngBounds(g(t))},distance:function(t,e){return this.options.crs.distance(w(t),w(e))},containerPointToLayerPoint:function(t){return m(t).subtract(this._getMapPanePos())},layerPointToContainerPoint:function(t){return m(t).add(this._getMapPanePos())},containerPointToLatLng:function(t){t=this.containerPointToLayerPoint(m(t));return this.layerPointToLatLng(t)},latLngToContainerPoint:function(t){return this.layerPointToContainerPoint(this.latLngToLayerPoint(w(t)))},mouseEventToContainerPoint:function(t){return De(t,this._container)},mouseEventToLayerPoint:function(t){return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(t))},mouseEventToLatLng:function(t){return this.layerPointToLatLng(this.mouseEventToLayerPoint(t))},_initContainer:function(t){t=this._container=_e(t);if(!t)throw new Error("Map container not found.");if(t._leaflet_id)throw new Error("Map container is already initialized.");S(t,"scroll",this._onScroll,this),this._containerId=h(t)},_initLayout:function(){var t=this._container,e=(this._fadeAnimated=this.options.fadeAnimation&&b.any3d,M(t,"leaflet-container"+(b.touch?" leaflet-touch":"")+(b.retina?" leaflet-retina":"")+(b.ielt9?" leaflet-oldie":"")+(b.safari?" leaflet-safari":"")+(this._fadeAnimated?" leaflet-fade-anim":"")),pe(t,"position"));"absolute"!==e&&"relative"!==e&&"fixed"!==e&&"sticky"!==e&&(t.style.position="relative"),this._initPanes(),this._initControlPos&&this._initControlPos()},_initPanes:function(){var t=this._panes={};this._paneRenderers={},this._mapPane=this.createPane("mapPane",this._container),Z(this._mapPane,new p(0,0)),this.createPane("tilePane"),this.createPane("overlayPane"),this.createPane("shadowPane"),this.createPane("markerPane"),this.createPane("tooltipPane"),this.createPane("popupPane"),this.options.markerZoomAnimation||(M(t.markerPane,"leaflet-zoom-hide"),M(t.shadowPane,"leaflet-zoom-hide"))},_resetView:function(t,e,i){Z(this._mapPane,new p(0,0));var n=!this._loaded,o=(this._loaded=!0,e=this._limitZoom(e),this.fire("viewprereset"),this._zoom!==e);this._moveStart(o,i)._move(t,e)._moveEnd(o),this.fire("viewreset"),n&&this.fire("load")},_moveStart:function(t,e){return t&&this.fire("zoomstart"),e||this.fire("movestart"),this},_move:function(t,e,i,n){void 0===e&&(e=this._zoom);var o=this._zoom!==e;return this._zoom=e,this._lastCenter=t,this._pixelOrigin=this._getNewPixelOrigin(t),n?i&&i.pinch&&this.fire("zoom",i):((o||i&&i.pinch)&&this.fire("zoom",i),this.fire("move",i)),this},_moveEnd:function(t){return t&&this.fire("zoomend"),this.fire("moveend")},_stop:function(){return r(this._flyToFrame),this._panAnim&&this._panAnim.stop(),this},_rawPanBy:function(t){Z(this._mapPane,this._getMapPanePos().subtract(t))},_getZoomSpan:function(){return this.getMaxZoom()-this.getMinZoom()},_panInsideMaxBounds:function(){this._enforcingBounds||this.panInsideBounds(this.options.maxBounds)},_checkIfLoaded:function(){if(!this._loaded)throw new Error("Set map center and zoom first.")},_initEvents:function(t){this._targets={};var e=t?k:S;e((this._targets[h(this._container)]=this)._container,"click dblclick mousedown mouseup mouseover mouseout mousemove contextmenu keypress keydown keyup",this._handleDOMEvent,this),this.options.trackResize&&e(window,"resize",this._onResize,this),b.any3d&&this.options.transform3DLimit&&(t?this.off:this.on).call(this,"moveend",this._onMoveEnd)},_onResize:function(){r(this._resizeRequest),this._resizeRequest=x(function(){this.invalidateSize({debounceMoveend:!0})},this)},_onScroll:function(){this._container.scrollTop=0,this._container.scrollLeft=0},_onMoveEnd:function(){var t=this._getMapPanePos();Math.max(Math.abs(t.x),Math.abs(t.y))>=this.options.transform3DLimit&&this._resetView(this.getCenter(),this.getZoom())},_findEventTargets:function(t,e){for(var i,n=[],o="mouseout"===e||"mouseover"===e,s=t.target||t.srcElement,r=!1;s;){if((i=this._targets[h(s)])&&("click"===e||"preclick"===e)&&this._draggableMoved(i)){r=!0;break}if(i&&i.listens(e,!0)){if(o&&!We(s,t))break;if(n.push(i),o)break}if(s===this._container)break;s=s.parentNode}return n=n.length||r||o||!this.listens(e,!0)?n:[this]},_isClickDisabled:function(t){for(;t&&t!==this._container;){if(t._leaflet_disable_click)return!0;t=t.parentNode}},_handleDOMEvent:function(t){var e,i=t.target||t.srcElement;!this._loaded||i._leaflet_disable_events||"click"===t.type&&this._isClickDisabled(i)||("mousedown"===(e=t.type)&&Me(i),this._fireDOMEvent(t,e))},_mouseEvents:["click","dblclick","mouseover","mouseout","contextmenu"],_fireDOMEvent:function(t,e,i){"click"===t.type&&((a=l({},t)).type="preclick",this._fireDOMEvent(a,a.type,i));var n=this._findEventTargets(t,e);if(i){for(var o=[],s=0;sthis.options.zoomAnimationThreshold)return!1;var n=this.getZoomScale(e),n=this._getCenterOffset(t)._divideBy(1-1/n);if(!0!==i.animate&&!this.getSize().contains(n))return!1;x(function(){this._moveStart(!0,i.noMoveStart||!1)._animateZoom(t,e,!0)},this)}return!0},_animateZoom:function(t,e,i,n){this._mapPane&&(i&&(this._animatingZoom=!0,this._animateToCenter=t,this._animateToZoom=e,M(this._mapPane,"leaflet-zoom-anim")),this.fire("zoomanim",{center:t,zoom:e,noUpdate:n}),this._tempFireZoomEvent||(this._tempFireZoomEvent=this._zoom!==this._animateToZoom),this._move(this._animateToCenter,this._animateToZoom,void 0,!0),setTimeout(a(this._onZoomTransitionEnd,this),250))},_onZoomTransitionEnd:function(){this._animatingZoom&&(this._mapPane&&z(this._mapPane,"leaflet-zoom-anim"),this._animatingZoom=!1,this._move(this._animateToCenter,this._animateToZoom,void 0,!0),this._tempFireZoomEvent&&this.fire("zoom"),delete this._tempFireZoomEvent,this.fire("move"),this._moveEnd(!0))}});function Ue(t){return new B(t)}var B=et.extend({options:{position:"topright"},initialize:function(t){c(this,t)},getPosition:function(){return this.options.position},setPosition:function(t){var e=this._map;return e&&e.removeControl(this),this.options.position=t,e&&e.addControl(this),this},getContainer:function(){return this._container},addTo:function(t){this.remove(),this._map=t;var e=this._container=this.onAdd(t),i=this.getPosition(),t=t._controlCorners[i];return M(e,"leaflet-control"),-1!==i.indexOf("bottom")?t.insertBefore(e,t.firstChild):t.appendChild(e),this._map.on("unload",this.remove,this),this},remove:function(){return this._map&&(T(this._container),this.onRemove&&this.onRemove(this._map),this._map.off("unload",this.remove,this),this._map=null),this},_refocusOnMap:function(t){this._map&&t&&0",e=document.createElement("div");return e.innerHTML=t,e.firstChild},_addItem:function(t){var e,i=document.createElement("label"),n=this._map.hasLayer(t.layer),n=(t.overlay?((e=document.createElement("input")).type="checkbox",e.className="leaflet-control-layers-selector",e.defaultChecked=n):e=this._createRadioElement("leaflet-base-layers_"+h(this),n),this._layerControlInputs.push(e),e.layerId=h(t.layer),S(e,"click",this._onInputClick,this),document.createElement("span")),o=(n.innerHTML=" "+t.name,document.createElement("span"));return i.appendChild(o),o.appendChild(e),o.appendChild(n),(t.overlay?this._overlaysList:this._baseLayersList).appendChild(i),this._checkDisabledLayers(),i},_onInputClick:function(){if(!this._preventClick){var t,e,i=this._layerControlInputs,n=[],o=[];this._handlingClick=!0;for(var s=i.length-1;0<=s;s--)t=i[s],e=this._getLayer(t.layerId).layer,t.checked?n.push(e):t.checked||o.push(e);for(s=0;se.options.maxZoom},_expandIfNotCollapsed:function(){return this._map&&!this.options.collapsed&&this.expand(),this},_expandSafely:function(){var t=this._section,e=(this._preventClick=!0,S(t,"click",O),this.expand(),this);setTimeout(function(){k(t,"click",O),e._preventClick=!1})}})),qe=B.extend({options:{position:"topleft",zoomInText:'',zoomInTitle:"Zoom in",zoomOutText:'',zoomOutTitle:"Zoom out"},onAdd:function(t){var e="leaflet-control-zoom",i=P("div",e+" leaflet-bar"),n=this.options;return this._zoomInButton=this._createButton(n.zoomInText,n.zoomInTitle,e+"-in",i,this._zoomIn),this._zoomOutButton=this._createButton(n.zoomOutText,n.zoomOutTitle,e+"-out",i,this._zoomOut),this._updateDisabled(),t.on("zoomend zoomlevelschange",this._updateDisabled,this),i},onRemove:function(t){t.off("zoomend zoomlevelschange",this._updateDisabled,this)},disable:function(){return this._disabled=!0,this._updateDisabled(),this},enable:function(){return this._disabled=!1,this._updateDisabled(),this},_zoomIn:function(t){!this._disabled&&this._map._zoomthis._map.getMinZoom()&&this._map.zoomOut(this._map.options.zoomDelta*(t.shiftKey?3:1))},_createButton:function(t,e,i,n,o){i=P("a",i,n);return i.innerHTML=t,i.href="#",i.title=e,i.setAttribute("role","button"),i.setAttribute("aria-label",e),Ie(i),S(i,"click",Re),S(i,"click",o,this),S(i,"click",this._refocusOnMap,this),i},_updateDisabled:function(){var t=this._map,e="leaflet-disabled";z(this._zoomInButton,e),z(this._zoomOutButton,e),this._zoomInButton.setAttribute("aria-disabled","false"),this._zoomOutButton.setAttribute("aria-disabled","false"),!this._disabled&&t._zoom!==t.getMinZoom()||(M(this._zoomOutButton,e),this._zoomOutButton.setAttribute("aria-disabled","true")),!this._disabled&&t._zoom!==t.getMaxZoom()||(M(this._zoomInButton,e),this._zoomInButton.setAttribute("aria-disabled","true"))}}),Ge=(A.mergeOptions({zoomControl:!0}),A.addInitHook(function(){this.options.zoomControl&&(this.zoomControl=new qe,this.addControl(this.zoomControl))}),B.extend({options:{position:"bottomleft",maxWidth:100,metric:!0,imperial:!0},onAdd:function(t){var e="leaflet-control-scale",i=P("div",e),n=this.options;return this._addScales(n,e+"-line",i),t.on(n.updateWhenIdle?"moveend":"move",this._update,this),t.whenReady(this._update,this),i},onRemove:function(t){t.off(this.options.updateWhenIdle?"moveend":"move",this._update,this)},_addScales:function(t,e,i){t.metric&&(this._mScale=P("div",e,i)),t.imperial&&(this._iScale=P("div",e,i))},_update:function(){var t=this._map,e=t.getSize().y/2,t=t.distance(t.containerPointToLatLng([0,e]),t.containerPointToLatLng([this.options.maxWidth,e]));this._updateScales(t)},_updateScales:function(t){this.options.metric&&t&&this._updateMetric(t),this.options.imperial&&t&&this._updateImperial(t)},_updateMetric:function(t){var e=this._getRoundNum(t);this._updateScale(this._mScale,e<1e3?e+" m":e/1e3+" km",e/t)},_updateImperial:function(t){var e,i,t=3.2808399*t;5280'+(b.inlineSvg?' ':"")+"Leaflet"},initialize:function(t){c(this,t),this._attributions={}},onAdd:function(t){for(var e in(t.attributionControl=this)._container=P("div","leaflet-control-attribution"),Ie(this._container),t._layers)t._layers[e].getAttribution&&this.addAttribution(t._layers[e].getAttribution());return this._update(),t.on("layeradd",this._addAttribution,this),this._container},onRemove:function(t){t.off("layeradd",this._addAttribution,this)},_addAttribution:function(t){t.layer.getAttribution&&(this.addAttribution(t.layer.getAttribution()),t.layer.once("remove",function(){this.removeAttribution(t.layer.getAttribution())},this))},setPrefix:function(t){return this.options.prefix=t,this._update(),this},addAttribution:function(t){return t&&(this._attributions[t]||(this._attributions[t]=0),this._attributions[t]++,this._update()),this},removeAttribution:function(t){return t&&this._attributions[t]&&(this._attributions[t]--,this._update()),this},_update:function(){if(this._map){var t,e=[];for(t in this._attributions)this._attributions[t]&&e.push(t);var i=[];this.options.prefix&&i.push(this.options.prefix),e.length&&i.push(e.join(", ")),this._container.innerHTML=i.join(' ')}}}),n=(A.mergeOptions({attributionControl:!0}),A.addInitHook(function(){this.options.attributionControl&&(new Ke).addTo(this)}),B.Layers=Ve,B.Zoom=qe,B.Scale=Ge,B.Attribution=Ke,Ue.layers=function(t,e,i){return new Ve(t,e,i)},Ue.zoom=function(t){return new qe(t)},Ue.scale=function(t){return new Ge(t)},Ue.attribution=function(t){return new Ke(t)},et.extend({initialize:function(t){this._map=t},enable:function(){return this._enabled||(this._enabled=!0,this.addHooks()),this},disable:function(){return this._enabled&&(this._enabled=!1,this.removeHooks()),this},enabled:function(){return!!this._enabled}})),ft=(n.addTo=function(t,e){return t.addHandler(e,this),this},{Events:e}),Ye=b.touch?"touchstart mousedown":"mousedown",Xe=it.extend({options:{clickTolerance:3},initialize:function(t,e,i,n){c(this,n),this._element=t,this._dragStartTarget=e||t,this._preventOutline=i},enable:function(){this._enabled||(S(this._dragStartTarget,Ye,this._onDown,this),this._enabled=!0)},disable:function(){this._enabled&&(Xe._dragging===this&&this.finishDrag(!0),k(this._dragStartTarget,Ye,this._onDown,this),this._enabled=!1,this._moved=!1)},_onDown:function(t){var e,i;this._enabled&&(this._moved=!1,ve(this._element,"leaflet-zoom-anim")||(t.touches&&1!==t.touches.length?Xe._dragging===this&&this.finishDrag():Xe._dragging||t.shiftKey||1!==t.which&&1!==t.button&&!t.touches||((Xe._dragging=this)._preventOutline&&Me(this._element),Le(),re(),this._moving||(this.fire("down"),i=t.touches?t.touches[0]:t,e=Ce(this._element),this._startPoint=new p(i.clientX,i.clientY),this._startPos=Pe(this._element),this._parentScale=Ze(e),i="mousedown"===t.type,S(document,i?"mousemove":"touchmove",this._onMove,this),S(document,i?"mouseup":"touchend touchcancel",this._onUp,this)))))},_onMove:function(t){var e;this._enabled&&(t.touches&&1e&&(i.push(t[n]),o=n);oe.max.x&&(i|=2),t.ye.max.y&&(i|=8),i}function ri(t,e,i,n){var o=e.x,e=e.y,s=i.x-o,r=i.y-e,a=s*s+r*r;return 0this._layersMaxZoom&&this.setZoom(this._layersMaxZoom),void 0===this.options.minZoom&&this._layersMinZoom&&this.getZoom()t.y!=n.y>t.y&&t.x<(n.x-i.x)*(t.y-i.y)/(n.y-i.y)+i.x&&(l=!l);return l||yi.prototype._containsPoint.call(this,t,!0)}});var wi=ci.extend({initialize:function(t,e){c(this,e),this._layers={},t&&this.addData(t)},addData:function(t){var e,i,n,o=d(t)?t:t.features;if(o){for(e=0,i=o.length;es.x&&(r=i.x+a-s.x+o.x),i.x-r-n.x<(a=0)&&(r=i.x-n.x),i.y+e+o.y>s.y&&(a=i.y+e-s.y+o.y),i.y-a-n.y<0&&(a=i.y-n.y),(r||a)&&(this.options.keepInView&&(this._autopanning=!0),t.fire("autopanstart").panBy([r,a]))))},_getAnchor:function(){return m(this._source&&this._source._getPopupAnchor?this._source._getPopupAnchor():[0,0])}})),Ii=(A.mergeOptions({closePopupOnClick:!0}),A.include({openPopup:function(t,e,i){return this._initOverlay(Bi,t,e,i).openOn(this),this},closePopup:function(t){return(t=arguments.length?t:this._popup)&&t.close(),this}}),o.include({bindPopup:function(t,e){return this._popup=this._initOverlay(Bi,this._popup,t,e),this._popupHandlersAdded||(this.on({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!0),this},unbindPopup:function(){return this._popup&&(this.off({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!1,this._popup=null),this},openPopup:function(t){return this._popup&&(this instanceof ci||(this._popup._source=this),this._popup._prepareOpen(t||this._latlng)&&this._popup.openOn(this._map)),this},closePopup:function(){return this._popup&&this._popup.close(),this},togglePopup:function(){return this._popup&&this._popup.toggle(this),this},isPopupOpen:function(){return!!this._popup&&this._popup.isOpen()},setPopupContent:function(t){return this._popup&&this._popup.setContent(t),this},getPopup:function(){return this._popup},_openPopup:function(t){var e;this._popup&&this._map&&(Re(t),e=t.layer||t.target,this._popup._source!==e||e instanceof fi?(this._popup._source=e,this.openPopup(t.latlng)):this._map.hasLayer(this._popup)?this.closePopup():this.openPopup(t.latlng))},_movePopup:function(t){this._popup.setLatLng(t.latlng)},_onKeyPress:function(t){13===t.originalEvent.keyCode&&this._openPopup(t)}}),Ai.extend({options:{pane:"tooltipPane",offset:[0,0],direction:"auto",permanent:!1,sticky:!1,opacity:.9},onAdd:function(t){Ai.prototype.onAdd.call(this,t),this.setOpacity(this.options.opacity),t.fire("tooltipopen",{tooltip:this}),this._source&&(this.addEventParent(this._source),this._source.fire("tooltipopen",{tooltip:this},!0))},onRemove:function(t){Ai.prototype.onRemove.call(this,t),t.fire("tooltipclose",{tooltip:this}),this._source&&(this.removeEventParent(this._source),this._source.fire("tooltipclose",{tooltip:this},!0))},getEvents:function(){var t=Ai.prototype.getEvents.call(this);return this.options.permanent||(t.preclick=this.close),t},_initLayout:function(){var t="leaflet-tooltip "+(this.options.className||"")+" leaflet-zoom-"+(this._zoomAnimated?"animated":"hide");this._contentNode=this._container=P("div",t),this._container.setAttribute("role","tooltip"),this._container.setAttribute("id","leaflet-tooltip-"+h(this))},_updateLayout:function(){},_adjustPan:function(){},_setPosition:function(t){var e,i=this._map,n=this._container,o=i.latLngToContainerPoint(i.getCenter()),i=i.layerPointToContainerPoint(t),s=this.options.direction,r=n.offsetWidth,a=n.offsetHeight,h=m(this.options.offset),l=this._getAnchor(),i="top"===s?(e=r/2,a):"bottom"===s?(e=r/2,0):(e="center"===s?r/2:"right"===s?0:"left"===s?r:i.xthis.options.maxZoom||nthis.options.maxZoom||void 0!==this.options.minZoom&&oi.max.x)||!e.wrapLat&&(t.yi.max.y))return!1}return!this.options.bounds||(e=this._tileCoordsToBounds(t),g(this.options.bounds).overlaps(e))},_keyToBounds:function(t){return this._tileCoordsToBounds(this._keyToTileCoords(t))},_tileCoordsToNwSe:function(t){var e=this._map,i=this.getTileSize(),n=t.scaleBy(i),i=n.add(i);return[e.unproject(n,t.z),e.unproject(i,t.z)]},_tileCoordsToBounds:function(t){t=this._tileCoordsToNwSe(t),t=new s(t[0],t[1]);return t=this.options.noWrap?t:this._map.wrapLatLngBounds(t)},_tileCoordsToKey:function(t){return t.x+":"+t.y+":"+t.z},_keyToTileCoords:function(t){var t=t.split(":"),e=new p(+t[0],+t[1]);return e.z=+t[2],e},_removeTile:function(t){var e=this._tiles[t];e&&(T(e.el),delete this._tiles[t],this.fire("tileunload",{tile:e.el,coords:this._keyToTileCoords(t)}))},_initTile:function(t){M(t,"leaflet-tile");var e=this.getTileSize();t.style.width=e.x+"px",t.style.height=e.y+"px",t.onselectstart=u,t.onmousemove=u,b.ielt9&&this.options.opacity<1&&C(t,this.options.opacity)},_addTile:function(t,e){var i=this._getTilePos(t),n=this._tileCoordsToKey(t),o=this.createTile(this._wrapCoords(t),a(this._tileReady,this,t));this._initTile(o),this.createTile.length<2&&x(a(this._tileReady,this,t,null,o)),Z(o,i),this._tiles[n]={el:o,coords:t,current:!0},e.appendChild(o),this.fire("tileloadstart",{tile:o,coords:t})},_tileReady:function(t,e,i){e&&this.fire("tileerror",{error:e,tile:i,coords:t});var n=this._tileCoordsToKey(t);(i=this._tiles[n])&&(i.loaded=+new Date,this._map._fadeAnimated?(C(i.el,0),r(this._fadeFrame),this._fadeFrame=x(this._updateOpacity,this)):(i.active=!0,this._pruneTiles()),e||(M(i.el,"leaflet-tile-loaded"),this.fire("tileload",{tile:i.el,coords:t})),this._noTilesToLoad()&&(this._loading=!1,this.fire("load"),b.ielt9||!this._map._fadeAnimated?x(this._pruneTiles,this):setTimeout(a(this._pruneTiles,this),250)))},_getTilePos:function(t){return t.scaleBy(this.getTileSize()).subtract(this._level.origin)},_wrapCoords:function(t){var e=new p(this._wrapX?H(t.x,this._wrapX):t.x,this._wrapY?H(t.y,this._wrapY):t.y);return e.z=t.z,e},_pxBoundsToTileRange:function(t){var e=this.getTileSize();return new f(t.min.unscaleBy(e).floor(),t.max.unscaleBy(e).ceil().subtract([1,1]))},_noTilesToLoad:function(){for(var t in this._tiles)if(!this._tiles[t].loaded)return!1;return!0}});var Di=Ni.extend({options:{minZoom:0,maxZoom:18,subdomains:"abc",errorTileUrl:"",zoomOffset:0,tms:!1,zoomReverse:!1,detectRetina:!1,crossOrigin:!1,referrerPolicy:!1},initialize:function(t,e){this._url=t,(e=c(this,e)).detectRetina&&b.retina&&0')}}catch(t){}return function(t){return document.createElement("<"+t+' xmlns="urn:schemas-microsoft.com:vml" class="lvml">')}}(),zt={_initContainer:function(){this._container=P("div","leaflet-vml-container")},_update:function(){this._map._animatingZoom||(Wi.prototype._update.call(this),this.fire("update"))},_initPath:function(t){var e=t._container=Vi("shape");M(e,"leaflet-vml-shape "+(this.options.className||"")),e.coordsize="1 1",t._path=Vi("path"),e.appendChild(t._path),this._updateStyle(t),this._layers[h(t)]=t},_addPath:function(t){var e=t._container;this._container.appendChild(e),t.options.interactive&&t.addInteractiveTarget(e)},_removePath:function(t){var e=t._container;T(e),t.removeInteractiveTarget(e),delete this._layers[h(t)]},_updateStyle:function(t){var e=t._stroke,i=t._fill,n=t.options,o=t._container;o.stroked=!!n.stroke,o.filled=!!n.fill,n.stroke?(e=e||(t._stroke=Vi("stroke")),o.appendChild(e),e.weight=n.weight+"px",e.color=n.color,e.opacity=n.opacity,n.dashArray?e.dashStyle=d(n.dashArray)?n.dashArray.join(" "):n.dashArray.replace(/( *, *)/g," "):e.dashStyle="",e.endcap=n.lineCap.replace("butt","flat"),e.joinstyle=n.lineJoin):e&&(o.removeChild(e),t._stroke=null),n.fill?(i=i||(t._fill=Vi("fill")),o.appendChild(i),i.color=n.fillColor||n.color,i.opacity=n.fillOpacity):i&&(o.removeChild(i),t._fill=null)},_updateCircle:function(t){var e=t._point.round(),i=Math.round(t._radius),n=Math.round(t._radiusY||i);this._setPath(t,t._empty()?"M0 0":"AL "+e.x+","+e.y+" "+i+","+n+" 0,23592600")},_setPath:function(t,e){t._path.v=e},_bringToFront:function(t){fe(t._container)},_bringToBack:function(t){ge(t._container)}},qi=b.vml?Vi:ct,Gi=Wi.extend({_initContainer:function(){this._container=qi("svg"),this._container.setAttribute("pointer-events","none"),this._rootGroup=qi("g"),this._container.appendChild(this._rootGroup)},_destroyContainer:function(){T(this._container),k(this._container),delete this._container,delete this._rootGroup,delete this._svgSize},_update:function(){var t,e,i;this._map._animatingZoom&&this._bounds||(Wi.prototype._update.call(this),e=(t=this._bounds).getSize(),i=this._container,this._svgSize&&this._svgSize.equals(e)||(this._svgSize=e,i.setAttribute("width",e.x),i.setAttribute("height",e.y)),Z(i,t.min),i.setAttribute("viewBox",[t.min.x,t.min.y,e.x,e.y].join(" ")),this.fire("update"))},_initPath:function(t){var e=t._path=qi("path");t.options.className&&M(e,t.options.className),t.options.interactive&&M(e,"leaflet-interactive"),this._updateStyle(t),this._layers[h(t)]=t},_addPath:function(t){this._rootGroup||this._initContainer(),this._rootGroup.appendChild(t._path),t.addInteractiveTarget(t._path)},_removePath:function(t){T(t._path),t.removeInteractiveTarget(t._path),delete this._layers[h(t)]},_updatePath:function(t){t._project(),t._update()},_updateStyle:function(t){var e=t._path,t=t.options;e&&(t.stroke?(e.setAttribute("stroke",t.color),e.setAttribute("stroke-opacity",t.opacity),e.setAttribute("stroke-width",t.weight),e.setAttribute("stroke-linecap",t.lineCap),e.setAttribute("stroke-linejoin",t.lineJoin),t.dashArray?e.setAttribute("stroke-dasharray",t.dashArray):e.removeAttribute("stroke-dasharray"),t.dashOffset?e.setAttribute("stroke-dashoffset",t.dashOffset):e.removeAttribute("stroke-dashoffset")):e.setAttribute("stroke","none"),t.fill?(e.setAttribute("fill",t.fillColor||t.color),e.setAttribute("fill-opacity",t.fillOpacity),e.setAttribute("fill-rule",t.fillRule||"evenodd")):e.setAttribute("fill","none"))},_updatePoly:function(t,e){this._setPath(t,dt(t._parts,e))},_updateCircle:function(t){var e=t._point,i=Math.max(Math.round(t._radius),1),n="a"+i+","+(Math.max(Math.round(t._radiusY),1)||i)+" 0 1,0 ",e=t._empty()?"M0 0":"M"+(e.x-i)+","+e.y+n+2*i+",0 "+n+2*-i+",0 ";this._setPath(t,e)},_setPath:function(t,e){t._path.setAttribute("d",e)},_bringToFront:function(t){fe(t._path)},_bringToBack:function(t){ge(t._path)}});function Ki(t){return b.svg||b.vml?new Gi(t):null}b.vml&&Gi.include(zt),A.include({getRenderer:function(t){t=(t=t.options.renderer||this._getPaneRenderer(t.options.pane)||this.options.renderer||this._renderer)||(this._renderer=this._createRenderer());return this.hasLayer(t)||this.addLayer(t),t},_getPaneRenderer:function(t){var e;return"overlayPane"!==t&&void 0!==t&&(void 0===(e=this._paneRenderers[t])&&(e=this._createRenderer({pane:t}),this._paneRenderers[t]=e),e)},_createRenderer:function(t){return this.options.preferCanvas&&Ui(t)||Ki(t)}});var Yi=xi.extend({initialize:function(t,e){xi.prototype.initialize.call(this,this._boundsToLatLngs(t),e)},setBounds:function(t){return this.setLatLngs(this._boundsToLatLngs(t))},_boundsToLatLngs:function(t){return[(t=g(t)).getSouthWest(),t.getNorthWest(),t.getNorthEast(),t.getSouthEast()]}});Gi.create=qi,Gi.pointsToPath=dt,wi.geometryToLayer=bi,wi.coordsToLatLng=Li,wi.coordsToLatLngs=Ti,wi.latLngToCoords=Mi,wi.latLngsToCoords=zi,wi.getFeature=Ci,wi.asFeature=Zi,A.mergeOptions({boxZoom:!0});var _t=n.extend({initialize:function(t){this._map=t,this._container=t._container,this._pane=t._panes.overlayPane,this._resetStateTimeout=0,t.on("unload",this._destroy,this)},addHooks:function(){S(this._container,"mousedown",this._onMouseDown,this)},removeHooks:function(){k(this._container,"mousedown",this._onMouseDown,this)},moved:function(){return this._moved},_destroy:function(){T(this._pane),delete this._pane},_resetState:function(){this._resetStateTimeout=0,this._moved=!1},_clearDeferredResetState:function(){0!==this._resetStateTimeout&&(clearTimeout(this._resetStateTimeout),this._resetStateTimeout=0)},_onMouseDown:function(t){if(!t.shiftKey||1!==t.which&&1!==t.button)return!1;this._clearDeferredResetState(),this._resetState(),re(),Le(),this._startPoint=this._map.mouseEventToContainerPoint(t),S(document,{contextmenu:Re,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseMove:function(t){this._moved||(this._moved=!0,this._box=P("div","leaflet-zoom-box",this._container),M(this._container,"leaflet-crosshair"),this._map.fire("boxzoomstart")),this._point=this._map.mouseEventToContainerPoint(t);var t=new f(this._point,this._startPoint),e=t.getSize();Z(this._box,t.min),this._box.style.width=e.x+"px",this._box.style.height=e.y+"px"},_finish:function(){this._moved&&(T(this._box),z(this._container,"leaflet-crosshair")),ae(),Te(),k(document,{contextmenu:Re,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseUp:function(t){1!==t.which&&1!==t.button||(this._finish(),this._moved&&(this._clearDeferredResetState(),this._resetStateTimeout=setTimeout(a(this._resetState,this),0),t=new s(this._map.containerPointToLatLng(this._startPoint),this._map.containerPointToLatLng(this._point)),this._map.fitBounds(t).fire("boxzoomend",{boxZoomBounds:t})))},_onKeyDown:function(t){27===t.keyCode&&(this._finish(),this._clearDeferredResetState(),this._resetState())}}),Ct=(A.addInitHook("addHandler","boxZoom",_t),A.mergeOptions({doubleClickZoom:!0}),n.extend({addHooks:function(){this._map.on("dblclick",this._onDoubleClick,this)},removeHooks:function(){this._map.off("dblclick",this._onDoubleClick,this)},_onDoubleClick:function(t){var e=this._map,i=e.getZoom(),n=e.options.zoomDelta,i=t.originalEvent.shiftKey?i-n:i+n;"center"===e.options.doubleClickZoom?e.setZoom(i):e.setZoomAround(t.containerPoint,i)}})),Zt=(A.addInitHook("addHandler","doubleClickZoom",Ct),A.mergeOptions({dragging:!0,inertia:!0,inertiaDeceleration:3400,inertiaMaxSpeed:1/0,easeLinearity:.2,worldCopyJump:!1,maxBoundsViscosity:0}),n.extend({addHooks:function(){var t;this._draggable||(t=this._map,this._draggable=new Xe(t._mapPane,t._container),this._draggable.on({dragstart:this._onDragStart,drag:this._onDrag,dragend:this._onDragEnd},this),this._draggable.on("predrag",this._onPreDragLimit,this),t.options.worldCopyJump&&(this._draggable.on("predrag",this._onPreDragWrap,this),t.on("zoomend",this._onZoomEnd,this),t.whenReady(this._onZoomEnd,this))),M(this._map._container,"leaflet-grab leaflet-touch-drag"),this._draggable.enable(),this._positions=[],this._times=[]},removeHooks:function(){z(this._map._container,"leaflet-grab"),z(this._map._container,"leaflet-touch-drag"),this._draggable.disable()},moved:function(){return this._draggable&&this._draggable._moved},moving:function(){return this._draggable&&this._draggable._moving},_onDragStart:function(){var t,e=this._map;e._stop(),this._map.options.maxBounds&&this._map.options.maxBoundsViscosity?(t=g(this._map.options.maxBounds),this._offsetLimit=_(this._map.latLngToContainerPoint(t.getNorthWest()).multiplyBy(-1),this._map.latLngToContainerPoint(t.getSouthEast()).multiplyBy(-1).add(this._map.getSize())),this._viscosity=Math.min(1,Math.max(0,this._map.options.maxBoundsViscosity))):this._offsetLimit=null,e.fire("movestart").fire("dragstart"),e.options.inertia&&(this._positions=[],this._times=[])},_onDrag:function(t){var e,i;this._map.options.inertia&&(e=this._lastTime=+new Date,i=this._lastPos=this._draggable._absPos||this._draggable._newPos,this._positions.push(i),this._times.push(e),this._prunePositions(e)),this._map.fire("move",t).fire("drag",t)},_prunePositions:function(t){for(;1e.max.x&&(t.x=this._viscousLimit(t.x,e.max.x)),t.y>e.max.y&&(t.y=this._viscousLimit(t.y,e.max.y)),this._draggable._newPos=this._draggable._startPos.add(t))},_onPreDragWrap:function(){var t=this._worldWidth,e=Math.round(t/2),i=this._initialWorldOffset,n=this._draggable._newPos.x,o=(n-e+i)%t+e-i,n=(n+e+i)%t-e-i,t=Math.abs(o+i)e.getMaxZoom()&&1