This commit is contained in:
2026-05-13 17:17:34 +05:30
parent 2660ef12b9
commit 0336dacf4b
2 changed files with 93 additions and 29 deletions

View File

@@ -1651,6 +1651,9 @@ pub struct RoadOut {
pub name: Option<String>,
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<Vec<RoadOut>, String> {
let rows: Vec<(i64, Option<String>, String, i64)> = sqlx::query_as(
"SELECT id, name, geojson, COALESCE(snap_ignored, 0) FROM roads",
let rows: Vec<(i64, Option<String>, 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<i64>,
// 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<RoadSnapshot>,
}
/// 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>, String, i64, Option<String>, i64)> =
let candidate_rows: Vec<(i64, Option<String>, String, i64, Option<String>, 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<i64> = Vec::new();
let mut before_snapshots: Vec<RoadSnapshot> = 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<String>,
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>, String, i64, Option<String>, i64),
(i64, Option<String>, String, i64, Option<String>, 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<RoadSnapshot>,
@@ -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)

View File

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