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) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
Claude Dev
2026-03-16 15:06:52 +01:00
Ursprung 5289bbf29b
Commit 77e83efae0
2 geänderte Dateien mit 185 neuen und 28 gelöschten Zeilen

Datei anzeigen

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

Datei anzeigen

@@ -187,22 +187,12 @@ const Tutorial = {
timeline.innerHTML = tlHtml; timeline.innerHTML = tlHtml;
} }
// Karte: "Keine Orte" ausblenden, Platzhalter einsetzen // Karte: Leaflet-Map mit Demo-Markern initialisieren
var mapEmpty = document.getElementById('map-empty'); var mapEmpty = document.getElementById('map-empty');
if (mapEmpty) mapEmpty.style.display = 'none'; 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 = '<div style="text-align:center;">'
+ '<div style="font-size:32px;margin-bottom:8px;opacity:0.5;">&#127758;</div>'
+ '<div>3 Orte erkannt: Hamburg, Burchardkai, Elbe</div></div>';
mapContainer.appendChild(mapPlaceholder);
}
var mapStats = document.getElementById('map-stats'); var mapStats = document.getElementById('map-stats');
if (mapStats) mapStats.textContent = '3 Orte'; if (mapStats) mapStats.textContent = '3 Orte / 9 Artikel';
this._initDemoMap();
// Meta // Meta
var metaUpdated = document.getElementById('meta-updated'); var metaUpdated = document.getElementById('meta-updated');
@@ -250,9 +240,8 @@ const Tutorial = {
var mapStats = document.getElementById('map-stats'); var mapStats = document.getElementById('map-stats');
if (mapStats) mapStats.innerHTML = s.mapStats; if (mapStats) mapStats.innerHTML = s.mapStats;
// Map-Platzhalter entfernen // Demo-Map entfernen
var mapPlaceholder = document.querySelector('.tutorial-map-placeholder'); this._destroyDemoMap();
if (mapPlaceholder) mapPlaceholder.remove();
// Meta // Meta
var metaUpdated = document.getElementById('meta-updated'); var metaUpdated = document.getElementById('meta-updated');
@@ -267,6 +256,127 @@ const Tutorial = {
this._savedState = null; 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: '<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>',
iconSize: [14, 14],
iconAnchor: [7, 7],
});
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;
// 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 // Highlight-Helfer: Einzelnes Sub-Element innerhalb einer Kachel markieren
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
@@ -632,19 +742,23 @@ const Tutorial = {
id: 'karte', id: 'karte',
target: '[gs-id="karte"]', target: '[gs-id="karte"]',
title: 'Geografische Verteilung', title: 'Geografische Verteilung',
text: 'Die Karte zeigt automatisch erkannte Orte aus den gesammelten Artikeln (Geoparsing).<br><br>' text: 'Die Karte zeigt per Geoparsing automatisch erkannte Orte aus den Quellen.<br><br>'
+ '<strong>Marker</strong> - Klicken Sie auf einen Marker für Details zum Ort und verknüpfte Artikel<br>' + '<strong style="color:#EF4444;">&#9679;</strong> <strong>Hauptereignisort</strong> - Zentraler Ort des Geschehens<br>'
+ '<strong>Cluster</strong> - Bei vielen Markern werden nahe Orte gruppiert<br>' + '<strong style="color:#F59E0B;">&#9679;</strong> <strong>Erwähnt</strong> - In Artikeln genannte Orte<br>'
+ '<strong>Orte einlesen</strong> - Startet das Geoparsing manuell neu<br>' + '<strong style="color:#3B82F6;">&#9679;</strong> <strong>Kontext</strong> - Relevante Umgebung<br><br>'
+ '<strong>Vollbild</strong> - Vergrößert die Karte auf den gesamten Bildschirm<br><br>' + 'Klicken Sie auf Marker für Details und verknüpfte Artikel. '
+ 'Besonders bei internationalen Lagen bietet die Karte einen schnellen Überblick über die räumliche Verteilung.', + 'Bei vielen Markern werden nahe Orte zu Clustern gruppiert.',
position: 'top', position: 'top',
disableNav: true,
onEnter: function() { onEnter: function() {
Tutorial._highlightSub('#geoparse-btn'); // Demo-Map Popup öffnen + invalidateSize
if (Tutorial._demoMap) {
Tutorial._demoMap.invalidateSize();
setTimeout(function() { setTimeout(function() {
Tutorial._clearSubHighlights(); if (Tutorial._demoMap) Tutorial._demoMap.setView([53.545, 9.98], 13);
Tutorial._highlightSub('#map-expand-btn'); }, 300);
}, 3000); }
Tutorial._simulateMapDemo();
}, },
onExit: function() { onExit: function() {
Tutorial._clearSubHighlights(); 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) // Typ-Wechsel-Demo (Step 4)
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------