diff --git a/src/geo.rs b/src/geo.rs index 66bbf50..8441913 100644 --- a/src/geo.rs +++ b/src/geo.rs @@ -12,43 +12,62 @@ pub fn interpolate_time(a: DateTime, b: DateTime, fraction: f64) -> Da a + Duration::milliseconds((duration_ms as f64 * fraction).round() as i64) } -/// Detects all county crossings between two consecutive GPS samples. +/// Detects all county crossings along an entire GPS track in a single +/// database round trip. /// -/// The geometry itself is handled by PostGIS. Because the endpoint counties -/// are used to constrain the query, the GiST geometry index remains useful. -/// ST_LineLocatePoint gives the position of the intersection on the original -/// track segment; that fraction is then used for millisecond timestamp -/// interpolation. -pub async fn crossings_between( +/// This used to run once per consecutive point pair, i.e. one round trip +/// per GPS sample (tens of thousands for a multi-hour activity), each +/// paying full network/connection overhead for what is usually a no-op +/// (most segments stay inside a single county). This version batches every +/// segment of the track into one query via `UNNEST ... WITH ORDINALITY`, +/// so the per-segment geometry logic is unchanged (endpoint county lookup +/// via the GiST index, boundary intersection, `ST_LineLocatePoint` for the +/// fractional position) but it now runs once per activity instead of once +/// per segment. +pub async fn crossings_for_track( db: &PgPool, - a: &TrackPoint, - b: &TrackPoint, + points: &[TrackPoint], ) -> Result> { + if points.len() < 2 { + return Ok(Vec::new()); + } + + let segment_count = points.len() - 1; + let mut lon1 = Vec::with_capacity(segment_count); + let mut lat1 = Vec::with_capacity(segment_count); + let mut lon2 = Vec::with_capacity(segment_count); + let mut lat2 = Vec::with_capacity(segment_count); + + for pair in points.windows(2) { + lon1.push(pair[0].lon); + lat1.push(pair[0].lat); + lon2.push(pair[1].lon); + lat2.push(pair[1].lat); + } + let rows = sqlx::query( r#" - WITH points AS ( + WITH segments AS ( SELECT - ST_SetSRID( - ST_MakePoint($1, $2), - 4326 - ) AS p1, - - ST_SetSRID( - ST_MakePoint($3, $4), - 4326 - ) AS p2 + ordinality AS idx, + ST_SetSRID(ST_MakePoint(lon1, lat1), 4326) AS p1, + ST_SetSRID(ST_MakePoint(lon2, lat2), 4326) AS p2 + FROM UNNEST($1::float8[], $2::float8[], $3::float8[], $4::float8[]) + WITH ORDINALITY AS t(lon1, lat1, lon2, lat2, ordinality) ), track AS ( SELECT + idx, p1, p2, ST_MakeLine(p1, p2) AS line - FROM points + FROM segments ), endpoint_counties AS ( SELECT + t.idx, a.id AS from_id, b.id AS to_id, t.line @@ -65,6 +84,7 @@ pub async fn crossings_between( intersections AS ( SELECT + e.idx, e.from_id, e.to_id, e.line, @@ -86,6 +106,7 @@ pub async fn crossings_between( ) SELECT + idx, from_id, to_id, @@ -102,13 +123,13 @@ pub async fn crossings_between( WHERE ST_Dimension(intersection) = 0 - ORDER BY fraction + ORDER BY idx, fraction "#, ) - .bind(a.lon) - .bind(a.lat) - .bind(b.lon) - .bind(b.lat) + .bind(&lon1) + .bind(&lat1) + .bind(&lon2) + .bind(&lat2) .fetch_all(db) .await?; @@ -125,6 +146,14 @@ pub async fn crossings_between( continue; } + // idx is the 1-based ordinality of the segment within the + // batched UNNEST, i.e. segment i is (points[i - 1], points[i]). + let idx: i64 = row.try_get("idx")?; + let segment = (idx - 1) as usize; + + let a = &points[segment]; + let b = &points[segment + 1]; + let from_county_id: i64 = row.try_get("from_id")?; let to_county_id: i64 = row.try_get("to_id")?; diff --git a/src/main.rs b/src/main.rs index e5308c8..2f5d2e8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1031,27 +1031,25 @@ pub async fn process_activity_with_client( let mut crossing_count = 0usize; - for pair in points.windows(2) { - let crossings = geo::crossings_between(&state.db, &pair[0], &pair[1]).await?; + let crossings = geo::crossings_for_track(&state.db, &points).await?; - for crossing in crossings { - let interval = IntervalsClient::matching_interval(&activity, crossing.track_index); + for crossing in crossings { + let interval = IntervalsClient::matching_interval(&activity, crossing.track_index); - db::save_crossing(&mut tx, activity_db_id, &crossing, interval.as_ref()).await?; + db::save_crossing(&mut tx, activity_db_id, &crossing, interval.as_ref()).await?; - crossing_count += 1; + crossing_count += 1; - tracing::info!( - activity_id = %activity_id, - time = %crossing.crossing_time, - from = crossing.from_county_id, - to = crossing.to_county_id, - track_index = crossing.track_index, - lat = crossing.lat, - lon = crossing.lon, - "county crossing detected" - ); - } + tracing::info!( + activity_id = %activity_id, + time = %crossing.crossing_time, + from = crossing.from_county_id, + to = crossing.to_county_id, + track_index = crossing.track_index, + lat = crossing.lat, + lon = crossing.lon, + "county crossing detected" + ); } db::mark_activity_processed(&mut tx, activity_db_id).await?;