From e230248f61ca9257dfbebc9d560831f48ae7a535 Mon Sep 17 00:00:00 2001 From: Claude Dev Date: Mon, 16 Mar 2026 17:48:14 +0100 Subject: [PATCH] Tutorial Karte: Echte Map in Kachel + Zwei-Step-Flow mit Legende Kachel-Ansicht (Step 17): - Echte Leaflet-Map mit OSM-Tiles und 3 Markern direkt in der Kachel (statt grauem Platzhalter), gezoomt auf Hamburg - Orte einlesen + Vollbild-Buttons werden nacheinander gehighlightet - Erklaerung der Geoparsing-Funktion in der Bubble Vollbild-Ansicht (Step 18 - neu): - Oeffnet Karten-Vollbild, startet bei Europa-Zoom, fliegt auf Hamburg - Bubble erklaert Legende detailliert (Farben + Kategorien + Artikelanzahl) - Cursor besucht alle 3 Marker nacheinander, oeffnet jeweiliges Popup fuer 2.5s (Burchardkai -> Innenstadt -> Elbe) - Nach Demo: Weiter-Button erscheint Refactoring: - Marker-Erstellung und Legende in wiederverwendbare Methoden extrahiert (_createDemoMarkers, _addDemoLegend) - Gemeinsame Konstanten fuer Locations, Farben, Labels Co-Authored-By: Claude Opus 4.6 (1M context) --- src/static/dashboard.html | 2 +- src/static/js/tutorial.js | 316 ++++++++++++++++---------------------- 2 files changed, 137 insertions(+), 181 deletions(-) diff --git a/src/static/dashboard.html b/src/static/dashboard.html index 5c24cfc..41adc07 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 2174bfc..131dd51 100644 --- a/src/static/js/tutorial.js +++ b/src/static/js/tutorial.js @@ -254,23 +254,12 @@ const Tutorial = { var articleCount = document.getElementById('article-count'); if (articleCount) articleCount.textContent = '7 Einträge'; - // Karte: Stats setzen + Platzhalter anzeigen, echte Map erst im Vollbild-Step + // Karte: Echte Leaflet-Map in der Kachel initialisieren var mapEmpty = document.getElementById('map-empty'); if (mapEmpty) mapEmpty.style.display = 'none'; var mapStats = document.getElementById('map-stats'); if (mapStats) mapStats.textContent = '3 Orte / 9 Artikel'; - var mapContainer = document.getElementById('map-container'); - if (mapContainer) { - var ph = document.createElement('div'); - ph.className = 'tutorial-demo tutorial-map-placeholder'; - ph.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;border-radius:var(--radius);'; - ph.innerHTML = '
' - + '
🌎
' - + '
3 Orte erkannt: Hamburg, Burchardkai, Elbe
' - + '
Karte wird im Vollbild-Schritt angezeigt
'; - mapContainer.appendChild(ph); - } + this._initDemoMapInTile(); // Meta var metaUpdated = document.getElementById('meta-updated'); @@ -318,10 +307,8 @@ const Tutorial = { var mapStats = document.getElementById('map-stats'); if (mapStats) mapStats.innerHTML = s.mapStats; - // Demo-Map und Platzhalter entfernen + // Demo-Map entfernen (Kachel + Fullscreen) this._destroyDemoMap(); - var mapPh = document.querySelector('.tutorial-map-placeholder'); - if (mapPh) mapPh.remove(); // Meta var metaUpdated = document.getElementById('meta-updated'); @@ -346,80 +333,45 @@ const Tutorial = { // ----------------------------------------------------------------------- _demoMap: null, _demoMapMarkers: [], + _demoMapLegend: null, + _demoMapTileMap: null, // Map-Instanz in der Kachel - _initDemoMap() { - if (typeof L === 'undefined') return; - var container = document.getElementById('map-container'); - if (!container) return; + _DEMO_MAP_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' }, + ], + _DEMO_MAP_COLORS: { primary: '#EF4444', secondary: '#F59E0B', tertiary: '#3B82F6' }, + _DEMO_MAP_LABELS: { primary: 'Hauptereignisort', secondary: 'Erwähnt', tertiary: 'Kontext' }, - // 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') !== 'light'; - if (isDark) { - L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png', { - attribution: '\u00a9 OpenStreetMap, \u00a9 CARTO', - maxZoom: 19, - }).addTo(this._demoMap); - } else { - L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.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' }; + _createDemoMarkers(map) { + var markers = []; var self = this; - - locations.forEach(function(loc) { - var color = catColors[loc.cat] || '#7b7b7b'; + this._DEMO_MAP_LOCATIONS.forEach(function(loc) { + var color = self._DEMO_MAP_COLORS[loc.cat]; var icon = L.divIcon({ className: 'tutorial-map-marker', html: '
', + + ';border:2px solid #fff;box-shadow:0 2px 8px rgba(0,0,0,0.5);">', iconSize: [14, 14], iconAnchor: [7, 7], }); var marker = L.marker([loc.lat, loc.lon], { icon: icon }); + var label = self._DEMO_MAP_LABELS[loc.cat]; var popupHtml = '
' + '
' + loc.name + '
' - + '
' + catLabels[loc.cat] + '
' + + '
' + label + '
' + '
' + loc.articles + ' Artikel
' + '
'; marker.bindPopup(popupHtml, { maxWidth: 250, className: 'map-popup-container' }); - marker.addTo(self._demoMap); - self._demoMapMarkers.push(marker); + marker.addTo(map); + markers.push(marker); }); + return markers; + }, - // Legende + _addDemoLegend(map) { + var self = this; var legend = L.control({ position: 'bottomright' }); legend.onAdd = function() { var div = L.DomUtil.create('div', 'map-legend-ctrl'); @@ -427,39 +379,61 @@ const Tutorial = { var html = 'Legende'; ['primary', 'secondary', 'tertiary'].forEach(function(cat) { html += '
' - + '' - + '' + catLabels[cat] + '
'; + + '' + + '' + self._DEMO_MAP_LABELS[cat] + ''; }); div.innerHTML = html; return div; }; - legend.addTo(this._demoMap); - this._demoMapLegend = legend; + legend.addTo(map); + return legend; + }, - // Resize-Fix - var map = this._demoMap; - [100, 300, 800].forEach(function(delay) { - setTimeout(function() { - if (map) map.invalidateSize(); - }, delay); - }); + // Map in der Dashboard-Kachel initialisieren + _initDemoMapInTile() { + if (typeof L === 'undefined') return; + var container = document.getElementById('map-container'); + if (!container) return; - // Hauptereignisort-Popup nach kurzer Verz\u00f6gerung \u00f6ffnen - var mainMarker = this._demoMapMarkers[0]; - if (mainMarker) { - setTimeout(function() { - if (map && mainMarker) mainMarker.openPopup(); - }, 1500); + // Container-Höhe sicherstellen + var gsItem = container.closest('.grid-stack-item'); + if (gsItem) { + var hdr = container.closest('.map-card'); + var headerEl = hdr ? hdr.querySelector('.card-header') : null; + var headerH = headerEl ? headerEl.offsetHeight : 40; + var available = gsItem.offsetHeight - headerH - 4; + container.style.height = Math.max(available, 200) + 'px'; } + + this._demoMapTileMap = L.map(container, { + zoomControl: true, + attributionControl: false, + }).setView([53.545, 9.98], 12); + + L.tileLayer('https://tile.openstreetmap.de/{z}/{x}/{y}.png', { + maxZoom: 18, noWrap: true, + }).addTo(this._demoMapTileMap); + + this._createDemoMarkers(this._demoMapTileMap); + this._addDemoLegend(this._demoMapTileMap); + + var map = this._demoMapTileMap; + [200, 500, 1000].forEach(function(d) { + setTimeout(function() { if (map) map.invalidateSize(); }, d); + }); }, _destroyDemoMap() { if (this._demoMap) { this._demoMap.remove(); this._demoMap = null; - this._demoMapMarkers = []; - this._demoMapLegend = null; } + if (this._demoMapTileMap) { + this._demoMapTileMap.remove(); + this._demoMapTileMap = null; + } + this._demoMapMarkers = []; + this._demoMapLegend = null; }, // ----------------------------------------------------------------------- @@ -966,30 +940,54 @@ const Tutorial = { Tutorial._clearSubHighlights(); }, }, - // 17 - Karte (Vollbild) + // 17 - Karte: Kachel-Ansicht { id: 'karte', - target: '.map-fullscreen-header', + target: '[gs-id="karte"]', title: 'Geografische Verteilung', - text: 'Die Karte zeigt per Geoparsing automatisch erkannte Orte aus den Quellen.

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

