baldav/frontend/js/router.js
4aiman d25cb0de49 BRouter Part B: show fords/rough tracks on the route + adventure opt-in
- brouter_route gets a format param; routeBrouter returns formatAsGeoJson (WayTags per
  segment) when asked; BRouter.routeGeoJson + GPS.parseRouteGeoJson extract geometry +
  dangers (ford / grade5|bad-trail / ferry) from the tags
- route editor routes road legs via geojson, marks fords (cyan) and rough tracks
  (orange) on the map with a '⚠ брод/грунт' counter, so the rider sees the hell BEFORE
  riding it
- 'Броды: избегать/разрешить' toggle picks trekking vs trekking-adventure (allow_adventure)
  and re-routes; new trekking-adventure.brf profile (asset + staging)
Parser validated against the home RouteServer on a real ford (47.5387,40.3164).
2026-10-26 21:46:56 +03:00

126 lines
5.8 KiB
JavaScript

// Offline routing via the embedded BRouter engine.
// JS → Rust command `brouter_route` → Kotlin (btools.router) → GPX back.
// BRouter.route(points, profile):
// points — array of [lon, lat] pairs (BRouter order; NOT [lat, lon])
// profile — "trekking" (planning) | "shortest" (gap fill); default "trekking"
// Returns a GPX string (parse with parseRouteGPX) or throws with the engine's error.
// Needs the region .rd5 segments present on device (files/brouter/segments4).
// RouteServer fallback hosts (the PC BRouter server, reachable at home). Used when the
// embedded engine isn't available (desktop) or errors — lets planning work on the desktop
// too. http_request is Rust-native (ureq), so it's not subject to the WebView CSP.
window.BRouter = {
_servers: ["http://192.168.1.18:17777", "http://mirror.invalid:17777"],
// nogos: [{lat,lon,radius}] — impassable circles the router must avoid.
_nogoParam(nogos) {
if (!nogos || !nogos.length) return "";
// RouteServer expects nogos=lon,lat,radiusMeters|... (radius in metres).
let s = nogos.filter(n => n && n.lat != null && n.lon != null)
.map(n => n.lon + "," + n.lat + "," + Math.round(n.radius || 25)).join("|");
return s ? "&nogos=" + encodeURIComponent(s) : "";
},
async _routeServer(pts, profile, nogos) {
let lonlats = pts.map(p => p[0] + "," + p[1]).join("|");
for (let base of this._servers) {
try {
let url = base + "/brouter?lonlats=" + encodeURIComponent(lonlats) + "&profile=" + profile + "&alternativeidx=0&format=gpx" + this._nogoParam(nogos);
let gpx = await window.__TAURI__.core.invoke("http_request", { url, method: "GET" });
if (gpx && gpx.indexOf("<gpx") >= 0) return gpx;
} catch (e) { /* try next server */ }
}
return null;
},
async route(points, profile, nogos) {
let pts = (points || []).map(p => [p[0], p[1]]);
profile = profile || "trekking";
nogos = (nogos || []).filter(n => n && n.lat != null && n.lon != null);
let isAndroid = /Android/i.test(navigator.userAgent || "");
// Desktop has no embedded engine (that's Kotlin/Android) — the native command would
// just time out; go straight to the PC RouteServer. Android uses the engine, and
// falls back to the RouteServer (reachable at home) only if the engine errors.
if (!isAndroid) {
let gpx = await this._routeServer(pts, profile, nogos);
if (gpx) return gpx;
throw new Error("brouter: RouteServer unreachable");
}
try {
let raw = await window.__TAURI__.core.invoke("brouter_route", { lonlats: JSON.stringify(pts), profile, nogos: JSON.stringify(nogos) });
let res = JSON.parse(raw);
if (res.ok) return res.gpx;
throw new Error(res.error || "engine error");
} catch (e) {
let gpx = await this._routeServer(pts, profile, nogos);
if (gpx) return gpx;
throw new Error("brouter: " + (e.message || e));
}
},
async _routeServerGeoJson(pts, profile, nogos) {
let lonlats = pts.map(p => p[0] + "," + p[1]).join("|");
for (let base of this._servers) {
try {
let url = base + "/brouter?lonlats=" + encodeURIComponent(lonlats) + "&profile=" + profile + "&alternativeidx=0&format=geojson" + this._nogoParam(nogos);
let gj = await window.__TAURI__.core.invoke("http_request", { url, method: "GET" });
if (gj && gj.indexOf("FeatureCollection") >= 0) return gj;
} catch (e) { /* try next server */ }
}
return null;
},
// Like route() but returns the GeoJSON string (per-segment WayTags) for danger detection.
// Parse with GPS.parseRouteGeoJson. Same engine/RouteServer fallback as route().
async routeGeoJson(points, profile, nogos) {
let pts = (points || []).map(p => [p[0], p[1]]);
profile = profile || "trekking";
nogos = (nogos || []).filter(n => n && n.lat != null && n.lon != null);
let isAndroid = /Android/i.test(navigator.userAgent || "");
if (!isAndroid) {
let gj = await this._routeServerGeoJson(pts, profile, nogos);
if (gj) return gj;
throw new Error("brouter: RouteServer unreachable");
}
try {
let raw = await window.__TAURI__.core.invoke("brouter_route", { lonlats: JSON.stringify(pts), profile, nogos: JSON.stringify(nogos), format: "geojson" });
let res = JSON.parse(raw);
if (res.ok && res.geojson) return res.geojson;
throw new Error(res.error || "engine error");
} catch (e) {
let gj = await this._routeServerGeoJson(pts, profile, nogos);
if (gj) return gj;
throw new Error("brouter: " + (e.message || e));
}
},
// --- Segment (.rd5) delivery (Stage 2) — Kotlin downloads into files/brouter/segments4 ---
// Kick off a tile download (e.g. "E35_N45"); returns immediately. Poll downloadStatus.
async downloadTile(tile) {
return window.__TAURI__.core.invoke("brouter_download_tile", { tile });
},
// { state: "downloading"|"done"|"error", bytes, total?, error? } — {} if not started yet.
async downloadStatus(tile) {
let raw = await window.__TAURI__.core.invoke("brouter_download_status", { tile });
try { return JSON.parse(raw); } catch (e) { return {}; }
},
// Downloaded tiles present on device: [{ name, bytes }].
async listTiles() {
let raw = await window.__TAURI__.core.invoke("brouter_list_tiles");
try { return JSON.parse(raw); } catch (e) { return []; }
},
// Convenience: download a tile and resolve when done (or reject on error). onProgress(bytes,total).
async downloadTileAndWait(tile, onProgress) {
await this.downloadTile(tile);
for (;;) {
await new Promise(r => setTimeout(r, 700));
let s = await this.downloadStatus(tile);
if (s.state === "downloading" && onProgress) onProgress(s.bytes || 0, s.total || 0);
if (s.state === "done") return s;
if (s.state === "error") throw new Error("brouter tile: " + (s.error || "download failed"));
}
},
};