From 285928f6f7c52b561c7c28e4914746ef0b04bf92 Mon Sep 17 00:00:00 2001 From: rene Date: Fri, 5 Jun 2026 14:23:22 +0200 Subject: [PATCH] =?UTF-8?q?Karten:=20Routen-=C3=9Cbersichtskarte=20klickba?= =?UTF-8?q?r=20+=20Tagebuch-Karten=20auf=20GL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- VERSION | 2 +- backend/static/index.html | 24 +++--- backend/static/js/app.js | 2 +- backend/static/js/map-gl-mini.js | 61 +++++++++++++++ backend/static/js/pages/diary.js | 128 ++++++++++++++----------------- backend/static/landing.html | 2 +- backend/static/sw.js | 2 +- 7 files changed, 134 insertions(+), 87 deletions(-) diff --git a/VERSION b/VERSION index 0f32703..0e7f8bf 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1201 \ No newline at end of file +1203 \ No newline at end of file diff --git a/backend/static/index.html b/backend/static/index.html index bdb57f4..c5f862e 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -86,14 +86,14 @@ Ban Yaro - + - - - - - + + + + + @@ -617,11 +617,11 @@ - - - - - + + + + + @@ -631,7 +631,7 @@ - + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index a186a68..517c478 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '1201'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '1203'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt window.APP_VER = APP_VER; // global verfügbar für andere Module (z.B. offline-indicator) window.APP_VERSION = APP_VERSION; diff --git a/backend/static/js/map-gl-mini.js b/backend/static/js/map-gl-mini.js index 92d07d6..e0608a5 100644 --- a/backend/static/js/map-gl-mini.js +++ b/backend/static/js/map-gl-mini.js @@ -115,7 +115,11 @@ _id: 'poly-' + (++_seq), _map: null, _opts: opts, + _handlers: {}, // ev → [fn] + _tooltip: null, + _tipPopup: null, _geo: function () { return { type: 'Feature', geometry: { type: 'LineString', coordinates: this._latlngs.map(_toLngLat) } }; }, + _hitId: function () { return this._id + '-hit'; }, _ensure: function () { var self = this, m = self._map; var add = function () { @@ -123,18 +127,75 @@ if (!m.getLayer(self._id)) m.addLayer({ id: self._id, type: 'line', source: self._id, layout: { 'line-cap': 'round', 'line-join': 'round' }, paint: { 'line-color': self._opts.color || '#C4843A', 'line-width': self._opts.weight || 4, 'line-opacity': self._opts.opacity != null ? self._opts.opacity : 0.9 } }); + // Breite, fast unsichtbare Hit-Linie → auf dem Handy gut antippbar. + if (self._opts.interactive !== false && !m.getLayer(self._hitId())) { + m.addLayer({ id: self._hitId(), type: 'line', source: self._id, + layout: { 'line-cap': 'round', 'line-join': 'round' }, + paint: { 'line-color': '#000', 'line-opacity': 0.01, 'line-width': 18 } }); + } + self._wireAll(); }; if (m.isStyleLoaded && m.isStyleLoaded()) add(); else m.once('load', add); }, + _wireOne: function (ev, fn) { + var self = this, m = self._map, hit = self._hitId(); + if (!m.getLayer(hit)) return; + if (ev === 'click') { + m.on('click', hit, function (e) { if (e.originalEvent) e.originalEvent.stopPropagation(); fn(e); }); + } else if (ev === 'mouseover') { + m.on('mouseenter', hit, function (e) { m.getCanvas().style.cursor = 'pointer'; fn(e); }); + } else if (ev === 'mouseout') { + m.on('mouseleave', hit, function (e) { m.getCanvas().style.cursor = ''; fn(e); }); + } + }, + _wireAll: function () { + var self = this; + Object.keys(self._handlers).forEach(function (ev) { + self._handlers[ev].forEach(function (fn) { self._wireOne(ev, fn); }); + self._handlers[ev]._wired = true; + }); + if (self._tooltip && !self._tipWired) self._wireTooltip(); + }, + _wireTooltip: function () { + var self = this, m = self._map, hit = self._hitId(); + if (!m.getLayer(hit)) return; + self._tipWired = true; + m.on('mousemove', hit, function (e) { + if (!self._tipPopup) self._tipPopup = new maplibregl.Popup({ closeButton: false, closeOnClick: false, offset: 10, className: 'rk-map-tip' }); + self._tipPopup.setLngLat(e.lngLat).setHTML(self._tooltip).addTo(m); + }); + m.on('mouseleave', hit, function () { if (self._tipPopup) { self._tipPopup.remove(); } }); + }, addTo: function (mapWrap) { this._map = mapWrap && mapWrap._gl ? mapWrap._gl : mapWrap; this._ensure(); return this; }, + on: function (ev, fn) { + (this._handlers[ev] = this._handlers[ev] || []).push(fn); + if (this._map && this._map.getLayer(this._hitId())) this._wireOne(ev, fn); + return this; + }, + bindTooltip: function (t) { + this._tooltip = typeof t === 'string' ? t : ''; + if (this._map && this._map.getLayer(this._hitId())) this._wireTooltip(); + return this; + }, + setStyle: function (s) { + var m = this._map; if (!m || !m.getLayer(this._id)) return this; + if (s.color != null) m.setPaintProperty(this._id, 'line-color', s.color); + if (s.weight != null) m.setPaintProperty(this._id, 'line-width', s.weight); + if (s.opacity != null) m.setPaintProperty(this._id, 'line-opacity', s.opacity); + return this; + }, setLatLngs: function (lls) { this._latlngs = lls || []; if (this._map && this._map.getSource(this._id)) this._map.getSource(this._id).setData(this._geo()); return this; }, + // Leaflet-kompatibel: Array von {lat,lng} (für fitBounds-Sammlung). + getLatLngs: function () { return this._latlngs.map(function (p) { return (p && p.lat != null) ? { lat: p.lat, lng: p.lng } : { lat: p[0], lng: p[1] }; }); }, getBounds: function () { return { _coords: this._latlngs.map(function (p) { return (p && p.lat != null) ? [p.lat, p.lng] : p; }) }; }, remove: function () { var m = this._map; if (!m) return this; + if (this._tipPopup) { try { this._tipPopup.remove(); } catch (e) {} } + if (m.getLayer(this._hitId())) m.removeLayer(this._hitId()); if (m.getLayer(this._id)) m.removeLayer(this._id); if (m.getSource(this._id)) m.removeSource(this._id); return this; diff --git a/backend/static/js/pages/diary.js b/backend/static/js/pages/diary.js index 2c553cf..d06cc5e 100644 --- a/backend/static/js/pages/diary.js +++ b/backend/static/js/pages/diary.js @@ -310,8 +310,10 @@ window.Page_diary = (() => { aria-label="Schließen">× `; + // #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 ? `
` : `
-
`, - iconSize: hasPhoto ? [44, 44] : [32, 32], - iconAnchor: hasPhoto ? [22, 22] : [16, 16], - className: '', - }); + `; + 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(`
${hasPhoto ? `` : ''}
${title}
${dateStr}
${loc.media_count > 1 ? `
📷 ${loc.media_count} Medien
` : ''}
→ Öffnen
-
`, { 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); + `, { 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: ` + 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 = ` - `, - 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(); + `; + 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 }; })(); diff --git a/backend/static/landing.html b/backend/static/landing.html index 407e845..d6a841b 100644 --- a/backend/static/landing.html +++ b/backend/static/landing.html @@ -4,7 +4,7 @@ - + Ban Yaro — Die Hunde-App für Deutschland, Österreich & Schweiz diff --git a/backend/static/sw.js b/backend/static/sw.js index 09b1b7e..d77c914 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -4,7 +4,7 @@ ============================================================ */ // ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab -const VER = '1201'; +const VER = '1203'; const CACHE_VERSION = `by-v${VER}`; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten