Karten: Routen-Übersichtskarte klickbar + Tagebuch-Karten auf GL

Punkt 2 (Routen-Übersicht 'Karte'): _renderRoutesOnMap crashte, weil die
Polyline-Facade kein bindTooltip/on/setStyle/getLatLngs kannte. In
map-gl-mini.js ergänzt — inkl. breiter, fast unsichtbarer Hit-Linie, damit
Routen auf dem Handy gut antippbar sind (Klick → Detail). Hover-Tooltip
(Name+km) + Hover-Highlight.

Punkt 4 (Tagebuch): beide Leaflet/OSM-Karten (Standort-Übersicht +
Einzeleintrag) auf UI.map.create + Facade-Marker migriert. popupopen-Wiring
(kennt die GL-Facade nicht) → Klick-Delegation auf dem Karten-Container.
Karten-Instanzen werden beim View-Wechsel/Verlassen freigegeben (destroy +
_clearDiaryMaps) gegen WebGL-Kontext-Leak. Detail/Übersicht fitten mehrfach
(Container-Timing).

Nebenbei: _loadPraise warf NotFoundError (insertBefore) — #diary-list liegt
in #diary-view-content, nicht direkt in _container. Jetzt vor der Liste in
deren echtem Elternknoten einfügen.

Verifiziert (headless, eingeloggt, echte Daten): Routenkarte 8 Marker klickbar
→ Detail; Detail+Vorschläge zoomen auf die Route; Tagebuch-Karte GL mit 108
Markern, Popup-Klick → Eintrag, keine Fehler.
This commit is contained in:
rene 2026-06-05 14:23:22 +02:00
parent 1defeec537
commit 285928f6f7
7 changed files with 134 additions and 87 deletions

View file

