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:
parent
5844f1ef51
commit
96119e02ef
10 changed files with 146 additions and 88 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
1195
|
1196
|
||||||
|
|
@ -86,14 +86,14 @@
|
||||||
<title>Ban Yaro</title>
|
<title>Ban Yaro</title>
|
||||||
|
|
||||||
<!-- Theme + theme-color Statusleiste vor CSS setzen -->
|
<!-- Theme + theme-color Statusleiste vor CSS setzen -->
|
||||||
<script src="/js/boot-early.js?v=1195"></script>
|
<script src="/js/boot-early.js?v=1196"></script>
|
||||||
|
|
||||||
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
|
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
|
||||||
<link rel="stylesheet" href="/css/design-system.css?v=1195">
|
<link rel="stylesheet" href="/css/design-system.css?v=1196">
|
||||||
<link rel="stylesheet" href="/css/layout.css?v=1195">
|
<link rel="stylesheet" href="/css/layout.css?v=1196">
|
||||||
<link rel="stylesheet" href="/css/components.css?v=1195">
|
<link rel="stylesheet" href="/css/components.css?v=1196">
|
||||||
<link rel="stylesheet" href="/css/utilities.css?v=1195">
|
<link rel="stylesheet" href="/css/utilities.css?v=1196">
|
||||||
<link rel="stylesheet" href="/css/lists.css?v=1195">
|
<link rel="stylesheet" href="/css/lists.css?v=1196">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|
@ -617,11 +617,11 @@
|
||||||
<div id="modal-container"></div>
|
<div id="modal-container"></div>
|
||||||
|
|
||||||
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
|
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
|
||||||
<script src="/js/api.js?v=1195"></script>
|
<script src="/js/api.js?v=1196"></script>
|
||||||
<script src="/js/ui.js?v=1195"></script>
|
<script src="/js/ui.js?v=1196"></script>
|
||||||
<script src="/js/app.js?v=1195"></script>
|
<script src="/js/app.js?v=1196"></script>
|
||||||
<script src="/js/worlds.js?v=1195"></script>
|
<script src="/js/worlds.js?v=1196"></script>
|
||||||
<script src="/js/offline-indicator.js?v=1195"></script>
|
<script src="/js/offline-indicator.js?v=1196"></script>
|
||||||
|
|
||||||
<!-- Feature-Seiten werden lazy geladen -->
|
<!-- Feature-Seiten werden lazy geladen -->
|
||||||
|
|
||||||
|
|
@ -631,7 +631,7 @@
|
||||||
|
|
||||||
|
|
||||||
<!-- Boot: Offline-Banner + SW-Registration (extrahiert für CSP) -->
|
<!-- Boot: Offline-Banner + SW-Registration (extrahiert für CSP) -->
|
||||||
<script src="/js/boot.js?v=1195"></script>
|
<script src="/js/boot.js?v=1196"></script>
|
||||||
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Router, State-Management, Navigation, Initialisierung.
|
Router, State-Management, Navigation, Initialisierung.
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const APP_VER = '1195'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
const APP_VER = '1196'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||||
const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt
|
const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt
|
||||||
window.APP_VER = APP_VER; // global verfügbar für andere Module (z.B. offline-indicator)
|
window.APP_VER = APP_VER; // global verfügbar für andere Module (z.B. offline-indicator)
|
||||||
window.APP_VERSION = APP_VERSION;
|
window.APP_VERSION = APP_VERSION;
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,11 @@
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
function _ll(latlon) { return [latlon[1], latlon[0]]; } // [lat,lon] → [lng,lat]
|
// [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 ----
|
// ---- Map-Wrapper ----
|
||||||
function _wrapMap(map) {
|
function _wrapMap(map) {
|
||||||
|
|
@ -33,10 +37,24 @@
|
||||||
addLayer: function (layer) { if (layer && layer.addTo) layer.addTo(this); return this; },
|
addLayer: function (layer) { if (layer && layer.addTo) layer.addTo(this); return this; },
|
||||||
hasLayer: function () { return true; },
|
hasLayer: function () { return true; },
|
||||||
remove: function () { try { map.remove(); } catch (e) {} },
|
remove: function () { try { map.remove(); } catch (e) {} },
|
||||||
on: function (ev, fn) { map.on(ev, fn); return this; },
|
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; },
|
off: function (ev, fn) { map.off(ev, fn); return this; },
|
||||||
getZoom: function () { return map.getZoom(); },
|
getZoom: function () { return map.getZoom(); },
|
||||||
getCenter: function () { var c = map.getCenter(); return { lat: c.lat, lng: c.lng }; },
|
getCenter: function () { var c = map.getCenter(); return { lat: c.lat, lng: c.lng }; },
|
||||||
|
// 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));
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,13 +92,64 @@
|
||||||
if (ev === 'click') el.addEventListener('click', function (e) { e.stopPropagation(); fn(e); });
|
if (ev === 'click') el.addEventListener('click', function (e) { e.stopPropagation(); fn(e); });
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
setLatLng: function (latlon) { m.setLngLat([latlon[1], latlon[0]]); 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; },
|
setOpacity: function (o) { el.style.opacity = o; return this; },
|
||||||
remove: function () { try { m.remove(); } catch (e) {} return this; },
|
remove: function () { try { m.remove(); } catch (e) {} return this; },
|
||||||
};
|
};
|
||||||
return wrap;
|
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,
|
||||||
|
_geo: function () { return { type: 'Feature', geometry: { type: 'LineString', coordinates: this._latlngs.map(_toLngLat) } }; },
|
||||||
|
_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 } });
|
||||||
|
};
|
||||||
|
if (m.isStyleLoaded && m.isStyleLoaded()) add(); else m.once('load', add);
|
||||||
|
},
|
||||||
|
addTo: function (mapWrap) { this._map = mapWrap && mapWrap._gl ? mapWrap._gl : mapWrap; this._ensure(); 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;
|
||||||
|
},
|
||||||
|
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 (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).
|
// Element aus HTML-String (für svgMarker mit custom HTML).
|
||||||
function _elFromHtml(html, size, anchorY) {
|
function _elFromHtml(html, size, anchorY) {
|
||||||
var wrap = document.createElement('div');
|
var wrap = document.createElement('div');
|
||||||
|
|
@ -134,6 +203,9 @@
|
||||||
return _wrapMarker(lat, lon, el, 'center');
|
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: nur als Bounds-Container (markers = Array von Wrappern mit _gl.getLngLat()).
|
||||||
featureGroup: function (markers) {
|
featureGroup: function (markers) {
|
||||||
var coords = (markers || []).map(function (m) {
|
var coords = (markers || []).map(function (m) {
|
||||||
|
|
|
||||||
|
|
@ -258,7 +258,7 @@ window.Page_events = (() => {
|
||||||
if (_clusterGroup) {
|
if (_clusterGroup) {
|
||||||
_map.removeLayer(_clusterGroup);
|
_map.removeLayer(_clusterGroup);
|
||||||
}
|
}
|
||||||
_clusterGroup = L.markerClusterGroup();
|
_clusterGroup = UI.map.clusterGroup();
|
||||||
_markers = [];
|
_markers = [];
|
||||||
|
|
||||||
const bounds = [];
|
const bounds = [];
|
||||||
|
|
|
||||||
|
|
@ -613,9 +613,9 @@ window.Page_routes = (() => {
|
||||||
zoomControl: false, attributionControl: false,
|
zoomControl: false, attributionControl: false,
|
||||||
});
|
});
|
||||||
_suggestMap.scrollWheelZoom.disable();
|
_suggestMap.scrollWheelZoom.disable();
|
||||||
const poly = L.polyline(lls, { color: '#C4843A', weight: 4, opacity: 0.9 }).addTo(_suggestMap);
|
const poly = UI.map.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);
|
UI.map.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);
|
UI.map.circleMarker(lls.at(-1), { radius:7, color:'#EF4444', fillColor:'#EF4444', fillOpacity:1, weight:2 }).addTo(_suggestMap);
|
||||||
_addRouteArrows(_suggestMap, track, '#3b82f6');
|
_addRouteArrows(_suggestMap, track, '#3b82f6');
|
||||||
_suggestMap.fitBounds(poly.getBounds(), { padding: [16, 16] });
|
_suggestMap.fitBounds(poly.getBounds(), { padding: [16, 16] });
|
||||||
setTimeout(() => _suggestMap?.invalidateSize(), 120);
|
setTimeout(() => _suggestMap?.invalidateSize(), 120);
|
||||||
|
|
@ -751,7 +751,7 @@ window.Page_routes = (() => {
|
||||||
center: [pos.lat, pos.lon], zoom: 15,
|
center: [pos.lat, pos.lon], zoom: 15,
|
||||||
zoomControl: false, attributionControl: false,
|
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
|
radius: 8, color: '#fff', weight: 2.5, fillColor: '#3b82f6', fillOpacity: 1
|
||||||
}).addTo(_recMap);
|
}).addTo(_recMap);
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -856,10 +856,10 @@ window.Page_routes = (() => {
|
||||||
btn.addEventListener('pointercancel', cancelHold);
|
btn.addEventListener('pointercancel', cancelHold);
|
||||||
document.getElementById('rk-rec-stats-bar').style.display = '';
|
document.getElementById('rk-rec-stats-bar').style.display = '';
|
||||||
|
|
||||||
if (_recMap && window.L) {
|
if (_recMap) {
|
||||||
// Bei Fortsetzung den bestehenden Track sofort einzeichnen
|
// Bei Fortsetzung den bestehenden Track sofort einzeichnen
|
||||||
const seed = (resume && _recTrack.length) ? _recTrack.map(p => [p.lat, p.lon]) : [];
|
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) {
|
if (seed.length) {
|
||||||
const last = seed[seed.length - 1];
|
const last = seed[seed.length - 1];
|
||||||
_recLocMarker?.setLatLng(last);
|
_recLocMarker?.setLatLng(last);
|
||||||
|
|
@ -1245,7 +1245,7 @@ window.Page_routes = (() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function _renderRoutesOnMap() {
|
function _renderRoutesOnMap() {
|
||||||
if (!_searchMap || !window.L) return;
|
if (!_searchMap) return;
|
||||||
|
|
||||||
// Alte Linien entfernen
|
// Alte Linien entfernen
|
||||||
_searchLines.forEach(({ line }) => line.remove());
|
_searchLines.forEach(({ line }) => line.remove());
|
||||||
|
|
@ -1257,15 +1257,15 @@ window.Page_routes = (() => {
|
||||||
const pts = (route.preview_track || []).map(p => [p.lat, p.lon]);
|
const pts = (route.preview_track || []).map(p => [p.lat, p.lon]);
|
||||||
if (pts.length < 2) return;
|
if (pts.length < 2) return;
|
||||||
|
|
||||||
const line = L.polyline(pts, {
|
const line = UI.map.polyline(pts, {
|
||||||
color: '#C4843A', weight: 4, opacity: 0.75,
|
color: '#C4843A', weight: 4, opacity: 0.75,
|
||||||
}).addTo(_searchMap);
|
}).addTo(_searchMap);
|
||||||
|
|
||||||
// Start-/End-Marker
|
// 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
|
radius: 6, color: '#22C55E', fillColor: '#22C55E', fillOpacity: 1, weight: 1.5
|
||||||
}).addTo(_searchMap);
|
}).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
|
radius: 6, color: '#EF4444', fillColor: '#EF4444', fillOpacity: 1, weight: 1.5
|
||||||
}).addTo(_searchMap);
|
}).addTo(_searchMap);
|
||||||
|
|
||||||
|
|
@ -1294,7 +1294,7 @@ window.Page_routes = (() => {
|
||||||
if (_data.length && _searchLines.size && !_userPos) {
|
if (_data.length && _searchLines.size && !_userPos) {
|
||||||
const allPts = [..._searchLines.values()].flatMap(({ line }) => line.getLatLngs());
|
const allPts = [..._searchLines.values()].flatMap(({ line }) => line.getLatLngs());
|
||||||
if (allPts.length) {
|
if (allPts.length) {
|
||||||
try { _searchMap.fitBounds(L.latLngBounds(allPts), { padding: [20, 20], maxZoom: 14 }); }
|
try { _searchMap.fitBounds(allPts, { padding: [20, 20], maxZoom: 14 }); }
|
||||||
catch {}
|
catch {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1563,33 +1563,14 @@ window.Page_routes = (() => {
|
||||||
document.querySelectorAll('.rk-mini-map').forEach(el => obs.observe(el));
|
document.querySelectorAll('.rk-mini-map').forEach(el => obs.observe(el));
|
||||||
};
|
};
|
||||||
|
|
||||||
if (window.L) { init(); return; }
|
init();
|
||||||
// Leaflet noch am Laden — kurz pollen
|
|
||||||
let tries = 0;
|
|
||||||
const poll = setInterval(() => {
|
|
||||||
if (window.L || ++tries > 30) { clearInterval(poll); if (window.L) init(); }
|
|
||||||
}, 100);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
function _buildMiniMap(el) {
|
||||||
const track = JSON.parse(el.dataset.track || '[]');
|
const track = JSON.parse(el.dataset.track || '[]');
|
||||||
const routeId = parseInt(el.dataset.id);
|
el.innerHTML = _svgPreview(track);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
|
|
@ -1777,8 +1758,8 @@ window.Page_routes = (() => {
|
||||||
_navMap.invalidateSize();
|
_navMap.invalidateSize();
|
||||||
|
|
||||||
// Route-Polylines: erledigt (grün) + ausstehend (orange)
|
// Route-Polylines: erledigt (grün) + ausstehend (orange)
|
||||||
const doneLine = L.polyline([], { color: '#22c55e', weight: 5, opacity: 0.85 }).addTo(_navMap);
|
const doneLine = UI.map.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 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] });
|
_navMap.fitBounds(remainLine.getBounds(), { padding: [20, 20] });
|
||||||
_addRouteArrows(_navMap, track, '#3b82f6');
|
_addRouteArrows(_navMap, track, '#3b82f6');
|
||||||
|
|
||||||
|
|
@ -1791,7 +1772,7 @@ window.Page_routes = (() => {
|
||||||
}, 250);
|
}, 250);
|
||||||
|
|
||||||
// Start/End-Marker (als Variable damit Reverse sie neu setzen kann)
|
// 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
|
radius: 8, color: '#fff', weight: 2, fillColor: color, fillOpacity: 1
|
||||||
}).addTo(_navMap);
|
}).addTo(_navMap);
|
||||||
let startPin = mkPin(track[0], '#22c55e');
|
let startPin = mkPin(track[0], '#22c55e');
|
||||||
|
|
@ -1806,19 +1787,14 @@ window.Page_routes = (() => {
|
||||||
pois.forEach(poi => {
|
pois.forEach(poi => {
|
||||||
const svgIcon = poi._svgIcon || 'map-pin';
|
const svgIcon = poi._svgIcon || 'map-pin';
|
||||||
const color = poi._color || '#6b7280';
|
const color = poi._color || '#6b7280';
|
||||||
const icon = L.divIcon({
|
const html = `<div style="background:${color};color:#fff;width:32px;height:32px;border-radius:50%;
|
||||||
className: '',
|
|
||||||
html: `<div style="background:${color};color:#fff;width:32px;height:32px;border-radius:50%;
|
|
||||||
display:flex;align-items:center;justify-content:center;
|
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)">
|
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">
|
<svg style="width:16px;height:16px;fill:currentColor" viewBox="0 0 256 256" aria-hidden="true">
|
||||||
<use href="/icons/phosphor.svg#${svgIcon}"></use>
|
<use href="/icons/phosphor.svg#${svgIcon}"></use>
|
||||||
</svg></div>`,
|
</svg></div>`;
|
||||||
iconSize: [32, 32],
|
UI.map.svgMarker(poi.lat, poi.lon, html, { size: 32 })
|
||||||
iconAnchor: [16, 16],
|
.bindTooltip(poi.name || poi._label)
|
||||||
});
|
|
||||||
L.marker([poi.lat, poi.lon], { icon })
|
|
||||||
.bindTooltip(poi.name || poi._label, { direction: 'top', offset: [0, -16] })
|
|
||||||
.bindPopup(`<strong>${UI.escape(poi.name||poi._label)}</strong>
|
.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.phone ? `<br>📞 <a href="tel:${UI.escape(poi.phone)}">${UI.escape(poi.phone)}</a>` : ''}
|
||||||
${poi.opening_hours ? `<br>🕐 ${UI.escape(poi.opening_hours)}` : ''}`)
|
${poi.opening_hours ? `<br>🕐 ${UI.escape(poi.opening_hours)}` : ''}`)
|
||||||
|
|
@ -1913,7 +1889,7 @@ window.Page_routes = (() => {
|
||||||
_navWatchId = navigator.geolocation.watchPosition(pos => {
|
_navWatchId = navigator.geolocation.watchPosition(pos => {
|
||||||
const { latitude: lat, longitude: lon } = pos.coords;
|
const { latitude: lat, longitude: lon } = pos.coords;
|
||||||
if (!locMarker) {
|
if (!locMarker) {
|
||||||
locMarker = L.circleMarker([lat, lon], {
|
locMarker = UI.map.circleMarker([lat, lon], {
|
||||||
radius: 10, color: '#fff', weight: 3, fillColor: '#3b82f6', fillOpacity: 1,
|
radius: 10, color: '#fff', weight: 3, fillColor: '#3b82f6', fillOpacity: 1,
|
||||||
className: 'rk-nav-loc-pulse'
|
className: 'rk-nav-loc-pulse'
|
||||||
}).addTo(_navMap);
|
}).addTo(_navMap);
|
||||||
|
|
@ -2239,11 +2215,11 @@ window.Page_routes = (() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Marker & Polylines
|
// Marker & Polylines
|
||||||
let greyBefore = 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 = L.polyline([], { color: '#ef4444', weight: 5, opacity: 0.9 }).addTo(trimMap);
|
let activeLine = UI.map.polyline([], { color: '#ef4444', weight: 5, opacity: 0.9 }).addTo(trimMap);
|
||||||
let greyAfter = L.polyline([], { color: '#9ca3af', weight: 3, opacity: 0.5 }).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
|
radius: 9, color: '#fff', weight: 2.5, fillColor: color, fillOpacity: 1
|
||||||
}).addTo(trimMap);
|
}).addTo(trimMap);
|
||||||
|
|
||||||
|
|
@ -2271,13 +2247,13 @@ window.Page_routes = (() => {
|
||||||
· <span class="text-muted">Original: ${origKm.toFixed(2)} km · ${origMin} min (bleibt angerechnet)</span>`;
|
· <span class="text-muted">Original: ${origKm.toFixed(2)} km · ${origMin} min (bleibt angerechnet)</span>`;
|
||||||
};
|
};
|
||||||
update();
|
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
|
// Nächsten Track-Punkt zu einem Klick finden
|
||||||
const nearestIdx = (latlng) => {
|
const nearestIdx = (latlng) => {
|
||||||
let best = 0, bestD = Infinity;
|
let best = 0, bestD = Infinity;
|
||||||
fullTrack.forEach((p, i) => {
|
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; }
|
if (d < bestD) { bestD = d; best = i; }
|
||||||
});
|
});
|
||||||
return best;
|
return best;
|
||||||
|
|
@ -2655,17 +2631,13 @@ window.Page_routes = (() => {
|
||||||
for (let i = 1; i < track.length - 1; i++) {
|
for (let i = 1; i < track.length - 1; i++) {
|
||||||
if (cum[i] >= next) {
|
if (cum[i] >= next) {
|
||||||
const deg = brng(track[i-1], track[i]);
|
const deg = brng(track[i-1], track[i]);
|
||||||
const icon = L.divIcon({
|
const html = `<svg width="20" height="20" viewBox="0 0 20 20"
|
||||||
className: '',
|
style="transform:rotate(${deg.toFixed(0)}deg);transform-origin:10px 10px;display:block;pointer-events:none">
|
||||||
html: `<svg width="20" height="20" viewBox="0 0 20 20"
|
|
||||||
style="transform:rotate(${deg.toFixed(0)}deg);transform-origin:10px 10px;display:block">
|
|
||||||
<path d="M10,3 L15,15 L10,12 L5,15 Z"
|
<path d="M10,3 L15,15 L10,12 L5,15 Z"
|
||||||
fill="${color}" fill-opacity="0.85"
|
fill="${color}" fill-opacity="0.85"
|
||||||
stroke="rgba(0,0,0,0.25)" stroke-width="1" stroke-linejoin="round"/>
|
stroke="rgba(0,0,0,0.25)" stroke-width="1" stroke-linejoin="round"/>
|
||||||
</svg>`,
|
</svg>`;
|
||||||
iconSize: [20, 20], iconAnchor: [10, 10],
|
UI.map.svgMarker(track[i].lat, track[i].lon, html, { size: 20 }).addTo(map);
|
||||||
});
|
|
||||||
L.marker([track[i].lat, track[i].lon], { icon, interactive: false, zIndexOffset: -100 }).addTo(map);
|
|
||||||
next += spacing;
|
next += spacing;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2677,10 +2649,10 @@ window.Page_routes = (() => {
|
||||||
center: lls[0], zoom: 14,
|
center: lls[0], zoom: 14,
|
||||||
zoomControl: false, attributionControl: false,
|
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');
|
_addRouteArrows(m, track, '#3b82f6');
|
||||||
L.circleMarker(lls[0], { radius:7, color:'#22C55E', fillColor:'#22C55E', fillOpacity:1 }).addTo(m);
|
UI.map.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.at(-1), { radius:7, color:'#EF4444', fillColor:'#EF4444', fillOpacity:1 }).addTo(m);
|
||||||
m.fitBounds(poly.getBounds(), { padding:[10,10] });
|
m.fitBounds(poly.getBounds(), { padding:[10,10] });
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -369,7 +369,7 @@ window.Page_walks = (() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function _renderMarkers() {
|
function _renderMarkers() {
|
||||||
if (!_map || !window.L) return;
|
if (!_map) return;
|
||||||
_markers.forEach(m => m.remove());
|
_markers.forEach(m => m.remove());
|
||||||
_markers = [];
|
_markers = [];
|
||||||
_data.forEach(w => {
|
_data.forEach(w => {
|
||||||
|
|
@ -386,8 +386,8 @@ window.Page_walks = (() => {
|
||||||
if (_markers.length === 1) {
|
if (_markers.length === 1) {
|
||||||
_map.setView(_markers[0].getLatLng(), 13);
|
_map.setView(_markers[0].getLatLng(), 13);
|
||||||
} else if (_markers.length > 1) {
|
} else if (_markers.length > 1) {
|
||||||
const group = L.featureGroup(_markers);
|
const group = UI.map.featureGroup(_markers);
|
||||||
_map.fitBounds(group.getBounds().pad(0.2));
|
_map.fitBounds(group, { padding: 50 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -507,8 +507,10 @@ const UI = (() => {
|
||||||
return L.marker([lat, lon], { icon });
|
return L.marker([lat, lon], { icon });
|
||||||
},
|
},
|
||||||
|
|
||||||
// Engine-neutral: Kreis-Marker (Leaflet L.circleMarker bzw. GL-HTML-Punkt).
|
// Engine-neutral: Kreis-Marker. Akzeptiert (lat, lon, opts) ODER ([lat,lon], opts) (Leaflet-Stil).
|
||||||
circleMarker(lat, lon, opts = {}) {
|
circleMarker(lat, lon, opts = {}) {
|
||||||
|
if (Array.isArray(lat)) { opts = lon || {}; lon = lat[1]; lat = lat[0]; }
|
||||||
|
else if (lat && lat.lat != null) { opts = lon || {}; lon = lat.lng; lat = lat.lat; }
|
||||||
if (_uiGL && window.MapGLMini) return MapGLMini.circleMarker(lat, lon, opts);
|
if (_uiGL && window.MapGLMini) return MapGLMini.circleMarker(lat, lon, opts);
|
||||||
return L.circleMarker([lat, lon], opts);
|
return L.circleMarker([lat, lon], opts);
|
||||||
},
|
},
|
||||||
|
|
@ -519,6 +521,18 @@ const UI = (() => {
|
||||||
return L.featureGroup(markers);
|
return L.featureGroup(markers);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Engine-neutral: Polylinie (Route/Track).
|
||||||
|
polyline(latlngs, opts = {}) {
|
||||||
|
if (_uiGL && window.MapGLMini) return MapGLMini.polyline(latlngs, opts);
|
||||||
|
return L.polyline(latlngs, opts);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Engine-neutral: Cluster-/Marker-Gruppe (GL: ohne Clustering, einfache Gruppe).
|
||||||
|
clusterGroup(opts = {}) {
|
||||||
|
if (_uiGL && window.MapGLMini) return MapGLMini.clusterGroup();
|
||||||
|
return L.markerClusterGroup(opts);
|
||||||
|
},
|
||||||
|
|
||||||
// Feature-Flag-Status der Vektor-Basemap (für Karten, die ihren Basemap-Layer
|
// Feature-Flag-Status der Vektor-Basemap (für Karten, die ihren Basemap-Layer
|
||||||
// selbst verwalten, z.B. pages/map.js).
|
// selbst verwalten, z.B. pages/map.js).
|
||||||
vectorEnabled() { return _vectorMapEnabled(); },
|
vectorEnabled() { return _vectorMapEnabled(); },
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="color-scheme" content="light dark">
|
<meta name="color-scheme" content="light dark">
|
||||||
<script src="/js/landing-init.js?v=1195"></script>
|
<script src="/js/landing-init.js?v=1196"></script>
|
||||||
<title>Ban Yaro — Die Hunde-App für Deutschland, Österreich & Schweiz</title>
|
<title>Ban Yaro — Die Hunde-App für Deutschland, Österreich & Schweiz</title>
|
||||||
<meta name="description" content="Ban Yaro: Die kostenlose All-in-One Hunde-App für DACH. Tagebuch, Giftköder-Alarm, Training mit KI, Forum, Wurfbörse, Stammbaum, Inzucht-Check — DSGVO-konform, offline-fähig, ohne App Store.">
|
<meta name="description" content="Ban Yaro: Die kostenlose All-in-One Hunde-App für DACH. Tagebuch, Giftköder-Alarm, Training mit KI, Forum, Wurfbörse, Stammbaum, Inzucht-Check — DSGVO-konform, offline-fähig, ohne App Store.">
|
||||||
<meta name="keywords" content="Hunde App, Hunde Community, Wurfbörse, Züchter, Welpen kaufen, Stammbaum Hund, Inzuchtkoeffizient, Hundezucht, Impfpass Hund, Giftköder Alarm, Gassi Community, Hundetraining App, Hunde Forum, Hunde KI, Hundefilm Datenbank, Welpen Marktplatz">
|
<meta name="keywords" content="Hunde App, Hunde Community, Wurfbörse, Züchter, Welpen kaufen, Stammbaum Hund, Inzuchtkoeffizient, Hundezucht, Impfpass Hund, Giftköder Alarm, Gassi Community, Hundetraining App, Hunde Forum, Hunde KI, Hundefilm Datenbank, Welpen Marktplatz">
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
// ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab
|
// ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab
|
||||||
const VER = '1195';
|
const VER = '1196';
|
||||||
const CACHE_VERSION = `by-v${VER}`;
|
const CACHE_VERSION = `by-v${VER}`;
|
||||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue