KI-Vision-Model, Breed-Scraper, Karte/Routen + Release v1292
Parallele Arbeit (auf Staging mitgetestet): KI-Vision-Model (VISION_MODEL in ki.py/routes, im KI-Status sichtbar), Breed-Scraper-Anpassungen (breed_enricher/breed_evaluator, evaluate_enrichment mit user_id), Karten-/Routen-Änderungen (map.js, routes.js), kleinere UI-Anpassungen (admin.js, components.css), docker-compose, MARKETING, nav-loop-Test. Version-Bump auf 1292 (VERSION, sw.js, app.js, index.html, landing.html).
This commit is contained in:
parent
51aad6cf1b
commit
f7370028da
17 changed files with 322 additions and 100 deletions
|
|
@ -13,5 +13,13 @@ for f in tests/js/test-map-offline*.js; do node "$f" backend/static/js/map-offli
|
|||
- r6: Standort-Grundversorgung (ensureHomeArea: lädt/skippt/Cap, überlebt clear)
|
||||
- r7: selektives Löschen (Korridor-Keep via keepTracks, manuelle Gebiete weg, Komplett-Wipe-Fallback)
|
||||
|
||||
Eigenständig (kein Stub-Argument nötig):
|
||||
|
||||
```
|
||||
node tests/js/test-nav-loop-closestidx.js
|
||||
```
|
||||
|
||||
- nav-loop-closestidx: Navi-Erst-Fix bei Runden springt nicht ans Track-Ende (spiegelt `_closestIdx` aus `js/pages/routes.js`) — Bugfix Angie/Deining 09.06.2026
|
||||
|
||||
⚠️ Node 21+: eingebautes `navigator`-Global — Stubs via `Object.defineProperty(globalThis, 'navigator', …)`,
|
||||
ein einfaches `global.navigator =` wird still verschluckt.
|
||||
|
|
|
|||
98
tests/js/test-nav-loop-closestidx.js
Normal file
98
tests/js/test-nav-loop-closestidx.js
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
// Navi-Erst-Fix bei RUNDEN: der Startindex darf nicht ans Track-Ende springen.
|
||||
//
|
||||
// Spiegelt die _closestIdx-Erst-Fix-Logik aus js/pages/routes.js (_startNav). An einem
|
||||
// Start/Ende-Knoten einer Runde ist der ENDPUNKT oft ein paar Meter näher als der
|
||||
// Startpunkt; die alte globale Suche sprang dann sofort ans Track-Ende → 100 % / 0 km ab
|
||||
// Sekunde 1 (Angie, Deining-Runde 09.06.2026). Bei Änderung BEIDE Stellen anpassen.
|
||||
//
|
||||
// Hinweis: bewusst eine Nachbildung — die echte Funktion ist eine Closure in _startNav
|
||||
// und nicht exportierbar, ohne routes.js umzubauen.
|
||||
|
||||
const _haversineKm = (lat1, lon1, lat2, lon2) => {
|
||||
const R = 6371, dLat = (lat2 - lat1) * Math.PI / 180, dLon = (lon2 - lon1) * Math.PI / 180;
|
||||
const a = Math.sin(dLat / 2) ** 2 +
|
||||
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon / 2) ** 2;
|
||||
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
};
|
||||
|
||||
// Erst-Fix-Index für gegebenen track + Userposition (1:1 aus routes.js).
|
||||
function firstFixIdx(track, lat, lon) {
|
||||
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 };
|
||||
};
|
||||
const isLoop = track.length > 2 &&
|
||||
_haversineKm(track[0].lat, track[0].lon,
|
||||
track[track.length - 1].lat, track[track.length - 1].lon) < 0.06;
|
||||
const g = search(0, track.length - 1);
|
||||
if (isLoop) {
|
||||
const win = Math.min(track.length - 1, Math.max(30, Math.floor(track.length * 0.15)));
|
||||
const s = search(0, win);
|
||||
return { idx: s.bestD < 0.15 ? s.best : g.best, isLoop };
|
||||
}
|
||||
const s = search(0, Math.min(track.length - 1, 30));
|
||||
return { idx: (s.bestD - g.bestD) * 1000 < 25 ? s.best : g.best, isLoop };
|
||||
}
|
||||
|
||||
// Die ALTE Logik (vor dem Fix) — nur zum Beweis, dass der Fix wirklich etwas ändert.
|
||||
function firstFixIdxOld(track, lat, lon) {
|
||||
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 };
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
||||
// --- Synthetische Deining-artige Runde -------------------------------------
|
||||
const C = { lat: 48.07, lon: 11.50 };
|
||||
const mLat = m => m / 111320;
|
||||
const mLon = (m, lat) => m / (111320 * Math.cos(lat * Math.PI / 180));
|
||||
// Punkt auf einem Kreis: Winkel von Nord, im Uhrzeigersinn.
|
||||
const onCircle = (deg, r) => {
|
||||
const rad = deg * Math.PI / 180;
|
||||
return { lat: C.lat + mLat(r * Math.cos(rad)), lon: C.lon + mLon(r * Math.sin(rad), C.lat) };
|
||||
};
|
||||
|
||||
const N = 40, R = 80; // 40 Punkte auf 80-m-Kreis, lange Runde von 0°→329°
|
||||
const track = [];
|
||||
for (let i = 0; i < N; i++) track.push(onCircle(i / (N - 1) * 329, R));
|
||||
// User steht 3 m außerhalb des ENDpunkts (329°) → näher am Ende als am Start.
|
||||
const user = onCircle(329, R + 3);
|
||||
|
||||
const startEndM = _haversineKm(track[0].lat, track[0].lon,
|
||||
track[N - 1].lat, track[N - 1].lon) * 1000;
|
||||
const dStart = _haversineKm(user.lat, user.lon, track[0].lat, track[0].lon) * 1000;
|
||||
const dEnd = _haversineKm(user.lat, user.lon, track[N - 1].lat, track[N - 1].lon) * 1000;
|
||||
console.log(`Runde: Start↔Ende ${startEndM.toFixed(0)} m | User→Start ${dStart.toFixed(0)} m, User→Ende ${dEnd.toFixed(0)} m`);
|
||||
|
||||
// 1. Loop wird erkannt (Start ≈ Ende < 60 m)
|
||||
const res = firstFixIdx(track, user.lat, user.lon);
|
||||
if (!res.isLoop) throw new Error('Runde nicht als Loop erkannt');
|
||||
|
||||
// 2. Erst-Fix landet im STARTbereich, NICHT am Track-Ende
|
||||
console.log('Erst-Fix-Index:', res.idx, '(von', N - 1 + ')');
|
||||
if (res.idx > Math.floor(N * 0.15)) throw new Error(`Erst-Fix sprang weg vom Start (idx ${res.idx})`);
|
||||
|
||||
// 3. Beweis: die alte Logik wäre hier ans Ende gesprungen (100 %)
|
||||
const old = firstFixIdxOld(track, user.lat, user.lon);
|
||||
console.log('Alte Logik-Index:', old);
|
||||
if (old !== N - 1) throw new Error('Erwartet: alte Logik springt ans Ende — Testfall trifft den Bug nicht mehr');
|
||||
|
||||
// 4. Punkt-zu-Punkt-Route (kein Loop): User am Start → 0 %, am Ende → bleibt sinnvoll
|
||||
const ptp = [];
|
||||
for (let i = 0; i < N; i++) ptp.push({ lat: C.lat + mLat(i * 25), lon: C.lon }); // 25-m-Schritte nach Norden
|
||||
const ptpRes = firstFixIdx(ptp, ptp[0].lat, ptp[0].lon);
|
||||
if (ptpRes.isLoop) throw new Error('Gerade Route fälschlich als Loop erkannt');
|
||||
if (ptpRes.idx !== 0) throw new Error(`Punkt-zu-Punkt am Start sollte idx 0 sein, war ${ptpRes.idx}`);
|
||||
|
||||
console.log('\nALLE NAV-LOOP-TESTS BESTANDEN');
|
||||
Loading…
Add table
Add a link
Reference in a new issue