From 77e83efae0c593888879ee5596c04ea282ad2219 Mon Sep 17 00:00:00 2001 From: Claude Dev Date: Mon, 16 Mar 2026 15:06:52 +0100 Subject: [PATCH] Tutorial: Echte Leaflet-Karte mit Hamburg-Markern statt Platzhalter - Initialisiert eine echte Leaflet-Map auf Hamburg (Zoom 13) mit 3 Demo-Markern: Burchardkai Terminal (Hauptereignisort), Hamburg Innenstadt, Elbe/Hafengebiet - Farbcodierte Marker mit Legende (Hauptereignisort/Erwaehnt/Kontext) - Marker-Popups mit Artikelanzahl, Hauptmarker oeffnet automatisch - Karten-Step ist jetzt eine interaktive Demo (disableNav): Cursor faehrt zum Marker und klickt ihn, dann werden Geoparse-Button und Vollbild-Button nacheinander gehighlightet - Theme-abhaengige Tile-Layer (dark/light) - Map wird beim Tutorial-Ende sauber entfernt Co-Authored-By: Claude Opus 4.6 (1M context) --- src/static/dashboard.html | 2 +- src/static/js/tutorial.js | 211 +++++++++++++++++++++++++++++++++----- 2 files changed, 185 insertions(+), 28 deletions(-) diff --git a/src/static/dashboard.html b/src/static/dashboard.html index a5b0deb..43bfc79 100644 --- a/src/static/dashboard.html +++ b/src/static/dashboard.html @@ -764,7 +764,7 @@ - + diff --git a/src/static/js/tutorial.js b/src/static/js/tutorial.js index c164f30..43e38e8 100644 --- a/src/static/js/tutorial.js +++ b/src/static/js/tutorial.js @@ -187,22 +187,12 @@ const Tutorial = { timeline.innerHTML = tlHtml; } - // Karte: "Keine Orte" ausblenden, Platzhalter einsetzen + // Karte: Leaflet-Map mit Demo-Markern initialisieren var mapEmpty = document.getElementById('map-empty'); if (mapEmpty) mapEmpty.style.display = 'none'; - var mapContainer = document.getElementById('map-container'); - if (mapContainer) { - var mapPlaceholder = document.createElement('div'); - mapPlaceholder.className = 'tutorial-demo tutorial-map-placeholder'; - mapPlaceholder.style.cssText = 'width:100%;height:100%;display:flex;align-items:center;justify-content:center;' - + 'background:var(--bg-secondary);color:var(--text-secondary);font-size:13px;'; - mapPlaceholder.innerHTML = '
' - + '
🌎
' - + '
3 Orte erkannt: Hamburg, Burchardkai, Elbe
'; - mapContainer.appendChild(mapPlaceholder); - } var mapStats = document.getElementById('map-stats'); - if (mapStats) mapStats.textContent = '3 Orte'; + if (mapStats) mapStats.textContent = '3 Orte / 9 Artikel'; + this._initDemoMap(); // Meta var metaUpdated = document.getElementById('meta-updated'); @@ -250,9 +240,8 @@ const Tutorial = { var mapStats = document.getElementById('map-stats'); if (mapStats) mapStats.innerHTML = s.mapStats; - // Map-Platzhalter entfernen - var mapPlaceholder = document.querySelector('.tutorial-map-placeholder'); - if (mapPlaceholder) mapPlaceholder.remove(); + // Demo-Map entfernen + this._destroyDemoMap(); // Meta var metaUpdated = document.getElementById('meta-updated'); @@ -267,6 +256,127 @@ const Tutorial = { this._savedState = null; }, + // ----------------------------------------------------------------------- + // Demo-Karte mit Leaflet + // ----------------------------------------------------------------------- + _demoMap: null, + _demoMapMarkers: [], + + _initDemoMap() { + if (typeof L === 'undefined') return; + var container = document.getElementById('map-container'); + if (!container) return; + + // Container-Höhe sicherstellen + var gsItem = container.closest('.grid-stack-item'); + if (gsItem) { + var headerEl = container.closest('.map-card'); + var hdr = headerEl ? headerEl.querySelector('.card-header') : null; + var headerH = hdr ? hdr.offsetHeight : 40; + var available = gsItem.offsetHeight - headerH - 4; + container.style.height = Math.max(available, 200) + 'px'; + } else if (container.offsetHeight < 50) { + container.style.height = '300px'; + } + + // Falls UI._map existiert, vorher sichern + if (typeof UI !== 'undefined' && UI._map) { + this._savedUIMap = true; + } + + this._demoMap = L.map(container, { + zoomControl: true, + attributionControl: true, + }).setView([53.545, 9.98], 13); + + // Tile-Layer (Theme-abhängig) + var isDark = !document.documentElement.getAttribute('data-theme') || document.documentElement.getAttribute('data-theme') !== 'light'; + if (isDark) { + L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { + attribution: '\u00a9 OpenStreetMap, \u00a9 CARTO', + maxZoom: 19, + }).addTo(this._demoMap); + } else { + L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { + attribution: '\u00a9 OpenStreetMap, \u00a9 CARTO', + maxZoom: 19, + }).addTo(this._demoMap); + } + + // Demo-Marker + var locations = [ + { lat: 53.5325, lon: 9.9275, name: 'Burchardkai Terminal', articles: 6, cat: 'primary' }, + { lat: 53.5460, lon: 9.9690, name: 'Hamburg Innenstadt', articles: 2, cat: 'secondary' }, + { lat: 53.5380, lon: 9.9400, name: 'Elbe / Hafengebiet', articles: 1, cat: 'tertiary' }, + ]; + + var catColors = { primary: '#EF4444', secondary: '#F59E0B', tertiary: '#3B82F6' }; + var catLabels = { primary: 'Hauptereignisort', secondary: 'Erw\u00e4hnt', tertiary: 'Kontext' }; + var self = this; + + locations.forEach(function(loc) { + var color = catColors[loc.cat] || '#7b7b7b'; + var icon = L.divIcon({ + className: 'tutorial-map-marker', + html: '
', + iconSize: [14, 14], + iconAnchor: [7, 7], + }); + var marker = L.marker([loc.lat, loc.lon], { icon: icon }); + var popupHtml = '
' + + '
' + loc.name + '
' + + '
' + catLabels[loc.cat] + '
' + + '
' + loc.articles + ' Artikel
' + + '
'; + marker.bindPopup(popupHtml, { maxWidth: 250, className: 'map-popup-container' }); + marker.addTo(self._demoMap); + self._demoMapMarkers.push(marker); + }); + + // Legende + var legend = L.control({ position: 'bottomright' }); + legend.onAdd = function() { + var div = L.DomUtil.create('div', 'map-legend-ctrl'); + L.DomEvent.disableClickPropagation(div); + var html = 'Legende'; + ['primary', 'secondary', 'tertiary'].forEach(function(cat) { + html += '
' + + '' + + '' + catLabels[cat] + '
'; + }); + div.innerHTML = html; + return div; + }; + legend.addTo(this._demoMap); + this._demoMapLegend = legend; + + // Resize-Fix + var map = this._demoMap; + [100, 300, 800].forEach(function(delay) { + setTimeout(function() { + if (map) map.invalidateSize(); + }, delay); + }); + + // Hauptereignisort-Popup nach kurzer Verz\u00f6gerung \u00f6ffnen + var mainMarker = this._demoMapMarkers[0]; + if (mainMarker) { + setTimeout(function() { + if (map && mainMarker) mainMarker.openPopup(); + }, 1500); + } + }, + + _destroyDemoMap() { + if (this._demoMap) { + this._demoMap.remove(); + this._demoMap = null; + this._demoMapMarkers = []; + this._demoMapLegend = null; + } + }, + // ----------------------------------------------------------------------- // Highlight-Helfer: Einzelnes Sub-Element innerhalb einer Kachel markieren // ----------------------------------------------------------------------- @@ -632,19 +742,23 @@ const Tutorial = { id: 'karte', target: '[gs-id="karte"]', title: 'Geografische Verteilung', - text: 'Die Karte zeigt automatisch erkannte Orte aus den gesammelten Artikeln (Geoparsing).