' - + 'Klicken Sie auf Marker f\u00fcr Details und verkn\u00fcpfte Artikel. ' - + 'Bei vielen Markern werden nahe Orte zu Clustern gruppiert.

' - + 'Orte einlesen startet das Geoparsing manuell neu. ' - + 'Vollbild zeigt die Karte in dieser gro\u00dfen Ansicht.', + text: 'Die Karte zeigt per Geoparsing automatisch erkannte Orte aus den Quellen.

' + + 'Orte einlesen - Startet das Geoparsing manuell neu
' + + 'Vollbild - Vergr\u00f6\u00dfert die Karte auf den gesamten Bildschirm

' + + 'Im n\u00e4chsten Schritt \u00f6ffnen wir die Vollbildansicht und schauen uns die Marker im Detail an.', + position: 'top', + onEnter: function() { + // Tile-Map Resize triggern + if (Tutorial._demoMapTileMap) { + Tutorial._demoMapTileMap.invalidateSize(); + setTimeout(function() { + if (Tutorial._demoMapTileMap) Tutorial._demoMapTileMap.setView([53.545, 9.98], 12); + }, 200); + } + Tutorial._highlightSub('#geoparse-btn'); + Tutorial._stepTimeout(function() { + Tutorial._clearSubHighlights(); + Tutorial._highlightSub('#map-expand-btn'); + }, 2500); + }, + onExit: function() { + Tutorial._clearSubHighlights(); + }, + }, + // 18 - Karte: Vollbild + Zoom + Marker-Demo + { + id: 'karte-fullscreen', + target: '.map-fullscreen-header', + title: 'Karte im Vollbild', + text: 'Die Karte zoomt jetzt auf den Ereignisort. Die Marker zeigen:

