Compare commits
2 commits
a27695d9c6
...
11922c1d22
| Author | SHA1 | Date | |
|---|---|---|---|
| 11922c1d22 | |||
| 63c9be68c6 |
5 changed files with 429 additions and 2 deletions
|
|
@ -447,6 +447,12 @@ async def maplibre_perf_test():
|
|||
# Wegwerf-Perf-Test: MapLibre GPU + 600 Cluster-Marker auf DACH-Basemap (Handy-Test).
|
||||
return FileResponse(os.path.join(STATIC_DIR, "maplibre-perf-test.html"), media_type="text/html")
|
||||
|
||||
|
||||
@app.get("/maplibre-markers-test")
|
||||
async def maplibre_markers_test():
|
||||
# Headless-Proof für map-gl-markers.js (Cluster/Icons/Danger/Toggle/Popup, ohne Auth).
|
||||
return FileResponse(os.path.join(STATIC_DIR, "maplibre-markers-test.html"), media_type="text/html")
|
||||
|
||||
# User-generierte Medien (Fotos aus Tagebuch, Giftköder-Alarm, etc.)
|
||||
MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
|
||||
os.makedirs(MEDIA_DIR, exist_ok=True)
|
||||
|
|
|
|||
191
backend/static/js/map-gl-markers.js
Normal file
191
backend/static/js/map-gl-markers.js
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
// GL-Marker-Subsystem für die zentrale Karte (MapLibre). Eigenständig + headless
|
||||
// testbar, BEVOR es in map.js verdrahtet wird. Pro Kategorie eine GeoJSON-Source mit
|
||||
// cluster:true → Cluster-Kreise (circle, GPU) + Einzel-POIs (symbol mit Phosphor-Icon).
|
||||
// Faithful zum Leaflet-Look: Kategorie-Farbe + weißes Phosphor-Icon auf Kreis.
|
||||
// Cluster-ZAHLEN brauchen Glyphs → später (Größe kodiert Dichte). Danger-Radien als Polygon.
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var _map = null;
|
||||
var _types = {}; // { key: { color, iconName, danger } }
|
||||
var _dangerRadiusM = 100;
|
||||
var _popupHTML = null; // (props, key) -> htmlString
|
||||
var _popupWire = null; // (props, key, closeFn) -> void
|
||||
var _activePopup = null;
|
||||
var _dangerKeys = [];
|
||||
|
||||
function _empty() { return { type: 'FeatureCollection', features: [] }; }
|
||||
|
||||
// POIs ({lat,lon,...}) → GeoJSON-Features ([lng,lat] + flache Properties).
|
||||
function _toFeatures(pois) {
|
||||
return {
|
||||
type: 'FeatureCollection',
|
||||
features: (pois || []).filter(function (p) { return p && p.lat != null && p.lon != null; })
|
||||
.map(function (p) {
|
||||
var props = {};
|
||||
Object.keys(p).forEach(function (k) {
|
||||
var v = p[k];
|
||||
if (v != null && typeof v !== 'object') props[k] = v;
|
||||
});
|
||||
return { type: 'Feature', properties: props, geometry: { type: 'Point', coordinates: [p.lon, p.lat] } };
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
// Kreis-Polygon (Meter-genau) für Danger-Radius.
|
||||
function _circlePolygon(lon, lat, radiusM, steps) {
|
||||
steps = steps || 36;
|
||||
var coords = [], r = radiusM / 6378137, latR = lat * Math.PI / 180, lonR = lon * Math.PI / 180;
|
||||
for (var i = 0; i <= steps; i++) {
|
||||
var brng = i / steps * 2 * Math.PI;
|
||||
var lat2 = Math.asin(Math.sin(latR) * Math.cos(r) + Math.cos(latR) * Math.sin(r) * Math.cos(brng));
|
||||
var lon2 = lonR + Math.atan2(Math.sin(brng) * Math.sin(r) * Math.cos(latR), Math.cos(r) - Math.sin(latR) * Math.sin(lat2));
|
||||
coords.push([lon2 * 180 / Math.PI, lat2 * 180 / Math.PI]);
|
||||
}
|
||||
return { type: 'Polygon', coordinates: [coords] };
|
||||
}
|
||||
|
||||
// Phosphor-Icon (weiß) auf farbigem Kreis → ImageData für map.addImage.
|
||||
function _iconImage(spriteDoc, iconName, color) {
|
||||
return new Promise(function (resolve) {
|
||||
var s = 64, c = document.createElement('canvas'); c.width = c.height = s;
|
||||
var x = c.getContext('2d');
|
||||
function base() {
|
||||
x.clearRect(0, 0, s, s);
|
||||
x.beginPath(); x.arc(s / 2, s / 2, s / 2 - 5, 0, Math.PI * 2);
|
||||
x.fillStyle = color; x.fill();
|
||||
x.lineWidth = 4; x.strokeStyle = 'rgba(52,68,36,0.55)'; x.stroke();
|
||||
}
|
||||
var sym = spriteDoc && iconName && spriteDoc.getElementById(iconName);
|
||||
if (!sym) { base(); resolve(x.getImageData(0, 0, s, s)); return; }
|
||||
var vb = sym.getAttribute('viewBox') || '0 0 256 256';
|
||||
var svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="' + vb + '" fill="white">' + sym.innerHTML + '</svg>';
|
||||
var im = new Image();
|
||||
im.onload = function () { base(); var ic = s * 0.52; x.drawImage(im, (s - ic) / 2, (s - ic) / 2, ic, ic); resolve(x.getImageData(0, 0, s, s)); };
|
||||
im.onerror = function () { base(); resolve(x.getImageData(0, 0, s, s)); };
|
||||
im.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg);
|
||||
});
|
||||
}
|
||||
|
||||
function _buildIcons() {
|
||||
var doc = null;
|
||||
return fetch('/icons/phosphor.svg').then(function (r) { return r.text(); })
|
||||
.then(function (txt) { doc = new DOMParser().parseFromString(txt, 'image/svg+xml'); })
|
||||
.catch(function () { doc = null; })
|
||||
.then(function () {
|
||||
var keys = Object.keys(_types);
|
||||
return keys.reduce(function (chain, key) {
|
||||
return chain.then(function () {
|
||||
if (_map.hasImage('poi-' + key)) return;
|
||||
return _iconImage(doc, _types[key].iconName, _types[key].color).then(function (img) {
|
||||
if (!_map.hasImage('poi-' + key)) _map.addImage('poi-' + key, img, { pixelRatio: 2 });
|
||||
});
|
||||
});
|
||||
}, Promise.resolve());
|
||||
});
|
||||
}
|
||||
|
||||
function _addCategoryLayers() {
|
||||
Object.keys(_types).forEach(function (key) {
|
||||
var src = 'poi-' + key, color = _types[key].color;
|
||||
if (!_map.getSource(src)) {
|
||||
_map.addSource(src, { type: 'geojson', data: _empty(), cluster: true, clusterRadius: 50, clusterMaxZoom: 16 });
|
||||
}
|
||||
if (!_map.getLayer('cl-' + key)) {
|
||||
_map.addLayer({ id: 'cl-' + key, type: 'circle', source: src, filter: ['has', 'point_count'],
|
||||
paint: {
|
||||
'circle-color': color, 'circle-opacity': 0.92,
|
||||
'circle-stroke-color': 'rgba(52,68,36,0.65)', 'circle-stroke-width': 2,
|
||||
'circle-radius': ['step', ['get', 'point_count'], 14, 10, 18, 50, 24],
|
||||
} });
|
||||
}
|
||||
if (!_map.getLayer('pt-' + key)) {
|
||||
_map.addLayer({ id: 'pt-' + key, type: 'symbol', source: src, filter: ['!', ['has', 'point_count']],
|
||||
layout: { 'icon-image': 'poi-' + key, 'icon-allow-overlap': true, 'icon-ignore-placement': true, 'icon-size': 0.9 } });
|
||||
}
|
||||
// Click: Einzel-POI → Popup; Cluster → reinzoomen
|
||||
_map.on('click', 'pt-' + key, function (e) { _onPoiClick(e, key); });
|
||||
_map.on('click', 'cl-' + key, function (e) {
|
||||
var f = e.features[0];
|
||||
_map.getSource(src).getClusterExpansionZoom(f.properties.cluster_id, function (err, z) {
|
||||
if (!err) _map.easeTo({ center: f.geometry.coordinates, zoom: z });
|
||||
});
|
||||
});
|
||||
_map.on('mouseenter', 'pt-' + key, function () { _map.getCanvas().style.cursor = 'pointer'; });
|
||||
_map.on('mouseleave', 'pt-' + key, function () { _map.getCanvas().style.cursor = ''; });
|
||||
});
|
||||
|
||||
// Danger-Radius-Layer (poison/giftkoeder), unter den Markern.
|
||||
if (_dangerKeys.length && !_map.getSource('danger')) {
|
||||
_map.addSource('danger', { type: 'geojson', data: _empty() });
|
||||
var firstSymbol = 'cl-' + Object.keys(_types)[0];
|
||||
_map.addLayer({ id: 'danger-fill', type: 'fill', source: 'danger',
|
||||
paint: { 'fill-color': '#DC2626', 'fill-opacity': 0.12 } },
|
||||
_map.getLayer(firstSymbol) ? firstSymbol : undefined);
|
||||
_map.addLayer({ id: 'danger-line', type: 'line', source: 'danger',
|
||||
paint: { 'line-color': '#DC2626', 'line-width': 2, 'line-opacity': 0.7 } },
|
||||
_map.getLayer(firstSymbol) ? firstSymbol : undefined);
|
||||
}
|
||||
}
|
||||
|
||||
function _onPoiClick(e, key) {
|
||||
if (!e.features || !e.features.length) return;
|
||||
var f = e.features[0];
|
||||
var props = f.properties || {};
|
||||
if (_activePopup) { _activePopup.remove(); _activePopup = null; }
|
||||
var html = _popupHTML ? _popupHTML(props, key) : ('<b>' + (props.name || key) + '</b>');
|
||||
if (!html) return;
|
||||
_activePopup = new maplibregl.Popup({ maxWidth: '260px' })
|
||||
.setLngLat(f.geometry.coordinates).setHTML(html).addTo(_map);
|
||||
if (_popupWire) {
|
||||
var pop = _activePopup;
|
||||
setTimeout(function () { _popupWire(props, key, function () { pop.remove(); }); }, 50);
|
||||
}
|
||||
}
|
||||
|
||||
// Danger-Source aus allen aktuell gesetzten Danger-POIs neu aufbauen.
|
||||
var _dangerPois = {}; // { key: [pois] }
|
||||
function _refreshDanger() {
|
||||
if (!_map.getSource('danger')) return;
|
||||
var feats = [];
|
||||
_dangerKeys.forEach(function (k) {
|
||||
(_dangerPois[k] || []).forEach(function (p) {
|
||||
if (p.lat != null && p.lon != null) feats.push({ type: 'Feature', properties: {}, geometry: _circlePolygon(p.lon, p.lat, _dangerRadiusM) });
|
||||
});
|
||||
});
|
||||
_map.getSource('danger').setData({ type: 'FeatureCollection', features: feats });
|
||||
}
|
||||
|
||||
// ---- Öffentliche API ----
|
||||
var API = {
|
||||
// init(map, { types, dangerKeys, dangerRadiusM, popupHTML, popupWire }) → Promise (Icons geladen)
|
||||
init: function (map, opts) {
|
||||
_map = map; opts = opts || {};
|
||||
_types = opts.types || {};
|
||||
_dangerKeys = opts.dangerKeys || [];
|
||||
_dangerRadiusM = opts.dangerRadiusM || 100;
|
||||
_popupHTML = opts.popupHTML || null;
|
||||
_popupWire = opts.popupWire || null;
|
||||
_addCategoryLayers();
|
||||
return _buildIcons();
|
||||
},
|
||||
// POIs einer Kategorie setzen (ersetzt alle).
|
||||
setLayer: function (key, pois) {
|
||||
var src = _map && _map.getSource('poi-' + key);
|
||||
if (!src) return;
|
||||
src.setData(_toFeatures(pois));
|
||||
if (_dangerKeys.indexOf(key) !== -1) { _dangerPois[key] = pois || []; _refreshDanger(); }
|
||||
},
|
||||
clear: function (key) { API.setLayer(key, []); },
|
||||
setVisible: function (key, on) {
|
||||
if (!_map) return;
|
||||
var vis = on ? 'visible' : 'none';
|
||||
['cl-' + key, 'pt-' + key].forEach(function (id) {
|
||||
if (_map.getLayer(id)) _map.setLayoutProperty(id, 'visibility', vis);
|
||||
});
|
||||
},
|
||||
ready: function () { return !!(_map && _map.getSource('poi-' + Object.keys(_types)[0])); },
|
||||
};
|
||||
|
||||
window.MapGLMarkers = API;
|
||||
})();
|
||||
64
backend/static/js/maplibre-markers-test.js
Normal file
64
backend/static/js/maplibre-markers-test.js
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
// Headless-Proof für map-gl-markers.js: 4 Kategorien, ~490 Fake-Marker, Cluster,
|
||||
// Phosphor-Icons, Danger-Radien, Sichtbarkeits-Toggle, Popups — alles ohne App/Auth.
|
||||
(function () {
|
||||
'use strict';
|
||||
var st = document.getElementById('status');
|
||||
function set(t) { if (st) st.textContent = t; }
|
||||
|
||||
var TYPES = {
|
||||
restaurant: { color: '#F97316', iconName: 'fork-knife' },
|
||||
freilauf: { color: '#22C55E', iconName: 'dog' },
|
||||
tierarzt: { color: '#EF4444', iconName: 'first-aid' },
|
||||
poison: { color: '#DC2626', iconName: 'skull', danger: true },
|
||||
};
|
||||
var COUNTS = { restaurant: 250, freilauf: 150, tierarzt: 80, poison: 8 };
|
||||
|
||||
function genPois(n, seedStart) {
|
||||
var feats = [], seed = seedStart;
|
||||
function rnd() { seed = (seed * 9301 + 49297) % 233280; return seed / 233280; }
|
||||
for (var i = 0; i < n; i++) {
|
||||
feats.push({ lat: 48.03 + rnd() * 0.24, lon: 11.40 + rnd() * 0.36, name: 'POI ' + i, source: 'osm' });
|
||||
}
|
||||
return feats;
|
||||
}
|
||||
|
||||
try {
|
||||
var proto = new pmtiles.Protocol();
|
||||
maplibregl.addProtocol('pmtiles', proto.tile);
|
||||
var map = new maplibregl.Map({
|
||||
container: 'map', style: MapGLStyle.build({ dark: false }),
|
||||
center: [11.576, 48.137], zoom: 12, dragRotate: false,
|
||||
});
|
||||
map.addControl(new maplibregl.NavigationControl({ showCompass: false }), 'top-left');
|
||||
map.addControl(new maplibregl.ScaleControl());
|
||||
|
||||
map.on('error', function (e) { set('⚠️ ' + (e && e.error ? e.error.message : 'Fehler')); if (e && e.error) console.error(e.error); });
|
||||
|
||||
map.on('load', function () {
|
||||
MapGLMarkers.init(map, {
|
||||
types: TYPES, dangerKeys: ['poison'], dangerRadiusM: 100,
|
||||
popupHTML: function (p, key) { return '<b>' + (p.name || key) + '</b><br><small>' + key + '</small><br><button id="mp-x">OK</button>'; },
|
||||
popupWire: function (p, key, close) { document.getElementById('mp-x') && document.getElementById('mp-x').addEventListener('click', close); },
|
||||
}).then(function () {
|
||||
var seeds = { restaurant: 11, freilauf: 77, tierarzt: 123, poison: 200 };
|
||||
Object.keys(TYPES).forEach(function (k) { MapGLMarkers.setLayer(k, genPois(COUNTS[k], seeds[k])); });
|
||||
// Toggle-Buttons
|
||||
var box = document.getElementById('toggles');
|
||||
Object.keys(TYPES).forEach(function (k) {
|
||||
var b = document.createElement('button'); b.textContent = k; b.dataset.on = '1';
|
||||
b.style.borderColor = TYPES[k].color;
|
||||
b.addEventListener('click', function () {
|
||||
var on = b.dataset.on === '1' ? false : true;
|
||||
b.dataset.on = on ? '1' : '0'; b.classList.toggle('off', !on);
|
||||
MapGLMarkers.setVisible(k, on);
|
||||
});
|
||||
box.appendChild(b);
|
||||
});
|
||||
var total = COUNTS.restaurant + COUNTS.freilauf + COUNTS.tierarzt + COUNTS.poison;
|
||||
set('✅ ' + total + ' Marker (4 Kat.) — Cluster + Phosphor-Icons + Danger-Radius + Toggle + Popup');
|
||||
}).catch(function (e) { set('❌ init: ' + (e && e.message ? e.message : e)); console.error(e); });
|
||||
});
|
||||
} catch (e) {
|
||||
set('❌ ' + (e && e.message ? e.message : e));
|
||||
}
|
||||
})();
|
||||
|
|
@ -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: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> 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
|
||||
// ----------------------------------------------------------
|
||||
|
|
|
|||
28
backend/static/maplibre-markers-test.html
Normal file
28
backend/static/maplibre-markers-test.html
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<title>Ban Yaro — GL-Marker-Subsystem-Test</title>
|
||||
<link rel="stylesheet" href="/js/vendor/maplibre-gl.css">
|
||||
<style>
|
||||
html,body{margin:0;height:100%}
|
||||
#map{position:absolute;inset:0}
|
||||
#hud{position:absolute;top:10px;left:54px;z-index:5;background:rgba(255,255,255,.92);
|
||||
padding:8px 12px;border-radius:8px;font:13px system-ui,sans-serif;box-shadow:0 1px 6px rgba(0,0,0,.2);max-width:78vw}
|
||||
#toggles{position:absolute;bottom:14px;left:10px;z-index:5;display:flex;gap:6px;flex-wrap:wrap}
|
||||
#toggles button{font:12px system-ui;padding:6px 10px;border-radius:14px;border:1px solid #bbb;background:#fff}
|
||||
#toggles button.off{opacity:.45}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="map"></div>
|
||||
<div id="hud"><b>GL-Marker-Test</b><br><span id="status">lädt…</span></div>
|
||||
<div id="toggles"></div>
|
||||
<script src="/js/vendor/maplibre-gl.js"></script>
|
||||
<script src="/js/vendor/pmtiles.js"></script>
|
||||
<script src="/js/map-gl-style.js"></script>
|
||||
<script src="/js/map-gl-markers.js"></script>
|
||||
<script src="/js/maplibre-markers-test.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue