map and chainage calculation

This commit is contained in:
2026-07-07 22:42:47 +05:30
parent 05ce1f2cdb
commit 308d6bc129
17 changed files with 4178 additions and 48 deletions

View File

@@ -52,14 +52,16 @@ pub async fn claim(
user: AuthUser,
AxPath(id): AxPath<i32>,
) -> ApiResult<ClaimResponse> {
let row: Option<(i32, String, String, Option<i32>, Option<i32>, Option<f64>, Option<i32>, chrono::DateTime<chrono::Utc>, Option<Value>)> =
#[allow(clippy::type_complexity)]
let row: Option<(i32, String, String, Option<i32>, Option<i32>, Option<f64>, Option<i32>, chrono::DateTime<chrono::Utc>, Option<Value>, String, Option<f64>, Option<f64>, Option<f64>, Option<f64>)> =
sqlx::query_as(
r#"
UPDATE videos
SET claimed_by=$2, claimed_at=now(), lease_expires_at=now() + ($3::int * interval '1 second')
WHERE id=$1
AND (claimed_by IS NULL OR claimed_by=$2 OR lease_expires_at < now())
RETURNING id, file_name, rel_path, width, height, fps, frame_count, lease_expires_at, raw_json
RETURNING id, file_name, rel_path, width, height, fps, frame_count, lease_expires_at, raw_json,
road_type, chainage_start_m, chainage_end_m, sr_chainage_start_m, sr_chainage_end_m
"#,
)
.bind(id)
@@ -69,7 +71,8 @@ pub async fn claim(
.await
.map_err(err)?;
let Some((video_id, file_name, rel_path, width, height, fps, frame_count, lease_expires_at, raw_json)) = row
let Some((video_id, file_name, rel_path, width, height, fps, frame_count, lease_expires_at, raw_json,
road_type, ch_s, ch_e, sr_s, sr_e)) = row
else {
// Distinguish "doesn't exist" from "not claimable".
let exists: Option<i32> = sqlx::query_scalar("SELECT id FROM videos WHERE id=$1")
@@ -88,6 +91,21 @@ pub async fn claim(
log_event(&s.db, video_id, &user.username, "claim", json!({})).await;
// Inject the server-authoritative road_type + chainage into the doc the worker
// pulls (only when a doc exists — a pending video legitimately has none; push
// re-stamps regardless, so the metadata always persists). Each annotation also
// gets its frame-exact lat/lon + chainage.
let annotations = match raw_json {
Some(mut doc) => {
crate::stamp_road_meta(&mut doc, &road_type, (ch_s, ch_e, sr_s, sr_e));
if let Some(geo) = crate::chainage::video_geo(&s.db, video_id).await {
crate::chainage::stamp_annotation_geo(&mut doc, &geo);
}
doc
}
None => Value::Null,
};
Ok(Json(ClaimResponse {
video_id,
file_name,
@@ -98,7 +116,7 @@ pub async fn claim(
frame_count,
lease_expires_at,
download_url: format!("/api/videos/{video_id}/download"),
annotations: raw_json.unwrap_or(Value::Null),
annotations,
}))
}
@@ -223,13 +241,18 @@ pub async fn push(
// lease lapsed (auto-released, or re-claimed by someone else) the worker must
// re-claim before pushing — their local work is never lost, it just can't clobber a
// claim they no longer hold. Admins may always push.
let row: Option<(Option<String>, Option<chrono::DateTime<chrono::Utc>>)> =
sqlx::query_as("SELECT claimed_by, lease_expires_at FROM videos WHERE id=$1")
#[allow(clippy::type_complexity)]
let row: Option<(Option<String>, Option<chrono::DateTime<chrono::Utc>>, String, Option<f64>, Option<f64>, Option<f64>, Option<f64>)> =
sqlx::query_as(
r#"SELECT claimed_by, lease_expires_at, road_type,
chainage_start_m, chainage_end_m, sr_chainage_start_m, sr_chainage_end_m
FROM videos WHERE id=$1"#,
)
.bind(id)
.fetch_optional(&s.db)
.await
.map_err(err)?;
let (claimed_by, lease_expires_at) =
let (claimed_by, lease_expires_at, road_type, ch_s, ch_e, sr_s, sr_e) =
row.ok_or((StatusCode::NOT_FOUND, "video not found".to_string()))?;
if !user.is_admin() {
let holds_active_claim = claimed_by.as_deref() == Some(user.username.as_str())
@@ -250,6 +273,34 @@ pub async fn push(
let doc: ExportDoc = serde_json::from_value(body.clone())
.map_err(|e| (StatusCode::BAD_REQUEST, format!("invalid export doc: {e}")))?;
// Adopt the doc's fps/frame_count/resolution BEFORE stamping. A raw (from-scratch)
// NAS video has no fps in our DB — but the desktop decoded the real file, so its
// fps is authoritative. Without this, per-annotation lat/lon/chainage can't be
// computed (frame→time needs fps) and the geo silently wouldn't be written.
// COALESCE(doc, existing) so a doc that omits a field never wipes it.
if let Some(v) = doc.video.as_ref() {
let _ = sqlx::query(
r#"UPDATE videos SET fps=COALESCE($2, fps), frame_count=COALESCE($3, frame_count),
width=COALESCE($4, width), height=COALESCE($5, height) WHERE id=$1"#,
)
.bind(id)
.bind(v.fps)
.bind(v.frame_count.map(|x| x as i32))
.bind(v.width.map(|x| x as i32))
.bind(v.height.map(|x| x as i32))
.execute(&s.db)
.await;
}
// Re-stamp the server-authoritative road_type + chainage onto the stored doc —
// whatever the worker's client sent (or dropped), the server's values win. Every
// annotation gets its frame-exact lat/lon + chainage too (from the fps just set).
let mut body = body;
crate::stamp_road_meta(&mut body, &road_type, (ch_s, ch_e, sr_s, sr_e));
if let Some(geo) = crate::chainage::video_geo(&s.db, id).await {
crate::chainage::stamp_annotation_geo(&mut body, &geo);
}
let ann_count = (doc.fixed_annotations.len() + doc.range_annotations.len()) as i32;
// Recompute the video-level annotator fields from the pushed doc (same idea as