- 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
297 lines
15 KiB
JavaScript
297 lines
15 KiB
JavaScript
// 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; } };
|
||
},
|
||
};
|
||
})();
|