From 6eb854444d11e2fbfebaf15d32e93e90b64b6c5d Mon Sep 17 00:00:00 2001 From: ravi Date: Mon, 4 May 2026 21:02:20 +0530 Subject: [PATCH] osm generate code fix --- src-tauri/src/commands.rs | 239 +++++++++++++++++++++++--------------- src/App.tsx | 116 ++++++++++++------ 2 files changed, 227 insertions(+), 128 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 32d4f17..bb2c7bb 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -3711,57 +3711,98 @@ pub struct BboxOut { const OSM_HIGHWAY_CLASSES: &str = "motorway|trunk|primary|secondary|tertiary|motorway_link|trunk_link|primary_link|secondary_link|tertiary_link|residential|unclassified|service"; +#[derive(Debug, Serialize)] +pub struct GenerateOsmResult { + pub ways_kept: usize, + pub source: String, + pub points_used: usize, + pub videos_used: usize, +} + #[tauri::command] pub async fn generate_osm_near_assets( - radius_m: Option, - max_samples: Option, + min_lat: f64, + min_lng: f64, + max_lat: f64, + max_lng: f64, + keep_threshold_m: Option, path: String, overpass_url: Option, state: State<'_, AppState>, -) -> Result { - // Tighter defaults — only roads the assets are *on*, not nearby: - // radius_m = 60 m (capture buffer for GPS drift + side offset) - // keep_threshold = 30 m (post-filter: an asset must be within 30 m of - // the road centerline to count as "on" the road) - // min_votes = 2 (a road must collect at least this many asset - // hits to be kept; one stray asset isn't enough) - let radius = radius_m.unwrap_or(60.0); - let keep_threshold_m: f64 = 30.0; - let min_votes: usize = 2; - let cap = max_samples.unwrap_or(800).max(1); - // Pull all live asset centres. The point bbox columns are populated at - // import time so the SELECT is cheap. - let coords: Vec<(f64, f64)> = sqlx::query_as::<_, (f64, f64)>( - "SELECT lat, lng FROM assets +) -> Result { + // New flow: Overpass query is the visible bbox (gives every way in view, + // including highways that earlier around: + min_votes=2 dropped). The + // returned ways are then filtered against a unified reference set built + // from per-video metadata polyline points + live asset coords. A way is + // kept if any of its segments comes within `keep_threshold_m` of any + // reference point. This covers highways with sparse assets via the + // metadata polyline (the GPS track always touches the highway it drove) + // and urban side streets via the dense asset cloud. + let threshold_m = keep_threshold_m.unwrap_or(50.0).max(1.0); + if !(min_lat < max_lat) || !(min_lng < max_lng) { + return Err("invalid bbox: min must be < max for both lat and lng".into()); + } + + // --- Build the reference-point set (lng, lat). --- + // Pull asset centres for the post-filter votes. + let asset_coords: Vec<(f64, f64)> = sqlx::query_as::<_, (f64, f64)>( + "SELECT lng, lat FROM assets WHERE deleted = 0 AND lat IS NOT NULL AND lng IS NOT NULL", ) .fetch_all(&state.pool) .await .map_err(|e| e.to_string())?; - if coords.is_empty() { - return Err("no live assets in DB to generate roads near".to_string()); - } - // Stride-sample for the around: query body. We use the *full* asset - // list further down for vote counting, so the sample is purely a - // query-size optimisation. - let stride = (coords.len().div_ceil(cap)).max(1); - let sampled: Vec<&(f64, f64)> = coords.iter().step_by(stride).collect(); - // Build a `(lat,lng,lat,lng,...)` list. Overpass `around:r,...` returns - // ways with any node within `r` metres of any of the given points. - let mut latlng = String::with_capacity(sampled.len() * 22); - for (i, (lat, lng)) in sampled.iter().enumerate() { - if i > 0 { - latlng.push(','); + // Pull every metadata polyline point (DB-persisted, stored as + // [[lng, lat], ...] to match deck.gl convention). + let metadata_rows: Vec<(String,)> = sqlx::query_as( + "SELECT track_json FROM video_metadata", + ) + .fetch_all(&state.pool) + .await + .map_err(|e| e.to_string())?; + let mut ref_points: Vec<(f64, f64)> = Vec::new(); + let mut videos_used = 0usize; + for (json,) in &metadata_rows { + let track: Vec<[f64; 2]> = match serde_json::from_str(json) { + Ok(t) => t, + Err(_) => continue, + }; + if track.is_empty() { + continue; + } + videos_used += 1; + for p in track { + ref_points.push((p[0], p[1])); } - latlng.push_str(&format!("{},{}", lat, lng)); } + for c in &asset_coords { + ref_points.push(*c); + } + if ref_points.is_empty() { + return Err( + "nothing to filter against. Import per-video metadata or assets first \ + so the generator knows which Overpass-returned roads to keep." + .into(), + ); + } + let source = if videos_used > 0 && !asset_coords.is_empty() { + "metadata+assets" + } else if videos_used > 0 { + "metadata" + } else { + "assets" + }; + + // --- Overpass: bbox query, no around: list. --- let query = format!( "[out:json][timeout:180];\ - (way[\"highway\"~\"{classes}\"](around:{r},{pts}););\ + (way[\"highway\"~\"{classes}\"]({mn_lat},{mn_lng},{mx_lat},{mx_lng}););\ out body geom;", classes = OSM_HIGHWAY_CLASSES, - r = radius as i64, - pts = latlng, + mn_lat = min_lat, + mn_lng = min_lng, + mx_lat = max_lat, + mx_lng = max_lng, ); let url = overpass_url .unwrap_or_else(|| "https://overpass-api.de/api/interpreter".to_string()); @@ -3843,16 +3884,14 @@ pub async fn generate_osm_near_assets( })); } - // Post-filter: keep only roads where >= min_votes assets sit within - // keep_threshold_m of the centerline. The around: query alone returns - // every road within `radius` m of any asset — including parallel/cross - // streets that just happen to be close. The vote step picks out the - // road(s) the assets actually follow. - let kept = filter_features_by_asset_votes( + // Post-filter: keep a way if any of its segments comes within + // `threshold_m` of any reference point (metadata polyline points or + // asset coords). Permissive — needs only ONE hit, vs the old + // min_votes=2 which dropped sparse-asset highways. + let kept = filter_features_by_reference_points( features, - &coords, - keep_threshold_m, - min_votes, + &ref_points, + threshold_m, ); let fc = serde_json::json!({ @@ -3863,7 +3902,14 @@ pub async fn generate_osm_near_assets( tokio::fs::write(&path, serde_json::to_string_pretty(&fc).map_err(|e| e.to_string())?) .await .map_err(|e| e.to_string())?; - Ok(count) + Ok(GenerateOsmResult { + ways_kept: count, + source: source.to_string(), + // Reuse points_used to surface the size of the reference set the + // post-filter checked against (asset coords + every metadata vertex). + points_used: ref_points.len(), + videos_used, + }) } fn kept_count(fc: &serde_json::Value) -> usize { @@ -3905,12 +3951,28 @@ fn point_segment_distance_m( (ex * ex + ey * ey).sqrt() } -fn filter_features_by_asset_votes( +/// Permissive filter: keep a way if any of its segments comes within +/// `threshold_m` of any reference point. Reference points are passed as +/// `(lng, lat)`. Single hit is enough — designed for the bbox+filter flow +/// where we want every road touched by either metadata polylines or assets, +/// including highways with sparse asset coverage. +/// +/// Builds a spatial-hash grid keyed by `cell_deg = threshold_m / 111320`, +/// then for each way walks its bbox cell + a 1-ring neighbourhood; at most +/// one exact distance check per nearby reference point. ~O(W × P_local). +fn filter_features_by_reference_points( features: Vec, - asset_coords: &[(f64, f64)], + ref_points: &[(f64, f64)], threshold_m: f64, - min_votes: usize, ) -> Vec { + use std::collections::HashMap; + let cell_deg = (threshold_m / 111_320.0).max(1e-7); + let mut grid: HashMap<(i64, i64), Vec> = HashMap::new(); + for (i, (lng, lat)) in ref_points.iter().enumerate() { + let gx = (lng / cell_deg).floor() as i64; + let gy = (lat / cell_deg).floor() as i64; + grid.entry((gx, gy)).or_default().push(i); + } let mut out: Vec = Vec::new(); for feat in features { let pts: Vec<[f64; 2]> = feat @@ -3931,53 +3993,46 @@ fn filter_features_by_asset_votes( if pts.len() < 2 { continue; } - // Road bbox + buffer in degrees (so we can skip far-away assets cheaply). - let mut mn_lng = f64::INFINITY; - let mut mx_lng = f64::NEG_INFINITY; - let mut mn_lat = f64::INFINITY; - let mut mx_lat = f64::NEG_INFINITY; - for &[lng, lat] in &pts { - if lng < mn_lng { mn_lng = lng; } - if lng > mx_lng { mx_lng = lng; } - if lat < mn_lat { mn_lat = lat; } - if lat > mx_lat { mx_lat = lat; } - } - let lat0 = ((mn_lat + mx_lat) * 0.5).to_radians(); - let m_per_lng = (111_320.0_f64 * lat0.cos()).max(1.0); - let buf_lat = threshold_m / 111_320.0; - let buf_lng = threshold_m / m_per_lng; - - let mut votes: usize = 0; - for &(a_lat, a_lng) in asset_coords { - if a_lng < mn_lng - buf_lng - || a_lng > mx_lng + buf_lng - || a_lat < mn_lat - buf_lat - || a_lat > mx_lat + buf_lat - { - continue; - } - let mut hit = false; - for w in pts.windows(2) { - let d = point_segment_distance_m( - a_lng, a_lat, - w[0][0], w[0][1], - w[1][0], w[1][1], - ); - if d < threshold_m { - hit = true; - break; - } - } - if hit { - votes += 1; - if votes >= min_votes { - // Early exit — we already have enough votes; no need to - // count exactly how many more. - break; + let mut keep = false; + // Walk segments at sub-cell intervals so a long segment crossing a + // populated cell can't slip through the grid. + let step_deg = cell_deg * 0.5; + 'segs: for w in pts.windows(2) { + let a = w[0]; + let b = w[1]; + let dx = b[0] - a[0]; + let dy = b[1] - a[1]; + let len = (dx * dx + dy * dy).sqrt(); + let steps = ((len / step_deg).ceil() as usize).max(1); + for s in 0..=steps { + let t = s as f64 / steps as f64; + let lng = a[0] + dx * t; + let lat = a[1] + dy * t; + let gx = (lng / cell_deg).floor() as i64; + let gy = (lat / cell_deg).floor() as i64; + for dy_c in -1..=1 { + for dx_c in -1..=1 { + if let Some(idxs) = grid.get(&(gx + dx_c, gy + dy_c)) { + for &idx in idxs { + let p = ref_points[idx]; + // Cheap equirectangular distance — accurate + // enough for 50 m thresholding. + let lat0 = ((lat + p.1) * 0.5).to_radians(); + let m_per_lng = (111_320.0_f64 * lat0.cos()).max(1.0); + let mx = (p.0 - lng) * m_per_lng; + let my = (p.1 - lat) * 111_320.0; + let d = (mx * mx + my * my).sqrt(); + if d <= threshold_m { + keep = true; + break 'segs; + } + } + } + } } } } - if votes >= min_votes { + if keep { out.push(feat); } } diff --git a/src/App.tsx b/src/App.tsx index b2235bc..048a05b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -207,7 +207,14 @@ function AppShell({ range_assets: number; osm_roads: number; scope_features: number; - }>({ fixed_assets: 0, range_assets: 0, osm_roads: 0, scope_features: 0 }); + video_metadata: number; + }>({ + fixed_assets: 0, + range_assets: 0, + osm_roads: 0, + scope_features: 0, + video_metadata: 0, + }); const [recentActions, setRecentActions] = useState([]); const [status, setStatus] = useState(""); const [busy, setBusy] = useState(false); @@ -1580,6 +1587,7 @@ function AppShell({ range_assets: number; osm_roads: number; scope_features: number; + video_metadata: number; }>("data_source_counts"); setDataCounts(c); } catch { @@ -1830,46 +1838,82 @@ function AppShell({ folder.endsWith("/") || folder.endsWith("\\") ? "" : "/"; const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19); const hasAssets = dataCounts.fixed_assets + dataCounts.range_assets > 0; + const hasMetadata = (dataCounts.video_metadata ?? 0) > 0 + || Object.keys(metadataByVideo).length > 0; + if (!hasAssets && !hasMetadata) { + setStatus( + "Import per-video metadata or assets first — the generator filters Overpass results against them.", + ); + return; + } + // We need a bbox to query Overpass against. Prefer the current visible + // viewport (gives the user direct control over scope); fall back to the + // bbox of all loaded assets if the viewport isn't valid yet (e.g. user + // never panned the map). Either way the bbox is what Overpass filters + // on, and the post-filter then narrows by metadata + asset proximity. + let b = boundsRef.current; + if (!b) { + try { + const ab = await invoke<{ + min_lat: number; + min_lng: number; + max_lat: number; + max_lng: number; + } | null>("assets_bbox"); + if (ab) { + b = { + south: ab.min_lat, + north: ab.max_lat, + west: ab.min_lng, + east: ab.max_lng, + }; + } + } catch { + /* fall through to error below */ + } + } + if (!b) { + setStatus( + "No visible map bbox and no asset bbox available — pan/zoom the map or import assets first.", + ); + return; + } setBusy(true); setOsmFetchActive(true); try { - if (hasAssets) { - // Tight per-asset filter: fetch a small candidate set with - // around:60m, then keep only roads where ≥2 assets sit within 30 m - // of the centerline. That way the output is the road(s) the - // assets are actually on, not every street that happens to be - // within bbox or radius. - setStatus("Fetching OSM near assets from Overpass…"); - const path = `${folder}${sep}osm_on_asset_roads_${ts}.geojson`; - const n = await invoke("generate_osm_near_assets", { - radiusM: 60, - maxSamples: 800, - path, - }); - setStatus( - `Generated ${n} road(s) carrying assets → ${path}. Use Import → OSM roads to load.`, - ); - } else if (boundsRef.current) { - const b = boundsRef.current; - const path = `${folder}${sep}osm_${b.south.toFixed(4)}_${b.west.toFixed(4)}_${b.north.toFixed(4)}_${b.east.toFixed(4)}_${ts}.geojson`; - setStatus("Fetching OSM for visible viewport from Overpass…"); - const n = await invoke("generate_osm_for_bbox", { - minLat: b.south, - minLng: b.west, - maxLat: b.north, - maxLng: b.east, - path, - }); - setStatus( - `Generated ${n} OSM way(s) from viewport → ${path}. Use Import → OSM roads to load.`, - ); - } else { - setStatus( - "No assets imported and no map viewport set. Import assets or pan/zoom the map first.", - ); - } + const sourceHint = hasMetadata && hasAssets + ? "metadata + assets" + : hasMetadata + ? "metadata" + : "assets"; + setStatus( + `Fetching OSM for visible bbox from Overpass, then filtering by ${sourceHint}…`, + ); + const path = `${folder}${sep}osm_on_asset_roads_${ts}.geojson`; + const r = await invoke<{ + ways_kept: number; + source: string; + points_used: number; + videos_used: number; + }>("generate_osm_near_assets", { + minLat: b.south, + minLng: b.west, + maxLat: b.north, + maxLng: b.east, + keepThresholdM: 50, + path, + }); + const refDesc = + r.videos_used > 0 + ? `${r.points_used} ref points (${r.videos_used} video track(s) + assets)` + : `${r.points_used} asset ref points`; + setStatus( + `Generated ${r.ways_kept} road(s) [bbox + ${r.source}, ${refDesc}] → ${path}. Use Import → OSM roads to load.`, + ); + return; } catch (e) { setStatus(`Generate OSM failed: ${e}`); + return; } finally { setOsmFetchActive(false); setBusy(false);