Compare commits
4 commits
6565d6a999
...
0967623342
| Author | SHA1 | Date | |
|---|---|---|---|
| 0967623342 | |||
| ca23b3ec46 | |||
| a1b2644cce | |||
| 502f0f4921 |
13 changed files with 164 additions and 4212 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
1240
|
||||
1242
|
||||
|
|
@ -86,14 +86,14 @@
|
|||
<title>Ban Yaro</title>
|
||||
|
||||
<!-- Theme + theme-color Statusleiste vor CSS setzen -->
|
||||
<script src="/js/boot-early.js?v=1240"></script>
|
||||
<script src="/js/boot-early.js?v=1242"></script>
|
||||
|
||||
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
|
||||
<link rel="stylesheet" href="/css/design-system.css?v=1240">
|
||||
<link rel="stylesheet" href="/css/layout.css?v=1240">
|
||||
<link rel="stylesheet" href="/css/components.css?v=1240">
|
||||
<link rel="stylesheet" href="/css/utilities.css?v=1240">
|
||||
<link rel="stylesheet" href="/css/lists.css?v=1240">
|
||||
<link rel="stylesheet" href="/css/design-system.css?v=1242">
|
||||
<link rel="stylesheet" href="/css/layout.css?v=1242">
|
||||
<link rel="stylesheet" href="/css/components.css?v=1242">
|
||||
<link rel="stylesheet" href="/css/utilities.css?v=1242">
|
||||
<link rel="stylesheet" href="/css/lists.css?v=1242">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
|
@ -612,11 +612,11 @@
|
|||
<div id="modal-container"></div>
|
||||
|
||||
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
|
||||
<script src="/js/api.js?v=1240"></script>
|
||||
<script src="/js/ui.js?v=1240"></script>
|
||||
<script src="/js/app.js?v=1240"></script>
|
||||
<script src="/js/worlds.js?v=1240"></script>
|
||||
<script src="/js/offline-indicator.js?v=1240"></script>
|
||||
<script src="/js/api.js?v=1242"></script>
|
||||
<script src="/js/ui.js?v=1242"></script>
|
||||
<script src="/js/app.js?v=1242"></script>
|
||||
<script src="/js/worlds.js?v=1242"></script>
|
||||
<script src="/js/offline-indicator.js?v=1242"></script>
|
||||
|
||||
<!-- Feature-Seiten werden lazy geladen -->
|
||||
|
||||
|
|
@ -626,7 +626,7 @@
|
|||
|
||||
|
||||
<!-- Boot: Offline-Banner + SW-Registration (extrahiert für CSP) -->
|
||||
<script src="/js/boot.js?v=1240"></script>
|
||||
<script src="/js/boot.js?v=1242"></script>
|
||||
|
||||
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Router, State-Management, Navigation, Initialisierung.
|
||||
============================================================ */
|
||||
|
||||
const APP_VER = '1240'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VER = '1242'; // ← 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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -329,7 +404,10 @@ window.Page_routes = (() => {
|
|||
_filterOpen = !_filterOpen;
|
||||
const panel = document.getElementById('rk-filter-panel');
|
||||
const btn = document.getElementById('rk-filter-btn');
|
||||
if (panel) panel.style.display = _filterOpen ? '' : 'none';
|
||||
// KLASSE toggeln, nicht style.display: .hidden hat display:none !important
|
||||
// (design-system.css) — Inline-Style kommt dagegen nie an. Kaputt seit 27a3f95
|
||||
// („Filter standardmäßig zu" setzte die Klasse ins Markup, Toggle blieb auf style).
|
||||
if (panel) panel.classList.toggle('hidden', !_filterOpen);
|
||||
if (btn) btn.classList.toggle('active', _filterOpen);
|
||||
}
|
||||
|
||||
|
|
@ -362,7 +440,8 @@ window.Page_routes = (() => {
|
|||
if (searchRow) searchRow.style.display = 'none';
|
||||
if (actRow) actRow.style.display = 'none';
|
||||
const filterPanel = document.getElementById('rk-filter-panel');
|
||||
if (filterPanel) filterPanel.style.display = 'none';
|
||||
if (filterPanel) { filterPanel.classList.add('hidden'); _filterOpen = false; }
|
||||
document.getElementById('rk-filter-btn')?.classList.remove('active');
|
||||
if (!App.hasPro(_appState?.user)) {
|
||||
document.getElementById('rk-list')?.replaceChildren();
|
||||
const gate = document.createElement('div');
|
||||
|
|
@ -1670,6 +1749,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 +1774,7 @@ window.Page_routes = (() => {
|
|||
background:var(--c-surface);border-bottom:1px solid var(--c-border);flex-shrink:0">
|
||||
<button id="rk-nav-back" style="background:none;border:none;color:var(--c-primary);font-size:16px;cursor:pointer;padding:4px 0">← Zurück</button>
|
||||
<span style="font-weight:700;font-size:14px;flex:1;text-align:center;margin:0 8px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${UI.escape(route.name)}</span>
|
||||
<button id="rk-nav-sound-btn" style="background:none;border:none;color:${NavSound.enabled() ? 'var(--c-primary)' : 'var(--c-text-secondary)'};cursor:pointer;padding:4px 6px;display:flex;align-items:center" title="Navi-Sounds (2× Wuff = links, 1× = rechts, Kläffen = falscher Weg)">${UI.icon(NavSound.enabled() ? 'speaker-high' : 'speaker-none')}</button>
|
||||
<button id="rk-nav-center-btn" style="background:none;border:none;color:var(--c-primary);cursor:pointer;padding:4px 6px;display:flex;align-items:center" title="Auf Standort zentrieren">${UI.icon('crosshair')}</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -1894,6 +1980,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 +2031,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 +2185,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 = {};
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<script src="/js/landing-init.js?v=1240"></script>
|
||||
<script src="/js/landing-init.js?v=1242"></script>
|
||||
<title>Ban Yaro — Die Hunde-App für Deutschland, Österreich & Schweiz</title>
|
||||
<meta name="description" content="Ban Yaro: Die kostenlose All-in-One Hunde-App für DACH. Tagebuch, Giftköder-Alarm, Training mit KI, Forum, Wurfbörse, Stammbaum, Inzucht-Check — DSGVO-konform, offline-fähig, ohne App Store.">
|
||||
<meta name="keywords" content="Hunde App, Hunde Community, Wurfbörse, Züchter, Welpen kaufen, Stammbaum Hund, Inzuchtkoeffizient, Hundezucht, Impfpass Hund, Giftköder Alarm, Gassi Community, Hundetraining App, Hunde Forum, Hunde KI, Hundefilm Datenbank, Welpen Marktplatz">
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
============================================================ */
|
||||
|
||||
// ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab
|
||||
const VER = '1240';
|
||||
const VER = '1242';
|
||||
const CACHE_VERSION = `by-v${VER}`;
|
||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ services:
|
|||
dwd-radar:
|
||||
build: ./tools/dwd-radar
|
||||
image: banyaro-dwd-radar
|
||||
container_name: banyaro-dwd-radar
|
||||
# KEIN container_name: Staging + Prod teilen sich den Docker-Host — der Compose-
|
||||
# Projektname (= Verzeichnis banyaro / banyaro-staging) hält die Container auseinander.
|
||||
mem_limit: 1g
|
||||
volumes:
|
||||
- ./data/radar:/out
|
||||
|
|
|
|||
3
tools/dwd-radar/.gitignore
vendored
Normal file
3
tools/dwd-radar/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
poc/*.tif
|
||||
poc/*.tar.bz2
|
||||
poc/DE1200_RV*
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue