undo osm
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user