link_pair bug
This commit is contained in:
@@ -21,6 +21,8 @@ struct FixedAssetInput {
|
|||||||
deleted: bool,
|
deleted: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
side: Option<String>,
|
side: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
lane_side: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@@ -85,6 +87,8 @@ struct RangeAssetInput {
|
|||||||
deleted: bool,
|
deleted: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
side: Option<String>,
|
side: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
lane_side: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn opt_num_or_str_to_f64(v: &Option<NumOrStr>) -> Option<f64> {
|
fn opt_num_or_str_to_f64(v: &Option<NumOrStr>) -> Option<f64> {
|
||||||
@@ -204,6 +208,7 @@ pub struct Asset {
|
|||||||
pub merged_into: Option<i64>,
|
pub merged_into: Option<i64>,
|
||||||
pub in_scope: Option<i64>,
|
pub in_scope: Option<i64>,
|
||||||
pub side: Option<String>,
|
pub side: Option<String>,
|
||||||
|
pub lane_side: Option<String>,
|
||||||
pub link_pair_id: Option<i64>,
|
pub link_pair_id: Option<i64>,
|
||||||
pub link_locked: Option<i64>,
|
pub link_locked: Option<i64>,
|
||||||
}
|
}
|
||||||
@@ -216,6 +221,17 @@ fn normalize_side(s: &str) -> Option<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lane side stays as canonical "LHS" / "RHS" — these label the carriageway
|
||||||
|
// itself (from the import-time site_id pair), distinct from the per-asset
|
||||||
|
// left/right-of-vehicle `side`.
|
||||||
|
fn normalize_lane_side(s: &str) -> Option<String> {
|
||||||
|
match s.trim().to_uppercase().as_str() {
|
||||||
|
"LHS" | "LEFT" | "L" => Some("LHS".to_string()),
|
||||||
|
"RHS" | "RIGHT" | "R" => Some("RHS".to_string()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Per-row insert helpers extracted so the auto-detect importer
|
// Per-row insert helpers extracted so the auto-detect importer
|
||||||
// (`import_assets_auto`) can share the exact INSERT/ON CONFLICT logic with
|
// (`import_assets_auto`) can share the exact INSERT/ON CONFLICT logic with
|
||||||
// the type-specific importers — keeps re-import preserve-edits semantics
|
// the type-specific importers — keeps re-import preserve-edits semantics
|
||||||
@@ -226,13 +242,14 @@ async fn insert_fixed_row(
|
|||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let (lat, lng) = input.coord.to_lat_lng()?;
|
let (lat, lng) = input.coord.to_lat_lng()?;
|
||||||
let side = input.side.as_deref().and_then(normalize_side);
|
let side = input.side.as_deref().and_then(normalize_side);
|
||||||
|
let lane_side = input.lane_side.as_deref().and_then(normalize_lane_side);
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO assets (
|
INSERT INTO assets (
|
||||||
row_id, asset_type, asset_name, video_name,
|
row_id, asset_type, asset_name, video_name,
|
||||||
lat, lng, min_lat, max_lat, min_lng, max_lng,
|
lat, lng, min_lat, max_lat, min_lng, max_lng,
|
||||||
image_path, deleted, side
|
image_path, deleted, side, lane_side
|
||||||
) VALUES (?, 'fixed', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, 'fixed', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT(row_id) DO UPDATE SET
|
ON CONFLICT(row_id) DO UPDATE SET
|
||||||
asset_type = excluded.asset_type,
|
asset_type = excluded.asset_type,
|
||||||
asset_name = excluded.asset_name,
|
asset_name = excluded.asset_name,
|
||||||
@@ -256,7 +273,8 @@ async fn insert_fixed_row(
|
|||||||
end_lat = NULL,
|
end_lat = NULL,
|
||||||
end_lng = NULL,
|
end_lng = NULL,
|
||||||
deleted = CASE WHEN assets.deleted = 1 THEN 1 ELSE excluded.deleted END,
|
deleted = CASE WHEN assets.deleted = 1 THEN 1 ELSE excluded.deleted END,
|
||||||
side = CASE WHEN assets.modified = 0 THEN excluded.side ELSE assets.side END
|
side = CASE WHEN assets.modified = 0 THEN excluded.side ELSE assets.side END,
|
||||||
|
lane_side = CASE WHEN assets.modified = 0 THEN excluded.lane_side ELSE assets.lane_side END
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(&input.row_id)
|
.bind(&input.row_id)
|
||||||
@@ -271,6 +289,7 @@ async fn insert_fixed_row(
|
|||||||
.bind(&input.image_path)
|
.bind(&input.image_path)
|
||||||
.bind(input.deleted as i64)
|
.bind(input.deleted as i64)
|
||||||
.bind(&side)
|
.bind(&side)
|
||||||
|
.bind(&lane_side)
|
||||||
.execute(&mut **tx)
|
.execute(&mut **tx)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
@@ -296,6 +315,7 @@ async fn insert_range_row(
|
|||||||
let min_lng = mid_lng.min(s_lng).min(e_lng);
|
let min_lng = mid_lng.min(s_lng).min(e_lng);
|
||||||
let max_lng = mid_lng.max(s_lng).max(e_lng);
|
let max_lng = mid_lng.max(s_lng).max(e_lng);
|
||||||
let side = input.side.as_deref().and_then(normalize_side);
|
let side = input.side.as_deref().and_then(normalize_side);
|
||||||
|
let lane_side = input.lane_side.as_deref().and_then(normalize_lane_side);
|
||||||
|
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
@@ -303,8 +323,8 @@ async fn insert_range_row(
|
|||||||
row_id, asset_type, asset_name, video_name,
|
row_id, asset_type, asset_name, video_name,
|
||||||
lat, lng, start_lat, start_lng, end_lat, end_lng,
|
lat, lng, start_lat, start_lng, end_lat, end_lng,
|
||||||
min_lat, max_lat, min_lng, max_lng,
|
min_lat, max_lat, min_lng, max_lng,
|
||||||
image_path1, image_path2, image_path3, deleted, side
|
image_path1, image_path2, image_path3, deleted, side, lane_side
|
||||||
) VALUES (?, 'range', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, 'range', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT(row_id) DO UPDATE SET
|
ON CONFLICT(row_id) DO UPDATE SET
|
||||||
asset_type = excluded.asset_type,
|
asset_type = excluded.asset_type,
|
||||||
asset_name = excluded.asset_name,
|
asset_name = excluded.asset_name,
|
||||||
@@ -328,7 +348,8 @@ async fn insert_range_row(
|
|||||||
-- consumer that prefers it over the triple.
|
-- consumer that prefers it over the triple.
|
||||||
image_path = NULL,
|
image_path = NULL,
|
||||||
deleted = CASE WHEN assets.deleted = 1 THEN 1 ELSE excluded.deleted END,
|
deleted = CASE WHEN assets.deleted = 1 THEN 1 ELSE excluded.deleted END,
|
||||||
side = CASE WHEN assets.modified = 0 THEN excluded.side ELSE assets.side END
|
side = CASE WHEN assets.modified = 0 THEN excluded.side ELSE assets.side END,
|
||||||
|
lane_side = CASE WHEN assets.modified = 0 THEN excluded.lane_side ELSE assets.lane_side END
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(&input.row_id)
|
.bind(&input.row_id)
|
||||||
@@ -349,6 +370,7 @@ async fn insert_range_row(
|
|||||||
.bind(&input.image_path3)
|
.bind(&input.image_path3)
|
||||||
.bind(input.deleted as i64)
|
.bind(input.deleted as i64)
|
||||||
.bind(&side)
|
.bind(&side)
|
||||||
|
.bind(&lane_side)
|
||||||
.execute(&mut **tx)
|
.execute(&mut **tx)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
@@ -755,7 +777,7 @@ pub async fn update_asset_position(
|
|||||||
"SELECT id, row_id, asset_type, asset_name, video_name,
|
"SELECT id, row_id, asset_type, asset_name, video_name,
|
||||||
lat, lng, start_lat, start_lng, end_lat, end_lng,
|
lat, lng, start_lat, start_lng, end_lat, end_lng,
|
||||||
image_path, image_path1, image_path2, image_path3,
|
image_path, image_path1, image_path2, image_path3,
|
||||||
deleted, modified, merged_into, in_scope, side, link_pair_id, link_locked
|
deleted, modified, merged_into, in_scope, side, lane_side, link_pair_id, link_locked
|
||||||
FROM assets WHERE id = ?",
|
FROM assets WHERE id = ?",
|
||||||
)
|
)
|
||||||
.bind(id)
|
.bind(id)
|
||||||
@@ -875,7 +897,7 @@ pub async fn bulk_translate(
|
|||||||
"SELECT id, row_id, asset_type, asset_name, video_name,
|
"SELECT id, row_id, asset_type, asset_name, video_name,
|
||||||
lat, lng, start_lat, start_lng, end_lat, end_lng,
|
lat, lng, start_lat, start_lng, end_lat, end_lng,
|
||||||
image_path, image_path1, image_path2, image_path3,
|
image_path, image_path1, image_path2, image_path3,
|
||||||
deleted, modified, merged_into, in_scope, side, link_pair_id, link_locked
|
deleted, modified, merged_into, in_scope, side, lane_side, link_pair_id, link_locked
|
||||||
FROM assets WHERE id IN ({})",
|
FROM assets WHERE id IN ({})",
|
||||||
placeholders
|
placeholders
|
||||||
);
|
);
|
||||||
@@ -1004,7 +1026,7 @@ pub async fn reset_assets_to_original(
|
|||||||
"SELECT id, row_id, asset_type, asset_name, video_name,
|
"SELECT id, row_id, asset_type, asset_name, video_name,
|
||||||
lat, lng, start_lat, start_lng, end_lat, end_lng,
|
lat, lng, start_lat, start_lng, end_lat, end_lng,
|
||||||
image_path, image_path1, image_path2, image_path3,
|
image_path, image_path1, image_path2, image_path3,
|
||||||
deleted, modified, merged_into, in_scope, side, link_pair_id, link_locked
|
deleted, modified, merged_into, in_scope, side, lane_side, link_pair_id, link_locked
|
||||||
FROM assets WHERE id IN ({})",
|
FROM assets WHERE id IN ({})",
|
||||||
placeholders
|
placeholders
|
||||||
);
|
);
|
||||||
@@ -1228,7 +1250,7 @@ pub async fn merge_polylines(
|
|||||||
"SELECT id, row_id, asset_type, asset_name, video_name,
|
"SELECT id, row_id, asset_type, asset_name, video_name,
|
||||||
lat, lng, start_lat, start_lng, end_lat, end_lng,
|
lat, lng, start_lat, start_lng, end_lat, end_lng,
|
||||||
image_path, image_path1, image_path2, image_path3,
|
image_path, image_path1, image_path2, image_path3,
|
||||||
deleted, modified, merged_into, in_scope, side, link_pair_id, link_locked
|
deleted, modified, merged_into, in_scope, side, lane_side, link_pair_id, link_locked
|
||||||
FROM assets WHERE id = ?",
|
FROM assets WHERE id = ?",
|
||||||
)
|
)
|
||||||
.bind(primary_id)
|
.bind(primary_id)
|
||||||
@@ -1239,7 +1261,7 @@ pub async fn merge_polylines(
|
|||||||
"SELECT id, row_id, asset_type, asset_name, video_name,
|
"SELECT id, row_id, asset_type, asset_name, video_name,
|
||||||
lat, lng, start_lat, start_lng, end_lat, end_lng,
|
lat, lng, start_lat, start_lng, end_lat, end_lng,
|
||||||
image_path, image_path1, image_path2, image_path3,
|
image_path, image_path1, image_path2, image_path3,
|
||||||
deleted, modified, merged_into, in_scope, side, link_pair_id, link_locked
|
deleted, modified, merged_into, in_scope, side, lane_side, link_pair_id, link_locked
|
||||||
FROM assets WHERE id = ?",
|
FROM assets WHERE id = ?",
|
||||||
)
|
)
|
||||||
.bind(secondary_id)
|
.bind(secondary_id)
|
||||||
@@ -1992,13 +2014,28 @@ pub async fn export_assets(
|
|||||||
"SELECT id, row_id, asset_type, asset_name, video_name,
|
"SELECT id, row_id, asset_type, asset_name, video_name,
|
||||||
lat, lng, start_lat, start_lng, end_lat, end_lng,
|
lat, lng, start_lat, start_lng, end_lat, end_lng,
|
||||||
image_path, image_path1, image_path2, image_path3,
|
image_path, image_path1, image_path2, image_path3,
|
||||||
deleted, modified, merged_into, in_scope, side, link_pair_id, link_locked
|
deleted, modified, merged_into, in_scope, side, lane_side, link_pair_id, link_locked
|
||||||
FROM assets WHERE deleted = 0 ORDER BY asset_type, asset_name, id",
|
FROM assets WHERE deleted = 0 ORDER BY asset_type, asset_name, id",
|
||||||
)
|
)
|
||||||
.fetch_all(&state.pool)
|
.fetch_all(&state.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
// Resolve link partner ids → row_ids. The raw `link_pair_id` is an
|
||||||
|
// internal autoincrement that changes on every re-import, so a CSV
|
||||||
|
// consumer can't tell who linked to whom by looking at the number.
|
||||||
|
// Look up the partner's row_id once and surface it as a parallel column
|
||||||
|
// ("link_pair_row_id") in every format. Falls back to None when the
|
||||||
|
// partner row is deleted (and thus not in `assets`) — flagged so the
|
||||||
|
// user can see the dangling link.
|
||||||
|
let row_id_by_id: std::collections::HashMap<i64, String> = assets
|
||||||
|
.iter()
|
||||||
|
.map(|a| (a.id, a.row_id.clone()))
|
||||||
|
.collect();
|
||||||
|
let partner_row_id = |pid: Option<i64>| -> Option<String> {
|
||||||
|
pid.and_then(|p| row_id_by_id.get(&p).cloned())
|
||||||
|
};
|
||||||
|
|
||||||
// Pre-fetch all bboxes once so every format branch that wants to surface
|
// Pre-fetch all bboxes once so every format branch that wants to surface
|
||||||
// them can do so without re-querying. Grouped by asset_id → slot.
|
// them can do so without re-querying. Grouped by asset_id → slot.
|
||||||
let bbox_rows_all: Vec<(i64, String, f64, f64, f64, f64)> = sqlx::query_as(
|
let bbox_rows_all: Vec<(i64, String, f64, f64, f64, f64)> = sqlx::query_as(
|
||||||
@@ -2038,6 +2075,7 @@ pub async fn export_assets(
|
|||||||
"image_path2": a.image_path2,
|
"image_path2": a.image_path2,
|
||||||
"image_path3": a.image_path3,
|
"image_path3": a.image_path3,
|
||||||
"link_pair_id": a.link_pair_id,
|
"link_pair_id": a.link_pair_id,
|
||||||
|
"link_pair_row_id": partner_row_id(a.link_pair_id),
|
||||||
"link_locked": a.link_locked,
|
"link_locked": a.link_locked,
|
||||||
});
|
});
|
||||||
// Inline bbox into the GeoJSON feature's properties using the
|
// Inline bbox into the GeoJSON feature's properties using the
|
||||||
@@ -2188,6 +2226,9 @@ pub async fn export_assets(
|
|||||||
"coord": [la, ln],
|
"coord": [la, ln],
|
||||||
"deleted": a.deleted != 0,
|
"deleted": a.deleted != 0,
|
||||||
"side": a.side,
|
"side": a.side,
|
||||||
|
"lane_side": a.lane_side,
|
||||||
|
"link_pair_row_id": partner_row_id(a.link_pair_id),
|
||||||
|
"link_locked": a.link_locked,
|
||||||
});
|
});
|
||||||
if let Some(slots) = bbox_by_asset.get(&a.id) {
|
if let Some(slots) = bbox_by_asset.get(&a.id) {
|
||||||
if let Some(bb) = slots.get("fixed") {
|
if let Some(bb) = slots.get("fixed") {
|
||||||
@@ -2215,6 +2256,9 @@ pub async fn export_assets(
|
|||||||
"image_path3": a.image_path3,
|
"image_path3": a.image_path3,
|
||||||
"deleted": a.deleted != 0,
|
"deleted": a.deleted != 0,
|
||||||
"side": a.side,
|
"side": a.side,
|
||||||
|
"lane_side": a.lane_side,
|
||||||
|
"link_pair_row_id": partner_row_id(a.link_pair_id),
|
||||||
|
"link_locked": a.link_locked,
|
||||||
});
|
});
|
||||||
if let Some(slots) = bbox_by_asset.get(&a.id) {
|
if let Some(slots) = bbox_by_asset.get(&a.id) {
|
||||||
if let Some(bb) = slots.get("start") {
|
if let Some(bb) = slots.get("start") {
|
||||||
@@ -2244,7 +2288,7 @@ pub async fn export_assets(
|
|||||||
// Each row populates only the slots its asset_type uses (fixed
|
// Each row populates only the slots its asset_type uses (fixed
|
||||||
// assets fill the unprefixed `bbox_*`, range assets fill
|
// assets fill the unprefixed `bbox_*`, range assets fill
|
||||||
// `start_/mid_/end_bbox_*`); unused slots stay empty.
|
// `start_/mid_/end_bbox_*`); unused slots stay empty.
|
||||||
s.push_str("id,row_id,asset_type,asset_name,video_name,side,in_scope,deleted,modified,lat,lng,start_lat,start_lng,end_lat,end_lng,link_pair_id,link_locked,image_path,image_path1,image_path2,image_path3,bbox_x1,bbox_y1,bbox_x2,bbox_y2,start_bbox_x1,start_bbox_y1,start_bbox_x2,start_bbox_y2,mid_bbox_x1,mid_bbox_y1,mid_bbox_x2,mid_bbox_y2,end_bbox_x1,end_bbox_y1,end_bbox_x2,end_bbox_y2\n");
|
s.push_str("id,row_id,asset_type,asset_name,video_name,side,lane_side,in_scope,deleted,modified,lat,lng,start_lat,start_lng,end_lat,end_lng,link_pair_id,link_pair_row_id,link_locked,image_path,image_path1,image_path2,image_path3,bbox_x1,bbox_y1,bbox_x2,bbox_y2,start_bbox_x1,start_bbox_y1,start_bbox_x2,start_bbox_y2,mid_bbox_x1,mid_bbox_y1,mid_bbox_x2,mid_bbox_y2,end_bbox_x1,end_bbox_y1,end_bbox_x2,end_bbox_y2\n");
|
||||||
let csv_q = |v: Option<&str>| -> String {
|
let csv_q = |v: Option<&str>| -> String {
|
||||||
let s = v.unwrap_or("");
|
let s = v.unwrap_or("");
|
||||||
if s.contains(',') || s.contains('"') || s.contains('\n') {
|
if s.contains(',') || s.contains('"') || s.contains('\n') {
|
||||||
@@ -2267,14 +2311,16 @@ pub async fn export_assets(
|
|||||||
let bb_start = slots.and_then(|m| m.get("start"));
|
let bb_start = slots.and_then(|m| m.get("start"));
|
||||||
let bb_mid = slots.and_then(|m| m.get("mid"));
|
let bb_mid = slots.and_then(|m| m.get("mid"));
|
||||||
let bb_end = slots.and_then(|m| m.get("end"));
|
let bb_end = slots.and_then(|m| m.get("end"));
|
||||||
|
let partner = partner_row_id(a.link_pair_id);
|
||||||
s.push_str(&format!(
|
s.push_str(&format!(
|
||||||
"{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}\n",
|
"{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}\n",
|
||||||
a.id,
|
a.id,
|
||||||
csv_q(Some(&a.row_id)),
|
csv_q(Some(&a.row_id)),
|
||||||
a.asset_type,
|
a.asset_type,
|
||||||
csv_q(Some(&a.asset_name)),
|
csv_q(Some(&a.asset_name)),
|
||||||
csv_q(Some(&a.video_name)),
|
csv_q(Some(&a.video_name)),
|
||||||
csv_q(a.side.as_deref()),
|
csv_q(a.side.as_deref()),
|
||||||
|
csv_q(a.lane_side.as_deref()),
|
||||||
i(a.in_scope),
|
i(a.in_scope),
|
||||||
a.deleted,
|
a.deleted,
|
||||||
a.modified,
|
a.modified,
|
||||||
@@ -2285,6 +2331,7 @@ pub async fn export_assets(
|
|||||||
f(a.end_lat),
|
f(a.end_lat),
|
||||||
f(a.end_lng),
|
f(a.end_lng),
|
||||||
i(a.link_pair_id),
|
i(a.link_pair_id),
|
||||||
|
csv_q(partner.as_deref()),
|
||||||
i(a.link_locked),
|
i(a.link_locked),
|
||||||
csv_q(a.image_path.as_deref()),
|
csv_q(a.image_path.as_deref()),
|
||||||
csv_q(a.image_path1.as_deref()),
|
csv_q(a.image_path1.as_deref()),
|
||||||
@@ -2975,7 +3022,7 @@ pub async fn find_duplicate_clusters(
|
|||||||
"SELECT id, row_id, asset_type, asset_name, video_name,
|
"SELECT id, row_id, asset_type, asset_name, video_name,
|
||||||
lat, lng, start_lat, start_lng, end_lat, end_lng,
|
lat, lng, start_lat, start_lng, end_lat, end_lng,
|
||||||
image_path, image_path1, image_path2, image_path3,
|
image_path, image_path1, image_path2, image_path3,
|
||||||
deleted, modified, merged_into, in_scope, side, link_pair_id, link_locked
|
deleted, modified, merged_into, in_scope, side, lane_side, link_pair_id, link_locked
|
||||||
FROM assets WHERE deleted = 0",
|
FROM assets WHERE deleted = 0",
|
||||||
)
|
)
|
||||||
.fetch_all(&state.pool)
|
.fetch_all(&state.pool)
|
||||||
@@ -5770,7 +5817,7 @@ pub async fn snap_to_road(
|
|||||||
"SELECT id, row_id, asset_type, asset_name, video_name,
|
"SELECT id, row_id, asset_type, asset_name, video_name,
|
||||||
lat, lng, start_lat, start_lng, end_lat, end_lng,
|
lat, lng, start_lat, start_lng, end_lat, end_lng,
|
||||||
image_path, image_path1, image_path2, image_path3,
|
image_path, image_path1, image_path2, image_path3,
|
||||||
deleted, modified, merged_into, in_scope, side, link_pair_id, link_locked
|
deleted, modified, merged_into, in_scope, side, lane_side, link_pair_id, link_locked
|
||||||
FROM assets WHERE id = ?",
|
FROM assets WHERE id = ?",
|
||||||
)
|
)
|
||||||
.bind(asset_id)
|
.bind(asset_id)
|
||||||
@@ -6415,7 +6462,7 @@ pub async fn redo_action(action_id: i64, state: State<'_, AppState>) -> Result<(
|
|||||||
"SELECT id, row_id, asset_type, asset_name, video_name,
|
"SELECT id, row_id, asset_type, asset_name, video_name,
|
||||||
lat, lng, start_lat, start_lng, end_lat, end_lng,
|
lat, lng, start_lat, start_lng, end_lat, end_lng,
|
||||||
image_path, image_path1, image_path2, image_path3,
|
image_path, image_path1, image_path2, image_path3,
|
||||||
deleted, modified, merged_into, in_scope, side, link_pair_id, link_locked
|
deleted, modified, merged_into, in_scope, side, lane_side, link_pair_id, link_locked
|
||||||
FROM assets WHERE id = ?",
|
FROM assets WHERE id = ?",
|
||||||
)
|
)
|
||||||
.bind(id)
|
.bind(id)
|
||||||
@@ -6493,7 +6540,7 @@ pub async fn redo_action(action_id: i64, state: State<'_, AppState>) -> Result<(
|
|||||||
"SELECT id, row_id, asset_type, asset_name, video_name,
|
"SELECT id, row_id, asset_type, asset_name, video_name,
|
||||||
lat, lng, start_lat, start_lng, end_lat, end_lng,
|
lat, lng, start_lat, start_lng, end_lat, end_lng,
|
||||||
image_path, image_path1, image_path2, image_path3,
|
image_path, image_path1, image_path2, image_path3,
|
||||||
deleted, modified, merged_into, in_scope, side, link_pair_id, link_locked
|
deleted, modified, merged_into, in_scope, side, lane_side, link_pair_id, link_locked
|
||||||
FROM assets WHERE id = ?",
|
FROM assets WHERE id = ?",
|
||||||
)
|
)
|
||||||
.bind(id)
|
.bind(id)
|
||||||
@@ -6746,7 +6793,7 @@ pub async fn redo_action(action_id: i64, state: State<'_, AppState>) -> Result<(
|
|||||||
"SELECT id, row_id, asset_type, asset_name, video_name,
|
"SELECT id, row_id, asset_type, asset_name, video_name,
|
||||||
lat, lng, start_lat, start_lng, end_lat, end_lng,
|
lat, lng, start_lat, start_lng, end_lat, end_lng,
|
||||||
image_path, image_path1, image_path2, image_path3,
|
image_path, image_path1, image_path2, image_path3,
|
||||||
deleted, modified, merged_into, in_scope, side, link_pair_id, link_locked
|
deleted, modified, merged_into, in_scope, side, lane_side, link_pair_id, link_locked
|
||||||
FROM assets WHERE id = ?",
|
FROM assets WHERE id = ?",
|
||||||
)
|
)
|
||||||
.bind(id)
|
.bind(id)
|
||||||
@@ -6855,7 +6902,7 @@ pub async fn list_assets(state: State<'_, AppState>) -> Result<Vec<Asset>, Strin
|
|||||||
"SELECT id, row_id, asset_type, asset_name, video_name,
|
"SELECT id, row_id, asset_type, asset_name, video_name,
|
||||||
lat, lng, start_lat, start_lng, end_lat, end_lng,
|
lat, lng, start_lat, start_lng, end_lat, end_lng,
|
||||||
image_path, image_path1, image_path2, image_path3,
|
image_path, image_path1, image_path2, image_path3,
|
||||||
deleted, modified, merged_into, in_scope, side, link_pair_id, link_locked
|
deleted, modified, merged_into, in_scope, side, lane_side, link_pair_id, link_locked
|
||||||
FROM assets WHERE deleted = 0",
|
FROM assets WHERE deleted = 0",
|
||||||
)
|
)
|
||||||
.fetch_all(&state.pool)
|
.fetch_all(&state.pool)
|
||||||
|
|||||||
@@ -87,6 +87,12 @@ async fn apply_schema(pool: &SqlitePool) -> Result<()> {
|
|||||||
)
|
)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await;
|
.await;
|
||||||
|
// Lane side: "LHS" / "RHS" derived externally (e.g. lower vs higher
|
||||||
|
// site_id in the import pipeline). Independent of `side` which is the
|
||||||
|
// asset's left/right position relative to the vehicle.
|
||||||
|
let _ = sqlx::query("ALTER TABLE assets ADD COLUMN lane_side TEXT")
|
||||||
|
.execute(pool)
|
||||||
|
.await;
|
||||||
// Original positions captured at import time. Never overwritten by edits or
|
// Original positions captured at import time. Never overwritten by edits or
|
||||||
// re-imports; used to support "Reset to original" (single + bulk).
|
// re-imports; used to support "Reset to original" (single + bulk).
|
||||||
for col in [
|
for col in [
|
||||||
|
|||||||
43
src/App.tsx
43
src/App.tsx
@@ -29,6 +29,9 @@ const POPUP_ASPECT_VAL: Record<PopupAspect, number> = {
|
|||||||
type Filters = {
|
type Filters = {
|
||||||
videos: Set<string>; // empty = all
|
videos: Set<string>; // empty = all
|
||||||
sides: Set<string>; // empty = all
|
sides: Set<string>; // empty = all
|
||||||
|
// Carriageway/lane the asset's site_id belongs to (LHS / RHS). Stays
|
||||||
|
// distinct from `sides`, which is left/right-of-vehicle.
|
||||||
|
lanes: Set<string>;
|
||||||
types: Set<"fixed" | "range">; // empty = all
|
types: Set<"fixed" | "range">; // empty = all
|
||||||
names: Set<string>; // empty = all (asset_name)
|
names: Set<string>; // empty = all (asset_name)
|
||||||
};
|
};
|
||||||
@@ -402,6 +405,7 @@ function AppShell({
|
|||||||
const [filters, setFilters] = useState<Filters>({
|
const [filters, setFilters] = useState<Filters>({
|
||||||
videos: new Set(),
|
videos: new Set(),
|
||||||
sides: new Set(),
|
sides: new Set(),
|
||||||
|
lanes: new Set(),
|
||||||
types: new Set(),
|
types: new Set(),
|
||||||
names: new Set(),
|
names: new Set(),
|
||||||
});
|
});
|
||||||
@@ -731,15 +735,18 @@ function AppShell({
|
|||||||
const counts = useMemo(() => {
|
const counts = useMemo(() => {
|
||||||
const byVideo = new Map<string, number>();
|
const byVideo = new Map<string, number>();
|
||||||
const bySide = new Map<string, number>();
|
const bySide = new Map<string, number>();
|
||||||
|
const byLane = new Map<string, number>();
|
||||||
const byType = new Map<string, number>();
|
const byType = new Map<string, number>();
|
||||||
for (const a of assets) {
|
for (const a of assets) {
|
||||||
const vn = displayVideo(a.video_name);
|
const vn = displayVideo(a.video_name);
|
||||||
byVideo.set(vn, (byVideo.get(vn) ?? 0) + 1);
|
byVideo.set(vn, (byVideo.get(vn) ?? 0) + 1);
|
||||||
const s = a.side ?? "(none)";
|
const s = a.side ?? "(none)";
|
||||||
bySide.set(s, (bySide.get(s) ?? 0) + 1);
|
bySide.set(s, (bySide.get(s) ?? 0) + 1);
|
||||||
|
const l = a.lane_side ?? "(none)";
|
||||||
|
byLane.set(l, (byLane.get(l) ?? 0) + 1);
|
||||||
byType.set(a.asset_type, (byType.get(a.asset_type) ?? 0) + 1);
|
byType.set(a.asset_type, (byType.get(a.asset_type) ?? 0) + 1);
|
||||||
}
|
}
|
||||||
return { byVideo, bySide, byType };
|
return { byVideo, bySide, byLane, byType };
|
||||||
}, [assets]);
|
}, [assets]);
|
||||||
|
|
||||||
const filteredAssets = useMemo(() => {
|
const filteredAssets = useMemo(() => {
|
||||||
@@ -750,6 +757,10 @@ function AppShell({
|
|||||||
const s = a.side ?? "(none)";
|
const s = a.side ?? "(none)";
|
||||||
if (!filters.sides.has(s)) return false;
|
if (!filters.sides.has(s)) return false;
|
||||||
}
|
}
|
||||||
|
if (filters.lanes.size) {
|
||||||
|
const l = a.lane_side ?? "(none)";
|
||||||
|
if (!filters.lanes.has(l)) return false;
|
||||||
|
}
|
||||||
if (filters.types.size && !filters.types.has(a.asset_type)) return false;
|
if (filters.types.size && !filters.types.has(a.asset_type)) return false;
|
||||||
if (filters.names.size && !filters.names.has(a.asset_name)) return false;
|
if (filters.names.size && !filters.names.has(a.asset_name)) return false;
|
||||||
// Video filter: explicit list. If allVideoNames is loaded and the asset's
|
// Video filter: explicit list. If allVideoNames is loaded and the asset's
|
||||||
@@ -781,11 +792,15 @@ function AppShell({
|
|||||||
const s = a.side ?? "(none)";
|
const s = a.side ?? "(none)";
|
||||||
if (!filters.sides.has(s)) continue;
|
if (!filters.sides.has(s)) continue;
|
||||||
}
|
}
|
||||||
|
if (filters.lanes.size) {
|
||||||
|
const l = a.lane_side ?? "(none)";
|
||||||
|
if (!filters.lanes.has(l)) continue;
|
||||||
|
}
|
||||||
if (filters.types.size && !filters.types.has(a.asset_type)) continue;
|
if (filters.types.size && !filters.types.has(a.asset_type)) continue;
|
||||||
m.set(a.asset_name, (m.get(a.asset_name) ?? 0) + 1);
|
m.set(a.asset_name, (m.get(a.asset_name) ?? 0) + 1);
|
||||||
}
|
}
|
||||||
return m;
|
return m;
|
||||||
}, [assets, filters.videos, filters.sides, filters.types]);
|
}, [assets, filters.videos, filters.sides, filters.lanes, filters.types]);
|
||||||
|
|
||||||
function toggleSet<T>(set: Set<T>, value: T): Set<T> {
|
function toggleSet<T>(set: Set<T>, value: T): Set<T> {
|
||||||
const next = new Set(set);
|
const next = new Set(set);
|
||||||
@@ -798,6 +813,7 @@ function AppShell({
|
|||||||
setFilters({
|
setFilters({
|
||||||
videos: new Set(),
|
videos: new Set(),
|
||||||
sides: new Set(),
|
sides: new Set(),
|
||||||
|
lanes: new Set(),
|
||||||
types: new Set(),
|
types: new Set(),
|
||||||
names: new Set(),
|
names: new Set(),
|
||||||
});
|
});
|
||||||
@@ -4250,6 +4266,7 @@ function AppShell({
|
|||||||
disabled={
|
disabled={
|
||||||
filters.videos.size +
|
filters.videos.size +
|
||||||
filters.sides.size +
|
filters.sides.size +
|
||||||
|
filters.lanes.size +
|
||||||
filters.types.size +
|
filters.types.size +
|
||||||
filters.names.size ===
|
filters.names.size ===
|
||||||
0
|
0
|
||||||
@@ -4358,6 +4375,28 @@ function AppShell({
|
|||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
<h4 title="Carriageway derived from the import-time site_id pair (lower site_id → LHS, higher → RHS)">
|
||||||
|
Lane
|
||||||
|
</h4>
|
||||||
|
{Array.from(counts.byLane.entries())
|
||||||
|
.sort()
|
||||||
|
.map(([lane, n]) => (
|
||||||
|
<label key={lane} className="filter-row">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={filters.lanes.has(lane)}
|
||||||
|
onChange={() =>
|
||||||
|
setFilters((f) => ({
|
||||||
|
...f,
|
||||||
|
lanes: toggleSet(f.lanes, lane),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span className="label">{lane}</span>
|
||||||
|
<span className="count">{n}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
|
||||||
<h4
|
<h4
|
||||||
style={{ cursor: "pointer", userSelect: "none" }}
|
style={{ cursor: "pointer", userSelect: "none" }}
|
||||||
onClick={() => setVideosExpanded((v) => !v)}
|
onClick={() => setVideosExpanded((v) => !v)}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export type Asset = {
|
|||||||
merged_into: number | null;
|
merged_into: number | null;
|
||||||
in_scope: number | null;
|
in_scope: number | null;
|
||||||
side: string | null;
|
side: string | null;
|
||||||
|
lane_side: string | null;
|
||||||
link_pair_id: number | null;
|
link_pair_id: number | null;
|
||||||
link_locked: number | null;
|
link_locked: number | null;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user