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:
parent
8e720ecd17
commit
cd98078682
5 changed files with 80 additions and 3 deletions
|
|
@ -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:** удобство и корректность.
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue