diff --git a/VERSION b/VERSION index d19618e..baa70bd 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1179 \ No newline at end of file +1180 \ No newline at end of file diff --git a/backend/static/index.html b/backend/static/index.html index 78d55fd..e8f270c 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 9c378f5..a736339 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 = '1179'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '1180'; // ← 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-markers.js b/backend/static/js/map-gl-markers.js index abdf84a..defa7a8 100644 --- a/backend/static/js/map-gl-markers.js +++ b/backend/static/js/map-gl-markers.js @@ -11,6 +11,7 @@ var _dangerRadiusM = 100; var _popupHTML = null; // (props, key) -> htmlString var _popupWire = null; // (props, key, closeFn) -> void + var _onClick = null; // (props, key) -> true = Klick behandelt, Popup unterdrücken var _activePopup = null; var _dangerKeys = []; @@ -132,6 +133,7 @@ if (!e.features || !e.features.length) return; var f = e.features[0]; var props = f.properties || {}; + if (_onClick && _onClick(props, key) === true) return; // Klick anderweitig behandelt if (_activePopup) { _activePopup.remove(); _activePopup = null; } var html = _popupHTML ? _popupHTML(props, key) : ('' + (props.name || key) + ''); if (!html) return; @@ -166,6 +168,7 @@ _dangerRadiusM = opts.dangerRadiusM || 100; _popupHTML = opts.popupHTML || null; _popupWire = opts.popupWire || null; + _onClick = opts.onClick || null; _addCategoryLayers(); return _buildIcons(); }, diff --git a/backend/static/js/pages/map.js b/backend/static/js/pages/map.js index 88617fc..66c0b6b 100644 --- a/backend/static/js/pages/map.js +++ b/backend/static/js/pages/map.js @@ -172,7 +172,7 @@ window.Page_map = (() => { API.getLocation().then(pos => { _userPos = pos; if (_frankfurtTimer) { clearTimeout(_frankfurtTimer); _frankfurtTimer = null; } - _map?.flyTo([pos.lat, pos.lon], 14, { duration: 1.2 }); + _mapFlyTo(pos.lat, pos.lon, 14, { duration: 1.2 }); _weatherLoaded = true; _loadWeather(pos.lat, pos.lon); }).catch(() => { @@ -187,8 +187,8 @@ window.Page_map = (() => { function refresh() { // Leaflet kennt die Container-Größe nach Seitenwechsel nicht — neu berechnen - setTimeout(() => { _map?.invalidateSize(); _scheduleOsmLoad(); }, 150); - setTimeout(() => _map?.invalidateSize(), 600); + setTimeout(() => { _mapResize(); _scheduleOsmLoad(); }, 150); + setTimeout(() => _mapResize(), 600); _loadAll(); } function onDogChange() {} @@ -357,7 +357,7 @@ window.Page_map = (() => { document.getElementById('map-locate-btn').addEventListener('click', () => { _sdEl?.classList.remove('open'); if (_userPos) { - _map?.setView([_userPos.lat, _userPos.lon], 16); + _mapSetView(_userPos.lat, _userPos.lon, 16); } else { UI.toast.error('Standort noch nicht verfügbar.'); } @@ -425,6 +425,7 @@ window.Page_map = (() => { let _tempDebounce = null; async function _toggleRadar() { + if (_engineGL) { UI.toast.info('Regenradar im neuen Karten-Modus in Kürze.'); return; } if (!App.hasPro(_appState?.user)) { UI.toast.info('Regenradar ist ein Pro-Feature.'); return; @@ -445,6 +446,7 @@ window.Page_map = (() => { } async function _toggleTemp() { + if (_engineGL) { UI.toast.info('Temperatur-Layer im neuen Karten-Modus in Kürze.'); return; } if (!App.hasPro(_appState?.user)) { UI.toast.info('Temperatur-Layer ist ein Pro-Feature.'); return; @@ -690,13 +692,14 @@ window.Page_map = (() => { const seq = (srcs) => srcs.reduce((p, src) => p.then(() => new Promise((res, rej) => { if ((src.includes('maplibre-gl.js') && window.maplibregl) || (src.includes('pmtiles.js') && window.pmtiles) || - (src.includes('map-gl-style') && window.MapGLStyle)) return res(); + (src.includes('map-gl-style') && window.MapGLStyle) || + (src.includes('map-gl-markers') && window.MapGLMarkers)) return res(); const s = document.createElement('script'); s.src = src + v; s.onload = res; s.onerror = rej; document.head.appendChild(s); })), Promise.resolve()); - return seq(['/js/vendor/maplibre-gl.js', '/js/vendor/pmtiles.js', '/js/map-gl-style.js']).then(() => { - if (!(window.maplibregl && window.pmtiles && window.MapGLStyle)) throw new Error('MapLibre nicht geladen'); + return seq(['/js/vendor/maplibre-gl.js', '/js/vendor/pmtiles.js', '/js/map-gl-style.js', '/js/map-gl-markers.js']).then(() => { + if (!(window.maplibregl && window.pmtiles && window.MapGLStyle && window.MapGLMarkers)) throw new Error('MapLibre nicht geladen'); if (!_pmtilesProtoReg) { const proto = new pmtiles.Protocol(); maplibregl.addProtocol('pmtiles', proto.tile); @@ -783,23 +786,148 @@ window.Page_map = (() => { if (!_map || !_engineGL) return; _glLayersReady = false; _map.setStyle(MapGLStyle.build({ dark: _isDarkMode() })); - // setStyle entfernt eigene Sources/Layer → nach Style-Load neu anlegen. - _map.once('styledata', () => { _initPoiLayersGL(); _scheduleOsmLoad(); }); + // setStyle entfernt eigene Sources/Layer → nach Style-Load neu anlegen + Daten neu setzen. + _map.once('styledata', () => { + _initPoiLayersGL(); + Object.keys(TYPEN).forEach(_glPushLayer); + _scheduleOsmLoad(); + }); + } + + // GL-Datenmodell: POI-DATEN (nicht Marker) pro Kategorie. own = eigene Orte/Giftköder/ + // Züchter (aus _loadAll), osm = Scan-Ergebnisse. Beim Setzen werden beide gemerged. + let _glOsm = {}; + let _glOwn = {}; + function _glPushLayer(key) { + if (!_engineGL || !window.MapGLMarkers) return; + MapGLMarkers.setLayer(key, (_glOwn[key] || []).concat(_glOsm[key] || [])); + } + + function _iconNameOf(t) { + const m = /#([a-z0-9-]+)"/.exec(t && t.icon || ''); + return m ? m[1] : null; } - // POI-Sources/Layer in MapLibre anlegen — wird in Build-Runde 2 gefüllt. function _initPoiLayersGL() { - if (!_map || !_engineGL || _glLayersReady) return; + if (!_map || !_engineGL || !window.MapGLMarkers || _glLayersReady) return; _glLayersReady = true; - // (Build-Runde 2: GeoJSON-Sources + Cluster/Symbol-Layer + Icons) + const types = {}; + Object.keys(TYPEN).forEach(k => { + types[k] = { color: TYPEN[k].color, iconName: _iconNameOf(TYPEN[k]), danger: DANGER_RADIUS[k] !== undefined }; + }); + MapGLMarkers.init(_map, { + types, + dangerKeys: Object.keys(DANGER_RADIUS), + dangerRadiusM: 100, + onClick: (props) => { + if (props._kind === 'poison_alarm') { App.navigate('poison'); return true; } + if (props._kind === 'place') { + UI.toast.info(`${props.name || ''}${props.adresse ? ' · ' + props.adresse : ''}`.trim() || 'Eigener Ort'); + return true; + } + return false; + }, + popupHTML: (props, key) => _buildPoiPopupHTML(props, key), + popupWire: (props, key, close) => _wirePoiPopup(props, key, close), + }); + } + + // Popup-HTML für GL (spiegelt _showMarkerPopup; Züchter separat). + function _buildPoiPopupHTML(props, layerKey) { + const t = TYPEN[layerKey] || {}; + if (props._kind === 'breeder') { + const rasse = props.rasse_text ? `
${UI.escape(props.rasse_text)}
` : ''; + const stadt = props.stadt ? `
${UI.escape(props.stadt)}
` : ''; + return `
+
${t.icon || ''} ${UI.escape(props.zwingername || '')}
+ ${rasse}${stadt}
`; + } + const label = props.name || t.label || ''; + const isOwn = props.source === 'user' && (props.own === true || props.own === 'true' || props.own === 1); + const isUser = props.source === 'user'; + const DOG_TYPES = ['restaurant', 'hotel', 'shop', 'tierarzt', 'hundesalon']; + const dogBtn = (props.source === 'osm' && DOG_TYPES.includes(layerKey)) + ? `
Hund willkommen?
+
+ + +
` : ''; + const actionBtn = isOwn + ? `` + : ``; + const openHours = props.opening_hours ? `
${UI.escape(String(props.opening_hours))}
` : ''; + const phone = props.phone ? `
${UI.escape(String(props.phone))}
` : ''; + const website = props.website ? `
Website
` : ''; + return `
+
${t.icon || ''} ${UI.escape(String(label))}
+ ${props.notiz ? `
${UI.escape(String(props.notiz))}
` : ''} + ${openHours}${phone}${website} +
+ ${isUser ? ` Community-Pin${props.username ? ' · ' + UI.escape(String(props.username)) + '' : ''}` + : ' OpenStreetMap'} +
${dogBtn}${actionBtn}
`; + } + + function _wirePoiPopup(props, layerKey, close) { + if (props._kind === 'breeder') { + document.getElementById('breeder-profile-btn')?.addEventListener('click', () => { + close(); App.navigate('breeder', true, { zwingername: props.zwingername }); + }); + return; + } + const isOwn = props.source === 'user' && (props.own === true || props.own === 'true' || props.own === 1); + document.getElementById('mp-action')?.addEventListener('click', () => { + close(); + if (isOwn) _deleteUserPoi(props.user_poi_id, null, layerKey); + else _showReportDialog({ source: props.source, id: props.id, user_poi_id: props.user_poi_id, lat: props.lat, lon: props.lon }); + }); + const sendDog = async (welcome) => { + const yes = document.getElementById('mp-dogyes'), no = document.getElementById('mp-dogno'); + if (yes) yes.disabled = true; if (no) no.disabled = true; + try { + const r = await API.post('/osm-contrib/dog-friendly', { + osm_id: props.id, osm_type: 'node', poi_type: layerKey, lat: props.lat, lon: props.lon, welcome, + }); + UI.toast.success((welcome ? 'Hund willkommen' : 'Hund nicht willkommen') + (r.submitted ? ' — eingetragen 🐾' : ' — wird übertragen 🐾')); + close(); + } catch (e) { + UI.toast.error(e?.message || 'Konnte nicht eintragen.'); + if (yes) yes.disabled = false; if (no) no.disabled = false; + } + }; + document.getElementById('mp-dogyes')?.addEventListener('click', () => sendDog(true)); + document.getElementById('mp-dogno')?.addEventListener('click', () => sendDog(false)); } // ---------------------------------------------------------- // Standort-Tracking — pulsierender blauer Punkt // ---------------------------------------------------------- function _startLocationTracking() { - if (!navigator.geolocation || !_map || !window.L) return; + if (!navigator.geolocation || !_map) return; + if (_engineGL) { + _watchId = navigator.geolocation.watchPosition( + pos => { + const { latitude: lat, longitude: lon } = pos.coords; + _userPos = { lat, lon }; + if (!_weatherLoaded) { _weatherLoaded = true; _loadWeather(lat, lon); } + if (_locationMarker) { + _locationMarker.setLngLat([lon, lat]); + } else { + const elx = document.createElement('div'); + elx.className = 'loc-icon'; + elx.innerHTML = '
'; + _locationMarker = new maplibregl.Marker({ element: elx, anchor: 'center' }) + .setLngLat([lon, lat]).addTo(_map); + } + }, + () => {}, + { enableHighAccuracy: true, maximumAge: 5000, timeout: 15000 } + ); + return; + } + + if (!window.L) return; const icon = L.divIcon({ className: 'loc-icon', html: '
', @@ -1110,54 +1238,69 @@ window.Page_map = (() => { _overpassTimer = setTimeout(_loadOsmLayers, 600); } + // OSM-Marker-Zählung (ohne eigene Orte), engine-neutral. + function _osmCountOf(k) { + if (_engineGL) return (_glOsm[k] || []).length; + return (_layers[k] || []).filter(m => !m._ownPlace).length; + } + function _osmTotalCount() { + if (_engineGL) return Object.values(_glOsm).reduce((a, arr) => a + (arr ? arr.length : 0), 0); + return Object.values(_layers).flat().filter(m => !m._ownPlace).length; + } + + // OSM-Marker eines Layers leeren (engine-neutral, eigene Orte bleiben). + function _clearOsmLayer(k) { + if (_engineGL) { _glOsm[k] = []; _glPushLayer(k); return; } + _layers[k].filter(m => !m._ownPlace).forEach(m => m._dangerCircle?.remove()); + _clusterGroups[k]?.clearLayers(); + _layers[k] = _layers[k].filter(m => m._ownPlace); + } + async function _loadOsmLayers() { - if (!_map || !window.L || _overpassActive) return; - const zoom = _map.getZoom(); + if (!_map || _overpassActive) return; + if (!_engineGL && !window.L) return; + const zoom = _mapGetZoom(); // Unter Zoom 10: alles ausblenden if (zoom < 10) { - Object.keys(OSM_LAYER_MAP).forEach(k => { - _layers[k].filter(m => !m._ownPlace).forEach(m => m._dangerCircle?.remove()); - _clusterGroups[k]?.clearLayers(); - _layers[k] = _layers[k].filter(m => m._ownPlace); - }); + Object.keys(OSM_LAYER_MAP).forEach(_clearOsmLayer); _setOsmStatus(''); return; } // Zoom 10–13: normale OSM-Layer ausblenden, EARLY_LAYERS behalten/laden if (zoom < 14) { - Object.keys(OSM_LAYER_MAP).filter(k => !EARLY_LAYERS.has(k)).forEach(k => { - _layers[k].filter(m => !m._ownPlace).forEach(m => m._dangerCircle?.remove()); - _clusterGroups[k]?.clearLayers(); - _layers[k] = _layers[k].filter(m => m._ownPlace); - }); + Object.keys(OSM_LAYER_MAP).filter(k => !EARLY_LAYERS.has(k)).forEach(_clearOsmLayer); } _overpassActive = true; - _map.invalidateSize(); - const b = _map.getBounds().pad(0.15); - const bbox = { south: b.getSouth(), west: b.getWest(), north: b.getNorth(), east: b.getEast() }; + _mapResize(); + let bbox; + if (_engineGL) { + const p = _mapPaddedBounds(0.15); + bbox = { south: p.south, west: p.west, north: p.north, east: p.east }; + } else { + const b = _map.getBounds().pad(0.15); + bbox = { south: b.getSouth(), west: b.getWest(), north: b.getNorth(), east: b.getEast() }; + } // Welche Layer bei diesem Zoom geladen werden const activeLayers = zoom >= 14 ? Object.entries(OSM_LAYER_MAP) : Object.entries(OSM_LAYER_MAP).filter(([k]) => EARLY_LAYERS.has(k)); - // OSM-Marker eines Layers ersetzen, eigene Orte behalten + // OSM-Marker eines Layers ersetzen, eigene Orte behalten (engine-neutral) function _replaceOsmMarkers(layerKey, pois) { + if (_engineGL) { _glOsm[layerKey] = pois || []; _glPushLayer(layerKey); return; } const cluster = _getCluster(layerKey); - // Alte OSM-Marker entfernen const oldOsm = _layers[layerKey].filter(m => !m._ownPlace); oldOsm.forEach(m => m._dangerCircle?.remove()); cluster.removeLayers(oldOsm); _layers[layerKey] = _layers[layerKey].filter(m => m._ownPlace); - // Neue Marker erstellen und in Cluster packen const t = TYPEN[layerKey]; const newMarkers = pois.map(poi => _createOsmMarker(poi, layerKey, t)); cluster.addLayers(newMarkers); _layers[layerKey].push(...newMarkers); - // Sicherstellen dass der Cluster auf der Karte ist (kann durch vorherigen Toggle fehlen) if (_visible[layerKey] !== false && _map && !_map.hasLayer(cluster)) { cluster.addTo(_map); } @@ -1186,19 +1329,19 @@ window.Page_map = (() => { const params = new URLSearchParams({ type: osmType, ...bbox }); try { const pois = await fetch(`/api/osm/pois?${params}`).then(r => r.json()); - const osmCount = (_layers[layerKey] || []).filter(m => !m._ownPlace).length; + const osmCount = _osmCountOf(layerKey); if (pois.length !== osmCount) _replaceOsmMarkers(layerKey, pois); _done++; const pct = Math.round(20 + _done / _total * 80); - const total = Object.values(_layers).flat().filter(m => !m._ownPlace).length; + const total = _osmTotalCount(); _setOsmStatus(pct < 100 ? `Scanne…` : `${total} Marker`, pct); return pois.length; } catch { _done++; const pct = Math.round(20 + _done / _total * 80); - const total = Object.values(_layers).flat().filter(m => !m._ownPlace).length; + const total = _osmTotalCount(); _setOsmStatus(pct < 100 ? `Scanne…` : `${total} Marker`, pct); - return (_layers[layerKey] || []).filter(m => !m._ownPlace).length; + return _osmCountOf(layerKey); } }); try { @@ -1207,7 +1350,7 @@ window.Page_map = (() => { _overpassActive = false; } - const totalLoaded = Object.values(_layers).flat().filter(m => !m._ownPlace).length; + const totalLoaded = _osmTotalCount(); const allHidden = Object.keys(OSM_LAYER_MAP).every(k => _visible[k] === false); if (totalLoaded > 0 && allHidden) { _setOsmStatus('Layer deaktiviert — Liste antippen', 100); @@ -1408,9 +1551,16 @@ window.Page_map = (() => { function _confirmPlacement(latlng) { _tempMarker?.remove(); - _tempMarker = L.circleMarker([latlng.lat, latlng.lng], { - radius: 10, color: '#F59E0B', fillColor: '#F59E0B', fillOpacity: 0.6, - }).addTo(_map); + if (_engineGL) { + const dot = document.createElement('div'); + dot.style.cssText = 'width:20px;height:20px;border-radius:50%;background:#F59E0B;opacity:.7;border:2px solid #fff;box-shadow:0 1px 4px rgba(0,0,0,.4)'; + _tempMarker = new maplibregl.Marker({ element: dot, anchor: 'center' }) + .setLngLat([latlng.lng, latlng.lat]).addTo(_map); + } else { + _tempMarker = L.circleMarker([latlng.lat, latlng.lng], { + radius: 10, color: '#F59E0B', fillColor: '#F59E0B', fillOpacity: 0.6, + }).addTo(_map); + } let _selectedTypes = new Set(['giftkoeder']); @@ -1547,9 +1697,16 @@ window.Page_map = (() => { method: 'DELETE', credentials: 'include', }); if (!res.ok) throw new Error(); - _clusterGroups[layerKey]?.removeLayer(marker); - marker._dangerCircle?.remove(); - _layers[layerKey] = _layers[layerKey].filter(m => m !== marker); + if (_engineGL) { + const drop = arr => (arr || []).filter(p => String(p.user_poi_id) !== String(poiId)); + _glOsm[layerKey] = drop(_glOsm[layerKey]); + _glOwn[layerKey] = drop(_glOwn[layerKey]); + _glPushLayer(layerKey); + } else { + _clusterGroups[layerKey]?.removeLayer(marker); + marker._dangerCircle?.remove(); + _layers[layerKey] = _layers[layerKey].filter(m => m !== marker); + } UI.toast.success('Marker gelöscht.'); } catch { UI.toast.error('Fehler beim Löschen.'); } } @@ -1560,16 +1717,18 @@ window.Page_map = (() => { async function _loadAll() { // Falls Overpass-Job steckengeblieben: zurücksetzen _overpassActive = false; - // Cluster-Gruppen leeren (OSM-Marker) - Object.values(_clusterGroups).forEach(cg => cg.clearLayers()); - // Eigene-Orte-Marker direkt von Karte entfernen - Object.values(_layers).flat().filter(m => m._ownPlace).forEach(m => { - m._dangerCircle?.remove(); - m.remove(); - }); - // Giftköder-Kreise - (_layers.poison || []).forEach(m => m._dangerCircle?.remove()); - Object.keys(_layers).forEach(k => { _layers[k] = []; }); + if (_engineGL) { + _glOwn = {}; // eigene-Orte-Daten leeren + Object.keys(TYPEN).forEach(_glPushLayer); // OSM-Scan-Daten bleiben + } else { + Object.values(_clusterGroups).forEach(cg => cg.clearLayers()); + Object.values(_layers).flat().filter(m => m._ownPlace).forEach(m => { + m._dangerCircle?.remove(); + m.remove(); + }); + (_layers.poison || []).forEach(m => m._dangerCircle?.remove()); + Object.keys(_layers).forEach(k => { _layers[k] = []; }); + } const [places, poisonList, breederList] = await Promise.allSettled([ API.places.list(), @@ -1616,6 +1775,19 @@ window.Page_map = (() => { } function _addPlaces(places) { + if (_engineGL) { + const touched = new Set(); + places.forEach(place => { + if (!TYPEN[place.typ]) return; + (_glOwn[place.typ] = _glOwn[place.typ] || []).push({ + lat: place.lat, lon: place.lon, name: place.name, adresse: place.adresse, + _kind: 'place', source: 'place', + }); + touched.add(place.typ); + }); + touched.forEach(_glPushLayer); + return; + } if (!_map || !window.L) return; places.forEach(place => { const t = TYPEN[place.typ]; @@ -1629,6 +1801,16 @@ window.Page_map = (() => { } function _addPoison(items) { + if (_engineGL) { + items.forEach(p => { + (_glOwn.poison = _glOwn.poison || []).push({ + lat: p.lat, lon: p.lon, name: 'Giftk\u00f6der-Alarm', beschreibung: p.beschreibung, + _kind: 'poison_alarm', source: 'poison', + }); + }); + _glPushLayer('poison'); + return; + } if (!_map || !window.L) return; items.forEach(p => { const tooltip = `Giftk\u00f6der-Alarm${p.beschreibung ? ': ' + p.beschreibung : ''}`; @@ -1647,6 +1829,17 @@ window.Page_map = (() => { } function _addBreeders(breeders) { + if (_engineGL) { + breeders.forEach(b => { + if (b.location_lat == null || b.location_lng == null) return; + (_glOwn.zuechter = _glOwn.zuechter || []).push({ + lat: b.location_lat, lon: b.location_lng, _kind: 'breeder', source: 'breeder', + zwingername: b.zwingername, rasse_text: b.rasse_text, stadt: b.stadt, + }); + }); + _glPushLayer('zuechter'); + return; + } if (!_map || !window.L) return; const t = TYPEN.zuechter; const cluster = _getCluster('zuechter'); @@ -1721,6 +1914,18 @@ window.Page_map = (() => { function _applyVisibility(layer) { // poison-Toggle steuert auch giftkoeder-Community-Pins mit const keys = layer === 'poison' ? ['poison', 'giftkoeder'] : [layer]; + if (_engineGL) { + keys.forEach(k => { + const on = _visible[layer]; + _visible[k] = on; + if (window.MapGLMarkers) MapGLMarkers.setVisible(k, on); + if (k === 'poison' && _map && _map.getLayer && _map.getLayer('danger-fill')) { + const vis = on ? 'visible' : 'none'; + ['danger-fill', 'danger-line'].forEach(id => { if (_map.getLayer(id)) _map.setLayoutProperty(id, 'visibility', vis); }); + } + }); + return; + } keys.forEach(k => { const on = _visible[layer]; _visible[k] = on; @@ -1902,6 +2107,7 @@ window.Page_map = (() => { // GPS-Aufzeichnung // ---------------------------------------------------------- function _toggleRecording() { + if (_engineGL && !_recActive) { UI.toast.info('GPS-Aufzeichnung im neuen Karten-Modus in Kürze.'); return; } if (!_recActive) _startRecording(); else _stopRecording(); } @@ -1945,6 +2151,7 @@ window.Page_map = (() => { } async function _startRecording(resume) { + if (_engineGL) { if (!resume) UI.toast.info('GPS-Aufzeichnung im neuen Karten-Modus in Kürze.'); return; } if (!_appState.user) { UI.toast.warning('Bitte zuerst anmelden.'); App.navigate('settings'); @@ -2359,8 +2566,23 @@ window.Page_map = (() => { } function _flyToResult(r) { - if (!_map || !window.L) return; + if (!_map) return; _searchMarker?.remove(); + if (_engineGL) { + _mapFlyTo(r.lat, r.lon, 15, { duration: 1.0 }); + const pin = document.createElement('div'); + pin.style.cssText = 'width:30px;height:30px;border-radius:50% 50% 50% 0;transform:rotate(-45deg);background:#C4843A;border:2px solid #fff;box-shadow:0 2px 8px rgba(0,0,0,.4)'; + _searchMarker = new maplibregl.Marker({ element: pin, anchor: 'bottom' }) + .setLngLat([r.lon, r.lat]).addTo(_map); + const popup = new maplibregl.Popup({ maxWidth: '240px' }).setLngLat([r.lon, r.lat]) + .setHTML(`
${UI.escape(r.name)}
+ ${r.subtitle ? `
${UI.escape(r.subtitle)}
` : ''} + `) + .addTo(_map); + setTimeout(() => { document.getElementById('search-marker-close')?.addEventListener('click', () => { _clearSearch(); popup.remove(); }); }, 50); + return; + } + if (!window.L) return; _map.flyTo([r.lat, r.lon], 15, { duration: 1.0 }); _searchMarker = L.marker([r.lat, r.lon], { icon: L.divIcon({ diff --git a/backend/static/landing.html b/backend/static/landing.html index 1f4f444..8e2ceaa 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 eaebe8e..4736dbc 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 = '1179'; +const VER = '1180'; const CACHE_VERSION = `by-v${VER}`; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten