diff --git a/VERSION b/VERSION index 54bad47..dfb227d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1195 \ No newline at end of file +1196 \ No newline at end of file diff --git a/backend/static/index.html b/backend/static/index.html index 4effee6..dfcb4ab 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -86,14 +86,14 @@ Ban Yaro - + - - - - - + + + + + @@ -617,11 +617,11 @@ - - - - - + + + + + @@ -631,7 +631,7 @@ - + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index c5adc12..23813da 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ 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 window.APP_VER = APP_VER; // global verfügbar für andere Module (z.B. offline-indicator) window.APP_VERSION = APP_VERSION; diff --git a/backend/static/js/map-gl-mini.js b/backend/static/js/map-gl-mini.js index fdc34ea..b7d189a 100644 --- a/backend/static/js/map-gl-mini.js +++ b/backend/static/js/map-gl-mini.js @@ -6,7 +6,11 @@ (function () { '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 ---- function _wrapMap(map) { @@ -33,10 +37,24 @@ 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) { 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; }, getZoom: function () { return map.getZoom(); }, 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); }); 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; }, 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, + _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). function _elFromHtml(html, size, anchorY) { var wrap = document.createElement('div'); @@ -134,6 +203,9 @@ 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) { diff --git a/backend/static/js/pages/events.js b/backend/static/js/pages/events.js index 1aaa1a2..f1a99d2 100644 --- a/backend/static/js/pages/events.js +++ b/backend/static/js/pages/events.js @@ -258,7 +258,7 @@ window.Page_events = (() => { if (_clusterGroup) { _map.removeLayer(_clusterGroup); } - _clusterGroup = L.markerClusterGroup(); + _clusterGroup = UI.map.clusterGroup(); _markers = []; const bounds = []; diff --git a/backend/static/js/pages/routes.js b/backend/static/js/pages/routes.js index b5952e3..f6bc4b8 100644 --- a/backend/static/js/pages/routes.js +++ b/backend/static/js/pages/routes.js @@ -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 = '
🗺️
'; - 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: `
`, - iconSize: [32, 32], - iconAnchor: [16, 16], - }); - L.marker([poi.lat, poi.lon], { icon }) - .bindTooltip(poi.name || poi._label, { direction: 'top', offset: [0, -16] }) + `; + UI.map.svgMarker(poi.lat, poi.lon, html, { size: 32 }) + .bindTooltip(poi.name || poi._label) .bindPopup(`${UI.escape(poi.name||poi._label)} ${poi.phone ? `
📞 ${UI.escape(poi.phone)}` : ''} ${poi.opening_hours ? `
🕐 ${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 = (() => {  ·  Original: ${origKm.toFixed(2)} km · ${origMin} min (bleibt angerechnet)`; }; 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: ` + const html = ` - `, - iconSize: [20, 20], iconAnchor: [10, 10], - }); - L.marker([track[i].lat, track[i].lon], { icon, interactive: false, zIndexOffset: -100 }).addTo(map); + `; + 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; } diff --git a/backend/static/js/pages/walks.js b/backend/static/js/pages/walks.js index fadedb6..6dcdd4c 100644 --- a/backend/static/js/pages/walks.js +++ b/backend/static/js/pages/walks.js @@ -369,7 +369,7 @@ window.Page_walks = (() => { } function _renderMarkers() { - if (!_map || !window.L) return; + if (!_map) return; _markers.forEach(m => m.remove()); _markers = []; _data.forEach(w => { @@ -386,8 +386,8 @@ window.Page_walks = (() => { if (_markers.length === 1) { _map.setView(_markers[0].getLatLng(), 13); } else if (_markers.length > 1) { - const group = L.featureGroup(_markers); - _map.fitBounds(group.getBounds().pad(0.2)); + const group = UI.map.featureGroup(_markers); + _map.fitBounds(group, { padding: 50 }); } } diff --git a/backend/static/js/ui.js b/backend/static/js/ui.js index 60c0caf..a3f7893 100644 --- a/backend/static/js/ui.js +++ b/backend/static/js/ui.js @@ -507,8 +507,10 @@ const UI = (() => { 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 = {}) { + 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); return L.circleMarker([lat, lon], opts); }, @@ -519,6 +521,18 @@ const UI = (() => { 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 // selbst verwalten, z.B. pages/map.js). vectorEnabled() { return _vectorMapEnabled(); }, diff --git a/backend/static/landing.html b/backend/static/landing.html index 0ebb442..c84deca 100644 --- a/backend/static/landing.html +++ b/backend/static/landing.html @@ -4,7 +4,7 @@ - + Ban Yaro — Die Hunde-App für Deutschland, Österreich & Schweiz diff --git a/backend/static/sw.js b/backend/static/sw.js index 652cdcb..5068e7d 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -4,7 +4,7 @@ ============================================================ */ // ← 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_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten