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) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
Claude Dev
2026-03-16 17:48:14 +01:00
Ursprung 3b1e6c1496
Commit e230248f61
2 geänderte Dateien mit 137 neuen und 181 gelöschten Zeilen

Datei anzeigen

@@ -764,7 +764,7 @@
<script src="/static/js/api_network.js?v=20260316a"></script>
<script src="/static/js/network-graph.js?v=20260316a"></script>
<script src="/static/js/app_network.js?v=20260316a"></script>
<script src="/static/js/tutorial.js?v=20260316v"></script>
<script src="/static/js/tutorial.js?v=20260316w"></script>
<script src="/static/js/chat.js?v=20260316f"></script>
<script>document.addEventListener("DOMContentLoaded",function(){Chat.init();Tutorial.init()});</script>

Datei anzeigen

@@ -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 = '<div style="text-align:center;">'
+ '<div style="font-size:40px;margin-bottom:12px;opacity:0.4;">&#127758;</div>'
+ '<div>3 Orte erkannt: Hamburg, Burchardkai, Elbe</div>'
+ '<div style="font-size:11px;margin-top:4px;opacity:0.6;">Karte wird im Vollbild-Schritt angezeigt</div></div>';
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;
// 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 = [
_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' },
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: '<div style="width:14px;height:14px;border-radius:50%;background:' + color
+ ';border:2px solid #fff;box-shadow:0 2px 6px rgba(0,0,0,0.4);"></div>',
+ ';border:2px solid #fff;box-shadow:0 2px 8px rgba(0,0,0,0.5);"></div>',
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 = '<div class="map-popup">'
+ '<div class="map-popup-title">' + loc.name + '</div>'
+ '<div style="font-size:11px;margin:4px 0;"><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:' + color + ';margin-right:4px;vertical-align:middle;"></span>' + catLabels[loc.cat] + '</div>'
+ '<div style="font-size:11px;margin:4px 0;"><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:' + color + ';margin-right:4px;vertical-align:middle;"></span>' + label + '</div>'
+ '<div class="map-popup-count">' + loc.articles + ' Artikel</div>'
+ '</div>';
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 = '<strong style="display:block;margin-bottom:6px;">Legende</strong>';
['primary', 'secondary', 'tertiary'].forEach(function(cat) {
html += '<div class="map-legend-item" style="display:flex;align-items:center;gap:6px;margin:3px 0;">'
+ '<span style="width:10px;height:10px;border-radius:50%;background:' + catColors[cat] + ';flex-shrink:0;"></span>'
+ '<span>' + catLabels[cat] + '</span></div>';
+ '<span style="width:10px;height:10px;border-radius:50%;background:' + self._DEMO_MAP_COLORS[cat] + ';flex-shrink:0;"></span>'
+ '<span>' + self._DEMO_MAP_LABELS[cat] + '</span></div>';
});
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;
}
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.<br><br>'
+ '<strong style="color:#EF4444;">&#9679;</strong> <strong>Hauptereignisort</strong> - Zentraler Ort des Geschehens<br>'
+ '<strong style="color:#F59E0B;">&#9679;</strong> <strong>Erw\u00e4hnt</strong> - In Artikeln genannte Orte<br>'
+ '<strong style="color:#3B82F6;">&#9679;</strong> <strong>Kontext</strong> - Relevante Umgebung<br><br>'
+ 'Klicken Sie auf Marker f\u00fcr Details und verkn\u00fcpfte Artikel. '
+ 'Bei vielen Markern werden nahe Orte zu Clustern gruppiert.<br><br>'
+ '<strong>Orte einlesen</strong> startet das Geoparsing manuell neu. '
+ '<strong>Vollbild</strong> zeigt die Karte in dieser gro\u00dfen Ansicht.',
text: 'Die Karte zeigt per <strong>Geoparsing</strong> automatisch erkannte Orte aus den Quellen.<br><br>'
+ '<strong>Orte einlesen</strong> - Startet das Geoparsing manuell neu<br>'
+ '<strong>Vollbild</strong> - Vergr\u00f6\u00dfert die Karte auf den gesamten Bildschirm<br><br>'
+ '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:<br><br>'
+ '<strong style="color:#EF4444;">&#9679; Hauptereignisort</strong> - Burchardkai Terminal (6 Artikel)<br>'
+ '<strong style="color:#F59E0B;">&#9679; Erw\u00e4hnt</strong> - Hamburg Innenstadt (2 Artikel)<br>'
+ '<strong style="color:#3B82F6;">&#9679; Kontext</strong> - Elbe / Hafengebiet (1 Artikel)<br><br>'
+ 'Die <strong>Legende</strong> 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: '<div style="width:16px;height:16px;border-radius:50%;background:' + color
+ ';border:2px solid #fff;box-shadow:0 2px 8px rgba(0,0,0,0.5);"></div>',
iconSize: [16, 16],
iconAnchor: [8, 8],
});
var marker = L.marker([loc.lat, loc.lon], { icon: icon });
var popupHtml = '<div class="map-popup">'
+ '<div class="map-popup-title">' + loc.name + '</div>'
+ '<div style="font-size:11px;margin:4px 0;"><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:' + color + ';margin-right:4px;vertical-align:middle;"></span>' + catLabels[loc.cat] + '</div>'
+ '<div class="map-popup-count">' + loc.articles + ' Artikel</div>'
+ '</div>';
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 = '<strong style="display:block;margin-bottom:6px;">Legende</strong>';
['primary', 'secondary', 'tertiary'].forEach(function(cat) {
html += '<div class="map-legend-item" style="display:flex;align-items:center;gap:6px;margin:3px 0;">'
+ '<span style="width:10px;height:10px;border-radius:50%;background:' + catColors[cat] + ';flex-shrink:0;"></span>'
+ '<span>' + catLabels[cat] + '</span></div>';
});
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 point = this._demoMap.latLngToContainerPoint(latLng);
var mapEl = document.getElementById('tutorial-fs-map');
if (mapEl) {
if (!mapEl) { this._demoRunning = false; this._enableNavAfterDemo(); return; }
var mapRect = mapEl.getBoundingClientRect();
var markerX = mapRect.left + point.x;
var markerY = 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);
// 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 mx = mapRect.left + point.x;
var my = mapRect.top + point.y;
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._animateCursor(mx - 60, my - 50, mx, my, 500);
}
await this._wait(200);
marker.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();
}
}
marker.closePopup();
prevX = mx;
prevY = my;
}
this._hideCursor();