From ca23b3ec46ad514fbcb25432095305b73b4978ee Mon Sep 17 00:00:00 2001 From: rene Date: Sat, 6 Jun 2026 18:57:20 +0200 Subject: [PATCH] Navi-Sounds: 2x Wuff = links, 1x Wuff = rechts, Klaeffen = falscher Weg (Idee Rene) - NavSound (routes.js): WebAudio-Wuff-Synthese (Saegezahn-Sweep + Tiefpass, kein Asset, laeuft offline); echte Aufnahmen unter /sounds/wuff.mp3 + klaeffen.mp3 (z.B. von Yaro) werden automatisch bevorzugt - Abbiege-Erkennung: Turns einmalig aus dem Track (Peilung ueber 15-m- Stuetzpunkte gegen GPS-Zickzack, >=40 Grad), Ansage bei <=45 m davor, einmal pro Abbiegepunkt - Falscher Weg (>50 m, bestehende Warnung): Klaeffen beim Abkommen + Erinnerung alle 30 s; zurueck auf Route reset - Toggle im Navi-Header (Lautsprecher-Icon, by_nav_sound, Default AN), iOS-Audio-Unlock beim Navi-Start + Toggle (Probe-Wuff als Bestaetigung) Bump v1241 --- VERSION | 2 +- backend/static/index.html | 24 +++--- backend/static/js/app.js | 2 +- backend/static/js/pages/routes.js | 137 ++++++++++++++++++++++++++++++ backend/static/landing.html | 2 +- backend/static/sw.js | 2 +- 6 files changed, 153 insertions(+), 16 deletions(-) diff --git a/VERSION b/VERSION index 133c673..ca25bea 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1240 \ No newline at end of file +1241 \ No newline at end of file diff --git a/backend/static/index.html b/backend/static/index.html index 27c49a5..51aaa62 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 52be8e6..4203c8e 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 = '1240'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '1241'; // ← 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 f7dfeca..389424e 100644 --- a/backend/static/js/pages/routes.js +++ b/backend/static/js/pages/routes.js @@ -68,6 +68,81 @@ window.Page_routes = (() => { let _navCompassHeading = null; let _navHeadingSmoothed = null; + // ---------------------------------------------------------- + // NAVI-SOUNDS (Idee René 2026-06-06): links = 2× Wuff, rechts = 1× Wuff, + // falscher Weg = Kläffen. WebAudio-Synthese (kein Asset, läuft offline) — + // liegen echte Aufnahmen unter /sounds/wuff.mp3 + /sounds/klaeffen.mp3 + // (z.B. von Yaro 🐕), werden DIE bevorzugt. iOS: Audio braucht eine User-Geste + // → unlock() beim Navi-Start/Toggle. + // ---------------------------------------------------------- + const NavSound = (() => { + let ctx = null; + let files = null; // { wuff, klaeffen } HTMLAudio | leer = Synthese + const enabled = () => { try { return localStorage.getItem('by_nav_sound') !== '0'; } catch (e) { return true; } }; + + function _ctx() { + if (!ctx) ctx = new (window.AudioContext || window.webkitAudioContext)(); + if (ctx.state === 'suspended') ctx.resume().catch(() => {}); + return ctx; + } + // Ein synthetischer „Wuff": Sägezahn-Sweep durch Tiefpass, kurzer Attack, schneller Decay. + function _wuff(at, pitch = 1) { + const c = _ctx(), t = c.currentTime + at; + const o = c.createOscillator(), g = c.createGain(), f = c.createBiquadFilter(); + o.type = 'sawtooth'; + o.frequency.setValueAtTime(240 * pitch, t); + o.frequency.exponentialRampToValueAtTime(75 * pitch, t + 0.16); + f.type = 'lowpass'; f.frequency.value = 900 * pitch; f.Q.value = 3; + g.gain.setValueAtTime(0.0001, t); + g.gain.exponentialRampToValueAtTime(0.9, t + 0.02); + g.gain.exponentialRampToValueAtTime(0.0001, t + 0.22); + o.connect(f); f.connect(g); g.connect(c.destination); + o.start(t); o.stop(t + 0.25); + } + function _barks(n, pitch, gap) { + if (!enabled()) return; + const sample = files && (pitch > 1.3 ? files.klaeffen : files.wuff); + if (sample) { // echte Aufnahme: n-mal hintereinander + let i = 0; + const play = () => { + if (i++ >= n) return; + sample.currentTime = 0; + sample.play().catch(() => {}); + setTimeout(play, gap * 1000 + 200); + }; + play(); + return; + } + try { for (let i = 0; i < n; i++) _wuff(i * gap, pitch); } catch (e) {} + } + return { + enabled, + unlock() { // in User-Geste aufrufen (iOS-Autoplay-Policy) + try { + const c = _ctx(); + const b = c.createBuffer(1, 1, 22050), s = c.createBufferSource(); + s.buffer = b; s.connect(c.destination); s.start(0); + } catch (e) {} + // Echte Samples einmalig anfragen (404 → Synthese bleibt) + if (files === null) { + files = {}; + ['wuff', 'klaeffen'].forEach(name => { + const a = new Audio(`/sounds/${name}.mp3`); + a.preload = 'auto'; + a.addEventListener('canplaythrough', () => { files[name] = a; }, { once: true }); + a.addEventListener('error', () => {}, { once: true }); + }); + } + }, + links() { _barks(2, 1.0, 0.30); }, // 2× Wuff + rechts() { _barks(1, 1.0, 0.30); }, // 1× Wuff + klaeffen() { _barks(4, 1.7, 0.16); }, // schnelles, höheres Bellen + }; + })(); + let _navSndAnnouncedIdx = -1; // bis zu welchem Track-Index Abbiegungen angesagt wurden + let _navSndOffRoute = false; // Off-Route-Zustand (Kläffen beim Eintritt + alle 30 s) + let _navSndLastKlaeff = 0; + // Recording-Overlay state let _recOvl = null, _recMap = null; let _recFollow = true; // Karte folgt dem Standort bei Aufzeichnung (Drag pausiert) @@ -1670,6 +1745,12 @@ window.Page_routes = (() => { const track = route.gps_track || []; if (track.length < 2) return; + // Navi-Sounds: Audio in der User-Geste freischalten (iOS) + Ansage-Status zurücksetzen + NavSound.unlock(); + _navSndAnnouncedIdx = -1; + _navSndOffRoute = false; + _navSndLastKlaeff = 0; + _navMaxIdx = 0; _navRecorded = false; _navLastBearing = null; @@ -1689,6 +1770,7 @@ window.Page_routes = (() => { background:var(--c-surface);border-bottom:1px solid var(--c-border);flex-shrink:0"> ${UI.escape(route.name)} + @@ -1894,6 +1976,27 @@ window.Page_routes = (() => { return (Math.atan2(y, x) * 180 / Math.PI + 360) % 360; }; + // Abbiegepunkte EINMALIG aus dem Track ableiten: Peilung über ~15-m-Stützpunkte + // (sonst macht GPS-Zickzack aus jeder Geraden eine Kurve), Richtungsänderung ≥ 40° + // = Abbiegung, > 0 = rechts. Mindestabstand zwischen Ansagen ~25 m. + const _navTurns = (() => { + const out = []; + const distM = (a, b) => _haversineKm(a.lat, a.lon, b.lat, b.lon) * 1000; + let lastIdx = -1; + for (let i = 1; i < track.length - 1; i++) { + let p = i - 1, accP = distM(track[p], track[i]); + while (p > 0 && accP < 15) { p--; accP += distM(track[p], track[p + 1]); } + let n = i + 1, accN = distM(track[i], track[n]); + while (n < track.length - 1 && accN < 15) { n++; accN += distM(track[n - 1], track[n]); } + const d = ((_bearingTo(track[i], track[n]) - _bearingTo(track[p], track[i]) + 540) % 360) - 180; + if (Math.abs(d) >= 40 && (lastIdx < 0 || distM(track[lastIdx], track[i]) > 25)) { + out.push({ idx: i, right: d > 0 }); + lastIdx = i; + } + } + return out; + })(); + const _updateStats = (idx, distToRoute, userLat, userLon) => { if (idx > _navMaxIdx) { _navMaxIdx = idx; @@ -1924,12 +2027,33 @@ window.Page_routes = (() => { _navLastBearing = _bearingTo({ lat: userLat, lon: userLon }, track[idx + 1]); _updateDimArrow(); } + // Abbiege-Ansage (René: 2× Wuff = links, 1× = rechts): nächster Turn vor uns, + // angesagt sobald ≤ 45 m entfernt — einmal pro Abbiegepunkt. + if (userLat != null && distToRoute < 0.1) { + 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) { + _navSndAnnouncedIdx = next.idx; + if (next.right) NavSound.rechts(); else NavSound.links(); + } + } + } + const offWarn = document.getElementById('rk-nav-offwarn'); if (distToRoute * 1000 > 50) { offWarn.style.display = ''; if (navigator.vibrate) navigator.vibrate([200, 100, 200]); + // Falscher Weg = Kläffen (beim Abkommen + Erinnerung alle 30 s) + const _now = Date.now(); + if (!_navSndOffRoute || _now - _navSndLastKlaeff > 30000) { + _navSndOffRoute = true; + _navSndLastKlaeff = _now; + NavSound.klaeffen(); + } } else { offWarn.style.display = 'none'; + _navSndOffRoute = false; } // Polylines aktualisieren doneLine.setLatLngs(track.slice(0, idx + 1).map(p => [p.lat, p.lon])); @@ -2057,6 +2181,19 @@ window.Page_routes = (() => { if (locMarker) _navMap.setView(locMarker.getLatLng(), 16); }); + // Navi-Sounds an/aus (Klick = User-Geste → unlock + Probe-Wuff als Bestätigung) + document.getElementById('rk-nav-sound-btn')?.addEventListener('click', e => { + const on = !NavSound.enabled(); + try { localStorage.setItem('by_nav_sound', on ? '1' : '0'); } catch (err) {} + const btn = e.currentTarget; + btn.style.color = on ? 'var(--c-primary)' : 'var(--c-text-secondary)'; + btn.querySelector('use')?.setAttribute('href', `/icons/phosphor.svg#${on ? 'speaker-high' : 'speaker-none'}`); + if (on) { NavSound.unlock(); NavSound.rechts(); } + UI.toast.info(on + ? 'Navi-Sounds an: 2× Wuff = links, 1× Wuff = rechts, Kläffen = falscher Weg 🐕' + : 'Navi-Sounds aus.'); + }); + document.getElementById('rk-nav-pois')?.addEventListener('click', () => { if (!_navPois.length) { UI.toast.info('Keine POIs entlang dieser Route.'); return; } const byType = {}; diff --git a/backend/static/landing.html b/backend/static/landing.html index ce3ebde..27ae14c 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 bf72352..dd85b33 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 = '1240'; +const VER = '1241'; const CACHE_VERSION = `by-v${VER}`; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten