Stage 3 — planning: showRouteEditor gets a "🚲 По дорогам" toggle. Waypoints
stay editable markers; with it on, the line between them (and the saved
geometry) follows roads via BRouter (brouterMode/routeGeom, 450ms debounce on
edits). BRouter.route (router.js) now: Android -> embedded engine, otherwise
(desktop / at home) -> the PC RouteServer over HTTP (http_request bypasses the
WebView CSP; LAN 192.168.1.18:17777 / WAN 77.66.192.45:17777). Lets planning be
debugged on the desktop.
Stage 2 — segment UI: a "Сегменты" button on the roads screen opens
showBrouterSegments — region tiles (E35_N45/E40_N45), "есть" status, a
Download/Update button with percent progress via BRouter.downloadTileAndWait.
Verified: RouteServer returns 83 trkpt for a 3-waypoint trekking route (3594 m),
parseRouteGPX reads trkpt; both files compile into the apk. End-to-end UI (tap
waypoints -> routed line; in-app segment download) pending desktop/phone run.
80 lines
3.6 KiB
JavaScript
80 lines
3.6 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://77.66.192.45:17777"],
|
|
|
|
async _routeServer(pts, profile) {
|
|
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";
|
|
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) {
|
|
let pts = (points || []).map(p => [p[0], p[1]]);
|
|
profile = profile || "trekking";
|
|
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);
|
|
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 });
|
|
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);
|
|
if (gpx) return gpx;
|
|
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"));
|
|
}
|
|
},
|
|
};
|