From cd980786828052183f6fb715fb541b91404a0f2e Mon Sep 17 00:00:00 2001 From: 4aiman Date: Wed, 9 Sep 2026 17:43:56 +0300 Subject: [PATCH] BRouter: gap-fill along roads + release hardening MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stage 4 — gap fill: GPS.fillGaps(points) routes each GPS dropout (dt>gap threshold, chord>60m) with the shortest road path and splices the routed points in (flagged estimated), falling back to the chord when the road is >3x the chord (graph detoured). A 🧩 button on the track detail draws the road-filled path as a cyan dashed overlay — non-destructive (stats recompute over filled points is a follow-up). Also fixes doBrouter: parseRouteGPX returns {name,points}, not an array (planning would have kept the straight line). Release hardening: network_security_config.xml permits cleartext only to the mirror/RouteServer hosts (base off, debug-overrides on) — wired via android:networkSecurityConfig; proguard keeps btools.** so release minification doesn't strip the embedded engine. --- IMPLEMENTATION_PLAN.md | 13 +++++++- frontend/js/gps.js | 30 +++++++++++++++++++ frontend/js/main.js | 27 +++++++++++++++-- .../android/app/src/main/AndroidManifest.xml | 1 + .../main/res/xml/network_security_config.xml | 12 ++++++++ 5 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 src-tauri/gen/android/app/src/main/res/xml/network_security_config.xml diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index b2d6bbc..b1ef181 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -94,7 +94,18 @@ app data dir; профиль trekking (идея 2) и shortest (идея 1); л **Success:** записанный трек с дропаутом GPS показывает дорожный фолловинг вместо хорды, кроме случая «дорога в разы длиннее». **Tests:** трек с известным дропаутом на дороге → заполнение по дороге; дропаут на поле → хорда. -**Status:** not started +**Status:** СДЕЛАНО (v1, non-destructive). `GPS.fillGaps(points)` (gps.js): для каждого гэпа + (dt>gapThreshold и хорда>60м) прокладывает `shortest`, вставляет дорожные точки (флаг `estimated`); + фолбэк на хорду если дорога > 3× хорды. Кнопка «🧩» на детали трека — рисует дорожно-заполненный + путь голубым пунктиром оверлеем (данные не меняет). Пересчёт статистики по заполненным точкам — + следующий шаг (сейчас только визуализация). + +## Release-хардненинг (item 4, сделано) +- `network_security_config.xml` (res/xml): cleartext OFF глобально в release, разрешён только хостам + зеркала/RouteServer (192.168.1.1 / 192.168.1.18 / mirror.invalid); `debug-overrides` — всё в debug. + Подключён в AndroidManifest (`android:networkSecurityConfig`). +- `proguard-rules.pro`: `-keep class btools.** { *; }` + `-dontwarn btools.**` — минификация release + не вырежет встроенный движок. ## Stage 5 — Полировка (опционально) **Goal:** удобство и корректность. diff --git a/frontend/js/gps.js b/frontend/js/gps.js index c1c1c90..6d47ef1 100644 --- a/frontend/js/gps.js +++ b/frontend/js/gps.js @@ -1393,6 +1393,36 @@ GPS.parseRouteGPX = function(xmlString) { return { name, points }; }; +// Fill GPS dropouts along roads: for each gap (dt > gapThreshold and a real chord), route +// the SHORTEST road path between the pre/post-gap points and splice the routed points in +// (flagged estimated). Falls back to keeping the chord if the road path is > ~3x the chord +// (the graph detoured, e.g. across a field). Async (BRouter). Returns a NEW points array. +GPS.fillGaps = async function(points, opts) { + opts = opts || {}; + const gapThr = opts.gapThresholdSec ?? (GPS.config.gapThresholdSec ?? 30); + const ratio = opts.maxRatio ?? 3; + const sorted = points.slice().sort((a, b) => new Date(a.ts) - new Date(b.ts)); + const out = []; + for (let i = 0; i < sorted.length; i++) { + out.push(sorted[i]); + const a = sorted[i], b = sorted[i + 1]; + if (!b) continue; + const dt = (new Date(b.ts) - new Date(a.ts)) / 1000; + const chord = GPS.distance(a.lat, a.lon, b.lat, b.lon); + if (dt <= gapThr || chord < 60) continue; // not a gap worth filling + try { + const gpx = await BRouter.route([[a.lon, a.lat], [b.lon, b.lat]], "shortest"); + const rp = (GPS.parseRouteGPX(gpx) || {}).points || []; + if (rp.length < 3) continue; + let rlen = 0; + for (let k = 1; k < rp.length; k++) rlen += GPS.distance(rp[k-1].lat, rp[k-1].lon, rp[k].lat, rp[k].lon); + if (rlen > chord * ratio) continue; // road detoured too far — keep the chord + for (let k = 1; k < rp.length - 1; k++) out.push({ lat: rp[k].lat, lon: rp[k].lon, ts: a.ts, estimated: true }); + } catch (e) { /* keep the chord for this gap */ } + } + return out; +}; + GPS.importGPX = async function(xmlString, weightKg) { const { points, activityType } = GPS.parseGPX(xmlString); if (points.length < 2) throw new Error("GPX has fewer than 2 points"); diff --git a/frontend/js/main.js b/frontend/js/main.js index e5ebb7c..9638dba 100644 --- a/frontend/js/main.js +++ b/frontend/js/main.js @@ -3547,8 +3547,8 @@ async function showRouteEditor(container, routeId) { if (!brouterMode || points.length < 2) { routeGeom = null; updatePolyline(); updateStats(); return; } try { let gpx = await BRouter.route(points.map(p => [p.lon, p.lat]), "trekking"); - let rp = GPS.parseRouteGPX(gpx); - routeGeom = (rp && rp.length >= 2) ? rp.map(p => ({ lat: p.lat, lon: p.lon })) : null; + let rp = (GPS.parseRouteGPX(gpx) || {}).points || []; + routeGeom = (rp.length >= 2) ? rp.map(p => ({ lat: p.lat, lon: p.lon })) : null; } catch (e) { routeGeom = null; showToast("BRouter: " + (e.message || e)); } changed(); } @@ -4765,6 +4765,29 @@ async function showSessionDetail(sessionId) { addDarkMapControl(map); // dark-tiles toggle on the track view POI.attach(map); // POI overlay + category filter + // Gap-fill overlay: route GPS dropouts along roads (BRouter shortest) and draw the + // filled path as a cyan dashed overlay over the track. Non-destructive (no data change). + let gapFillLayer = null; + let GapCtl = L.Control.extend({ + options: { position: "topright" }, + onAdd: function () { + let b = L.DomUtil.create("button", "rec-map-btn"); + b.textContent = "🧩"; b.title = "Достроить пропуски по дорогам"; b.style.position = "static"; + L.DomEvent.disableClickPropagation(b); + b.onclick = async () => { + if (gapFillLayer) { map.removeLayer(gapFillLayer); gapFillLayer = null; b.classList.remove("active"); return; } + b.disabled = true; b.classList.add("active"); + try { + let filled = await GPS.fillGaps(points); + gapFillLayer = L.polyline(filled.map(p => [p.lat, p.lon]), { color: "#00e5ff", weight: 3, opacity: 0.9, dashArray: "6,6" }).addTo(map); + } catch (e) { showToast("Пропуски: " + (e.message || e)); b.classList.remove("active"); } + b.disabled = false; + }; + return b; + }, + }); + map.addControl(new GapCtl()); + // Active saved-route overlay on the detail map + a toggle button (default ON when // a route is selected). Shares baldav_route_shown with the recording screen. let sdRouteLayer = null, sdRouteDiv = null; diff --git a/src-tauri/gen/android/app/src/main/AndroidManifest.xml b/src-tauri/gen/android/app/src/main/AndroidManifest.xml index 7cfdf91..1b7560a 100644 --- a/src-tauri/gen/android/app/src/main/AndroidManifest.xml +++ b/src-tauri/gen/android/app/src/main/AndroidManifest.xml @@ -21,6 +21,7 @@ android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@style/Theme.baldav" + android:networkSecurityConfig="@xml/network_security_config" android:usesCleartextTraffic="${usesCleartextTraffic}"> + + + + + 192.168.1.1 + 192.168.1.18 + mirror.invalid + + +