Routen-Vorschau: echtes Karten-PNG (Basemap+Route) statt nackter SVG-Form
In der Routenliste fehlte der geografische Kontext — man sah nur die Routen- form auf grünem Grund, nicht WO sie liegt oder wo sie entlangführt. Lösung: UI.map.snapshot() rendert pro Track ein PNG aus EINEM geteilten Offscreen-GL-Kontext (gleicher Style wie die echte Karte: Straßen, Orte, Wald, Gewässer), zeichnet Route + Start/Ziel-Marker ein und cached das Ergebnis. So bekommt jede Karte ihren Kontext, ohne bei vielen Listen- einträgen das WebGL-Kontextlimit (iOS ~8) zu sprengen. - ui.js: Offscreen-Singleton + serielle Render-Queue + Cache (_glSnapshot) - routes.js: _buildMiniMap zeigt sofort SVG, upgradet dann aufs PNG - GL aus → null → SVG-Platzhalter bleibt (Produktion/Flag aus unverändert)
This commit is contained in:
parent
a0d16ba800
commit
1defeec537
7 changed files with 115 additions and 18 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
1200
|
||||
1201
|
||||
|
|
@ -86,14 +86,14 @@
|
|||
<title>Ban Yaro</title>
|
||||
|
||||
<!-- Theme + theme-color Statusleiste vor CSS setzen -->
|
||||
<script src="/js/boot-early.js?v=1200"></script>
|
||||
<script src="/js/boot-early.js?v=1201"></script>
|
||||
|
||||
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
|
||||
<link rel="stylesheet" href="/css/design-system.css?v=1200">
|
||||
<link rel="stylesheet" href="/css/layout.css?v=1200">
|
||||
<link rel="stylesheet" href="/css/components.css?v=1200">
|
||||
<link rel="stylesheet" href="/css/utilities.css?v=1200">
|
||||
<link rel="stylesheet" href="/css/lists.css?v=1200">
|
||||
<link rel="stylesheet" href="/css/design-system.css?v=1201">
|
||||
<link rel="stylesheet" href="/css/layout.css?v=1201">
|
||||
<link rel="stylesheet" href="/css/components.css?v=1201">
|
||||
<link rel="stylesheet" href="/css/utilities.css?v=1201">
|
||||
<link rel="stylesheet" href="/css/lists.css?v=1201">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
|
@ -617,11 +617,11 @@
|
|||
<div id="modal-container"></div>
|
||||
|
||||
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
|
||||
<script src="/js/api.js?v=1200"></script>
|
||||
<script src="/js/ui.js?v=1200"></script>
|
||||
<script src="/js/app.js?v=1200"></script>
|
||||
<script src="/js/worlds.js?v=1200"></script>
|
||||
<script src="/js/offline-indicator.js?v=1200"></script>
|
||||
<script src="/js/api.js?v=1201"></script>
|
||||
<script src="/js/ui.js?v=1201"></script>
|
||||
<script src="/js/app.js?v=1201"></script>
|
||||
<script src="/js/worlds.js?v=1201"></script>
|
||||
<script src="/js/offline-indicator.js?v=1201"></script>
|
||||
|
||||
<!-- Feature-Seiten werden lazy geladen -->
|
||||
|
||||
|
|
@ -631,7 +631,7 @@
|
|||
|
||||
|
||||
<!-- Boot: Offline-Banner + SW-Registration (extrahiert für CSP) -->
|
||||
<script src="/js/boot.js?v=1200"></script>
|
||||
<script src="/js/boot.js?v=1201"></script>
|
||||
|
||||
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(() => {});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
// ----------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<script src="/js/landing-init.js?v=1200"></script>
|
||||
<script src="/js/landing-init.js?v=1201"></script>
|
||||
<title>Ban Yaro — Die Hunde-App für Deutschland, Österreich & Schweiz</title>
|
||||
<meta name="description" content="Ban Yaro: Die kostenlose All-in-One Hunde-App für DACH. Tagebuch, Giftköder-Alarm, Training mit KI, Forum, Wurfbörse, Stammbaum, Inzucht-Check — DSGVO-konform, offline-fähig, ohne App Store.">
|
||||
<meta name="keywords" content="Hunde App, Hunde Community, Wurfbörse, Züchter, Welpen kaufen, Stammbaum Hund, Inzuchtkoeffizient, Hundezucht, Impfpass Hund, Giftköder Alarm, Gassi Community, Hundetraining App, Hunde Forum, Hunde KI, Hundefilm Datenbank, Welpen Marktplatz">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue