map and chainage calculation
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user