BRouter Stage 2 (primitive): download .rd5 segments inside the app
Kotlin owns the download so tiles land in the exact dir the engine reads (files/brouter/segments4) — no Rust path guessing. processBrouterCommand now dispatches: dltile:<tile> streams brouter.de/brouter/segments4/<tile>.rd5 to a .tmp then renames, writing progress/final state to brouter_dl_<tile>.json; listtiles writes brouter_tiles.json. Tile names are sanitized ([A-Za-z0-9_]). Rust relays: brouter_download_tile / brouter_download_status / brouter_list_tiles. JS: BRouter.downloadTile/downloadStatus/listTiles/downloadTileAndWait. Compiles into the apk. Not yet exercised on device (phone offline) — the tile selection UI and the end-to-end download->offline-route test are the remaining Stage 2 work. IMPLEMENTATION_PLAN updated.
This commit is contained in:
parent
bc63b34cb9
commit
b4046841be
4 changed files with 143 additions and 5 deletions
|
|
@ -40,10 +40,15 @@ app data dir; профиль trekking (идея 2) и shortest (идея 1); л
|
|||
с `brouter.de/brouter/segments4/<tile>.rd5`, прогресс, размер, дата. Хранить в app data dir
|
||||
(путь, который читает Stage 1).
|
||||
- Управление: список скачанного, удалить/обновить.
|
||||
**Success:** дома по проводу тайлы региона (E35_N45/E40_N45 — уточнить) скачиваются и лежат так,
|
||||
**Success:** дома по проводу тайлы региона (E35_N45/E40_N45 — подтверждены) скачиваются и лежат так,
|
||||
что Stage 1 их читает; в поле сеть не нужна.
|
||||
**Tests:** скачать тайл, проложить маршрут офлайн (сеть выключить) — работает.
|
||||
**Status:** not started
|
||||
**Status:** ПРИМИТИВ ГОТОВ (компилируется; тест на железе завтра). Kotlin качает тайл в
|
||||
files/brouter/segments4 (путь детерминирован — качает сам Kotlin, без Rust-догадок): команды
|
||||
`dltile:<tile>`/`listtiles` в processBrouterCommand, прогресс/статус в brouter_dl_<tile>.json.
|
||||
Rust-реле: `brouter_download_tile`/`brouter_download_status`/`brouter_list_tiles`. JS:
|
||||
`BRouter.downloadTile/downloadStatus/listTiles/downloadTileAndWait`. ОСТАЛОСЬ: UI-экран выбора
|
||||
тайлов по региону + сквозной тест на устройстве (скачать → офлайн-маршрут).
|
||||
|
||||
## Stage 3 — Идея 2: оффлайн-планирование по опорным точкам
|
||||
**Goal:** тап опорных точек → дорожный маршрут trekking → существующий предпросмотр/профиль.
|
||||
|
|
|
|||
|
|
@ -17,4 +17,35 @@ window.BRouter = {
|
|||
if (!res.ok) throw new Error("brouter: " + (res.error || "unknown error"));
|
||||
return res.gpx;
|
||||
},
|
||||
|
||||
// --- 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"));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -196,8 +196,62 @@ class MainActivity : TauriActivity() {
|
|||
val cmd = cmdFile.readText().trim()
|
||||
if (cmd.isEmpty()) return
|
||||
cmdFile.writeText("")
|
||||
if (!cmd.startsWith("route:")) return
|
||||
val id = cmd.substringAfter("route:")
|
||||
when {
|
||||
cmd.startsWith("dltile:") -> downloadTile(appDataDir, cmd.substringAfter("dltile:").trim())
|
||||
cmd == "listtiles" -> listTiles(appDataDir)
|
||||
cmd.startsWith("route:") -> routeCommand(appDataDir, cmd.substringAfter("route:"))
|
||||
}
|
||||
}
|
||||
|
||||
// Download one .rd5 segment tile from brouter.de into files/brouter/segments4 (the dir
|
||||
// the engine reads). Progress/final state go to brouter_dl_<tile>.json for JS to poll.
|
||||
private fun downloadTile(appDataDir: File, tileRaw: String) {
|
||||
// Sanitize: tile names are like E35_N45 / W5_S10 — allow only [A-Za-z0-9_].
|
||||
val tile = tileRaw.filter { it.isLetterOrDigit() || it == '_' }
|
||||
if (tile.isEmpty() || tile != tileRaw) return
|
||||
Thread {
|
||||
val statusFile = File(appDataDir, "brouter_dl_$tile.json")
|
||||
val dir = File(filesDir, "brouter/segments4"); dir.mkdirs()
|
||||
val dest = File(dir, "$tile.rd5"); val tmp = File(dir, "$tile.rd5.tmp")
|
||||
try {
|
||||
statusFile.writeText("""{"state":"downloading","bytes":0}""")
|
||||
val conn = URL("https://brouter.de/brouter/segments4/$tile.rd5").openConnection() as java.net.HttpURLConnection
|
||||
conn.connectTimeout = 15000; conn.readTimeout = 30000
|
||||
val total = conn.contentLength
|
||||
conn.inputStream.use { input -> tmp.outputStream().use { out ->
|
||||
val buf = ByteArray(65536); var n = input.read(buf); var got = 0L; var last = 0L
|
||||
while (n >= 0) {
|
||||
out.write(buf, 0, n); got += n
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - last > 500) { statusFile.writeText("""{"state":"downloading","bytes":$got,"total":$total}"""); last = now }
|
||||
n = input.read(buf)
|
||||
}
|
||||
}}
|
||||
conn.disconnect()
|
||||
if (dest.exists()) dest.delete()
|
||||
if (!tmp.renameTo(dest)) throw RuntimeException("rename failed")
|
||||
statusFile.writeText("""{"state":"done","bytes":${dest.length()}}""")
|
||||
} catch (e: Exception) {
|
||||
Log.e("BalDav", "BRouter tile download failed: $tile", e)
|
||||
tmp.delete()
|
||||
statusFile.writeText("""{"state":"error","error":${org.json.JSONObject.quote(e.message ?: e.toString())}}""")
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
// List downloaded .rd5 tiles → brouter_tiles.json ([{name, bytes}]).
|
||||
private fun listTiles(appDataDir: File) {
|
||||
val dir = File(filesDir, "brouter/segments4")
|
||||
val arr = org.json.JSONArray()
|
||||
for (f in (dir.listFiles() ?: arrayOf())) {
|
||||
if (f.name.endsWith(".rd5")) {
|
||||
val o = org.json.JSONObject(); o.put("name", f.name.removeSuffix(".rd5")); o.put("bytes", f.length()); arr.put(o)
|
||||
}
|
||||
}
|
||||
try { File(appDataDir, "brouter_tiles.json").writeText(arr.toString()) } catch (e: Exception) { Log.e("BalDav", "listTiles write failed", e) }
|
||||
}
|
||||
|
||||
private fun routeCommand(appDataDir: File, id: String) {
|
||||
Thread {
|
||||
val resultFile = File(appDataDir, "brouter_result_$id.json")
|
||||
val out = org.json.JSONObject()
|
||||
|
|
|
|||
|
|
@ -435,6 +435,54 @@ fn brouter_route(app: tauri::AppHandle, lonlats: String, profile: String) -> Res
|
|||
Err("brouter: timeout waiting for result".into())
|
||||
}
|
||||
|
||||
// Segment delivery (Stage 2). Kotlin owns the download so it lands in the exact dir the
|
||||
// engine reads (files/brouter/segments4). These commands just relay via the file bridge.
|
||||
fn brouter_write_cmd(app: &tauri::AppHandle, cmd: &str) -> Result<(), String> {
|
||||
let data_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
|
||||
std::fs::create_dir_all(&data_dir).ok();
|
||||
let _ = std::fs::write(data_dir.join("brouter_command.txt"), cmd);
|
||||
if let Some(r) = data_dir.parent().and_then(|p| p.parent()) {
|
||||
let _ = std::fs::write(r.join("brouter_command.txt"), cmd);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Start downloading a .rd5 tile (e.g. "E35_N45"). Returns immediately; poll status.
|
||||
#[tauri::command]
|
||||
fn brouter_download_tile(app: tauri::AppHandle, tile: String) -> Result<String, String> {
|
||||
brouter_write_cmd(&app, &format!("dltile:{}", tile))?;
|
||||
Ok("started".into())
|
||||
}
|
||||
|
||||
// Read the download status Kotlin writes (brouter_dl_<tile>.json): downloading/done/error.
|
||||
#[tauri::command]
|
||||
fn brouter_download_status(app: tauri::AppHandle, tile: String) -> Result<String, String> {
|
||||
let data_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
|
||||
let name = format!("brouter_dl_{}.json", tile);
|
||||
let mut candidates = vec![data_dir.join(&name)];
|
||||
if let Some(r) = data_dir.parent().and_then(|p| p.parent()) { candidates.push(r.join(&name)); }
|
||||
for p in &candidates {
|
||||
if let Ok(s) = std::fs::read_to_string(p) { if !s.trim().is_empty() { return Ok(s); } }
|
||||
}
|
||||
Ok("{}".into())
|
||||
}
|
||||
|
||||
// List downloaded tiles (asks Kotlin to refresh brouter_tiles.json, then reads it).
|
||||
#[tauri::command]
|
||||
fn brouter_list_tiles(app: tauri::AppHandle) -> Result<String, String> {
|
||||
brouter_write_cmd(&app, "listtiles")?;
|
||||
let data_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
|
||||
let mut candidates = vec![data_dir.join("brouter_tiles.json")];
|
||||
if let Some(r) = data_dir.parent().and_then(|p| p.parent()) { candidates.push(r.join("brouter_tiles.json")); }
|
||||
for _ in 0..50 {
|
||||
for p in &candidates {
|
||||
if let Ok(s) = std::fs::read_to_string(p) { if !s.trim().is_empty() { return Ok(s); } }
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
Ok("[]".into())
|
||||
}
|
||||
|
||||
// Read GPS points from service's JSONL file
|
||||
#[tauri::command]
|
||||
fn gps_service_read_points(app: tauri::AppHandle) -> Result<String, String> {
|
||||
|
|
@ -946,7 +994,7 @@ pub fn run() {
|
|||
.add_migrations("sqlite:baldav.db", migrations)
|
||||
.build(),
|
||||
)
|
||||
.invoke_handler(tauri::generate_handler![get_safe_insets, import_seed_products, import_tiles, download_file, get_points_filename, delete_points_file, gps_set_session_id, list_points_files, read_points_file, pick_file_open, get_picked_file, debug_native_paths, rebuild_roads, get_road_points, get_app_info, gps_service_command, gps_service_read_points, brouter_route, save_file, get_update_info, http_request, chzn_http, ble::ble_scan, ble::ble_scan_results, ble::ble_connect, ble::ble_disconnect, ble::ble_get_hr, ble::ble_is_connected, ble::ble_check_bluetooth, ble::ble_save_device, ble::ble_clear_hr])
|
||||
.invoke_handler(tauri::generate_handler![get_safe_insets, import_seed_products, import_tiles, download_file, get_points_filename, delete_points_file, gps_set_session_id, list_points_files, read_points_file, pick_file_open, get_picked_file, debug_native_paths, rebuild_roads, get_road_points, get_app_info, gps_service_command, gps_service_read_points, brouter_route, brouter_download_tile, brouter_download_status, brouter_list_tiles, save_file, get_update_info, http_request, chzn_http, ble::ble_scan, ble::ble_scan_results, ble::ble_connect, ble::ble_disconnect, ble::ble_get_hr, ble::ble_is_connected, ble::ble_check_bluetooth, ble::ble_save_device, ble::ble_clear_hr])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue