Elevation correction from ridden tracks (SRTM base + median offset)
get_elev_correction (Rust): per ~20m cell, the median (altitude_gps_m − altitude_srtm_m) offset over >= N distinct-session passes. SRTM stays the base; this is only the up/down nudge where you've actually ridden. JS applies a smoothed per-cell delta to a route's SRTM elevations in the preview (non-destructive; no coverage -> unchanged).
This commit is contained in:
parent
ac37f36a06
commit
0d5a5986ac
2 changed files with 92 additions and 2 deletions
|
|
@ -3996,10 +3996,51 @@ async function showRoutesScreen(container) {
|
|||
// Route preview: map (topo casing + tappable altitude/grade points) + elevation
|
||||
// profile chart + ascent/descent/max-grade — so you can see how hilly a hand-drawn
|
||||
// route is BEFORE riding it. Reuses fetchElevations / GPS.routeElevStats / Chart.js.
|
||||
// Elevation correction grid from ridden tracks (Rust get_elev_correction): per ~20m
|
||||
// cell, the median (measured − SRTM) offset over >= N passes. Cached per session.
|
||||
let _elevCorr = null;
|
||||
async function loadElevCorrection(force) {
|
||||
if (_elevCorr && !force) return _elevCorr;
|
||||
let minPasses = parseInt(localStorage.getItem("baldav_elev_corr_passes")) || 3;
|
||||
let raw = await window.__TAURI__.core.invoke("get_elev_correction", { minPasses });
|
||||
let arr = JSON.parse(raw) || [];
|
||||
let dLat = 0.00018, dLon = 0.00027, cells = new Map();
|
||||
for (let [lat, lon, delta] of arr) cells.set(Math.round(lat / dLat) + "," + Math.round(lon / dLon), delta);
|
||||
_elevCorr = { cells, dLat, dLon };
|
||||
return _elevCorr;
|
||||
}
|
||||
// SRTM stays the base; nudge each point up/down by the SMOOTHED ridden offset where
|
||||
// you've ridden enough. No coverage -> delta 0 -> unchanged. Non-destructive (returns
|
||||
// a copy), so the stored route keeps its original elevations.
|
||||
async function applyRiddenElevCorrection(pts) {
|
||||
let corr;
|
||||
try { corr = await loadElevCorrection(); } catch { return pts; }
|
||||
if (!corr || !corr.cells.size) return pts;
|
||||
let deltas = pts.map(p => {
|
||||
if (p.ele == null) return 0;
|
||||
let ci = Math.round(p.lat / corr.dLat), cj = Math.round(p.lon / corr.dLon);
|
||||
let v0 = corr.cells.get(ci + "," + cj);
|
||||
if (v0 != null) return v0;
|
||||
for (let di = -1; di <= 1; di++) for (let dj = -1; dj <= 1; dj++) {
|
||||
let v = corr.cells.get((ci + di) + "," + (cj + dj));
|
||||
if (v != null) return v;
|
||||
}
|
||||
return 0; // no ridden coverage here
|
||||
});
|
||||
let half = 3;
|
||||
return pts.map((p, i) => {
|
||||
if (p.ele == null) return p;
|
||||
let sum = 0, cnt = 0;
|
||||
for (let j = Math.max(0, i - half); j <= Math.min(pts.length - 1, i + half); j++) { sum += deltas[j]; cnt++; }
|
||||
let d = cnt ? sum / cnt : 0;
|
||||
return d ? { ...p, ele: p.ele + d } : p;
|
||||
});
|
||||
}
|
||||
|
||||
async function showRouteDetail(container, routeId) {
|
||||
let route = await DB.getRoute(routeId);
|
||||
if (!route || !route.points || route.points.length < 2) { showToast("Маршрут пуст"); showRoutesScreen(container); return; }
|
||||
let pts = route.points;
|
||||
let pts = await applyRiddenElevCorrection(route.points);
|
||||
let st = GPS.routeElevStats(pts);
|
||||
let mode = localStorage.getItem("baldav_route_color_mode") || "slope";
|
||||
let legendHtml = mode === "slope"
|
||||
|
|
|
|||
|
|
@ -441,6 +441,55 @@ fn get_ride_graph(app: tauri::AppHandle, min_speed_ms: f64) -> Result<String, St
|
|||
Ok(format!("{{\"nodes\":[{}],\"edges\":[{}]}}", nodes_json, edges_json))
|
||||
}
|
||||
|
||||
// Elevation correction grid from ridden tracks: per ~20m cell, the MEDIAN vertical
|
||||
// offset (measured altitude_gps_m minus terrain altitude_srtm_m) over all passes.
|
||||
// SRTM stays the base; this is only the up/down nudge where you've actually ridden.
|
||||
// Returns [[cellLat, cellLon, medianDelta, passes], ...] for cells ridden on at least
|
||||
// `min_passes` distinct sessions. The JS side adds a smoothed delta onto SRTM elevations.
|
||||
#[tauri::command]
|
||||
fn get_elev_correction(app: tauri::AppHandle, min_passes: u32) -> Result<String, String> {
|
||||
use std::collections::{HashMap, HashSet};
|
||||
let data_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
|
||||
let db_path = {
|
||||
let p1 = data_dir.join("../../baldav.db");
|
||||
let p2 = data_dir.join("baldav.db");
|
||||
let p3 = std::path::PathBuf::from(data_dir.to_str().unwrap_or("").replace("/files/dev.baldav.app", "")).join("baldav.db");
|
||||
if p1.exists() { p1 } else if p2.exists() { p2 } else { p3 }
|
||||
};
|
||||
let db = rusqlite::Connection::open(&db_path).map_err(|e| format!("open: {e}"))?;
|
||||
let d_lat = 0.00018_f64; // ~20 m (same grid as the ride graph)
|
||||
let d_lon = 0.00027_f64;
|
||||
let mut stmt = db.prepare(
|
||||
"SELECT lat, lon, altitude_gps_m, altitude_srtm_m, session_id FROM gps_points
|
||||
WHERE altitude_gps_m IS NOT NULL AND altitude_srtm_m IS NOT NULL AND COALESCE(walk,0)=0"
|
||||
).map_err(|e| format!("prepare: {e}"))?;
|
||||
let rows = stmt.query_map([], |r| {
|
||||
Ok((r.get::<_, f64>(0)?, r.get::<_, f64>(1)?, r.get::<_, f64>(2)?, r.get::<_, f64>(3)?, r.get::<_, i64>(4)?))
|
||||
}).map_err(|e| format!("query: {e}"))?;
|
||||
|
||||
let mut cells: HashMap<(i64, i64), (Vec<f64>, HashSet<i64>)> = HashMap::new();
|
||||
for row in rows {
|
||||
let (lat, lon, gps, srtm, sid) = row.map_err(|e| format!("row: {e}"))?;
|
||||
let delta = gps - srtm;
|
||||
if !delta.is_finite() || delta.abs() > 300.0 { continue; } // guard against garbage fixes
|
||||
let c = ((lat / d_lat).round() as i64, (lon / d_lon).round() as i64);
|
||||
let e = cells.entry(c).or_insert_with(|| (Vec::new(), HashSet::new()));
|
||||
e.0.push(delta);
|
||||
e.1.insert(sid);
|
||||
}
|
||||
let mut out: Vec<String> = Vec::new();
|
||||
for ((clat, clon), (mut deltas, sessions)) in cells {
|
||||
if (sessions.len() as u32) < min_passes { continue; }
|
||||
deltas.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
||||
let m = deltas.len();
|
||||
let median = if m % 2 == 1 { deltas[m / 2] } else { (deltas[m / 2 - 1] + deltas[m / 2]) / 2.0 };
|
||||
let lat = (clat as f64) * d_lat;
|
||||
let lon = (clon as f64) * d_lon;
|
||||
out.push(format!("[{:.6},{:.6},{:.2},{}]", lat, lon, median, sessions.len()));
|
||||
}
|
||||
Ok(format!("[{}]", out.join(",")))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct AppInfo {
|
||||
version: String,
|
||||
|
|
@ -1261,7 +1310,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_ride_graph, get_app_info, gps_service_command, set_hydration_config, set_live_config, send_sms, pick_contact, get_contact_result, gps_service_read_points, brouter_route, brouter_download_tile, brouter_download_status, brouter_list_tiles, save_file, export_image, read_file_base64, export_db, import_db, restart_app, 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, ble::ble_read_hr_log, ble::ble_read_strap_uptime, ble::ble_reset_strap_uptime, ble::ble_request_enable, read_state_file])
|
||||
.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_ride_graph, get_elev_correction, get_app_info, gps_service_command, set_hydration_config, set_live_config, send_sms, pick_contact, get_contact_result, gps_service_read_points, brouter_route, brouter_download_tile, brouter_download_status, brouter_list_tiles, save_file, export_image, read_file_base64, export_db, import_db, restart_app, 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, ble::ble_read_hr_log, ble::ble_read_strap_uptime, ble::ble_reset_strap_uptime, ble::ble_request_enable, read_state_file])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue