From a31d08a2dc2e59c97f03b9affccb36bf04c33ba2 Mon Sep 17 00:00:00 2001 From: rene Date: Sat, 6 Jun 2026 20:30:22 +0200 Subject: [PATCH 1/2] Karte: Blickrichtungs-Kegel (Kompass) + ruhigeres Folgen (Rene: 'weiss nicht wo es ist/hinschaut') MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Standort-Punkt zeigt jetzt einen Kompass-KEGEL (wie Google Maps): CSS-Kegel im loc-icon (GL + Leaflet), deviceorientation mit webkitCompassHeading (iOS) bzw. 360-alpha (Android), Glaettung ueber kuerzesten Winkelweg, rAF-throttled. iOS-Permission via User-Geste (Follow-/Standort-Button startet den Kompass). - Follow pant nur noch bei brauchbarem Fix (accuracy < 75m) — ungenaue Positionen liessen die Karte zappeln; GPS-Fixes frischer (maximumAge 2s statt 5s). Marker aktualisiert weiterhin jeden Fix. Bump v1249 --- VERSION | 2 +- backend/static/css/components.css | 19 +++++++++++ backend/static/index.html | 24 +++++++------- backend/static/js/app.js | 2 +- backend/static/js/pages/map.js | 54 +++++++++++++++++++++++++++---- backend/static/landing.html | 2 +- backend/static/sw.js | 2 +- 7 files changed, 83 insertions(+), 22 deletions(-) diff --git a/VERSION b/VERSION index f0b4b2f..6d60af2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1248 \ No newline at end of file +1249 \ No newline at end of file diff --git a/backend/static/css/components.css b/backend/static/css/components.css index ce50752..8d537f5 100644 --- a/backend/static/css/components.css +++ b/backend/static/css/components.css @@ -3625,6 +3625,25 @@ html.modal-open { top: 0; left: 0; animation: loc-pulse 2s ease-out infinite; } +/* Blickrichtungs-Kegel (Kompass): JS rotiert .loc-heading um die Marker-Mitte; + sichtbar erst mit Kompass-Daten (René 2026-06-06: „weiß nicht, wo es hinschaut"). */ +.loc-heading { + position: absolute; + left: 12px; top: 12px; /* Mitte des 24×24-Icons = Rotationsachse */ + width: 0; height: 0; + display: none; + pointer-events: none; +} +.loc-heading::before { + content: ''; + position: absolute; + left: -11px; + top: -28px; /* Kegel ragt von der Mitte nach oben */ + width: 22px; + height: 24px; + background: linear-gradient(to top, rgba(59,130,246,0.55), rgba(59,130,246,0.05)); + clip-path: polygon(50% 100%, 0 0, 100% 0); /* Spitze unten (Mitte), öffnet nach oben */ +} @keyframes loc-pulse { 0% { transform: scale(0.8); opacity: 1; } 100% { transform: scale(2.2); opacity: 0; } diff --git a/backend/static/index.html b/backend/static/index.html index 83cd111..86bc87e 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 a62d078..d897ddd 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 = '1248'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '1249'; // ← 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 3932710..130dd4f 100644 --- a/backend/static/js/pages/map.js +++ b/backend/static/js/pages/map.js @@ -369,6 +369,7 @@ window.Page_map = (() => { // 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; + _startCompass(); // User-Geste → iOS-Kompass-Permission _updateFollowBtn(); UI.toast.info('Karte folgt deinem Standort — zum Beenden Karte verschieben.'); } else { @@ -836,6 +837,42 @@ window.Page_map = (() => { const b = document.getElementById('map-follow-btn'); if (b) b.style.color = _followGps ? 'var(--c-primary)' : 'var(--c-text-secondary, #9ca3af)'; } + + // Kompass → Blickrichtungs-Kegel am Standort-Punkt (René 2026-06-06: „fühlt sich an, + // als ob es nicht weiß, wo es hinschaut"). iOS verlangt requestPermission in einer + // User-Geste → Start über Follow-/Standort-Button. Karte bleibt genordet (dragRotate + // aus), daher gilt: Bildschirm-Rotation des Kegels = Kompass-Heading direkt. + let _compassOn = false, _compassRaf = null; + async function _startCompass() { + if (_compassOn || !window.DeviceOrientationEvent) return; + _compassOn = true; + if (typeof DeviceOrientationEvent.requestPermission === 'function') { + try { await DeviceOrientationEvent.requestPermission(); } catch (e) {} + } + let smoothed = null; + window.addEventListener('deviceorientation', e => { + let raw = null; + if (e.webkitCompassHeading != null) raw = e.webkitCompassHeading; // iOS + else if (e.alpha != null && e.absolute !== false) raw = 360 - e.alpha; // Android + if (raw == null) return; + // Glätten über den kürzesten Winkelweg (sonst springt der Kegel bei 359↔0) + if (smoothed == null) smoothed = raw; + else { + const d = ((raw - smoothed + 540) % 360) - 180; + smoothed = (smoothed + d * 0.25 + 360) % 360; + } + if (_compassRaf) return; + _compassRaf = requestAnimationFrame(() => { + _compassRaf = null; + const cone = document.querySelector('#central-map .loc-heading'); + if (cone) { + cone.style.display = 'block'; + cone.style.transform = `rotate(${smoothed}deg)`; + } + }); + }, true); + } + function _ensureFollowBtn() { const host = document.getElementById('central-map'); if (!host || document.getElementById('map-follow-btn')) { _updateFollowBtn(); return; } @@ -853,6 +890,7 @@ window.Page_map = (() => { b.addEventListener('click', () => { if (!_userPos) { UI.toast.error('Standort noch nicht verfügbar.'); return; } _followGps = true; + _startCompass(); // User-Geste → iOS-Kompass-Permission _mapSetView(_userPos.lat, _userPos.lon, Math.max(14, Math.round(_mapGetZoom()))); _updateFollowBtn(); }); @@ -1150,14 +1188,18 @@ window.Page_map = (() => { } else { const elx = document.createElement('div'); elx.className = 'loc-icon'; - elx.innerHTML = '
'; + elx.innerHTML = '
'; _locationMarker = new maplibregl.Marker({ element: elx, anchor: 'center' }) .setLngLat([lon, lat]).addTo(_map); } - if (_followGps && !_recActive) _map.easeTo({ center: [lon, lat], duration: 600 }); + // Pan nur bei brauchbarem Fix — ungenaue Positionen (>75 m) lassen die + // Karte sonst zappeln („weiß nicht, wo es ist", René 2026-06-06). + if (_followGps && !_recActive && (pos.coords.accuracy ?? 999) < 75) { + _map.easeTo({ center: [lon, lat], duration: 600 }); + } }, () => {}, - { enableHighAccuracy: true, maximumAge: 5000, timeout: 15000 } + { enableHighAccuracy: true, maximumAge: 2000, timeout: 15000 } ); return; } @@ -1165,7 +1207,7 @@ window.Page_map = (() => { if (!window.L) return; const icon = L.divIcon({ className: 'loc-icon', - html: '
', + html: '
', iconSize: [24, 24], iconAnchor: [12, 12], }); @@ -1187,10 +1229,10 @@ window.Page_map = (() => { icon, zIndexOffset: 500, interactive: false, }).addTo(_map); } - if (_followGps && !_recActive) _map.panTo([lat, lon]); + if (_followGps && !_recActive && (pos.coords.accuracy ?? 999) < 75) _map.panTo([lat, lon]); }, () => {}, - { enableHighAccuracy: true, maximumAge: 5000, timeout: 15000 } + { enableHighAccuracy: true, maximumAge: 2000, timeout: 15000 } ); } diff --git a/backend/static/landing.html b/backend/static/landing.html index 05e1a20..65810d0 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 8ba790e..0f05461 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 = '1248'; +const VER = '1249'; 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 6d9a04fd100fbb231496bffaa53f6879cfb19402 Mon Sep 17 00:00:00 2001 From: rene Date: Sun, 7 Jun 2026 08:22:02 +0200 Subject: [PATCH 2/2] =?UTF-8?q?Navi:=20Fenster-Index=20statt=20globaler=20?= =?UTF-8?q?Suche=20=E2=80=94=20Loop-Routen=20brachen=20Bellen/Fortschritt/?= =?UTF-8?q?Gruen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Praxistest Rene (Gassirunde Siegenhofen, Loop Start=Ende): _closestIdx suchte GLOBAL -> am Start war der END-Index genauso nah -> Fortschritt sprang ans Track-Ende: nie Abbiege-Bellen (keine Turns mehr 'vor' einem), done-Linie malte sofort alles gruen, 99%. - _closestIdx: Fenster um den aktuellen Index (-15/+80), global nur beim ersten Fix (mit Start-Praeferenz bei Quasi-Gleichstand <25m) oder wenn verloren (>300m). Simulation auf der echten Route: 13/13 Turns feuern bei 41-49m Vorlauf. - Gelaufener Weg als BREADCRUMB: gruen = auf der Route, rot = daneben (Segment-Wechsel nahtlos); done-Linie (Track-Slice-Malen) entfernt — nie gelaufene Abschnitte bleiben jetzt orange - Off-Route-Schwelle 50->35m (Klaeffen kam ~5m zu spaet), Abbiege-Ansage 45->50m Bump v1250 --- VERSION | 2 +- backend/static/index.html | 24 ++++++------- backend/static/js/app.js | 2 +- backend/static/js/pages/routes.js | 56 +++++++++++++++++++++++++------ backend/static/landing.html | 2 +- backend/static/sw.js | 2 +- 6 files changed, 61 insertions(+), 27 deletions(-) diff --git a/VERSION b/VERSION index 6d60af2..746faa2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1249 \ No newline at end of file +1250 \ No newline at end of file diff --git a/backend/static/index.html b/backend/static/index.html index 86bc87e..926a1aa 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 d897ddd..4cb8380 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 = '1249'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '1250'; // ← 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/routes.js b/backend/static/js/pages/routes.js index 747e61b..665d368 100644 --- a/backend/static/js/pages/routes.js +++ b/backend/static/js/pages/routes.js @@ -1917,8 +1917,25 @@ window.Page_routes = (() => { _navMap.invalidateSize(); // Route-Polylines: erledigt (grün) + ausstehend (orange) - const doneLine = UI.map.polyline([], { color: '#22c55e', weight: 5, opacity: 0.85 }).addTo(_navMap); + // Geplante Route (orange). Der GELAUFENE Weg wird als Breadcrumb gezeichnet: + // grün = auf der Route, rot = daneben (René 2026-06-07 — vorher malte eine + // done-Linie einfach den Track grün, auch nie gelaufene Abschnitte). const remainLine = UI.map.polyline(track.map(p => [p.lat, p.lon]), { color: '#f97316', weight: 5, opacity: 0.9 }).addTo(_navMap); + let _walkSeg = null, _walkSegOff = null, _walkLast = null; + const _walkAdd = (lat, lon, off) => { + if (!_navMap) return; + if (_walkLast && _haversineKm(_walkLast[0], _walkLast[1], lat, lon) * 1000 < 2) return; // GPS-Rauschen + if (!_walkSeg || _walkSegOff !== off) { + _walkSegOff = off; + const seed = _walkLast ? [_walkLast, [lat, lon]] : [[lat, lon]]; // nahtloser Übergang + _walkSeg = UI.map.polyline(seed, { + color: off ? '#dc2626' : '#22c55e', weight: 5, opacity: 0.9, + }).addTo(_navMap); + } else { + _walkSeg.addLatLng([lat, lon]); + } + _walkLast = [lat, lon]; + }; _navMap.fitBounds(remainLine.getBounds(), { padding: [20, 20] }); _addRouteArrows(_navMap, track, '#3b82f6'); @@ -1974,13 +1991,30 @@ window.Page_routes = (() => { // Hilfsfunktionen const _navHaversine = (a, b) => _haversineKm(a.lat, a.lon, b.lat, b.lon); + // Fortschritts-Index NUR im Fenster um den aktuellen Index suchen — die globale + // Suche sprang bei RUNDEN (Start ≈ Ende) sofort ans Track-ENDE: nie Abbiege-Bellen, + // alles grün, 99 % ab Start (Praxistest René 2026-06-07, Gassirunde Siegenhofen). + // Global nur beim ersten Fix oder wenn verloren (Fenster-Treffer > 300 m entfernt). + let _navIdxInit = false; const _closestIdx = (lat, lon) => { - let best = 0, bestD = Infinity; - track.forEach((p, i) => { - const d = _haversineKm(lat, lon, p.lat, p.lon); - if (d < bestD) { bestD = d; best = i; } - }); - return best; + const search = (from, to) => { + let best = from, bestD = Infinity; + for (let i = from; i <= to; i++) { + const d = _haversineKm(lat, lon, track[i].lat, track[i].lon); + if (d < bestD) { bestD = d; best = i; } + } + return { best, bestD }; + }; + if (!_navIdxInit) { + _navIdxInit = true; + // Erster Fix: global, aber bei Quasi-Gleichstand (< 25 m) den START bevorzugen (Loop!) + const g = search(0, track.length - 1); + const s = search(0, Math.min(track.length - 1, 30)); + return (s.bestD - g.bestD) * 1000 < 25 ? s.best : g.best; + } + const w = search(Math.max(0, _navCurrentIdx - 15), Math.min(track.length - 1, _navCurrentIdx + 80)); + if (w.bestD <= 0.3) return w.best; + return search(0, track.length - 1).best; // verloren → neu orientieren }; const _remainingKm = (fromIdx) => { @@ -2055,7 +2089,7 @@ window.Page_routes = (() => { const next = _navTurns.find(t => t.idx > idx && t.idx > _navSndAnnouncedIdx); if (next) { const dM = _haversineKm(userLat, userLon, track[next.idx].lat, track[next.idx].lon) * 1000; - if (dM <= 45) { + if (dM <= 50) { _navSndAnnouncedIdx = next.idx; if (next.right) NavSound.rechts(); else NavSound.links(); } @@ -2063,7 +2097,7 @@ window.Page_routes = (() => { } const offWarn = document.getElementById('rk-nav-offwarn'); - if (distToRoute * 1000 > 50) { + if (distToRoute * 1000 > 35) { // 50→35 m: Kläffen kam ~5 m zu spät (René 2026-06-07) offWarn.style.display = ''; if (navigator.vibrate) navigator.vibrate([200, 100, 200]); // Falscher Weg = Kläffen (beim Abkommen + Erinnerung alle 30 s) @@ -2077,9 +2111,9 @@ window.Page_routes = (() => { offWarn.style.display = 'none'; _navSndOffRoute = false; } - // Polylines aktualisieren - doneLine.setLatLngs(track.slice(0, idx + 1).map(p => [p.lat, p.lon])); + // Verbleibende Route aktualisieren; der gelaufene Weg kommt vom Breadcrumb (s.o.) remainLine.setLatLngs(track.slice(idx).map(p => [p.lat, p.lon])); + if (userLat != null) _walkAdd(userLat, userLon, distToRoute * 1000 > 35); }; // GPS-Watch diff --git a/backend/static/landing.html b/backend/static/landing.html index 65810d0..2104b5f 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 0f05461..ff26458 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 = '1249'; +const VER = '1250'; const CACHE_VERSION = `by-v${VER}`; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten