BRouter: gap-fill along roads + release hardening

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.
This commit is contained in:
4aiman 2026-09-09 17:43:56 +03:00
commit cd98078682
5 changed files with 80 additions and 3 deletions

View file

@ -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:** удобство и корректность.

View file

@ -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");

View file

@ -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;

View file

@ -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}">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Release keeps cleartext OFF globally, but allows it to the BalDav mirror /
RouteServer hosts (HTTP :88 / :8088 / :17777). Debug allows all cleartext. -->
<network-security-config>
<base-config cleartextTrafficPermitted="false" />
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">192.168.1.1</domain>
<domain includeSubdomains="false">192.168.1.18</domain>
<domain includeSubdomains="false">mirror.invalid</domain>
</domain-config>
<debug-overrides cleartextTrafficPermitted="true" />
</network-security-config>