// 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: 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: '© OpenStreetMap 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; } }; }, }; })();