' - + 'Marker - Klicken Sie auf einen Marker für Details zum Ort und verknüpfte Artikel
' - + 'Cluster - Bei vielen Markern werden nahe Orte gruppiert
' - + 'Orte einlesen - Startet das Geoparsing manuell neu
' - + 'Vollbild - Vergrößert die Karte auf den gesamten Bildschirm

' - + 'Besonders bei internationalen Lagen bietet die Karte einen schnellen Überblick über die räumliche Verteilung.', + text: 'Die Karte zeigt per Geoparsing automatisch erkannte Orte aus den Quellen.

' + + ' Hauptereignisort - Zentraler Ort des Geschehens
' + + ' Erwähnt - In Artikeln genannte Orte
' + + ' Kontext - Relevante Umgebung

' + + 'Klicken Sie auf Marker für Details und verknüpfte Artikel. ' + + 'Bei vielen Markern werden nahe Orte zu Clustern gruppiert.', position: 'top', + disableNav: true, onEnter: function() { - Tutorial._highlightSub('#geoparse-btn'); - setTimeout(function() { - Tutorial._clearSubHighlights(); - Tutorial._highlightSub('#map-expand-btn'); - }, 3000); + // Demo-Map Popup öffnen + invalidateSize + if (Tutorial._demoMap) { + Tutorial._demoMap.invalidateSize(); + setTimeout(function() { + if (Tutorial._demoMap) Tutorial._demoMap.setView([53.545, 9.98], 13); + }, 300); + } + Tutorial._simulateMapDemo(); }, onExit: function() { Tutorial._clearSubHighlights(); @@ -1097,6 +1211,49 @@ const Tutorial = { }); }, + // ----------------------------------------------------------------------- + // Karten-Demo (Step 17): Marker klicken, Geoparse + Vollbild highlighten + // ----------------------------------------------------------------------- + async _simulateMapDemo() { + this._demoRunning = true; + + // 1. Auf Marker zeigen und klicken + if (this._demoMapMarkers.length > 0 && this._demoMap) { + var mainMarker = this._demoMapMarkers[0]; + var latLng = mainMarker.getLatLng(); + var point = this._demoMap.latLngToContainerPoint(latLng); + var mapContainer = document.getElementById('map-container'); + if (mapContainer) { + var mapRect = mapContainer.getBoundingClientRect(); + var markerX = mapRect.left + point.x; + var markerY = mapRect.top + point.y; + + this._showCursor(markerX - 40, markerY - 40, 'default'); + await this._wait(400); + await this._animateCursor(markerX - 40, markerY - 40, markerX, markerY, 600); + await this._wait(300); + mainMarker.openPopup(); + await this._wait(2000); + mainMarker.closePopup(); + } + } + + // 2. Geoparse-Button highlighten + this._hideCursor(); + this._highlightSub('#geoparse-btn'); + await this._wait(2000); + this._clearSubHighlights(); + + // 3. Vollbild-Button highlighten + this._highlightSub('#map-expand-btn'); + await this._wait(2000); + this._clearSubHighlights(); + + this._hideCursor(); + this._demoRunning = false; + this._enableNavAfterDemo(); + }, + // ----------------------------------------------------------------------- // Typ-Wechsel-Demo (Step 4) // -----------------------------------------------------------------------