banyaro/backend/static/js/map-gl-mini.js
rene 285928f6f7 Karten: Routen-Übersichtskarte klickbar + Tagebuch-Karten auf GL
Punkt 2 (Routen-Übersicht 'Karte'): _renderRoutesOnMap crashte, weil die
Polyline-Facade kein bindTooltip/on/setStyle/getLatLngs kannte. In
map-gl-mini.js ergänzt — inkl. breiter, fast unsichtbarer Hit-Linie, damit
Routen auf dem Handy gut antippbar sind (Klick → Detail). Hover-Tooltip
(Name+km) + Hover-Highlight.

Punkt 4 (Tagebuch): beide Leaflet/OSM-Karten (Standort-Übersicht +
Einzeleintrag) auf UI.map.create + Facade-Marker migriert. popupopen-Wiring
(kennt die GL-Facade nicht) → Klick-Delegation auf dem Karten-Container.
Karten-Instanzen werden beim View-Wechsel/Verlassen freigegeben (destroy +
_clearDiaryMaps) gegen WebGL-Kontext-Leak. Detail/Übersicht fitten mehrfach
(Container-Timing).

Nebenbei: _loadPraise warf NotFoundError (insertBefore) — #diary-list liegt
in #diary-view-content, nicht direkt in _container. Jetzt vor der Liste in
deren echtem Elternknoten einfügen.

Verifiziert (headless, eingeloggt, echte Daten): Routenkarte 8 Marker klickbar
→ Detail; Detail+Vorschläge zoomen auf die Route; Tagebuch-Karte GL mit 108
Markern, Popup-Klick → Eintrag, keine Fehler.
2026-06-05 14:23:22 +02:00

288 lines
14 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).
if (bb && !isNaN(bb.getWest()) && map.getContainer().clientWidth > 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: 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',
}));
// 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; } };
},
};
})();