Seitenkarten GL Runde 2: Events, Gassi, Routen + Facade-Erweiterung

- Facade: Polyline (geojson line-source, addTo/setLatLngs/getBounds/remove), clusterGroup,
  marker.getLatLng, map.distance(Haversine), on('click') normalisiert e.latlng aus e.lngLat, _ll objekttauglich
- events: L.markerClusterGroup→UI.map.clusterGroup
- walks: window.L-Guard, L.featureGroup→UI.map.featureGroup, fitBounds ohne .pad
- routes: L.polyline/L.circleMarker→UI.map.*, navMap/Pfeil-Marker→svgMarker, latLngBounds→coords,
  trimMap distance/click, Mini-Vorschauen auf SVG (kein WebGL-Limit, kein OSM-Raster)
This commit is contained in:
rene 2026-06-05 12:48:09 +02:00
parent 5844f1ef51
commit 96119e02ef
10 changed files with 146 additions and 88 deletions

View file

@ -613,9 +613,9 @@ window.Page_routes = (() => {
zoomControl: false, attributionControl: false,
});
_suggestMap.scrollWheelZoom.disable();
const poly = L.polyline(lls, { color: '#C4843A', weight: 4, opacity: 0.9 }).addTo(_suggestMap);
L.circleMarker(lls[0], { radius:7, color:'#22C55E', fillColor:'#22C55E', fillOpacity:1, weight:2 }).addTo(_suggestMap);
L.circleMarker(lls.at(-1), { radius:7, color:'#EF4444', fillColor:'#EF4444', fillOpacity:1, weight:2 }).addTo(_suggestMap);
const poly = UI.map.polyline(lls, { color: '#C4843A', weight: 4, opacity: 0.9 }).addTo(_suggestMap);
UI.map.circleMarker(lls[0], { radius:7, color:'#22C55E', fillColor:'#22C55E', fillOpacity:1, weight:2 }).addTo(_suggestMap);
UI.map.circleMarker(lls.at(-1), { radius:7, color:'#EF4444', fillColor:'#EF4444', fillOpacity:1, weight:2 }).addTo(_suggestMap);
_addRouteArrows(_suggestMap, track, '#3b82f6');
_suggestMap.fitBounds(poly.getBounds(), { padding: [16, 16] });
setTimeout(() => _suggestMap?.invalidateSize(), 120);
@ -751,7 +751,7 @@ window.Page_routes = (() => {
center: [pos.lat, pos.lon], zoom: 15,
zoomControl: false, attributionControl: false,
});
_recLocMarker = L.circleMarker([pos.lat, pos.lon], {
_recLocMarker = UI.map.circleMarker([pos.lat, pos.lon], {
radius: 8, color: '#fff', weight: 2.5, fillColor: '#3b82f6', fillOpacity: 1
}).addTo(_recMap);
} catch {
@ -856,10 +856,10 @@ window.Page_routes = (() => {
btn.addEventListener('pointercancel', cancelHold);
document.getElementById('rk-rec-stats-bar').style.display = '';
if (_recMap && window.L) {
if (_recMap) {
// Bei Fortsetzung den bestehenden Track sofort einzeichnen
const seed = (resume && _recTrack.length) ? _recTrack.map(p => [p.lat, p.lon]) : [];
_recPolyline = L.polyline(seed, { color: '#ef4444', weight: 5, opacity: 0.9 }).addTo(_recMap);
_recPolyline = UI.map.polyline(seed, { color: '#ef4444', weight: 5, opacity: 0.9 }).addTo(_recMap);
if (seed.length) {
const last = seed[seed.length - 1];
_recLocMarker?.setLatLng(last);
@ -1245,7 +1245,7 @@ window.Page_routes = (() => {
}
function _renderRoutesOnMap() {
if (!_searchMap || !window.L) return;
if (!_searchMap) return;
// Alte Linien entfernen
_searchLines.forEach(({ line }) => line.remove());
@ -1257,15 +1257,15 @@ window.Page_routes = (() => {
const pts = (route.preview_track || []).map(p => [p.lat, p.lon]);
if (pts.length < 2) return;
const line = L.polyline(pts, {
const line = UI.map.polyline(pts, {
color: '#C4843A', weight: 4, opacity: 0.75,
}).addTo(_searchMap);
// Start-/End-Marker
const startM = L.circleMarker(pts[0], {
const startM = UI.map.circleMarker(pts[0], {
radius: 6, color: '#22C55E', fillColor: '#22C55E', fillOpacity: 1, weight: 1.5
}).addTo(_searchMap);
const endM = L.circleMarker(pts[pts.length - 1], {
const endM = UI.map.circleMarker(pts[pts.length - 1], {
radius: 6, color: '#EF4444', fillColor: '#EF4444', fillOpacity: 1, weight: 1.5
}).addTo(_searchMap);
@ -1294,7 +1294,7 @@ window.Page_routes = (() => {
if (_data.length && _searchLines.size && !_userPos) {
const allPts = [..._searchLines.values()].flatMap(({ line }) => line.getLatLngs());
if (allPts.length) {
try { _searchMap.fitBounds(L.latLngBounds(allPts), { padding: [20, 20], maxZoom: 14 }); }
try { _searchMap.fitBounds(allPts, { padding: [20, 20], maxZoom: 14 }); }
catch {}
}
}
@ -1563,33 +1563,14 @@ window.Page_routes = (() => {
document.querySelectorAll('.rk-mini-map').forEach(el => obs.observe(el));
};
if (window.L) { init(); return; }
// Leaflet noch am Laden — kurz pollen
let tries = 0;
const poll = setInterval(() => {
if (window.L || ++tries > 30) { clearInterval(poll); if (window.L) init(); }
}, 100);
init();
}
// Mini-Vorschau: SVG-Routenform (keine eigene Karte → kein WebGL-Kontext-Limit bei vielen
// Listeneinträgen, kein OSM-Raster). Die Detail-/Navigations-Karten sind voll GL.
function _buildMiniMap(el) {
const track = JSON.parse(el.dataset.track || '[]');
const routeId = parseInt(el.dataset.id);
if (track.length < 2) {
el.innerHTML = '<div class="rk-preview-empty">🗺️</div>';
return;
}
const lls = track.map(p => [p.lat, p.lon]);
const m = L.map(el, {
zoomControl: false, attributionControl: false,
dragging: false, touchZoom: false, scrollWheelZoom: false,
doubleClickZoom: false, keyboard: false, boxZoom: false,
});
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 17 }).addTo(m);
const poly = L.polyline(lls, { color: '#C4843A', weight: 3, opacity: 0.9 }).addTo(m);
L.circleMarker(lls[0], { radius: 5, color: '#22C55E', fillColor: '#22C55E', fillOpacity: 1, weight: 1.5 }).addTo(m);
L.circleMarker(lls.at(-1), { radius: 5, color: '#EF4444', fillColor: '#EF4444', fillOpacity: 1, weight: 1.5 }).addTo(m);
m.fitBounds(poly.getBounds(), { padding: [8, 8] });
_miniMaps.set(routeId, m);
el.innerHTML = _svgPreview(track);
}
// ----------------------------------------------------------
@ -1777,8 +1758,8 @@ window.Page_routes = (() => {
_navMap.invalidateSize();
// Route-Polylines: erledigt (grün) + ausstehend (orange)
const doneLine = L.polyline([], { color: '#22c55e', weight: 5, opacity: 0.85 }).addTo(_navMap);
const remainLine = L.polyline(track.map(p => [p.lat, p.lon]), { color: '#f97316', weight: 5, opacity: 0.9 }).addTo(_navMap);
const doneLine = UI.map.polyline([], { color: '#22c55e', weight: 5, opacity: 0.85 }).addTo(_navMap);
const remainLine = UI.map.polyline(track.map(p => [p.lat, p.lon]), { color: '#f97316', weight: 5, opacity: 0.9 }).addTo(_navMap);
_navMap.fitBounds(remainLine.getBounds(), { padding: [20, 20] });
_addRouteArrows(_navMap, track, '#3b82f6');
@ -1791,7 +1772,7 @@ window.Page_routes = (() => {
}, 250);
// Start/End-Marker (als Variable damit Reverse sie neu setzen kann)
const mkPin = (p, color) => L.circleMarker([p.lat, p.lon], {
const mkPin = (p, color) => UI.map.circleMarker([p.lat, p.lon], {
radius: 8, color: '#fff', weight: 2, fillColor: color, fillOpacity: 1
}).addTo(_navMap);
let startPin = mkPin(track[0], '#22c55e');
@ -1806,19 +1787,14 @@ window.Page_routes = (() => {
pois.forEach(poi => {
const svgIcon = poi._svgIcon || 'map-pin';
const color = poi._color || '#6b7280';
const icon = L.divIcon({
className: '',
html: `<div style="background:${color};color:#fff;width:32px;height:32px;border-radius:50%;
const html = `<div style="background:${color};color:#fff;width:32px;height:32px;border-radius:50%;
display:flex;align-items:center;justify-content:center;
box-shadow:0 2px 5px rgba(0,0,0,0.35);border:2px solid rgba(255,255,255,.7)">
<svg style="width:16px;height:16px;fill:currentColor" viewBox="0 0 256 256" aria-hidden="true">
<use href="/icons/phosphor.svg#${svgIcon}"></use>
</svg></div>`,
iconSize: [32, 32],
iconAnchor: [16, 16],
});
L.marker([poi.lat, poi.lon], { icon })
.bindTooltip(poi.name || poi._label, { direction: 'top', offset: [0, -16] })
</svg></div>`;
UI.map.svgMarker(poi.lat, poi.lon, html, { size: 32 })
.bindTooltip(poi.name || poi._label)
.bindPopup(`<strong>${UI.escape(poi.name||poi._label)}</strong>
${poi.phone ? `<br>📞 <a href="tel:${UI.escape(poi.phone)}">${UI.escape(poi.phone)}</a>` : ''}
${poi.opening_hours ? `<br>🕐 ${UI.escape(poi.opening_hours)}` : ''}`)
@ -1913,7 +1889,7 @@ window.Page_routes = (() => {
_navWatchId = navigator.geolocation.watchPosition(pos => {
const { latitude: lat, longitude: lon } = pos.coords;
if (!locMarker) {
locMarker = L.circleMarker([lat, lon], {
locMarker = UI.map.circleMarker([lat, lon], {
radius: 10, color: '#fff', weight: 3, fillColor: '#3b82f6', fillOpacity: 1,
className: 'rk-nav-loc-pulse'
}).addTo(_navMap);
@ -2239,11 +2215,11 @@ window.Page_routes = (() => {
});
// Marker & Polylines
let greyBefore = L.polyline([], { color: '#9ca3af', weight: 3, opacity: 0.5 }).addTo(trimMap);
let activeLine = L.polyline([], { color: '#ef4444', weight: 5, opacity: 0.9 }).addTo(trimMap);
let greyAfter = L.polyline([], { color: '#9ca3af', weight: 3, opacity: 0.5 }).addTo(trimMap);
let greyBefore = UI.map.polyline([], { color: '#9ca3af', weight: 3, opacity: 0.5 }).addTo(trimMap);
let activeLine = UI.map.polyline([], { color: '#ef4444', weight: 5, opacity: 0.9 }).addTo(trimMap);
let greyAfter = UI.map.polyline([], { color: '#9ca3af', weight: 3, opacity: 0.5 }).addTo(trimMap);
const mkMarker = (lat, lon, color) => L.circleMarker([lat, lon], {
const mkMarker = (lat, lon, color) => UI.map.circleMarker([lat, lon], {
radius: 9, color: '#fff', weight: 2.5, fillColor: color, fillOpacity: 1
}).addTo(trimMap);
@ -2271,13 +2247,13 @@ window.Page_routes = (() => {
&nbsp;·&nbsp; <span class="text-muted">Original: ${origKm.toFixed(2)} km · ${origMin} min (bleibt angerechnet)</span>`;
};
update();
trimMap.fitBounds(L.polyline(fullTrack.map(p => [p.lat, p.lon])).getBounds(), { padding: [20, 20] });
trimMap.fitBounds(UI.map.polyline(fullTrack.map(p => [p.lat, p.lon])).getBounds(), { padding: [20, 20] });
// Nächsten Track-Punkt zu einem Klick finden
const nearestIdx = (latlng) => {
let best = 0, bestD = Infinity;
fullTrack.forEach((p, i) => {
const d = trimMap.distance(latlng, L.latLng(p.lat, p.lon));
const d = trimMap.distance(latlng, { lat: p.lat, lng: p.lon });
if (d < bestD) { bestD = d; best = i; }
});
return best;
@ -2655,17 +2631,13 @@ window.Page_routes = (() => {
for (let i = 1; i < track.length - 1; i++) {
if (cum[i] >= next) {
const deg = brng(track[i-1], track[i]);
const icon = L.divIcon({
className: '',
html: `<svg width="20" height="20" viewBox="0 0 20 20"
style="transform:rotate(${deg.toFixed(0)}deg);transform-origin:10px 10px;display:block">
const html = `<svg width="20" height="20" viewBox="0 0 20 20"
style="transform:rotate(${deg.toFixed(0)}deg);transform-origin:10px 10px;display:block;pointer-events:none">
<path d="M10,3 L15,15 L10,12 L5,15 Z"
fill="${color}" fill-opacity="0.85"
stroke="rgba(0,0,0,0.25)" stroke-width="1" stroke-linejoin="round"/>
</svg>`,
iconSize: [20, 20], iconAnchor: [10, 10],
});
L.marker([track[i].lat, track[i].lon], { icon, interactive: false, zIndexOffset: -100 }).addTo(map);
</svg>`;
UI.map.svgMarker(track[i].lat, track[i].lon, html, { size: 20 }).addTo(map);
next += spacing;
}
}
@ -2677,10 +2649,10 @@ window.Page_routes = (() => {
center: lls[0], zoom: 14,
zoomControl: false, attributionControl: false,
});
const poly = L.polyline(lls, { color: '#C4843A', weight: 4, opacity: 0.85 }).addTo(m);
const poly = UI.map.polyline(lls, { color: '#C4843A', weight: 4, opacity: 0.85 }).addTo(m);
_addRouteArrows(m, track, '#3b82f6');
L.circleMarker(lls[0], { radius:7, color:'#22C55E', fillColor:'#22C55E', fillOpacity:1 }).addTo(m);
L.circleMarker(lls.at(-1), { radius:7, color:'#EF4444', fillColor:'#EF4444', fillOpacity:1 }).addTo(m);
UI.map.circleMarker(lls[0], { radius:7, color:'#22C55E', fillColor:'#22C55E', fillOpacity:1 }).addTo(m);
UI.map.circleMarker(lls.at(-1), { radius:7, color:'#EF4444', fillColor:'#EF4444', fillOpacity:1 }).addTo(m);
m.fitBounds(poly.getBounds(), { padding:[10,10] });
return m;
}