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