@ -310,8 +310,10 @@ window.Page_diary = (() => {
aria-label="Schließen">×</button>
`;
// #diary-list liegt in #diary-view-content (nicht direkt in _container) → vor der
// Liste in IHREM echten Elternknoten einfügen, sonst wirft insertBefore (NotFoundError).
const list = _container.querySelector('#diary-list');
if (list) _container.insertBefore(card, list);
if (list && list.parentNode) list.parentNode.insertBefore(card, list);
card.querySelector('#diary-praise-close')?.addEventListener('click', () => {
card.style.opacity = '0';
@ -363,6 +365,13 @@ window.Page_diary = (() => {
let _currentView = 'list'; // 'list' | 'media' | 'calendar' | 'map'
let _totalStats = null; // {entries, photos, days} — Gesamtstatistik aus API
let _diaryMaps = []; // aktive Karten-Instanzen → beim View-Wechsel freigeben (GL-Kontext-Leak)
// Karten beim View-Wechsel/Verlassen sauber freigeben (sonst leakt der WebGL-Kontext).
function _clearDiaryMaps() {
_diaryMaps.forEach(m => { try { m && m.remove && m.remove(); } catch (e) {} });
_diaryMaps = [];
}
async function _loadStats() {
const dog = _appState.activeDog;
@ -431,6 +440,7 @@ window.Page_diary = (() => {
const content = _container.querySelector('#diary-view-content');
const loadMore = _container.querySelector('#diary-load-more');
if (!content) return;
_clearDiaryMaps(); // evtl. offene Karte (z.B. Map-Ansicht) freigeben
// "Weitere laden" nur in der Listenansicht sinnvoll
if (loadMore) loadMore.style.display = 'none';
if (_currentView === 'list') {
@ -470,16 +480,6 @@ window.Page_diary = (() => {
return;
}
// Leaflet laden
if (!window.L) {
await new Promise((res, rej) => {
const s = document.createElement('script');
s.src = `/js/leaflet.js?v=${APP_VER}`;
s.onload = res; s.onerror = rej;
document.head.appendChild(s);
});
}
const mapEl = content.querySelector('#diary-map-view');
if (!mapEl) return;
@ -488,8 +488,23 @@ window.Page_diary = (() => {
const lons = locations.map(l => l.gps_lon);
const bounds = [[Math.min(...lats), Math.min(...lons)], [Math.max(...lats), Math.max(...lons)]];
const map = L.map(mapEl, { zoomControl: true, attributionControl: false });
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(map);
// GL-Karte (gleicher Style wie die zentrale Karte), Fallback Leaflet über die Facade.
const map = await UI.map.create(mapEl, { zoomControl: true, attributionControl: false });
_diaryMaps.push(map);
// Popup-Klick → Eintrag öffnen (Delegation auf dem Karten-Container; engine-neutral,
// ersetzt das Leaflet-'popupopen'-Wiring, das die GL-Facade nicht kennt).
mapEl.addEventListener('click', async (e) => {
const pop = e.target.closest('.diary-map-popup');
if (!pop) return;
const id = parseInt(pop.dataset.id);
if (!_entries.find(en => en.id === id)) {
try { const fresh = await API.diary.get(_appState.activeDog.id, id); _entries.unshift(fresh); }
catch { return; }
}
if (map.closePopup) map.closePopup();
_openDetail(id);
});
// Marker für jeden Eintrag
locations.forEach(loc => {
@ -497,59 +512,36 @@ window.Page_diary = (() => {
const dateStr = loc.datum ? new Date(loc.datum+'T12:00').toLocaleDateString('de-DE', {day:'numeric',month:'short',year:'numeric'}) : '';
const title = UI.escape(loc.titel || loc.location_name || dateStr);
const icon = L.divIcon({
html: hasPhoto
const iconHtml = hasPhoto
? `<div style="width:44px;height:44px;border-radius:50%;overflow:hidden;border:3px solid var(--c-primary,#C4843A);box-shadow:0 2px 8px rgba(0,0,0,.3);background:#fff">
<img src="${UI.escape(loc.cover_preview_url || loc.cover_url)}" style="width:100%;height:100%;object-fit:cover" data-fb-src="${UI.escape(loc.cover_url)}">
</div>`
: `<div style="width:32px;height:32px;border-radius:50%;background:var(--c-primary,#C4843A);border:3px solid #fff;box-shadow:0 2px 8px rgba(0,0,0,.3);display:flex;align-items:center;justify-content:center">
<svg style="width:16px;height:16px;fill:#fff" viewBox="0 0 256 256"><path d="M128,16a96,96,0,1,0,96,96A96.11,96.11,0,0,0,128,16Zm0,176a80,80,0,1,1,80-80A80.09,80.09,0,0,1,128,192Zm0-104a24,24,0,1,0,24,24A24,24,0,0,0,128,88Z"/></svg>
</div>`,
iconSize: hasPhoto ? [44, 44] : [32, 32],
iconAnchor: hasPhoto ? [22, 22] : [16, 16],
className: '',
});
</div>`;
const _mSize = hasPhoto ? 44 : 32;
const marker = L.marker([loc.gps_lat, loc.gps_lon], { icon });
marker.bindPopup(`
UI.map.svgMarker(loc.gps_lat, loc.gps_lon, iconHtml, { size: _mSize, anchorY: _mSize / 2 })
.bindPopup(`
<div style="min-width:160px;cursor:pointer" class="diary-map-popup" data-id="${loc.id}">
${hasPhoto ? `<img src="${UI.escape(loc.cover_preview_url || loc.cover_url)}" style="width:100%;height:100px;object-fit:cover;border-radius:6px;display:block;margin-bottom:8px" data-fb-src="${UI.escape(loc.cover_url)}">` : ''}
<div style="font-weight:600;font-size:13px;margin-bottom:2px">${title}</div>
<div style="font-size:11px;color:#888">${dateStr}</div>
${loc.media_count > 1 ? `<div style="font-size:11px;color:#888;margin-top:2px">📷 ${loc.media_count} Medien</div>` : ''}
<div style="margin-top:6px;text-align:center;font-size:12px;color:var(--c-primary,#C4843A);font-weight:600"> Öffnen</div>
</div>`, { maxWidth: 200 });
marker.on('popupopen', () => {
setTimeout(() => {
document.querySelectorAll('.diary-map-popup').forEach(el => {
el.addEventListener('click', async () => {
map.closePopup();
const id = parseInt(el.dataset.id);
// Eintrag aus _entries holen oder per API nachladen
if (!_entries.find(e => e.id === id)) {
try {
const fresh = await API.diary.get(_appState.activeDog.id, id);
_entries.unshift(fresh);
} catch { return; }
}
_openDetail(id);
});
});
}, 50);
});
marker.addTo(map);
</div>`, { maxWidth: 200 })
.addTo(map);
});
// Karte auf alle Punkte zoomen
if (locations.length === 1) {
map.setView([locations[0].gps_lat, locations[0].gps_lon], 14);
} else {
map.fitBounds(bounds, { padding: [40, 40] });
}
setTimeout(() => map.invalidateSize(), 100);
// Karte auf alle Punkte zoomen — mehrfach (Container/Style können beim Erstellen
// noch nicht final sein → erneut fitten nach Layout/Tile-Load).
const _fit = () => {
map.invalidateSize();
if (locations.length === 1) map.setView([locations[0].gps_lat, locations[0].gps_lon], 14);
else map.fitBounds(bounds, { padding: [40, 40] });
};
_fit();
setTimeout(_fit, 200); setTimeout(_fit, 500);
}
function _renderMediaGrid(content) {
@ -1154,26 +1146,18 @@ window.Page_diary = (() => {
setTimeout(async () => {
const mapEl = view.querySelector('#diary-dv-map');
if (!mapEl) return;
if (!window.L) {
await new Promise((res, rej) => {
const s = document.createElement('script');
s.src = `/js/leaflet.js?v=${APP_VER}`;
s.onload = res; s.onerror = rej;
document.head.appendChild(s);
});
}
const map = L.map(mapEl, { zoomControl: true, attributionControl: false });
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(map);
const svgIcon = L.divIcon({
html: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="32" height="32">
const map = await UI.map.create(mapEl, {
center: [entry.gps_lat, entry.gps_lon], zoom: 15,
zoomControl: true, attributionControl: false,
});
_diaryMaps.push(map);
const iconHtml = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="32" height="32">
<circle cx="128" cy="128" r="96" fill="var(--c-primary,#C4843A)" opacity=".25"/>
<circle cx="128" cy="128" r="48" fill="var(--c-primary,#C4843A)"/>
</svg>`,
iconSize: [32, 32], iconAnchor: [16, 16], className: '',
});
L.marker([entry.gps_lat, entry.gps_lon], { icon: svgIcon }).addTo(map);
map.setView([entry.gps_lat, entry.gps_lon], 15);
map.invalidateSize();
</svg>`;
UI.map.svgMarker(entry.gps_lat, entry.gps_lon, iconHtml, { size: 32, anchorY: 16 }).addTo(map);
const _fit = () => { map.invalidateSize(); map.setView([entry.gps_lat, entry.gps_lon], 15); };
_fit(); setTimeout(_fit, 200); setTimeout(_fit, 500);
}, 150);
}
@ -1811,6 +1795,8 @@ window.Page_diary = (() => {
.trim();
}
return { init, refresh, openNew, onDogChange, openDetail: _openDetail };
function destroy() { _clearDiaryMaps(); }
return { init, refresh, openNew, onDogChange, openDetail: _openDetail, destroy };
})();