diff --git a/src/cli.rs b/src/cli.rs index 8620141..e7413eb 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -35,6 +35,10 @@ pub struct Args { /// " (borderpoi)" suffix. #[arg(long)] pub name: Option, + + /// GPX waypoint type. Defaults to "SPRINT" + #[arg(long, default_value = "SPRINT")] + pub waypoint_type: String, } impl Args { diff --git a/src/crossings.rs b/src/crossings.rs index 3745407..9dd38a1 100644 --- a/src/crossings.rs +++ b/src/crossings.rs @@ -1,11 +1,15 @@ use anyhow::{Context, Result}; -use geo::{algorithm::bounding_rect::BoundingRect, Contains, Coord, Geometry, LineString, Point}; -use rstar::{RTree, AABB}; +use geo::{ + algorithm::bounding_rect::BoundingRect, Contains, Coord, Geometry, LineString, Point, +}; +use rstar::{AABB, RTree}; use std::cmp::Ordering; use crate::{ county::County, - geometry::{deduplicate_hits, interpolate, segment_intersection, snap_to_segment}, + geometry::{ + deduplicate_hits, interpolate, segment_intersection, snap_to_segment, + }, }; #[derive(Debug, Clone)] @@ -23,6 +27,7 @@ struct Transition { #[derive(Debug, Clone)] pub struct Crossing { pub point: Point, + pub elevation: f64, pub from: County, pub to: County, pub segment_index: usize, @@ -33,6 +38,7 @@ pub struct Crossing { pub struct TrackPoint { pub lon: f64, pub lat: f64, + pub elevation: Option, } /// Find all administrative boundary crossings along a GPX track. @@ -40,21 +46,29 @@ pub struct TrackPoint { /// The original track is never modified. Crossing points are reconstructed /// from the original track segment and the intersection parameter, ensuring /// that the resulting waypoint lies exactly on the original segment. -pub fn find_crossings(track: &[TrackPoint], tree: &RTree) -> Result> { +/// +/// Elevation is linearly interpolated between the two original track points. +pub fn find_crossings( + track: &[TrackPoint], + tree: &RTree, +) -> Result> { let mut crossings = Vec::new(); for (segment_index, pair) in track.windows(2).enumerate() { - let start = Point::new(pair[0].lon, pair[0].lat); - let end = Point::new(pair[1].lon, pair[1].lat); + let start = &pair[0]; + let end = &pair[1]; + + let start_point = Point::new(start.lon, start.lat); + let end_point = Point::new(end.lon, end.lat); let segment = LineString::from(vec![ Coord { - x: start.x(), - y: start.y(), + x: start_point.x(), + y: start_point.y(), }, Coord { - x: end.x(), - y: end.y(), + x: end_point.x(), + y: end_point.y(), }, ]); @@ -74,21 +88,35 @@ pub fn find_crossings(track: &[TrackPoint], tree: &RTree) -> Result) -> Result, + end: Option, + position: f64, +) -> f64 { + match (start, end) { + (Some(start), Some(end)) => { + start + (end - start) * position.clamp(0.0, 1.0) + } + (Some(elevation), None) | (None, Some(elevation)) => elevation, + (None, None) => 0.0, + } +} + fn collect_segment_hits( start: Point, end: Point, @@ -118,7 +160,12 @@ fn collect_segment_hits( let mut hits = Vec::new(); for county in candidates { - collect_geometry_intersections(&county.geometry, start_coord, end_coord, &mut hits); + collect_geometry_intersections( + &county.geometry, + start_coord, + end_coord, + &mut hits, + ); } deduplicate_hits(hits) @@ -132,19 +179,39 @@ fn collect_geometry_intersections( ) { match geometry { Geometry::Polygon(polygon) => { - collect_ring_intersections(polygon.exterior(), start, end, hits); + collect_ring_intersections( + polygon.exterior(), + start, + end, + hits, + ); for interior in polygon.interiors() { - collect_ring_intersections(interior, start, end, hits); + collect_ring_intersections( + interior, + start, + end, + hits, + ); } } Geometry::MultiPolygon(multipolygon) => { for polygon in &multipolygon.0 { - collect_ring_intersections(polygon.exterior(), start, end, hits); + collect_ring_intersections( + polygon.exterior(), + start, + end, + hits, + ); for interior in polygon.interiors() { - collect_ring_intersections(interior, start, end, hits); + collect_ring_intersections( + interior, + start, + end, + hits, + ); } } } @@ -160,7 +227,9 @@ fn collect_ring_intersections( hits: &mut Vec, ) { for edge in ring.lines() { - if let Some((position, _point)) = segment_intersection(start, end, edge.start, edge.end) { + if let Some((position, _point)) = + segment_intersection(start, end, edge.start, edge.end) + { hits.push(BoundaryHit { position }); } } @@ -207,20 +276,27 @@ fn reconstruct_transitions( transitions } -fn county_at_point(point: Point, candidates: &[County]) -> Option { +fn county_at_point( + point: Point, + candidates: &[County], +) -> Option { candidates .iter() .find(|county| county.geometry.contains(&point)) .cloned() } -fn deduplicate_crossings(mut crossings: Vec) -> Vec { +fn deduplicate_crossings( + mut crossings: Vec, +) -> Vec { crossings.sort_by(|a, b| { - a.segment_index.cmp(&b.segment_index).then_with(|| { - a.position - .partial_cmp(&b.position) - .unwrap_or(Ordering::Equal) - }) + a.segment_index + .cmp(&b.segment_index) + .then_with(|| { + a.position + .partial_cmp(&b.position) + .unwrap_or(Ordering::Equal) + }) }); let mut result = Vec::new(); diff --git a/src/gpx_io.rs b/src/gpx_io.rs index c96988b..ccb841a 100644 --- a/src/gpx_io.rs +++ b/src/gpx_io.rs @@ -55,6 +55,7 @@ pub fn extract_track_points(gpx: &Gpx) -> Result> { points.push(TrackPoint { lon: point.x(), lat: point.y(), + elevation: waypoint.elevation, }); } } @@ -67,13 +68,13 @@ pub fn extract_track_points(gpx: &Gpx) -> Result> { Ok(points) } -pub fn append_crossing_waypoints(gpx: &mut Gpx, crossings: &[Crossing]) { +pub fn append_crossing_waypoints(gpx: &mut Gpx, crossings: &[Crossing], waypoint_type: &str) { for crossing in crossings.iter() { let mut waypoint = Waypoint::new(crossing.point); waypoint.name = Some(crossing.to.name.clone()); - waypoint.elevation = Some(0.0); + waypoint.elevation = Some(crossing.elevation); waypoint.description = Some(format!( "Administrative border crossing: {} [{}] -> {} [{}]", @@ -82,7 +83,7 @@ pub fn append_crossing_waypoints(gpx: &mut Gpx, crossings: &[Crossing]) { waypoint.comment = Some(format!("{} -> {}", crossing.from.name, crossing.to.name,)); - waypoint.type_ = Some("administrative_boundary".to_string()); + waypoint.type_ = Some(waypoint_type.to_owned()); gpx.waypoints.push(waypoint); } diff --git a/src/main.rs b/src/main.rs index 5d0a55e..303491c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -83,6 +83,10 @@ fn main() -> Result<()> { let mut output_gpx = gpx; + // set the generated track name(s). + for track in &mut output_gpx.tracks { + track.name = Some(name.clone()); + } output_gpx .metadata .get_or_insert_with(Default::default) @@ -91,6 +95,7 @@ fn main() -> Result<()> { append_crossing_waypoints( &mut output_gpx, &crossings, + &args.waypoint_type, ); write_gpx(