perf: batch county-crossing detection into one query per activity
geo::crossings_between issued one DB round trip per consecutive GPS sample pair (tens of thousands per multi-hour activity), even though almost every segment stays inside a single county and produces no crossing at all. geo::crossings_for_track batches all segments of a track into a single query via UNNEST ... WITH ORDINALITY. The per-segment geometry logic (endpoint county lookup via the GiST index, boundary intersection, ST_LineLocatePoint for the fractional position) is unchanged, it now just runs once per activity instead of once per segment.
This commit is contained in:
parent
2ab16a68b7
commit
5a49624435
2 changed files with 73 additions and 46 deletions
79
src/geo.rs
79
src/geo.rs
|
|
@ -12,43 +12,62 @@ pub fn interpolate_time(a: DateTime<Utc>, b: DateTime<Utc>, fraction: f64) -> Da
|
||||||
a + Duration::milliseconds((duration_ms as f64 * fraction).round() as i64)
|
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
|
/// This used to run once per consecutive point pair, i.e. one round trip
|
||||||
/// are used to constrain the query, the GiST geometry index remains useful.
|
/// per GPS sample (tens of thousands for a multi-hour activity), each
|
||||||
/// ST_LineLocatePoint gives the position of the intersection on the original
|
/// paying full network/connection overhead for what is usually a no-op
|
||||||
/// track segment; that fraction is then used for millisecond timestamp
|
/// (most segments stay inside a single county). This version batches every
|
||||||
/// interpolation.
|
/// segment of the track into one query via `UNNEST ... WITH ORDINALITY`,
|
||||||
pub async fn crossings_between(
|
/// 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,
|
db: &PgPool,
|
||||||
a: &TrackPoint,
|
points: &[TrackPoint],
|
||||||
b: &TrackPoint,
|
|
||||||
) -> Result<Vec<DetectedCrossing>> {
|
) -> Result<Vec<DetectedCrossing>> {
|
||||||
|
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(
|
let rows = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
WITH points AS (
|
WITH segments AS (
|
||||||
SELECT
|
SELECT
|
||||||
ST_SetSRID(
|
ordinality AS idx,
|
||||||
ST_MakePoint($1, $2),
|
ST_SetSRID(ST_MakePoint(lon1, lat1), 4326) AS p1,
|
||||||
4326
|
ST_SetSRID(ST_MakePoint(lon2, lat2), 4326) AS p2
|
||||||
) AS p1,
|
FROM UNNEST($1::float8[], $2::float8[], $3::float8[], $4::float8[])
|
||||||
|
WITH ORDINALITY AS t(lon1, lat1, lon2, lat2, ordinality)
|
||||||
ST_SetSRID(
|
|
||||||
ST_MakePoint($3, $4),
|
|
||||||
4326
|
|
||||||
) AS p2
|
|
||||||
),
|
),
|
||||||
|
|
||||||
track AS (
|
track AS (
|
||||||
SELECT
|
SELECT
|
||||||
|
idx,
|
||||||
p1,
|
p1,
|
||||||
p2,
|
p2,
|
||||||
ST_MakeLine(p1, p2) AS line
|
ST_MakeLine(p1, p2) AS line
|
||||||
FROM points
|
FROM segments
|
||||||
),
|
),
|
||||||
|
|
||||||
endpoint_counties AS (
|
endpoint_counties AS (
|
||||||
SELECT
|
SELECT
|
||||||
|
t.idx,
|
||||||
a.id AS from_id,
|
a.id AS from_id,
|
||||||
b.id AS to_id,
|
b.id AS to_id,
|
||||||
t.line
|
t.line
|
||||||
|
|
@ -65,6 +84,7 @@ pub async fn crossings_between(
|
||||||
|
|
||||||
intersections AS (
|
intersections AS (
|
||||||
SELECT
|
SELECT
|
||||||
|
e.idx,
|
||||||
e.from_id,
|
e.from_id,
|
||||||
e.to_id,
|
e.to_id,
|
||||||
e.line,
|
e.line,
|
||||||
|
|
@ -86,6 +106,7 @@ pub async fn crossings_between(
|
||||||
)
|
)
|
||||||
|
|
||||||
SELECT
|
SELECT
|
||||||
|
idx,
|
||||||
from_id,
|
from_id,
|
||||||
to_id,
|
to_id,
|
||||||
|
|
||||||
|
|
@ -102,13 +123,13 @@ pub async fn crossings_between(
|
||||||
WHERE
|
WHERE
|
||||||
ST_Dimension(intersection) = 0
|
ST_Dimension(intersection) = 0
|
||||||
|
|
||||||
ORDER BY fraction
|
ORDER BY idx, fraction
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(a.lon)
|
.bind(&lon1)
|
||||||
.bind(a.lat)
|
.bind(&lat1)
|
||||||
.bind(b.lon)
|
.bind(&lon2)
|
||||||
.bind(b.lat)
|
.bind(&lat2)
|
||||||
.fetch_all(db)
|
.fetch_all(db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|
@ -125,6 +146,14 @@ pub async fn crossings_between(
|
||||||
continue;
|
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 from_county_id: i64 = row.try_get("from_id")?;
|
||||||
|
|
||||||
let to_county_id: i64 = row.try_get("to_id")?;
|
let to_county_id: i64 = row.try_get("to_id")?;
|
||||||
|
|
|
||||||
|
|
@ -1031,8 +1031,7 @@ pub async fn process_activity_with_client(
|
||||||
|
|
||||||
let mut crossing_count = 0usize;
|
let mut crossing_count = 0usize;
|
||||||
|
|
||||||
for pair in points.windows(2) {
|
let crossings = geo::crossings_for_track(&state.db, &points).await?;
|
||||||
let crossings = geo::crossings_between(&state.db, &pair[0], &pair[1]).await?;
|
|
||||||
|
|
||||||
for crossing in crossings {
|
for crossing in crossings {
|
||||||
let interval = IntervalsClient::matching_interval(&activity, crossing.track_index);
|
let interval = IntervalsClient::matching_interval(&activity, crossing.track_index);
|
||||||
|
|
@ -1052,7 +1051,6 @@ pub async fn process_activity_with_client(
|
||||||
"county crossing detected"
|
"county crossing detected"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
db::mark_activity_processed(&mut tx, activity_db_id).await?;
|
db::mark_activity_processed(&mut tx, activity_db_id).await?;
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue