diff --git a/backend/static/js/pages/map.js b/backend/static/js/pages/map.js index a44a77d..88617fc 100644 --- a/backend/static/js/pages/map.js +++ b/backend/static/js/pages/map.js @@ -16,6 +16,10 @@ window.Page_map = (() => { let _tempMarker = null; let _tileLayer = null; let _usingVector = false; // true wenn Vektor-Basemap (PMTiles) statt OSM-Raster + let _engineGL = false; // true wenn MapLibre GL statt Leaflet (zentrale Karte) + let _maplibreLoaded = false; + let _glLayersReady = false; // GL: POI-Sources/Layer angelegt? + let _pmtilesProtoReg = false; // pmtiles-Protokoll bei MapLibre registriert? let _themeObserver = null; // Standort-Tracking @@ -153,8 +157,14 @@ window.Page_map = (() => { // Alle-Button Initialzustand const anyOnInit = Object.entries(_visible).some(([k, v]) => v && k !== 'giftkoeder'); document.getElementById('map-legend-all')?.classList.toggle('all-off', !anyOnInit); - await _loadLeaflet(); - _initMap(); // sofort mit Deutschland-Mitte starten + _engineGL = _useGL(); + if (_engineGL) { + await loadMapLibre(); + _initMapGL(); // MapLibre-GL-Karte (GPU) + } else { + await _loadLeaflet(); + _initMap(); // Leaflet-Raster (Default), sofort mit Deutschland-Mitte starten + } _startLocationTracking(); _loadAll(); _offerResume(); // unterbrochene Aufzeichnung anbieten @@ -656,6 +666,134 @@ window.Page_map = (() => { }); } + // ========================================================== + // MapLibre-GL-Engine (zentrale Karte) — GPU/Worker, performant. + // Flag-gated; Raster-Leaflet bleibt Default. [lng,lat]-Reihenfolge! + // ========================================================== + // Flag: ?mapgl=1/0 → localStorage 'by_map_gl'. Default AUS (bis vollständig portiert). + function _useGL() { + try { + const u = new URLSearchParams(location.search); + if (u.has('mapgl')) localStorage.setItem('by_map_gl', u.get('mapgl') === '0' ? '0' : '1'); + return localStorage.getItem('by_map_gl') === '1'; + } catch (e) { return false; } + } + + function loadMapLibre() { + if (_maplibreLoaded) return Promise.resolve(); + const v = '?v=' + (window.APP_VER || ''); + if (!document.querySelector('link[href*="maplibre-gl.css"]')) { + const l = document.createElement('link'); + l.rel = 'stylesheet'; l.href = '/js/vendor/maplibre-gl.css'; + document.head.appendChild(l); + } + const seq = (srcs) => srcs.reduce((p, src) => p.then(() => new Promise((res, rej) => { + if ((src.includes('maplibre-gl.js') && window.maplibregl) || + (src.includes('pmtiles.js') && window.pmtiles) || + (src.includes('map-gl-style') && window.MapGLStyle)) return res(); + const s = document.createElement('script'); + s.src = src + v; s.onload = res; s.onerror = rej; + document.head.appendChild(s); + })), Promise.resolve()); + return seq(['/js/vendor/maplibre-gl.js', '/js/vendor/pmtiles.js', '/js/map-gl-style.js']).then(() => { + if (!(window.maplibregl && window.pmtiles && window.MapGLStyle)) throw new Error('MapLibre nicht geladen'); + if (!_pmtilesProtoReg) { + const proto = new pmtiles.Protocol(); + maplibregl.addProtocol('pmtiles', proto.tile); + _pmtilesProtoReg = true; + } + _maplibreLoaded = true; + }); + } + + // ---- Engine-neutrale Facade (kapselt [lat,lon]↔[lng,lat] an EINER Stelle) ---- + function _mapFlyTo(lat, lon, zoom, opts) { + if (!_map) return; + if (_engineGL) _map.flyTo({ center: [lon, lat], zoom, duration: opts && opts.duration ? opts.duration * 1000 : 1200 }); + else _map.flyTo([lat, lon], zoom, opts); + } + function _mapSetView(lat, lon, zoom) { + if (!_map) return; + if (_engineGL) _map.jumpTo({ center: [lon, lat], zoom }); + else _map.setView([lat, lon], zoom); + } + function _mapGetZoom() { return _map ? _map.getZoom() : 0; } + function _mapResize() { if (!_map) return; if (_engineGL) _map.resize(); else _map.invalidateSize(); } + function _mapGetCenter() { + if (!_map) return null; + const c = _map.getCenter(); // beide Engines: {lat, lng} + return { lat: c.lat, lon: c.lng }; + } + // Bounds mit 15%-Padding (ersetzt Leaflets bounds.pad(0.15)) → {north,south,east,west} + function _mapPaddedBounds(pad) { + pad = pad == null ? 0.15 : pad; + const b = _map.getBounds(); + let n = b.getNorth(), s = b.getSouth(), e = b.getEast(), w = b.getWest(); + const dLat = (n - s) * pad, dLon = (e - w) * pad; + return { north: n + dLat, south: s - dLat, east: e + dLon, west: w - dLon }; + } + + function _initMapGL() { + const el = document.getElementById('central-map'); + if (!el || !window.maplibregl || _map) return; + _engineGL = true; + const center = _userPos ? [_userPos.lon, _userPos.lat] : [8.6821, 50.1109]; // Frankfurt [lng,lat] + const zoom = _userPos ? 14 : 10; + + _map = new maplibregl.Map({ + container: 'central-map', + style: MapGLStyle.build({ dark: _isDarkMode() }), + center, zoom, attributionControl: false, + maxZoom: 19, dragRotate: false, pitchWithRotate: false, + }); + _map.addControl(new maplibregl.NavigationControl({ showCompass: false }), 'top-left'); + _map.addControl(new maplibregl.ScaleControl()); + _map.addControl(new maplibregl.AttributionControl({ + compact: true, customAttribution: '© OpenStreetMap contributors', + })); + + if (!_userPos) { + _frankfurtTimer = setTimeout(() => _mapFlyTo(50.1109, 8.6821, 14, { duration: 2.5 }), 1200); + } + + // Theme-Wechsel → Style neu setzen (Sources/Layer danach neu anlegen). + _themeObserver = new MutationObserver(() => _onThemeChangeGL()); + _themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] }); + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', _onThemeChangeGL); + + _map.on('load', () => { + _initPoiLayersGL(); + _updateZoomDisplay(); + _scheduleOsmLoad(); + }); + _map.on('moveend', () => { + _autoRetryCount = 0; _updateZoomDisplay(); _scheduleOsmLoad(); + document.getElementById('map-crosshair')?.classList.remove('dragging'); + }); + _map.on('movestart', () => { + document.getElementById('map-crosshair')?.classList.add('dragging'); + }); + + window.addEventListener('resize', _mapResize); + setTimeout(_mapResize, 100); + setTimeout(_mapResize, 600); + } + + function _onThemeChangeGL() { + if (!_map || !_engineGL) return; + _glLayersReady = false; + _map.setStyle(MapGLStyle.build({ dark: _isDarkMode() })); + // setStyle entfernt eigene Sources/Layer → nach Style-Load neu anlegen. + _map.once('styledata', () => { _initPoiLayersGL(); _scheduleOsmLoad(); }); + } + + // POI-Sources/Layer in MapLibre anlegen — wird in Build-Runde 2 gefüllt. + function _initPoiLayersGL() { + if (!_map || !_engineGL || _glLayersReady) return; + _glLayersReady = true; + // (Build-Runde 2: GeoJSON-Sources + Cluster/Symbol-Layer + Icons) + } + // ---------------------------------------------------------- // Standort-Tracking — pulsierender blauer Punkt // ----------------------------------------------------------