undo osm
This commit is contained in:
@@ -1651,6 +1651,9 @@ pub struct RoadOut {
|
|||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub geojson: String,
|
pub geojson: String,
|
||||||
pub snap_ignored: bool,
|
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
|
// Generic over the executor: pass &state.pool for reads taken before any
|
||||||
@@ -2977,19 +2980,21 @@ pub async fn list_asset_names(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_osm_roads(state: State<'_, AppState>) -> Result<Vec<RoadOut>, String> {
|
pub async fn get_osm_roads(state: State<'_, AppState>) -> Result<Vec<RoadOut>, String> {
|
||||||
let rows: Vec<(i64, Option<String>, String, i64)> = sqlx::query_as(
|
let rows: Vec<(i64, Option<String>, String, i64, i64)> = sqlx::query_as(
|
||||||
"SELECT id, name, geojson, COALESCE(snap_ignored, 0) FROM roads",
|
"SELECT id, name, geojson,
|
||||||
|
COALESCE(snap_ignored, 0), COALESCE(modified, 0) FROM roads",
|
||||||
)
|
)
|
||||||
.fetch_all(&state.pool)
|
.fetch_all(&state.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
Ok(rows
|
Ok(rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(id, name, geojson, si)| RoadOut {
|
.map(|(id, name, geojson, si, m)| RoadOut {
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
geojson,
|
geojson,
|
||||||
snap_ignored: si != 0,
|
snap_ignored: si != 0,
|
||||||
|
modified: m != 0,
|
||||||
})
|
})
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
@@ -3342,8 +3347,8 @@ pub async fn merge_roads_by_name(
|
|||||||
"coordinates": coords_array,
|
"coordinates": coords_array,
|
||||||
});
|
});
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT INTO roads(name, geojson, min_lat, max_lat, min_lng, max_lng, oneway, highway, snap_ignored)
|
"INSERT INTO roads(name, geojson, min_lat, max_lat, min_lng, max_lng, oneway, highway, snap_ignored, modified)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1)",
|
||||||
)
|
)
|
||||||
.bind(&name)
|
.bind(&name)
|
||||||
.bind(geom.to_string())
|
.bind(geom.to_string())
|
||||||
@@ -3672,8 +3677,8 @@ pub async fn merge_roads_by_ids(
|
|||||||
"coordinates": coords_array,
|
"coordinates": coords_array,
|
||||||
});
|
});
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT INTO roads(name, geojson, min_lat, max_lat, min_lng, max_lng, oneway, highway, snap_ignored)
|
"INSERT INTO roads(name, geojson, min_lat, max_lat, min_lng, max_lng, oneway, highway, snap_ignored, modified)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1)",
|
||||||
)
|
)
|
||||||
.bind(if dom_name.is_empty() { None } else { Some(dom_name) })
|
.bind(if dom_name.is_empty() { None } else { Some(dom_name) })
|
||||||
.bind(geom.to_string())
|
.bind(geom.to_string())
|
||||||
@@ -4263,7 +4268,7 @@ pub async fn flip_road_direction(
|
|||||||
v if v > 0 => -1,
|
v if v > 0 => -1,
|
||||||
_ => 1,
|
_ => 1,
|
||||||
};
|
};
|
||||||
sqlx::query("UPDATE roads SET oneway = ? WHERE id = ?")
|
sqlx::query("UPDATE roads SET oneway = ?, modified = 1 WHERE id = ?")
|
||||||
.bind(next)
|
.bind(next)
|
||||||
.bind(id)
|
.bind(id)
|
||||||
.execute(&state.pool)
|
.execute(&state.pool)
|
||||||
@@ -4348,8 +4353,8 @@ pub async fn create_osm_road(
|
|||||||
"coordinates": coords,
|
"coordinates": coords,
|
||||||
});
|
});
|
||||||
let row: (i64,) = sqlx::query_as(
|
let row: (i64,) = sqlx::query_as(
|
||||||
"INSERT INTO roads(name, geojson, min_lat, max_lat, min_lng, max_lng)
|
"INSERT INTO roads(name, geojson, min_lat, max_lat, min_lng, max_lng, modified)
|
||||||
VALUES (?, ?, ?, ?, ?, ?) RETURNING id",
|
VALUES (?, ?, ?, ?, ?, ?, 1) RETURNING id",
|
||||||
)
|
)
|
||||||
.bind(&name)
|
.bind(&name)
|
||||||
.bind(geom.to_string())
|
.bind(geom.to_string())
|
||||||
@@ -4525,7 +4530,7 @@ pub async fn delete_road_section(
|
|||||||
});
|
});
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"UPDATE roads SET geojson = ?, min_lat = ?, max_lat = ?,
|
"UPDATE roads SET geojson = ?, min_lat = ?, max_lat = ?,
|
||||||
min_lng = ?, max_lng = ?
|
min_lng = ?, max_lng = ?, modified = 1
|
||||||
WHERE id = ?",
|
WHERE id = ?",
|
||||||
)
|
)
|
||||||
.bind(geom.to_string())
|
.bind(geom.to_string())
|
||||||
@@ -4563,8 +4568,8 @@ pub async fn delete_road_section(
|
|||||||
});
|
});
|
||||||
let inserted: (i64,) = sqlx::query_as(
|
let inserted: (i64,) = sqlx::query_as(
|
||||||
"INSERT INTO roads(name, geojson, min_lat, max_lat, min_lng, max_lng,
|
"INSERT INTO roads(name, geojson, min_lat, max_lat, min_lng, max_lng,
|
||||||
oneway, highway, snap_ignored)
|
oneway, highway, snap_ignored, modified)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id",
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1) RETURNING id",
|
||||||
)
|
)
|
||||||
.bind(&name)
|
.bind(&name)
|
||||||
.bind(geom.to_string())
|
.bind(geom.to_string())
|
||||||
@@ -4594,6 +4599,10 @@ pub struct LassoRoadDeleteResult {
|
|||||||
pub roads_deleted: usize,
|
pub roads_deleted: usize,
|
||||||
pub roads_split: usize,
|
pub roads_split: usize,
|
||||||
pub new_road_ids: Vec<i64>,
|
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.
|
/// 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; }
|
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(
|
sqlx::query_as(
|
||||||
"SELECT r.id, r.name, r.geojson, COALESCE(r.oneway, 0), r.highway,
|
"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
|
FROM roads r JOIN roads_rtree x ON x.id = r.id
|
||||||
WHERE x.max_lat >= ? AND x.min_lat <= ?
|
WHERE x.max_lat >= ? AND x.min_lat <= ?
|
||||||
AND x.max_lng >= ? AND x.min_lng <= ?",
|
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_deleted = 0usize;
|
||||||
let mut roads_split = 0usize;
|
let mut roads_split = 0usize;
|
||||||
let mut new_road_ids: Vec<i64> = Vec::new();
|
let mut new_road_ids: Vec<i64> = Vec::new();
|
||||||
|
let mut before_snapshots: Vec<RoadSnapshot> = Vec::new();
|
||||||
let examined = candidate_rows.len();
|
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) {
|
let val: serde_json::Value = match serde_json::from_str(&geojson) {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(_) => continue,
|
Err(_) => continue,
|
||||||
@@ -4720,6 +4730,15 @@ pub async fn delete_roads_in_lasso(
|
|||||||
|
|
||||||
if inside_flags.iter().all(|&b| b) {
|
if inside_flags.iter().all(|&b| b) {
|
||||||
// Every vertex inside; assume edges are too. Delete outright.
|
// 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 = ?")
|
sqlx::query("DELETE FROM roads WHERE id = ?")
|
||||||
.bind(id)
|
.bind(id)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
@@ -4798,8 +4817,17 @@ pub async fn delete_roads_in_lasso(
|
|||||||
close_run(&mut kept_runs, &mut cur);
|
close_run(&mut kept_runs, &mut cur);
|
||||||
|
|
||||||
// The road had at least one inside vertex (we early-returned the
|
// The road had at least one inside vertex (we early-returned the
|
||||||
// wholly-inside case above), so SOMETHING changed: delete the original
|
// wholly-inside case above), so SOMETHING changed: snapshot, delete
|
||||||
// and re-insert the surviving outside runs.
|
// 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 = ?")
|
sqlx::query("DELETE FROM roads WHERE id = ?")
|
||||||
.bind(id)
|
.bind(id)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
@@ -4817,8 +4845,8 @@ pub async fn delete_roads_in_lasso(
|
|||||||
});
|
});
|
||||||
let inserted: (i64,) = sqlx::query_as(
|
let inserted: (i64,) = sqlx::query_as(
|
||||||
"INSERT INTO roads(name, geojson, min_lat, max_lat, min_lng, max_lng,
|
"INSERT INTO roads(name, geojson, min_lat, max_lat, min_lng, max_lng,
|
||||||
oneway, highway, snap_ignored)
|
oneway, highway, snap_ignored, modified)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id",
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1) RETURNING id",
|
||||||
)
|
)
|
||||||
.bind(&name)
|
.bind(&name)
|
||||||
.bind(geom.to_string())
|
.bind(geom.to_string())
|
||||||
@@ -4840,6 +4868,7 @@ pub async fn delete_roads_in_lasso(
|
|||||||
roads_deleted,
|
roads_deleted,
|
||||||
roads_split,
|
roads_split,
|
||||||
new_road_ids,
|
new_road_ids,
|
||||||
|
before_snapshots,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4913,6 +4942,10 @@ pub struct RoadSnapshot {
|
|||||||
pub oneway: i64,
|
pub oneway: i64,
|
||||||
pub highway: Option<String>,
|
pub highway: Option<String>,
|
||||||
pub snap_ignored: i64,
|
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
|
/// 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 placeholders = vec!["?"; chunk.len()].join(",");
|
||||||
let sql = format!(
|
let sql = format!(
|
||||||
"SELECT id, name, geojson, COALESCE(oneway, 0), highway,
|
"SELECT id, name, geojson, COALESCE(oneway, 0), highway,
|
||||||
COALESCE(snap_ignored, 0)
|
COALESCE(snap_ignored, 0), COALESCE(modified, 0)
|
||||||
FROM roads WHERE id IN ({})",
|
FROM roads WHERE id IN ({})",
|
||||||
placeholders
|
placeholders
|
||||||
);
|
);
|
||||||
let mut q = sqlx::query_as::<
|
let mut q = sqlx::query_as::<
|
||||||
_,
|
_,
|
||||||
(i64, Option<String>, String, i64, Option<String>, i64),
|
(i64, Option<String>, String, i64, Option<String>, i64, i64),
|
||||||
>(&sql);
|
>(&sql);
|
||||||
for &i in chunk {
|
for &i in chunk {
|
||||||
q = q.bind(i);
|
q = q.bind(i);
|
||||||
}
|
}
|
||||||
let rows = q.fetch_all(&state.pool).await.map_err(|e| e.to_string())?;
|
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 {
|
out.push(RoadSnapshot {
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
@@ -4951,15 +4984,17 @@ pub async fn osm_road_snapshot(
|
|||||||
oneway,
|
oneway,
|
||||||
highway,
|
highway,
|
||||||
snap_ignored,
|
snap_ignored,
|
||||||
|
modified,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replay a list of snapshots back into `roads`, updating geometry/bbox/tags
|
/// Replay a list of snapshots back into `roads`. Upserts by id: existing rows
|
||||||
/// for each id. Rows that no longer exist (e.g. consumed by a merge) are
|
/// are updated in place; deleted rows are re-inserted with their original id
|
||||||
/// skipped — this undo is not designed to revive merged roads.
|
/// (lasso delete uses this path). The R-tree trigger handles inserts; updates
|
||||||
|
/// are mirrored manually because there's no AFTER UPDATE trigger.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn restore_road_state(
|
pub async fn restore_road_state(
|
||||||
snapshots: Vec<RoadSnapshot>,
|
snapshots: Vec<RoadSnapshot>,
|
||||||
@@ -4987,7 +5022,7 @@ pub async fn restore_road_state(
|
|||||||
"UPDATE roads
|
"UPDATE roads
|
||||||
SET name = ?, geojson = ?,
|
SET name = ?, geojson = ?,
|
||||||
min_lat = ?, max_lat = ?, min_lng = ?, max_lng = ?,
|
min_lat = ?, max_lat = ?, min_lng = ?, max_lng = ?,
|
||||||
oneway = ?, highway = ?, snap_ignored = ?
|
oneway = ?, highway = ?, snap_ignored = ?, modified = ?
|
||||||
WHERE id = ?",
|
WHERE id = ?",
|
||||||
)
|
)
|
||||||
.bind(&s.name)
|
.bind(&s.name)
|
||||||
@@ -4999,6 +5034,7 @@ pub async fn restore_road_state(
|
|||||||
.bind(s.oneway)
|
.bind(s.oneway)
|
||||||
.bind(&s.highway)
|
.bind(&s.highway)
|
||||||
.bind(s.snap_ignored)
|
.bind(s.snap_ignored)
|
||||||
|
.bind(s.modified)
|
||||||
.bind(s.id)
|
.bind(s.id)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
@@ -5024,6 +5060,29 @@ pub async fn restore_road_state(
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
applied += 1;
|
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())?;
|
tx.commit().await.map_err(|e| e.to_string())?;
|
||||||
@@ -5067,7 +5126,7 @@ pub async fn update_osm_road(
|
|||||||
"coordinates": coords,
|
"coordinates": coords,
|
||||||
});
|
});
|
||||||
sqlx::query(
|
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(geom.to_string())
|
||||||
.bind(min_lat)
|
.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")
|
sqlx::query("ALTER TABLE roads ADD COLUMN snap_ignored INTEGER NOT NULL DEFAULT 0")
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await;
|
.await;
|
||||||
|
let _ =
|
||||||
|
sqlx::query("ALTER TABLE roads ADD COLUMN modified INTEGER NOT NULL DEFAULT 0")
|
||||||
|
.execute(pool)
|
||||||
|
.await;
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"CREATE TABLE IF NOT EXISTS users (
|
"CREATE TABLE IF NOT EXISTS users (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -194,7 +198,8 @@ async fn apply_schema(pool: &SqlitePool) -> Result<()> {
|
|||||||
max_lng REAL NOT NULL,
|
max_lng REAL NOT NULL,
|
||||||
oneway INTEGER NOT NULL DEFAULT 0,
|
oneway INTEGER NOT NULL DEFAULT 0,
|
||||||
highway TEXT,
|
highway TEXT,
|
||||||
snap_ignored INTEGER NOT NULL DEFAULT 0
|
snap_ignored INTEGER NOT NULL DEFAULT 0,
|
||||||
|
modified INTEGER NOT NULL DEFAULT 0
|
||||||
)",
|
)",
|
||||||
)
|
)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
|
|||||||
Reference in New Issue
Block a user