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:
4aiman 2026-10-26 21:08:22 +03:00
commit 0d5a5986ac
2 changed files with 92 additions and 2 deletions

View file

@ -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"

View file

@ -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");
}