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 = '';
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 = '';
- 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();