From 0336dacf4b7fbb8440f6a0c2eb809009707cebe2 Mon Sep 17 00:00:00 2001 From: ravi Date: Wed, 13 May 2026 17:17:34 +0530 Subject: [PATCH] undo osm --- src-tauri/src/commands.rs | 115 ++++++++++++++++++++++++++++---------- src-tauri/src/db.rs | 7 ++- 2 files changed, 93 insertions(+), 29 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 0bc18e3..7cd609c 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1651,6 +1651,9 @@ pub struct RoadOut { pub name: Option, pub geojson: String, pub snap_ignored: bool, + // True if the road's geometry has been touched since import. Drives the + // green highlight in the OSM layer so users can see what they've changed. + pub modified: bool, } // Generic over the executor: pass &state.pool for reads taken before any @@ -2977,19 +2980,21 @@ pub async fn list_asset_names( #[tauri::command] pub async fn get_osm_roads(state: State<'_, AppState>) -> Result, String> { - let rows: Vec<(i64, Option, String, i64)> = sqlx::query_as( - "SELECT id, name, geojson, COALESCE(snap_ignored, 0) FROM roads", + let rows: Vec<(i64, Option, String, i64, i64)> = sqlx::query_as( + "SELECT id, name, geojson, + COALESCE(snap_ignored, 0), COALESCE(modified, 0) FROM roads", ) .fetch_all(&state.pool) .await .map_err(|e| e.to_string())?; Ok(rows .into_iter() - .map(|(id, name, geojson, si)| RoadOut { + .map(|(id, name, geojson, si, m)| RoadOut { id, name, geojson, snap_ignored: si != 0, + modified: m != 0, }) .collect()) } @@ -3342,8 +3347,8 @@ pub async fn merge_roads_by_name( "coordinates": coords_array, }); sqlx::query( - "INSERT INTO roads(name, geojson, min_lat, max_lat, min_lng, max_lng, oneway, highway, snap_ignored) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", + "INSERT INTO roads(name, geojson, min_lat, max_lat, min_lng, max_lng, oneway, highway, snap_ignored, modified) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1)", ) .bind(&name) .bind(geom.to_string()) @@ -3672,8 +3677,8 @@ pub async fn merge_roads_by_ids( "coordinates": coords_array, }); sqlx::query( - "INSERT INTO roads(name, geojson, min_lat, max_lat, min_lng, max_lng, oneway, highway, snap_ignored) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", + "INSERT INTO roads(name, geojson, min_lat, max_lat, min_lng, max_lng, oneway, highway, snap_ignored, modified) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1)", ) .bind(if dom_name.is_empty() { None } else { Some(dom_name) }) .bind(geom.to_string()) @@ -4263,7 +4268,7 @@ pub async fn flip_road_direction( v if v > 0 => -1, _ => 1, }; - sqlx::query("UPDATE roads SET oneway = ? WHERE id = ?") + sqlx::query("UPDATE roads SET oneway = ?, modified = 1 WHERE id = ?") .bind(next) .bind(id) .execute(&state.pool) @@ -4348,8 +4353,8 @@ pub async fn create_osm_road( "coordinates": coords, }); let row: (i64,) = sqlx::query_as( - "INSERT INTO roads(name, geojson, min_lat, max_lat, min_lng, max_lng) - VALUES (?, ?, ?, ?, ?, ?) RETURNING id", + "INSERT INTO roads(name, geojson, min_lat, max_lat, min_lng, max_lng, modified) + VALUES (?, ?, ?, ?, ?, ?, 1) RETURNING id", ) .bind(&name) .bind(geom.to_string()) @@ -4525,7 +4530,7 @@ pub async fn delete_road_section( }); sqlx::query( "UPDATE roads SET geojson = ?, min_lat = ?, max_lat = ?, - min_lng = ?, max_lng = ? + min_lng = ?, max_lng = ?, modified = 1 WHERE id = ?", ) .bind(geom.to_string()) @@ -4563,8 +4568,8 @@ pub async fn delete_road_section( }); let inserted: (i64,) = sqlx::query_as( "INSERT INTO roads(name, geojson, min_lat, max_lat, min_lng, max_lng, - oneway, highway, snap_ignored) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id", + oneway, highway, snap_ignored, modified) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1) RETURNING id", ) .bind(&name) .bind(geom.to_string()) @@ -4594,6 +4599,10 @@ pub struct LassoRoadDeleteResult { pub roads_deleted: usize, pub roads_split: usize, pub new_road_ids: Vec, + // Snapshots of every road that was mutated (fully deleted or partially + // clipped). The OSM-edit undo replays these via restore_road_state, which + // re-inserts rows whose ids no longer exist. + pub before_snapshots: Vec, } /// Bulk-delete road sections that fall inside a lasso polygon. @@ -4625,10 +4634,10 @@ pub async fn delete_roads_in_lasso( if lng > p_mx_lng { p_mx_lng = lng; } } - let candidate_rows: Vec<(i64, Option, String, i64, Option, i64)> = + let candidate_rows: Vec<(i64, Option, String, i64, Option, i64, i64)> = sqlx::query_as( "SELECT r.id, r.name, r.geojson, COALESCE(r.oneway, 0), r.highway, - COALESCE(r.snap_ignored, 0) + COALESCE(r.snap_ignored, 0), COALESCE(r.modified, 0) FROM roads r JOIN roads_rtree x ON x.id = r.id WHERE x.max_lat >= ? AND x.min_lat <= ? AND x.max_lng >= ? AND x.min_lng <= ?", @@ -4695,9 +4704,10 @@ pub async fn delete_roads_in_lasso( let mut roads_deleted = 0usize; let mut roads_split = 0usize; let mut new_road_ids: Vec = Vec::new(); + let mut before_snapshots: Vec = Vec::new(); let examined = candidate_rows.len(); - for (id, name, geojson, oneway, highway, snap_ignored) in candidate_rows { + for (id, name, geojson, oneway, highway, snap_ignored, modified) in candidate_rows { let val: serde_json::Value = match serde_json::from_str(&geojson) { Ok(v) => v, Err(_) => continue, @@ -4720,6 +4730,15 @@ pub async fn delete_roads_in_lasso( if inside_flags.iter().all(|&b| b) { // Every vertex inside; assume edges are too. Delete outright. + before_snapshots.push(RoadSnapshot { + id, + name: name.clone(), + geojson: geojson.clone(), + oneway, + highway: highway.clone(), + snap_ignored, + modified, + }); sqlx::query("DELETE FROM roads WHERE id = ?") .bind(id) .execute(&mut *tx) @@ -4798,8 +4817,17 @@ pub async fn delete_roads_in_lasso( close_run(&mut kept_runs, &mut cur); // The road had at least one inside vertex (we early-returned the - // wholly-inside case above), so SOMETHING changed: delete the original - // and re-insert the surviving outside runs. + // wholly-inside case above), so SOMETHING changed: snapshot, delete + // the original, and re-insert the surviving outside runs. + before_snapshots.push(RoadSnapshot { + id, + name: name.clone(), + geojson: geojson.clone(), + oneway, + highway: highway.clone(), + snap_ignored, + modified, + }); sqlx::query("DELETE FROM roads WHERE id = ?") .bind(id) .execute(&mut *tx) @@ -4817,8 +4845,8 @@ pub async fn delete_roads_in_lasso( }); let inserted: (i64,) = sqlx::query_as( "INSERT INTO roads(name, geojson, min_lat, max_lat, min_lng, max_lng, - oneway, highway, snap_ignored) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id", + oneway, highway, snap_ignored, modified) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1) RETURNING id", ) .bind(&name) .bind(geom.to_string()) @@ -4840,6 +4868,7 @@ pub async fn delete_roads_in_lasso( roads_deleted, roads_split, new_road_ids, + before_snapshots, }) } @@ -4913,6 +4942,10 @@ pub struct RoadSnapshot { pub oneway: i64, pub highway: Option, pub snap_ignored: i64, + // Snapshot the modified flag too — undoing a mutation should also revert + // the green-highlight indicator to its pre-mutation state. + #[serde(default)] + pub modified: i64, } /// Capture the full state of a small set of roads so the frontend can revert @@ -4931,19 +4964,19 @@ pub async fn osm_road_snapshot( let placeholders = vec!["?"; chunk.len()].join(","); let sql = format!( "SELECT id, name, geojson, COALESCE(oneway, 0), highway, - COALESCE(snap_ignored, 0) + COALESCE(snap_ignored, 0), COALESCE(modified, 0) FROM roads WHERE id IN ({})", placeholders ); let mut q = sqlx::query_as::< _, - (i64, Option, String, i64, Option, i64), + (i64, Option, String, i64, Option, i64, i64), >(&sql); for &i in chunk { q = q.bind(i); } let rows = q.fetch_all(&state.pool).await.map_err(|e| e.to_string())?; - for (id, name, geojson, oneway, highway, snap_ignored) in rows { + for (id, name, geojson, oneway, highway, snap_ignored, modified) in rows { out.push(RoadSnapshot { id, name, @@ -4951,15 +4984,17 @@ pub async fn osm_road_snapshot( oneway, highway, snap_ignored, + modified, }); } } Ok(out) } -/// Replay a list of snapshots back into `roads`, updating geometry/bbox/tags -/// for each id. Rows that no longer exist (e.g. consumed by a merge) are -/// skipped — this undo is not designed to revive merged roads. +/// Replay a list of snapshots back into `roads`. Upserts by id: existing rows +/// are updated in place; deleted rows are re-inserted with their original id +/// (lasso delete uses this path). The R-tree trigger handles inserts; updates +/// are mirrored manually because there's no AFTER UPDATE trigger. #[tauri::command] pub async fn restore_road_state( snapshots: Vec, @@ -4987,7 +5022,7 @@ pub async fn restore_road_state( "UPDATE roads SET name = ?, geojson = ?, min_lat = ?, max_lat = ?, min_lng = ?, max_lng = ?, - oneway = ?, highway = ?, snap_ignored = ? + oneway = ?, highway = ?, snap_ignored = ?, modified = ? WHERE id = ?", ) .bind(&s.name) @@ -4999,6 +5034,7 @@ pub async fn restore_road_state( .bind(s.oneway) .bind(&s.highway) .bind(s.snap_ignored) + .bind(s.modified) .bind(s.id) .execute(&mut *tx) .await @@ -5024,6 +5060,29 @@ pub async fn restore_road_state( .await .map_err(|e| e.to_string())?; applied += 1; + } else { + // Row no longer exists — re-insert with the original id. The + // AFTER INSERT trigger on roads will populate roads_rtree. + sqlx::query( + "INSERT INTO roads(id, name, geojson, min_lat, max_lat, + min_lng, max_lng, oneway, highway, snap_ignored, modified) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + ) + .bind(s.id) + .bind(&s.name) + .bind(&s.geojson) + .bind(mn_lat) + .bind(mx_lat) + .bind(mn_lng) + .bind(mx_lng) + .bind(s.oneway) + .bind(&s.highway) + .bind(s.snap_ignored) + .bind(s.modified) + .execute(&mut *tx) + .await + .map_err(|e| e.to_string())?; + applied += 1; } } tx.commit().await.map_err(|e| e.to_string())?; @@ -5067,7 +5126,7 @@ pub async fn update_osm_road( "coordinates": coords, }); sqlx::query( - "UPDATE roads SET geojson = ?, min_lat = ?, max_lat = ?, min_lng = ?, max_lng = ? WHERE id = ?", + "UPDATE roads SET geojson = ?, min_lat = ?, max_lat = ?, min_lng = ?, max_lng = ?, modified = 1 WHERE id = ?", ) .bind(geom.to_string()) .bind(min_lat) diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index 7de168c..104c32c 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -108,6 +108,10 @@ async fn apply_schema(pool: &SqlitePool) -> Result<()> { sqlx::query("ALTER TABLE roads ADD COLUMN snap_ignored INTEGER NOT NULL DEFAULT 0") .execute(pool) .await; + let _ = + sqlx::query("ALTER TABLE roads ADD COLUMN modified INTEGER NOT NULL DEFAULT 0") + .execute(pool) + .await; sqlx::query( "CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -194,7 +198,8 @@ async fn apply_schema(pool: &SqlitePool) -> Result<()> { max_lng REAL NOT NULL, oneway INTEGER NOT NULL DEFAULT 0, highway TEXT, - snap_ignored INTEGER NOT NULL DEFAULT 0 + snap_ignored INTEGER NOT NULL DEFAULT 0, + modified INTEGER NOT NULL DEFAULT 0 )", ) .execute(pool)