' + + '● Hauptereignisort - Burchardkai Terminal (6 Artikel)
' + + '● Erw\u00e4hnt - Hamburg Innenstadt (2 Artikel)
' + + '● Kontext - Elbe / Hafengebiet (1 Artikel)

' + + 'Die Legende unten rechts erkl\u00e4rt die Farbkategorien. ' + + 'Klicken Sie auf einen Marker f\u00fcr Details und verkn\u00fcpfte Artikel.', position: 'left', disableNav: true, onEnter: function() { - // Chat-Button verstecken im Vollbild var chatBtn = document.getElementById('chat-toggle-btn'); if (chatBtn) chatBtn.style.display = 'none'; Tutorial._openDemoMapFullscreen(); }, onExit: function() { Tutorial._closeDemoMapFullscreen(); - // Chat-Button wieder anzeigen var chatBtn = document.getElementById('chat-toggle-btn'); if (chatBtn) chatBtn.style.display = ''; Tutorial._clearSubHighlights(); @@ -1572,53 +1570,9 @@ const Tutorial = { noWrap: true, }).addTo(this._demoMap); - // Marker (aber noch nicht sichtbar bei Zoom 5) - 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; - this._demoMapMarkers = []; - - locations.forEach(function(loc) { - var color = catColors[loc.cat]; - var icon = L.divIcon({ - className: 'tutorial-map-marker', - html: '
', - iconSize: [16, 16], - iconAnchor: [8, 8], - }); - 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; + // Marker + Legende hinzufügen + this._demoMapMarkers = this._createDemoMarkers(this._demoMap); + this._demoMapLegend = this._addDemoLegend(this._demoMap); // Resize + animierter Zoom auf Hamburg var map = this._demoMap; @@ -1656,41 +1610,43 @@ const Tutorial = { async _simulateMapDemo() { this._demoRunning = true; + await this._wait(800); - await this._wait(500); + if (!this._demoMapMarkers.length || !this._demoMap) { + this._demoRunning = false; + this._enableNavAfterDemo(); + return; + } - // 1. Cursor zu Hauptmarker und klicken - if (this._demoMapMarkers.length > 0 && this._demoMap) { - var mainMarker = this._demoMapMarkers[0]; - var latLng = mainMarker.getLatLng(); + var mapEl = document.getElementById('tutorial-fs-map'); + if (!mapEl) { this._demoRunning = false; this._enableNavAfterDemo(); return; } + var mapRect = mapEl.getBoundingClientRect(); + + // Alle 3 Marker nacheinander besuchen + var names = ['Burchardkai Terminal (Hauptereignisort)', 'Hamburg Innenstadt (Erwähnt)', 'Elbe / Hafengebiet (Kontext)']; + var prevX, prevY; + + for (var i = 0; i < this._demoMapMarkers.length; i++) { + if (!this._isActive) break; + var marker = this._demoMapMarkers[i]; + var latLng = marker.getLatLng(); var point = this._demoMap.latLngToContainerPoint(latLng); - var mapEl = document.getElementById('tutorial-fs-map'); - if (mapEl) { - var mapRect = mapEl.getBoundingClientRect(); - var markerX = mapRect.left + point.x; - var markerY = mapRect.top + point.y; + var mx = mapRect.left + point.x; + var my = mapRect.top + point.y; - this._showCursor(markerX - 60, markerY - 60, 'default'); - await this._wait(300); - await this._animateCursor(markerX - 60, markerY - 60, markerX, markerY, 700); + if (prevX !== undefined) { + await this._animateCursor(prevX, prevY, mx, my, 600); + } else { + this._showCursor(mx - 60, my - 50, 'default'); await this._wait(200); - mainMarker.openPopup(); - await this._wait(2500); - mainMarker.closePopup(); - - // 2. Zum zweiten Marker - var secondMarker = this._demoMapMarkers[1]; - if (secondMarker) { - var p2 = this._demoMap.latLngToContainerPoint(secondMarker.getLatLng()); - var m2x = mapRect.left + p2.x; - var m2y = mapRect.top + p2.y; - await this._animateCursor(markerX, markerY, m2x, m2y, 600); - await this._wait(200); - secondMarker.openPopup(); - await this._wait(2000); - secondMarker.closePopup(); - } + await this._animateCursor(mx - 60, my - 50, mx, my, 500); } + await this._wait(200); + marker.openPopup(); + await this._wait(2500); + marker.closePopup(); + prevX = mx; + prevY = my; } this._hideCursor();