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/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>
|
||||||
|
|
||||||
|
|||||||
@@ -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;">🌎</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;">●</strong> <strong>Hauptereignisort</strong> - Zentraler Ort des Geschehens<br>'
|
||||||
+ '<strong>Cluster</strong> - Bei vielen Markern werden nahe Orte gruppiert<br>'
|
+ '<strong style="color:#F59E0B;">●</strong> <strong>Erwähnt</strong> - In Artikeln genannte Orte<br>'
|
||||||
+ '<strong>Orte einlesen</strong> - Startet das Geoparsing manuell neu<br>'
|
+ '<strong style="color:#3B82F6;">●</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
|
||||||
setTimeout(function() {
|
if (Tutorial._demoMap) {
|
||||||
Tutorial._clearSubHighlights();
|
Tutorial._demoMap.invalidateSize();
|
||||||
Tutorial._highlightSub('#map-expand-btn');
|
setTimeout(function() {
|
||||||
}, 3000);
|
if (Tutorial._demoMap) Tutorial._demoMap.setView([53.545, 9.98], 13);
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
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)
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren