From abd7447d29dfb72435f75fad63bfcb4d50d71eec Mon Sep 17 00:00:00 2001 From: rene Date: Sat, 6 Jun 2026 17:20:38 +0200 Subject: [PATCH 1/2] Karte: Follow-Mode + Live-Strecke bei Routen-Aufzeichnung (Wunsch Rene) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MapGLMini-Polyline hatte KEIN addLatLng (nur setLatLngs) -> TypeError im Rec-Overlay: Strecke unsichtbar, Marker fror ein, kein Folgen. Facade ergaenzt (Leaflet-kompatibel). - Rec-Overlay (routes.js): Follow-Mode default AN — Karte wandert mit dem Standort, Drag pausiert, Crosshair-Button (unten rechts) reaktiviert; erster Fix setzt Zoom 16, danach bleibt der Zoom erhalten - Zentrale Karte (map.js): Standort-Button aktiviert Follow (+Toast), dragstart beendet es (beide Engines); GPS-Tracking folgt sanft (easeTo); Aufzeichnung startet im Follow-Mode, _updateRecMap pant nur noch im Follow Bump v1238 --- VERSION | 2 +- backend/static/index.html | 24 ++++++++++++------------ backend/static/js/app.js | 2 +- backend/static/js/map-gl-mini.js | 7 +++++++ backend/static/js/pages/map.js | 14 ++++++++++++-- backend/static/js/pages/routes.js | 26 ++++++++++++++++++++++++++ backend/static/landing.html | 2 +- backend/static/sw.js | 2 +- 8 files changed, 61 insertions(+), 18 deletions(-) diff --git a/VERSION b/VERSION index 96a2d52..c63ea9e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1237 \ No newline at end of file +1238 \ No newline at end of file diff --git a/backend/static/index.html b/backend/static/index.html index fb7fa45..2731405 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -86,14 +86,14 @@ Ban Yaro - + - - - - - + + + + + @@ -612,11 +612,11 @@ - - - - - + + + + + @@ -626,7 +626,7 @@ - + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 37a62d1..6ad2b32 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 = '1237'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '1238'; // ← 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 5afd176..695d175 100644 --- a/backend/static/js/map-gl-mini.js +++ b/backend/static/js/map-gl-mini.js @@ -190,6 +190,13 @@ if (this._map && this._map.getSource(this._id)) this._map.getSource(this._id).setData(this._geo()); return this; }, + // Leaflet-kompatibel: Punkt anhängen (Live-Track bei Routen-Aufzeichnung). + // FEHLTE bis 2026-06-08 → TypeError im Rec-Overlay = Strecke blieb unsichtbar. + addLatLng: function (ll) { + this._latlngs.push(ll); + 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; }) }; }, diff --git a/backend/static/js/pages/map.js b/backend/static/js/pages/map.js index 6fd35bb..081d80e 100644 --- a/backend/static/js/pages/map.js +++ b/backend/static/js/pages/map.js @@ -17,6 +17,7 @@ window.Page_map = (() => { let _tileLayer = null; let _usingVector = false; // true wenn Vektor-Basemap (PMTiles) statt OSM-Raster let _engineGL = false; // true wenn MapLibre GL statt Leaflet (zentrale Karte) + let _followGps = false; // Karte folgt dem Standort (Standort-Button an, Drag aus) let _maplibreLoaded = false; let _glLayersReady = false; // GL: POI-Sources/Layer angelegt? let _pmtilesProtoReg = false; // pmtiles-Protokoll bei MapLibre registriert? @@ -364,6 +365,10 @@ window.Page_map = (() => { _sdEl?.classList.remove('open'); if (_userPos) { _mapSetView(_userPos.lat, _userPos.lon, 16); + // Follow-Mode (René 2026-06-08): Karte wandert ab jetzt mit dem Standort; + // manuelles Verschieben beendet das Folgen (dragstart-Listener im Map-Init). + _followGps = true; + UI.toast.info('Karte folgt deinem Standort — zum Beenden Karte verschieben.'); } else { UI.toast.error('Standort noch nicht verfügbar.'); } @@ -744,6 +749,7 @@ window.Page_map = (() => { window.addEventListener('resize', () => _map.invalidateSize()); _map.on('moveend zoomend', () => { _autoRetryCount = 0; _updateZoomDisplay(); _scheduleOsmLoad(); }); + _map.on('dragstart', () => { _followGps = false; }); // manuelles Verschieben beendet Follow setTimeout(() => { _updateZoomDisplay(); _scheduleOsmLoad(); }, 800); // Fadenkreuz-Animation beim Kartenverschieben @@ -921,6 +927,7 @@ window.Page_map = (() => { _map.on('movestart', () => { document.getElementById('map-crosshair')?.classList.add('dragging'); }); + _map.on('dragstart', () => { _followGps = false; }); // manuelles Verschieben beendet Follow window.addEventListener('resize', _mapResize); setTimeout(_mapResize, 100); @@ -1070,6 +1077,7 @@ window.Page_map = (() => { _locationMarker = new maplibregl.Marker({ element: elx, anchor: 'center' }) .setLngLat([lon, lat]).addTo(_map); } + if (_followGps && !_recActive) _map.easeTo({ center: [lon, lat], duration: 600 }); }, () => {}, { enableHighAccuracy: true, maximumAge: 5000, timeout: 15000 } @@ -1102,6 +1110,7 @@ window.Page_map = (() => { icon, zIndexOffset: 500, interactive: false, }).addTo(_map); } + if (_followGps && !_recActive) _map.panTo([lat, lon]); }, () => {}, { enableHighAccuracy: true, maximumAge: 5000, timeout: 15000 } @@ -2564,6 +2573,7 @@ window.Page_map = (() => { document.addEventListener('visibilitychange', _onVisibilityChange); _recTimerInt = setInterval(_updateRecStatus, 1000); + _followGps = true; // Aufzeichnung startet im Follow-Mode (Drag pausiert, Standort-Button reaktiviert) _recWatchId = navigator.geolocation.watchPosition( pos => { @@ -2690,7 +2700,7 @@ window.Page_map = (() => { if (_engineGL) { _recTrackGL(); _updateRecMarker(lat, lon); - _map.panTo([lon, lat]); // MapLibre: [lng,lat] + if (_followGps) _map.panTo([lon, lat]); // MapLibre: [lng,lat] — Drag pausiert Follow return; } if (!window.L) return; @@ -2701,7 +2711,7 @@ window.Page_map = (() => { _recPolyline.addLatLng(ll); } _updateRecMarker(lat, lon); - _map.panTo(ll); + if (_followGps) _map.panTo(ll); } function _updateRecStatus() { diff --git a/backend/static/js/pages/routes.js b/backend/static/js/pages/routes.js index 220496d..f7dfeca 100644 --- a/backend/static/js/pages/routes.js +++ b/backend/static/js/pages/routes.js @@ -70,6 +70,7 @@ window.Page_routes = (() => { // Recording-Overlay state let _recOvl = null, _recMap = null; + let _recFollow = true; // Karte folgt dem Standort bei Aufzeichnung (Drag pausiert) let _recActive = false; let _recTrack = [], _recDistKm = 0, _recStartTime = null; let _recTimerInt = null, _recWatchId = null; @@ -762,6 +763,29 @@ window.Page_routes = (() => { _recLocMarker = UI.map.circleMarker([pos.lat, pos.lon], { radius: 8, color: '#fff', weight: 2.5, fillColor: '#3b82f6', fillOpacity: 1 }).addTo(_recMap); + // Follow-Mode (René 2026-06-08): Karte wandert mit dem Standort. Manuelles + // Verschieben pausiert das Folgen; Crosshair-Button schaltet es wieder ein. + _recFollow = true; + const fwrap = ovl.querySelector('#rk-rec-map-wrap'); + if (!fwrap.style.position) fwrap.style.position = 'relative'; + const fb = document.createElement('button'); + fb.id = 'rk-rec-follow'; + fb.type = 'button'; + fb.title = 'Karte folgt dem Standort'; + fb.style.cssText = 'position:absolute;right:10px;bottom:10px;z-index:500;width:42px;height:42px;' + + 'border-radius:50%;border:none;background:var(--c-surface,#fff);' + + 'box-shadow:0 2px 8px rgba(0,0,0,.3);display:flex;align-items:center;justify-content:center;cursor:pointer'; + fb.innerHTML = UI.icon('crosshair'); + const updFb = () => { fb.style.color = _recFollow ? 'var(--c-primary)' : 'var(--c-text-secondary, #9ca3af)'; }; + updFb(); + fb.addEventListener('click', () => { + _recFollow = true; + const last = _recTrack[_recTrack.length - 1] || pos; + _recMap?.setView([last.lat, last.lon]); + updFb(); + }); + fwrap.appendChild(fb); + try { _recMap.on('dragstart', () => { _recFollow = false; updFb(); }); } catch (e) {} } catch { const mapWrap = ovl.querySelector('#rk-rec-map-wrap'); if (mapWrap) mapWrap.innerHTML = @@ -892,7 +916,9 @@ window.Page_routes = (() => { _persistRec(); _recPolyline?.addLatLng([lat, lon]); _recLocMarker?.setLatLng([lat, lon]); + // Follow-Mode: Karte wandert mit (erster Fix setzt den Zoom, danach bleibt er) if (_recTrack.length === 1) _recMap?.setView([lat, lon], 16); + else if (_recFollow) _recMap?.setView([lat, lon]); _updateRecStats(); }, () => {}, { enableHighAccuracy: true, maximumAge: 2000 }); diff --git a/backend/static/landing.html b/backend/static/landing.html index 956917e..58e10c5 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 c3fa9b5..e3f8052 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 = '1237'; +const VER = '1238'; const CACHE_VERSION = `by-v${VER}`; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten From 6a06c9be7ed0db385575fe4ace433ff8807da258 Mon Sep 17 00:00:00 2001 From: rene Date: Sat, 6 Jun 2026 17:26:20 +0200 Subject: [PATCH 2/2] POI-Bewertung: Live-Praesenz zaehlt als GPS-Beleg (Rene stand am Ort, wurde abgewiesen) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vorher zaehlte NUR eine aufgezeichnete Tour der letzten 48h (<=50m am POI, >=2 Punkte) — die aktuelle Geraeteposition floss gar nicht ein. - DogFriendlyIn: optional user_lat/user_lon (Frontend schickt _userPos mit) - Beleg-Weg a): Geraetestandort <= 75m am POI (50m Radius + GPS-Toleranz) -> route_id=None markiert Live-Beleg; Weg b) Tour-Beleg unveraendert - Fehlermeldung nennt jetzt beide Wege (hin gehen ODER Standort aktivieren) - pytest 39 passed Bump v1239 --- VERSION | 2 +- backend/routes/osm_contrib.py | 67 +++++++++++++++++++++------------- backend/static/index.html | 24 ++++++------ backend/static/js/app.js | 2 +- backend/static/js/pages/map.js | 4 ++ backend/static/landing.html | 2 +- backend/static/sw.js | 2 +- 7 files changed, 61 insertions(+), 42 deletions(-) diff --git a/VERSION b/VERSION index c63ea9e..caa2f5a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1238 \ No newline at end of file +1239 \ No newline at end of file diff --git a/backend/routes/osm_contrib.py b/backend/routes/osm_contrib.py index 672be37..2c1ec6e 100644 --- a/backend/routes/osm_contrib.py +++ b/backend/routes/osm_contrib.py @@ -35,6 +35,8 @@ GPS_RADIUS_M = 50 # max. Abstand POI ↔ nächster Track-Punkt DWELL_MIN_POINTS = 2 # mind. so viele Track-Punkte im Radius (Verweil-Proxy) ROUTE_RECENCY_H = 48 # Tour darf max. so alt sein POI_NEAR_M = 80 # eingereichte Position muss so nah am POI sein +LIVE_NEAR_M = 75 # Live-Präsenz: Gerätestandort ≤ so nah am POI (René 2026-06-08: + # ~50 m Radius + GPS-Toleranz — er stand am Ort, ohne Aufzeichnung) DAILY_CAP = 20 # max. Beiträge pro Tag/User # --- Gamification-Schwellen --- @@ -49,6 +51,9 @@ class DogFriendlyIn(BaseModel): lat: float lon: float welcome: bool = True # True → dog=yes, False → dog=no (Pächterwechsel) + # Aktuelle Gerätekoordinate (Live-Präsenz-Beleg) — optional, vom Frontend mitgeschickt + user_lat: Optional[float] = None + user_lon: Optional[float] = None def _verified_count(conn, uid: int) -> int: @@ -154,33 +159,42 @@ async def mark_dog_friendly(body: DogFriendlyIn, user=Depends(get_current_user)) if today_n >= DAILY_CAP: raise HTTPException(429, "Tageslimit erreicht — morgen geht's weiter.") - # 3) GPS-Beleg: kürzliche Tour, die am POI vorbeiführt (+ Verweil-Proxy) - routes = conn.execute( - "SELECT id, gps_track FROM routes " - "WHERE user_id=? AND created_at > datetime('now', ?) ORDER BY created_at DESC", - (uid, f'-{ROUTE_RECENCY_H} hours') - ).fetchall() - best = None # (route_id, min_dist, points_near) - for r in routes: - try: - track = json.loads(r['gps_track']) - except Exception: - continue - near, mind = 0, float('inf') - for p in track: - d = haversine_m(body.lat, body.lon, p['lat'], p['lon']) - if d < mind: - mind = d - if d <= GPS_RADIUS_M: - near += 1 - if mind <= GPS_RADIUS_M and near >= DWELL_MIN_POINTS: - if best is None or mind < best[1]: - best = (r['id'], mind, near) + # 3) GPS-Beleg — ZWEI Wege (René 2026-06-08: stand physisch am Ort und wurde + # abgewiesen, weil nur aufgezeichnete Touren zählten): + # a) LIVE-PRÄSENZ: aktuelle Gerätekoordinate ≤ LIVE_NEAR_M am POI + # b) TOUR-BELEG: kürzliche Tour, die am POI vorbeiführt (+ Verweil-Proxy) + best = None # (route_id|None, dist_m, points_near) + if body.user_lat is not None and body.user_lon is not None: + live_d = haversine_m(body.user_lat, body.user_lon, body.lat, body.lon) + if live_d <= LIVE_NEAR_M: + best = (None, live_d, 0) # route_id=None markiert Live-Beleg + if not best: + routes = conn.execute( + "SELECT id, gps_track FROM routes " + "WHERE user_id=? AND created_at > datetime('now', ?) ORDER BY created_at DESC", + (uid, f'-{ROUTE_RECENCY_H} hours') + ).fetchall() + for r in routes: + try: + track = json.loads(r['gps_track']) + except Exception: + continue + near, mind = 0, float('inf') + for p in track: + d = haversine_m(body.lat, body.lon, p['lat'], p['lon']) + if d < mind: + mind = d + if d <= GPS_RADIUS_M: + near += 1 + if mind <= GPS_RADIUS_M and near >= DWELL_MIN_POINTS: + if best is None or mind < best[1]: + best = (r['id'], mind, near) if not best: raise HTTPException( 422, - "Kein GPS-Beleg: In deinen letzten Touren ist kein Besuch an diesem Ort. " - "Geh mit deinem Hund dorthin, dann kannst du ihn eintragen." + "Kein GPS-Beleg: Du bist gerade nicht an diesem Ort und in deinen letzten " + "Touren ist kein Besuch dort. Geh mit deinem Hund hin (Standort aktiviert), " + "dann kannst du ihn eintragen." ) # 4) Positions-Sanity gegen die bekannte POI-Koordinate @@ -223,8 +237,9 @@ async def mark_dog_friendly(body: DogFriendlyIn, user=Depends(get_current_user)) except Exception as e: logger.warning("OSM-Upload später erneut (contrib %s): %s", contrib_id, e) - logger.info("dog=%s erfasst: user %s, osm %s, Tour %s (%.0fm, %d Pkt), submitted=%s", - value, uid, body.osm_id, best[0], best[1], best[2], submitted) + logger.info("dog=%s erfasst: user %s, osm %s, Beleg %s (%.0fm, %d Pkt), submitted=%s", + value, uid, body.osm_id, + f"Tour {best[0]}" if best[0] else "Live-Präsenz", best[1], best[2], submitted) return { "status": "erfasst", "value": value, "verified": True, "submitted": submitted, "verified_count": total, "badge": total >= BADGE_AT, diff --git a/backend/static/index.html b/backend/static/index.html index 2731405..00962f9 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -86,14 +86,14 @@ Ban Yaro - + - - - - - + + + + + @@ -612,11 +612,11 @@ - - - - - + + + + + @@ -626,7 +626,7 @@ - + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 6ad2b32..311c0b0 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 = '1238'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '1239'; // ← 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/pages/map.js b/backend/static/js/pages/map.js index 081d80e..cfefb62 100644 --- a/backend/static/js/pages/map.js +++ b/backend/static/js/pages/map.js @@ -1044,6 +1044,8 @@ window.Page_map = (() => { 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, + // Live-Präsenz-Beleg: wer am Ort steht, darf auch ohne aufgezeichnete Tour bewerten + user_lat: _userPos?.lat ?? null, user_lon: _userPos?.lon ?? null, }); UI.toast.success((welcome ? 'Hund willkommen' : 'Hund nicht willkommen') + (r.submitted ? ' — eingetragen 🐾' : ' — wird übertragen 🐾')); close(); @@ -1667,6 +1669,8 @@ window.Page_map = (() => { const r = await API.post('/osm-contrib/dog-friendly', { osm_id: poi.id, osm_type: 'node', poi_type: layerKey, lat: poi.lat, lon: poi.lon, welcome, + // Live-Präsenz-Beleg: wer am Ort steht, darf auch ohne aufgezeichnete Tour bewerten + user_lat: _userPos?.lat ?? null, user_lon: _userPos?.lon ?? null, }); UI.toast.success((welcome ? 'Hund willkommen' : 'Hund nicht willkommen') + (r.submitted ? ' — eingetragen 🐾' : ' — wird übertragen 🐾')); diff --git a/backend/static/landing.html b/backend/static/landing.html index 58e10c5..fe4239d 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 e3f8052..9a830b9 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 = '1238'; +const VER = '1239'; const CACHE_VERSION = `by-v${VER}`; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten