Tutorial: Karte im Vollbild mit Markern, echte Drag/Resize-Animationen

Karte (Step 17):
- Oeffnet jetzt die Karten-Vollbild-Ansicht mit eigenem Leaflet-Map
- Zoomt auf Hamburg (Zoom 13) mit 3 farbcodierten Markern
- Cursor faehrt zu Markern, oeffnet Popups (Burchardkai, Innenstadt)
- Legende erklaert Kategorien (Hauptereignisort/Erwaehnt/Kontext)
- Funktionen Orte einlesen + Vollbild werden in der Bubble erklaert
- Map wird beim Step-Exit sauber aus dem Fullscreen entfernt

Drag-Demo (Step 18):
- Kachel bewegt sich jetzt visuell per CSS transform mit dem Cursor
- 150px nach rechts, dann zurueck - echte Verschiebe-Animation
- Kachel erhaelt erhoehten z-index waehrend der Animation

Resize-Demo (Step 19):
- Kachel aendert visuell Breite/Hoehe mit dem Cursor
- 80px breiter + 50px hoeher, dann zurueck
- Echte Groessenaenderung sichtbar statt nur Cursor-Bewegung

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
Claude Dev
2026-03-16 16:09:23 +01:00
Ursprung 6d09c0a5fa
Commit 3a2ea7a8c7
2 geänderte Dateien mit 299 neuen und 38 gelöschten Zeilen

Datei anzeigen

@@ -17,7 +17,7 @@
<link rel="stylesheet" href="/static/vendor/MarkerCluster.css">
<link rel="stylesheet" href="/static/vendor/MarkerCluster.Default.css">
<link rel="stylesheet" href="/static/css/network.css?v=20260316a">
<link rel="stylesheet" href="/static/css/style.css?v=20260316f">
<link rel="stylesheet" href="/static/css/style.css?v=20260316g">
</head>
<body>
<a href="#main-content" class="skip-link">Zum Hauptinhalt springen</a>
@@ -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=20260316h"></script>
<script src="/static/js/tutorial.js?v=20260316i"></script>
<script src="/static/js/chat.js?v=20260316f"></script>
<script>document.addEventListener("DOMContentLoaded",function(){Chat.init();Tutorial.init()});</script>

Datei anzeigen

@@ -188,12 +188,11 @@ const Tutorial = {
timeline.innerHTML = tlHtml;
}
// Karte: Leaflet-Map mit Demo-Markern initialisieren
// Karte: Stats setzen, Map wird erst im Vollbild-Step initialisiert
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';
this._initDemoMap();
// Meta
var metaUpdated = document.getElementById('meta-updated');
@@ -841,30 +840,26 @@ const Tutorial = {
Tutorial._clearSubHighlights();
},
},
// 17 - Karte
// 17 - Karte (Vollbild)
{
id: 'karte',
target: '[gs-id="karte"]',
target: '#map-fullscreen-overlay',
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ähnt</strong> - In Artikeln genannte Orte<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ür Details und verknüpfte Artikel. '
+ 'Bei vielen Markern werden nahe Orte zu Clustern gruppiert.',
position: 'top',
+ '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.',
position: 'left',
disableNav: true,
onEnter: function() {
// 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();
Tutorial._openDemoMapFullscreen();
},
onExit: function() {
Tutorial._closeDemoMapFullscreen();
Tutorial._clearSubHighlights();
},
},
@@ -1346,43 +1341,153 @@ const Tutorial = {
},
// -----------------------------------------------------------------------
// Karten-Demo (Step 17): Marker klicken, Geoparse + Vollbild highlighten
// Karten-Demo: Vollbild mit Markern und Cursor-Demo
// -----------------------------------------------------------------------
_openDemoMapFullscreen() {
var overlay = document.getElementById('map-fullscreen-overlay');
var fsContainer = document.getElementById('map-fullscreen-container');
var fsStats = document.getElementById('map-fullscreen-stats');
if (!overlay || !fsContainer) return;
// Overlay anzeigen (\u00fcber Tutorial-Z-Index)
overlay.classList.add('active');
overlay.style.zIndex = '9002';
if (fsStats) fsStats.textContent = '3 Orte / 9 Artikel';
// Alte Demo-Map entfernen falls vorhanden
this._destroyDemoMap();
// Neue Map im Fullscreen-Container erstellen
fsContainer.innerHTML = '';
var mapDiv = document.createElement('div');
mapDiv.id = 'tutorial-fs-map';
mapDiv.style.cssText = 'width:100%;height:100%;';
fsContainer.appendChild(mapDiv);
var isDark = !document.documentElement.getAttribute('data-theme') || document.documentElement.getAttribute('data-theme') !== 'light';
this._demoMap = L.map(mapDiv, {
zoomControl: true,
attributionControl: true,
}).setView([53.545, 9.98], 13);
var tileUrl = isDark
? 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png';
L.tileLayer(tileUrl, {
attribution: '\u00a9 OpenStreetMap, \u00a9 CARTO',
maxZoom: 19,
}).addTo(this._demoMap);
// 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;
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;
// Resize + Start Demo
var map = this._demoMap;
setTimeout(function() {
if (map) map.invalidateSize();
setTimeout(function() {
if (map) map.setView([53.545, 9.98], 13);
self._simulateMapDemo();
}, 300);
}, 200);
},
_closeDemoMapFullscreen() {
var overlay = document.getElementById('map-fullscreen-overlay');
if (overlay) {
overlay.classList.remove('active');
overlay.style.zIndex = '';
}
var fsContainer = document.getElementById('map-fullscreen-container');
if (fsContainer) fsContainer.innerHTML = '';
this._destroyDemoMap();
},
async _simulateMapDemo() {
this._demoRunning = true;
// 1. Auf Marker zeigen und klicken
await this._wait(500);
// 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 mapContainer = document.getElementById('map-container');
if (mapContainer) {
var mapRect = mapContainer.getBoundingClientRect();
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;
this._showCursor(markerX - 40, markerY - 40, 'default');
await this._wait(400);
await this._animateCursor(markerX - 40, markerY - 40, markerX, markerY, 600);
this._showCursor(markerX - 60, markerY - 60, 'default');
await this._wait(300);
await this._animateCursor(markerX - 60, markerY - 60, markerX, markerY, 700);
await this._wait(200);
mainMarker.openPopup();
await this._wait(2000);
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();
}
}
}
// 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();
@@ -1642,6 +1747,162 @@ const Tutorial = {
this._enableNavAfterDemo();
},
// -----------------------------------------------------------------------
// Drag-Demo: Kachel visuell verschieben
// -----------------------------------------------------------------------
async _simulateDrag() {
this._demoRunning = true;
var tile = document.querySelector('[gs-id="lagebild"]');
var header = tile ? tile.querySelector('.card-header') : null;
if (!header || !tile) { this._demoRunning = false; this._enableNavAfterDemo(); return; }
var rect = header.getBoundingClientRect();
var startX = rect.left + rect.width / 2;
var startY = rect.top + rect.height / 2;
var moveX = 150;
// Cursor erscheint
this._showCursor(startX - 50, startY - 30, 'default');
await this._wait(300);
await this._animateCursor(startX - 50, startY - 30, startX, startY, 500);
await this._wait(300);
// Grabbing
this._els.cursor.classList.remove('tutorial-cursor-default');
this._els.cursor.classList.add('tutorial-cursor-grabbing');
await this._wait(200);
// Kachel visuell verschieben (CSS transform)
tile.style.transition = 'none';
tile.style.zIndex = '9002';
var self = this;
var start = null;
await new Promise(function(resolve) {
function frame(ts) {
if (!self._isActive) { resolve(); return; }
if (!start) start = ts;
var progress = Math.min((ts - start) / 1200, 1);
var eased = progress < 0.5 ? 4*progress*progress*progress : 1 - Math.pow(-2*progress+2,3)/2;
var dx = moveX * eased;
tile.style.transform = 'translateX(' + dx + 'px)';
self._els.cursor.style.left = (startX + dx) + 'px';
if (progress < 1) requestAnimationFrame(frame); else resolve();
}
requestAnimationFrame(frame);
});
await this._wait(300);
// Zurück
this._els.cursor.classList.remove('tutorial-cursor-grabbing');
this._els.cursor.classList.add('tutorial-cursor-default');
start = null;
await new Promise(function(resolve) {
function frame(ts) {
if (!self._isActive) { resolve(); return; }
if (!start) start = ts;
var progress = Math.min((ts - start) / 800, 1);
var eased = progress < 0.5 ? 4*progress*progress*progress : 1 - Math.pow(-2*progress+2,3)/2;
var dx = moveX * (1 - eased);
tile.style.transform = 'translateX(' + dx + 'px)';
self._els.cursor.style.left = (startX + dx) + 'px';
if (progress < 1) requestAnimationFrame(frame); else resolve();
}
requestAnimationFrame(frame);
});
// Aufräumen
tile.style.transform = '';
tile.style.transition = '';
tile.style.zIndex = '';
this._hideCursor();
await this._wait(200);
this._demoRunning = false;
this._enableNavAfterDemo();
},
// -----------------------------------------------------------------------
// Resize-Demo: Kachel visuell vergrößern
// -----------------------------------------------------------------------
async _simulateResize() {
this._demoRunning = true;
var tile = document.querySelector('[gs-id="faktencheck"]');
if (!tile) { this._demoRunning = false; this._enableNavAfterDemo(); return; }
var rect = tile.getBoundingClientRect();
var startX = rect.right - 6;
var startY = rect.bottom - 6;
var expandX = 80;
var expandY = 50;
// Cursor am Rand
this._showCursor(startX - 40, startY - 40, 'default');
await this._wait(300);
await this._animateCursor(startX - 40, startY - 40, startX, startY, 400);
await this._wait(200);
// Resize-Cursor
this._els.cursor.classList.remove('tutorial-cursor-default');
this._els.cursor.classList.add('tutorial-cursor-resize');
await this._wait(200);
// Kachel vergrößern (visuell)
tile.style.transition = 'none';
tile.style.zIndex = '9002';
var origW = tile.offsetWidth;
var origH = tile.offsetHeight;
var self = this;
var start = null;
await new Promise(function(resolve) {
function frame(ts) {
if (!self._isActive) { resolve(); return; }
if (!start) start = ts;
var progress = Math.min((ts - start) / 1000, 1);
var eased = progress < 0.5 ? 4*progress*progress*progress : 1 - Math.pow(-2*progress+2,3)/2;
var dx = expandX * eased;
var dy = expandY * eased;
tile.style.width = (origW + dx) + 'px';
tile.style.height = (origH + dy) + 'px';
self._els.cursor.style.left = (startX + dx) + 'px';
self._els.cursor.style.top = (startY + dy) + 'px';
if (progress < 1) requestAnimationFrame(frame); else resolve();
}
requestAnimationFrame(frame);
});
await this._wait(500);
// Zurück
start = null;
await new Promise(function(resolve) {
function frame(ts) {
if (!self._isActive) { resolve(); return; }
if (!start) start = ts;
var progress = Math.min((ts - start) / 700, 1);
var eased = progress < 0.5 ? 4*progress*progress*progress : 1 - Math.pow(-2*progress+2,3)/2;
var dx = expandX * (1 - eased);
var dy = expandY * (1 - eased);
tile.style.width = (origW + dx) + 'px';
tile.style.height = (origH + dy) + 'px';
self._els.cursor.style.left = (startX + dx) + 'px';
self._els.cursor.style.top = (startY + dy) + 'px';
if (progress < 1) requestAnimationFrame(frame); else resolve();
}
requestAnimationFrame(frame);
});
// Aufräumen
tile.style.width = '';
tile.style.height = '';
tile.style.transition = '';
tile.style.zIndex = '';
this._hideCursor();
await this._wait(200);
this._demoRunning = false;
this._enableNavAfterDemo();
},
// -----------------------------------------------------------------------
// Keyboard
// -----------------------------------------------------------------------