Routen-Detailkarte: WebGL-Kontext-Leak gefixt → bleibt GL + zoomt auf Route

Eigentliche Ursache von 'Detailkarte zoomt nicht auf die Route': die Karte war
auf dem Gerät gar keine GL-Karte mehr, sondern der Leaflet+OSM-RASTER-Fallback.
Grund: _detailMap (GL-Kontext) wurde beim Schließen des Modals NIE freigegeben —
jede geöffnete Route leakte einen WebGL-Kontext. Nach ~8 wirft MapLibre, und
UI.map.create fällt auf Leaflet+OSM zurück. Genau die Mapnik-Kacheln aus Renés
Screenshots (und die OSM-Attribution, die wir doch loswerden wollten).

Fixes:
- _detailMap modulweit + im onClose des Detail-Modals freigeben.
- routes.js destroy(): _detailMap/_suggestMap/_searchMap + Mini-Maps beim
  Verlassen der Seite freigeben.
- ui.js: Offscreen-Snapshot-Kontext nach 15s Leerlauf freigeben (hielt dauerhaft
  einen Kontext; Cache bleibt → kein Neu-Rendern).
- _fitRouteMap fittet jetzt aufs 'load'/'idle'-Event der Karte (iOS verwirft ein
  fitBounds VOR dem ersten Render) statt nur auf feste Timeouts.

Verifiziert (headless): 12 Detail-Öffnungen in Folge bleiben ALLE GL
(Leaflet:false), GL-Canvas-Zahl bleibt bei 1–2 statt zu wachsen. Vorher leakte
jede Öffnung einen Kontext.
This commit is contained in:
rene 2026-06-05 15:10:12 +02:00
parent d203ab17a8
commit 720971d252
7 changed files with 71 additions and 30 deletions

View file

@ -937,7 +937,7 @@ const UI = (() => {
// ----------------------------------------------------------
// TRACK-VORSCHAU-SNAPSHOT — ein Offscreen-GL-Kontext rendert PNGs (Basemap+Route)
// ----------------------------------------------------------
let _snapMap = null, _snapReady = null, _snapChain = Promise.resolve();
let _snapMap = null, _snapReady = null, _snapChain = Promise.resolve(), _snapReleaseTimer = null;
const _snapCache = new Map(); // key → data-URL
const _EMPTY_FC = { type: 'FeatureCollection', features: [] };
@ -1001,15 +1001,29 @@ const UI = (() => {
}));
}
// Offscreen-GL-Kontext nach Leerlauf freigeben — nicht dauerhaft halten, sonst belegt
// er einen der knappen iOS-WebGL-Kontexte und beschleunigt das Limit (Detailkarten
// fielen dann auf Leaflet+OSM-Raster zurück). Der PNG-Cache bleibt → kein Neu-Rendern.
function _releaseSnapMap() {
_snapReleaseTimer = null;
if (_snapMap) { try { _snapMap.remove(); } catch (e) {} _snapMap = null; }
_snapReady = null;
}
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));
if (_snapReleaseTimer) { clearTimeout(_snapReleaseTimer); _snapReleaseTimer = null; }
// Serielle Verarbeitung am gemeinsamen Offscreen-Kontext.
const run = _snapChain.then(() => _renderSnap(track, key)).catch(() => null);
_snapChain = run.catch(() => {});
run.then(() => {
if (_snapReleaseTimer) clearTimeout(_snapReleaseTimer);
_snapReleaseTimer = setTimeout(_releaseSnapMap, 15000);
});
return run;
}