diff --git a/VERSION b/VERSION index 130e16f..0f32703 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1200 \ No newline at end of file +1201 \ No newline at end of file diff --git a/backend/static/index.html b/backend/static/index.html index ee9c1d3..bdb57f4 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 095a643..a186a68 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 = '1200'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '1201'; // ← 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/pages/routes.js b/backend/static/js/pages/routes.js index b0d1414..f25a4bc 100644 --- a/backend/static/js/pages/routes.js +++ b/backend/static/js/pages/routes.js @@ -1566,11 +1566,21 @@ window.Page_routes = (() => { init(); } - // Mini-Vorschau: SVG-Routenform (keine eigene Karte → kein WebGL-Kontext-Limit bei vielen - // Listeneinträgen, kein OSM-Raster). Die Detail-/Navigations-Karten sind voll GL. + // Mini-Vorschau: zuerst sofort die SVG-Routenform (kein Warten), dann — sobald + // gerendert — auf ein echtes Karten-PNG (Basemap + Route) upgraden. Das PNG kommt + // aus EINEM geteilten Offscreen-GL-Kontext (UI.map.snapshot, mit Cache), damit viele + // Listeneinträge nicht das WebGL-Kontextlimit sprengen. Ist GL aus → SVG bleibt. function _buildMiniMap(el) { const track = JSON.parse(el.dataset.track || '[]'); el.innerHTML = _svgPreview(track); + if (track.length < 2 || !UI.map.snapshot) return; + UI.map.snapshot(track, { key: 'r' + (el.dataset.id || '') }).then(url => { + if (!url || !el.isConnected) return; // GL aus/Fehler → SVG-Platzhalter bleibt + el.style.backgroundImage = `url("${url}")`; + el.style.backgroundSize = 'cover'; + el.style.backgroundPosition = 'center'; + el.innerHTML = ''; // SVG-Platzhalter entfernen (PNG enthält die Route) + }).catch(() => {}); } // ---------------------------------------------------------- diff --git a/backend/static/js/ui.js b/backend/static/js/ui.js index a3f7893..b0cd636 100644 --- a/backend/static/js/ui.js +++ b/backend/static/js/ui.js @@ -543,6 +543,14 @@ const UI = (() => { await loadProtomaps(); return MapVector.basemapLayer(opts); }, + + // Rendert für einen Track (Array {lat,lon}) ein PNG-Vorschaubild MIT Basemap + // (gleicher GL-Style wie die echte Karte) und liefert eine data-URL. + // EIN einziger Offscreen-GL-Kontext, serielle Verarbeitung, Cache pro key — + // so bekommt jede Routenkarte ihren geografischen Kontext, ohne das WebGL- + // Kontextlimit zu sprengen (Problem bei N Live-Mini-Karten auf iOS). + // Liefert null wenn GL aus ist (Aufrufer nutzt dann seinen SVG-Fallback). + snapshot(track, opts = {}) { return _glSnapshot(track, opts); }, }; // ---------------------------------------------------------- @@ -926,6 +934,85 @@ const UI = (() => { return _maplibreUIPromise; } + // ---------------------------------------------------------- + // TRACK-VORSCHAU-SNAPSHOT — ein Offscreen-GL-Kontext rendert PNGs (Basemap+Route) + // ---------------------------------------------------------- + let _snapMap = null, _snapReady = null, _snapChain = Promise.resolve(); + const _snapCache = new Map(); // key → data-URL + const _EMPTY_FC = { type: 'FeatureCollection', features: [] }; + + function _ensureSnapMap() { + if (_snapReady) return _snapReady; + _snapReady = loadMapLibreUI().then(() => new Promise((resolve, reject) => { + const el = document.createElement('div'); + // Aspekt wie .rk-card-preview (360×140); MapLibre rendert in devicePixelRatio → scharf. + el.style.cssText = 'position:fixed;left:-10000px;top:0;width:360px;height:140px;pointer-events:none;visibility:hidden;'; + document.body.appendChild(el); + const isDark = document.documentElement.dataset.theme === 'dark'; + const m = new maplibregl.Map({ + container: el, style: MapGLStyle.build({ dark: isDark }), + center: [10.4515, 51.1657], zoom: 6, + interactive: false, attributionControl: false, + preserveDrawingBuffer: true, fadeDuration: 0, + }); + m.on('error', () => {}); // einzelne Tile-Fehler nicht eskalieren + m.once('load', () => { + m.addSource('snap-line', { type: 'geojson', data: _EMPTY_FC }); + m.addSource('snap-pts', { type: 'geojson', data: _EMPTY_FC }); + m.addLayer({ id: 'snap-line', type: 'line', source: 'snap-line', + layout: { 'line-cap': 'round', 'line-join': 'round' }, + paint: { 'line-color': '#C4843A', 'line-width': 4, 'line-opacity': 0.95 } }); + m.addLayer({ id: 'snap-pts', type: 'circle', source: 'snap-pts', + paint: { 'circle-radius': 6, 'circle-color': ['get', 'color'], + 'circle-stroke-color': '#fff', 'circle-stroke-width': 2 } }); + _snapMap = m; + resolve(m); + }); + setTimeout(() => { if (!_snapMap) reject(new Error('snap-map load timeout')); }, 8000); + })); + return _snapReady; + } + + function _renderSnap(track, key) { + return _ensureSnapMap().then(m => new Promise(resolve => { + const line = track.map(p => [p.lon, p.lat]); + m.getSource('snap-line').setData({ type: 'Feature', properties: {}, + geometry: { type: 'LineString', coordinates: line } }); + const a = track[0], b = track[track.length - 1]; + m.getSource('snap-pts').setData({ type: 'FeatureCollection', features: [ + { type: 'Feature', properties: { color: '#22C55E' }, geometry: { type: 'Point', coordinates: [a.lon, a.lat] } }, + { type: 'Feature', properties: { color: '#EF4444' }, geometry: { type: 'Point', coordinates: [b.lon, b.lat] } }, + ] }); + const bounds = line.reduce((bb, c) => bb.extend(c), new maplibregl.LngLatBounds(line[0], line[0])); + try { m.fitBounds(bounds, { padding: 22, duration: 0, maxZoom: 16 }); } catch (e) {} + let done = false; + const finish = () => { + if (done) return; done = true; + m.off('idle', finish); + requestAnimationFrame(() => { + let url = null; + try { url = m.getCanvas().toDataURL('image/png'); } catch (e) {} + if (url) _snapCache.set(key, url); + resolve(url); + }); + }; + m.on('idle', finish); + setTimeout(finish, 4000); // Fallback falls Tiles hängen + })); + } + + function _glSnapshot(track, opts = {}) { + if (!_uiUseGL()) return Promise.resolve(null); // GL aus → SVG-Fallback beim Aufrufer + if (!track || track.length < 2) return Promise.resolve(null); + const key = opts.key || ('t' + track.length + ',' + track[0].lat + ',' + track[0].lon + ',' + + track[track.length - 1].lat + ',' + track[track.length - 1].lon); + if (_snapCache.has(key)) return Promise.resolve(_snapCache.get(key)); + // Serielle Verarbeitung am gemeinsamen Offscreen-Kontext. + const run = _snapChain.then(() => _renderSnap(track, key)).catch(() => null); + _snapChain = run.catch(() => {}); + return run; + } + // ---------------------------------------------------------- // VEKTOR-BASEMAP (protomaps-leaflet + eigene PMTiles) — lazy laden [DEAKTIVIERT] // ---------------------------------------------------------- diff --git a/backend/static/landing.html b/backend/static/landing.html index 239a3f6..407e845 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 5aac805..09b1b7e 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 = '1200'; +const VER = '1201'; const CACHE_VERSION = `by-v${VER}`; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten