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:
@@ -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=20260316d"></script>
|
||||
<script src="/static/js/tutorial.js?v=20260316e"></script>
|
||||
<script src="/static/js/chat.js?v=20260316e"></script>
|
||||
<script>document.addEventListener("DOMContentLoaded",function(){Chat.init();Tutorial.init()});</script>
|
||||
|
||||
|
||||
@@ -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 = '<div style="text-align:center;">'
|
||||
+ '<div style="font-size:32px;margin-bottom:8px;opacity:0.5;">🌎</div>'
|
||||
+ '<div>3 Orte erkannt: Hamburg, Burchardkai, Elbe</div></div>';
|
||||
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: '<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
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -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).<br><br>'
|
||||
+ '<strong>Marker</strong> - Klicken Sie auf einen Marker für Details zum Ort und verknüpfte Artikel<br>'
|
||||
+ '<strong>Cluster</strong> - Bei vielen Markern werden nahe Orte gruppiert<br>'
|
||||
+ '<strong>Orte einlesen</strong> - Startet das Geoparsing manuell neu<br>'
|
||||
+ '<strong>Vollbild</strong> - Vergrößert die Karte auf den gesamten Bildschirm<br><br>'
|
||||
+ '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.<br><br>'
|
||||
+ '<strong style="color:#EF4444;">●</strong> <strong>Hauptereignisort</strong> - Zentraler Ort des Geschehens<br>'
|
||||
+ '<strong style="color:#F59E0B;">●</strong> <strong>Erwähnt</strong> - In Artikeln genannte Orte<br>'
|
||||
+ '<strong style="color:#3B82F6;">●</strong> <strong>Kontext</strong> - Relevante Umgebung<br><br>'
|
||||
+ '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)
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren