banyaro/backend/static/js/map-gl-mini.js
rene abd7447d29 Karte: Follow-Mode + Live-Strecke bei Routen-Aufzeichnung (Wunsch Rene)
- MapGLMini-Polyline hatte KEIN addLatLng (nur setLatLngs) -> TypeError im
  Rec-Overlay: Strecke unsichtbar, Marker fror ein, kein Folgen. Facade
  ergaenzt (Leaflet-kompatibel).
- Rec-Overlay (routes.js): Follow-Mode default AN — Karte wandert mit dem
  Standort, Drag pausiert, Crosshair-Button (unten rechts) reaktiviert;
  erster Fix setzt Zoom 16, danach bleibt der Zoom erhalten
- Zentrale Karte (map.js): Standort-Button aktiviert Follow (+Toast),
  dragstart beendet es (beide Engines); GPS-Tracking folgt sanft (easeTo);
  Aufzeichnung startet im Follow-Mode, _updateRecMap pant nur noch im Follow
Bump v1238
2026-06-06 17:20:38 +02:00

297 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Leaflet-kompatible MapLibre-Facade für die SEITENKARTEN (Giftköder, Verlorene,
// Events, Gassi, Routen). Liefert Wrapper, die die von den Seiten genutzte Leaflet-
// API nachbilden (setView/fitBounds/invalidateSize/addTo/bindPopup/openPopup/on/remove),
// sodass die Seiten fast unverändert auf demselben GL-Style (MapGLStyle) laufen.
// Koordinaten nach außen [lat,lon] (Leaflet-Konvention), intern MapLibre [lng,lat].
(function () {
'use strict';
// [lat,lon]-Array ODER {lat,lng}-Objekt → [lng,lat] für MapLibre.
function _ll(latlon) {
if (latlon && latlon.lat != null) return [latlon.lng, latlon.lat];
return [latlon[1], latlon[0]];
}
// ---- Map-Wrapper ----
function _wrapMap(map) {
return {
_gl: map,
_isGL: true,
setView: function (latlon, zoom) { map.jumpTo({ center: _ll(latlon), zoom: zoom }); return this; },
flyTo: function (latlon, zoom, opts) {
map.flyTo({ center: _ll(latlon), zoom: zoom, duration: opts && opts.duration ? opts.duration * 1000 : 1000 });
return this;
},
panTo: function (latlon) { map.panTo(_ll(latlon)); return this; },
fitBounds: function (b, opts) {
var bb = _toBounds(b);
// Nur fitten wenn Bounds gültig UND der Container eine Größe hat (im Modal
// ist er beim Erstellen 0×0 → fitBounds würde NaN werfen; der Re-Fit nach
// Modal-Animation greift dann).
var _c = map.getContainer();
if (bb && !isNaN(bb.getWest()) && _c.clientWidth > 0 && _c.clientHeight > 0) {
var pad = 30;
if (opts && opts.padding) pad = Array.isArray(opts.padding) ? opts.padding[0] : opts.padding;
try { map.fitBounds(bb, { padding: pad, maxZoom: opts && opts.maxZoom, duration: 0 }); } catch (e) {}
}
return this;
},
invalidateSize: function () { map.resize(); return this; },
removeLayer: function (layer) { if (layer && layer.remove) layer.remove(); return this; },
addLayer: function (layer) { if (layer && layer.addTo) layer.addTo(this); return this; },
hasLayer: function () { return true; },
remove: function () { try { map.remove(); } catch (e) {} },
on: function (ev, fn) {
if (ev === 'click') {
map.on('click', function (e) { if (e.lngLat && !e.latlng) e.latlng = { lat: e.lngLat.lat, lng: e.lngLat.lng }; fn(e); });
} else { map.on(ev, fn); }
return this;
},
off: function (ev, fn) { map.off(ev, fn); return this; },
getZoom: function () { return map.getZoom(); },
getCenter: function () { var c = map.getCenter(); return { lat: c.lat, lng: c.lng }; },
// Leaflet-Handler-Stub (z.B. _suggestMap.scrollWheelZoom.disable()).
scrollWheelZoom: { disable: function () { try { map.scrollZoom.disable(); } catch (e) {} }, enable: function () { try { map.scrollZoom.enable(); } catch (e) {} } },
// Distanz in Metern (Haversine) — Ersatz für Leaflets map.distance.
distance: function (a, b) {
var la = a.lat != null ? a.lat : a[0], lo = a.lng != null ? a.lng : a[1];
var lb = b.lat != null ? b.lat : b[0], ob = b.lng != null ? b.lng : b[1];
var R = 6371000, p1 = la * Math.PI / 180, p2 = lb * Math.PI / 180;
var dp = (lb - la) * Math.PI / 180, dl = (ob - lo) * Math.PI / 180;
var x = Math.sin(dp / 2) * Math.sin(dp / 2) + Math.cos(p1) * Math.cos(p2) * Math.sin(dl / 2) * Math.sin(dl / 2);
return 2 * R * Math.asin(Math.sqrt(x));
},
};
}
// Bounds aus: Array von [lat,lon] | featureGroup-Wrapper (_coords) | Leaflet-Bounds.
function _toBounds(b) {
if (!b) return null;
var coords = null;
if (Array.isArray(b)) coords = b;
else if (b._coords) coords = b._coords;
else if (typeof b.getSouthWest === 'function') {
var sw = b.getSouthWest(), ne = b.getNorthEast();
return new maplibregl.LngLatBounds([sw.lng, sw.lat], [ne.lng, ne.lat]);
}
if (!coords || !coords.length) return null;
var bb = new maplibregl.LngLatBounds();
coords.forEach(function (c) { bb.extend(_ll(c)); });
return bb;
}
// ---- Marker-Wrapper (HTML-Marker; svgMarker + circleMarker) ----
function _wrapMarker(lat, lon, el, anchor) {
var m = new maplibregl.Marker({ element: el, anchor: anchor || 'center' }).setLngLat([lon, lat]);
var wrap = {
_gl: m,
_el: el,
addTo: function (mapWrap) { m.addTo(mapWrap && mapWrap._gl ? mapWrap._gl : mapWrap); return this; },
bindPopup: function (html, opts) {
m.setPopup(new maplibregl.Popup({ maxWidth: (opts && opts.maxWidth ? opts.maxWidth + 'px' : '260px'), closeButton: true, offset: 18 }).setHTML(html));
return this;
},
openPopup: function () { var p = m.getPopup(); if (p && !p.isOpen()) m.togglePopup(); return this; },
closePopup: function () { var p = m.getPopup(); if (p && p.isOpen()) m.togglePopup(); return this; },
bindTooltip: function (t) { try { el.title = typeof t === 'string' ? t.replace(/<[^>]*>/g, '') : ''; } catch (e) {} return this; },
on: function (ev, fn) {
if (ev === 'click') el.addEventListener('click', function (e) { e.stopPropagation(); fn(e); });
return this;
},
setLatLng: function (latlon) { m.setLngLat(_ll(latlon)); return this; },
getLatLng: function () { var c = m.getLngLat(); return { lat: c.lat, lng: c.lng }; },
setOpacity: function (o) { el.style.opacity = o; return this; },
remove: function () { try { m.remove(); } catch (e) {} return this; },
};
return wrap;
}
// ---- Polyline-Wrapper (GL geojson line-source/-layer) ----
var _seq = 0;
function _toLngLat(p) { return (p && p.lat != null) ? [p.lng, p.lat] : [p[1], p[0]]; } // L.latLng | [lat,lon]
function _wrapPolyline(latlngs, opts) {
opts = opts || {};
return {
_latlngs: latlngs || [],
_id: 'poly-' + (++_seq),
_map: null,
_opts: opts,
_handlers: {}, // ev → [fn]
_tooltip: null,
_tipPopup: null,
_geo: function () { return { type: 'Feature', geometry: { type: 'LineString', coordinates: this._latlngs.map(_toLngLat) } }; },
_hitId: function () { return this._id + '-hit'; },
_ensure: function () {
var self = this, m = self._map;
var add = function () {
if (!m.getSource(self._id)) m.addSource(self._id, { type: 'geojson', data: self._geo() });
if (!m.getLayer(self._id)) m.addLayer({ id: self._id, type: 'line', source: self._id,
layout: { 'line-cap': 'round', 'line-join': 'round' },
paint: { 'line-color': self._opts.color || '#C4843A', 'line-width': self._opts.weight || 4, 'line-opacity': self._opts.opacity != null ? self._opts.opacity : 0.9 } });
// Breite, fast unsichtbare Hit-Linie → auf dem Handy gut antippbar.
if (self._opts.interactive !== false && !m.getLayer(self._hitId())) {
m.addLayer({ id: self._hitId(), type: 'line', source: self._id,
layout: { 'line-cap': 'round', 'line-join': 'round' },
paint: { 'line-color': '#000', 'line-opacity': 0.01, 'line-width': 18 } });
}
self._wireAll();
};
if (m.isStyleLoaded && m.isStyleLoaded()) add(); else m.once('load', add);
},
_wireOne: function (ev, fn) {
var self = this, m = self._map, hit = self._hitId();
if (!m.getLayer(hit)) return;
if (ev === 'click') {
m.on('click', hit, function (e) { if (e.originalEvent) e.originalEvent.stopPropagation(); fn(e); });
} else if (ev === 'mouseover') {
m.on('mouseenter', hit, function (e) { m.getCanvas().style.cursor = 'pointer'; fn(e); });
} else if (ev === 'mouseout') {
m.on('mouseleave', hit, function (e) { m.getCanvas().style.cursor = ''; fn(e); });
}
},
_wireAll: function () {
var self = this;
Object.keys(self._handlers).forEach(function (ev) {
self._handlers[ev].forEach(function (fn) { self._wireOne(ev, fn); });
self._handlers[ev]._wired = true;
});
if (self._tooltip && !self._tipWired) self._wireTooltip();
},
_wireTooltip: function () {
var self = this, m = self._map, hit = self._hitId();
if (!m.getLayer(hit)) return;
self._tipWired = true;
m.on('mousemove', hit, function (e) {
if (!self._tipPopup) self._tipPopup = new maplibregl.Popup({ closeButton: false, closeOnClick: false, offset: 10, className: 'rk-map-tip' });
self._tipPopup.setLngLat(e.lngLat).setHTML(self._tooltip).addTo(m);
});
m.on('mouseleave', hit, function () { if (self._tipPopup) { self._tipPopup.remove(); } });
},
addTo: function (mapWrap) { this._map = mapWrap && mapWrap._gl ? mapWrap._gl : mapWrap; this._ensure(); return this; },
on: function (ev, fn) {
(this._handlers[ev] = this._handlers[ev] || []).push(fn);
if (this._map && this._map.getLayer(this._hitId())) this._wireOne(ev, fn);
return this;
},
bindTooltip: function (t) {
this._tooltip = typeof t === 'string' ? t : '';
if (this._map && this._map.getLayer(this._hitId())) this._wireTooltip();
return this;
},
setStyle: function (s) {
var m = this._map; if (!m || !m.getLayer(this._id)) return this;
if (s.color != null) m.setPaintProperty(this._id, 'line-color', s.color);
if (s.weight != null) m.setPaintProperty(this._id, 'line-width', s.weight);
if (s.opacity != null) m.setPaintProperty(this._id, 'line-opacity', s.opacity);
return this;
},
setLatLngs: function (lls) {
this._latlngs = lls || [];
if (this._map && this._map.getSource(this._id)) this._map.getSource(this._id).setData(this._geo());
return this;
},
// Leaflet-kompatibel: Punkt anhängen (Live-Track bei Routen-Aufzeichnung).
// FEHLTE bis 2026-06-08 → TypeError im Rec-Overlay = Strecke blieb unsichtbar.
addLatLng: function (ll) {
this._latlngs.push(ll);
if (this._map && this._map.getSource(this._id)) this._map.getSource(this._id).setData(this._geo());
return this;
},
// Leaflet-kompatibel: Array von {lat,lng} (für fitBounds-Sammlung).
getLatLngs: function () { return this._latlngs.map(function (p) { return (p && p.lat != null) ? { lat: p.lat, lng: p.lng } : { lat: p[0], lng: p[1] }; }); },
getBounds: function () { return { _coords: this._latlngs.map(function (p) { return (p && p.lat != null) ? [p.lat, p.lng] : p; }) }; },
remove: function () {
var m = this._map; if (!m) return this;
if (this._tipPopup) { try { this._tipPopup.remove(); } catch (e) {} }
if (m.getLayer(this._hitId())) m.removeLayer(this._hitId());
if (m.getLayer(this._id)) m.removeLayer(this._id);
if (m.getSource(this._id)) m.removeSource(this._id);
return this;
},
};
}
// ---- Gruppe (Cluster-Ersatz: fügt Marker direkt hinzu; GL clustert Seitenkarten nicht) ----
function _wrapGroup() {
return {
_markers: [], _map: null,
addLayer: function (m) { this._markers.push(m); if (this._map) m.addTo(this._map); return this; },
addLayers: function (ms) { (ms || []).forEach(this.addLayer, this); return this; },
removeLayers: function (ms) { (ms || []).forEach(function (m) { m.remove(); }); this._markers = this._markers.filter(function (m) { return (ms || []).indexOf(m) === -1; }); return this; },
addTo: function (mapWrap) { this._map = mapWrap; this._markers.forEach(function (m) { m.addTo(mapWrap); }); return this; },
clearLayers: function () { this._markers.forEach(function (m) { m.remove(); }); this._markers = []; return this; },
remove: function () { this.clearLayers(); this._map = null; return this; },
};
}
// Element aus HTML-String (für svgMarker mit custom HTML).
function _elFromHtml(html, size, anchorY) {
var wrap = document.createElement('div');
wrap.innerHTML = html;
var el = wrap.firstElementChild || wrap;
el.style.cursor = 'pointer';
return el;
}
window.MapGLMini = {
createMap: function (container, opts) {
opts = opts || {};
var el = typeof container === 'string' ? document.getElementById(container) : container;
var center = opts.center || [51.1657, 10.4515];
var map = new maplibregl.Map({
container: el,
style: MapGLStyle.build({ dark: !!opts.dark }),
center: _ll(center), zoom: opts.zoom != null ? opts.zoom : 6,
attributionControl: false, dragRotate: false, pitchWithRotate: false, maxZoom: 19,
});
map.touchZoomRotate.disableRotation();
map.touchPitch.disable();
try { el.style.touchAction = 'none'; } catch (e) {}
if (opts.zoomControl !== false) map.addControl(new maplibregl.NavigationControl({ showCompass: false }), 'top-left');
map.addControl(new maplibregl.AttributionControl({
compact: true, customAttribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}));
MapGLStyle.collapseAttribution(map); // nur ⓘ, nicht ausgeschrieben
// Container kann beim Erstellen (Modal/Animation) noch 0×0 sein → mehrfach resizen.
var _rz = function () { try { map.resize(); } catch (e) {} };
requestAnimationFrame(_rz);
setTimeout(_rz, 120); setTimeout(_rz, 400);
return _wrapMap(map);
},
// svgMarker: custom HTML-Icon. opts: { size, anchorY }
svgMarker: function (lat, lon, html, opts) {
opts = opts || {};
var el = _elFromHtml(html);
// anchorY: Pixel von oben zum Ankerpunkt (Leaflet iconAnchor). 'bottom' wenn anchorY≈size.
var anchor = 'center';
if (opts.anchorY != null && opts.size) {
anchor = opts.anchorY >= opts.size * 0.8 ? 'bottom' : 'center';
}
return _wrapMarker(lat, lon, el, anchor);
},
circleMarker: function (lat, lon, opts) {
opts = opts || {};
var r = opts.radius || 8;
var el = document.createElement('div');
el.style.cssText = 'width:' + (r * 2) + 'px;height:' + (r * 2) + 'px;border-radius:50%;background:' +
(opts.fillColor || opts.color || '#3B82F6') + ';border:' + (opts.weight || 2) + 'px solid ' +
(opts.color || '#fff') + ';opacity:' + (opts.fillOpacity != null ? opts.fillOpacity : 1) +
';box-shadow:0 1px 4px rgba(0,0,0,.35);cursor:pointer';
return _wrapMarker(lat, lon, el, 'center');
},
polyline: function (latlngs, opts) { return _wrapPolyline(latlngs, opts); },
clusterGroup: function () { return _wrapGroup(); },
// featureGroup: nur als Bounds-Container (markers = Array von Wrappern mit _gl.getLngLat()).
featureGroup: function (markers) {
var coords = (markers || []).map(function (m) {
var ll = m && m._gl && m._gl.getLngLat ? m._gl.getLngLat() : null;
return ll ? [ll.lat, ll.lng] : null;
}).filter(Boolean);
return { _coords: coords, getBounds: function () { return { _coords: coords }; }, addTo: function () { return this; } };
},
};
})();