waypoint and track names

This commit is contained in:
Jonas Rabenstein 2026-08-12 02:36:10 +02:00
commit aa2dcbe16c
4 changed files with 115 additions and 29 deletions

View file

@ -35,6 +35,10 @@ pub struct Args {
/// " (borderpoi)" suffix. /// " (borderpoi)" suffix.
#[arg(long)] #[arg(long)]
pub name: Option<String>, pub name: Option<String>,
/// GPX waypoint type. Defaults to "SPRINT"
#[arg(long, default_value = "SPRINT")]
pub waypoint_type: String,
} }
impl Args { impl Args {

View file

@ -1,11 +1,15 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use geo::{algorithm::bounding_rect::BoundingRect, Contains, Coord, Geometry, LineString, Point}; use geo::{
use rstar::{RTree, AABB}; algorithm::bounding_rect::BoundingRect, Contains, Coord, Geometry, LineString, Point,
};
use rstar::{AABB, RTree};
use std::cmp::Ordering; use std::cmp::Ordering;
use crate::{ use crate::{
county::County, county::County,
geometry::{deduplicate_hits, interpolate, segment_intersection, snap_to_segment}, geometry::{
deduplicate_hits, interpolate, segment_intersection, snap_to_segment,
},
}; };
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -23,6 +27,7 @@ struct Transition {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Crossing { pub struct Crossing {
pub point: Point<f64>, pub point: Point<f64>,
pub elevation: f64,
pub from: County, pub from: County,
pub to: County, pub to: County,
pub segment_index: usize, pub segment_index: usize,
@ -33,6 +38,7 @@ pub struct Crossing {
pub struct TrackPoint { pub struct TrackPoint {
pub lon: f64, pub lon: f64,
pub lat: f64, pub lat: f64,
pub elevation: Option<f64>,
} }
/// Find all administrative boundary crossings along a GPX track. /// 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 /// The original track is never modified. Crossing points are reconstructed
/// from the original track segment and the intersection parameter, ensuring /// from the original track segment and the intersection parameter, ensuring
/// that the resulting waypoint lies exactly on the original segment. /// that the resulting waypoint lies exactly on the original segment.
pub fn find_crossings(track: &[TrackPoint], tree: &RTree<County>) -> Result<Vec<Crossing>> { ///
/// Elevation is linearly interpolated between the two original track points.
pub fn find_crossings(
track: &[TrackPoint],
tree: &RTree<County>,
) -> Result<Vec<Crossing>> {
let mut crossings = Vec::new(); let mut crossings = Vec::new();
for (segment_index, pair) in track.windows(2).enumerate() { for (segment_index, pair) in track.windows(2).enumerate() {
let start = Point::new(pair[0].lon, pair[0].lat); let start = &pair[0];
let end = Point::new(pair[1].lon, pair[1].lat); 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![ let segment = LineString::from(vec![
Coord { Coord {
x: start.x(), x: start_point.x(),
y: start.y(), y: start_point.y(),
}, },
Coord { Coord {
x: end.x(), x: end_point.x(),
y: end.y(), y: end_point.y(),
}, },
]); ]);
@ -74,21 +88,35 @@ pub fn find_crossings(track: &[TrackPoint], tree: &RTree<County>) -> Result<Vec<
continue; continue;
} }
let hits = collect_segment_hits(start, end, &candidates); let hits = collect_segment_hits(start_point, end_point, &candidates);
if hits.is_empty() { if hits.is_empty() {
continue; continue;
} }
let transitions = reconstruct_transitions(start, end, &candidates, &hits); let transitions =
reconstruct_transitions(start_point, end_point, &candidates, &hits);
for transition in transitions { for transition in transitions {
// Reconstruct the point from the original GPX segment. // Reconstruct the point from the original GPX segment.
// This is the actual snapping step. // This is the actual snapping step.
let snapped_point = snap_to_segment(start, end, transition.position); let snapped_point =
snap_to_segment(start_point, end_point, transition.position);
// Interpolate the elevation at the exact crossing position.
//
// If one track point has no elevation, use the other one.
// If neither has elevation, fall back to zero because Garmin
// requires an elevation value for waypoint distance handling.
let elevation = interpolate_elevation(
start.elevation,
end.elevation,
transition.position,
);
crossings.push(Crossing { crossings.push(Crossing {
point: snapped_point, point: snapped_point,
elevation,
from: transition.from, from: transition.from,
to: transition.to, to: transition.to,
segment_index, segment_index,
@ -100,6 +128,20 @@ pub fn find_crossings(track: &[TrackPoint], tree: &RTree<County>) -> Result<Vec<
Ok(deduplicate_crossings(crossings)) Ok(deduplicate_crossings(crossings))
} }
fn interpolate_elevation(
start: Option<f64>,
end: Option<f64>,
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( fn collect_segment_hits(
start: Point<f64>, start: Point<f64>,
end: Point<f64>, end: Point<f64>,
@ -118,7 +160,12 @@ fn collect_segment_hits(
let mut hits = Vec::new(); let mut hits = Vec::new();
for county in candidates { 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) deduplicate_hits(hits)
@ -132,19 +179,39 @@ fn collect_geometry_intersections(
) { ) {
match geometry { match geometry {
Geometry::Polygon(polygon) => { Geometry::Polygon(polygon) => {
collect_ring_intersections(polygon.exterior(), start, end, hits); collect_ring_intersections(
polygon.exterior(),
start,
end,
hits,
);
for interior in polygon.interiors() { for interior in polygon.interiors() {
collect_ring_intersections(interior, start, end, hits); collect_ring_intersections(
interior,
start,
end,
hits,
);
} }
} }
Geometry::MultiPolygon(multipolygon) => { Geometry::MultiPolygon(multipolygon) => {
for polygon in &multipolygon.0 { 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() { 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<BoundaryHit>, hits: &mut Vec<BoundaryHit>,
) { ) {
for edge in ring.lines() { 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 }); hits.push(BoundaryHit { position });
} }
} }
@ -207,20 +276,27 @@ fn reconstruct_transitions(
transitions transitions
} }
fn county_at_point(point: Point<f64>, candidates: &[County]) -> Option<County> { fn county_at_point(
point: Point<f64>,
candidates: &[County],
) -> Option<County> {
candidates candidates
.iter() .iter()
.find(|county| county.geometry.contains(&point)) .find(|county| county.geometry.contains(&point))
.cloned() .cloned()
} }
fn deduplicate_crossings(mut crossings: Vec<Crossing>) -> Vec<Crossing> { fn deduplicate_crossings(
mut crossings: Vec<Crossing>,
) -> Vec<Crossing> {
crossings.sort_by(|a, b| { crossings.sort_by(|a, b| {
a.segment_index.cmp(&b.segment_index).then_with(|| { a.segment_index
a.position .cmp(&b.segment_index)
.partial_cmp(&b.position) .then_with(|| {
.unwrap_or(Ordering::Equal) a.position
}) .partial_cmp(&b.position)
.unwrap_or(Ordering::Equal)
})
}); });
let mut result = Vec::new(); let mut result = Vec::new();

View file

@ -55,6 +55,7 @@ pub fn extract_track_points(gpx: &Gpx) -> Result<Vec<TrackPoint>> {
points.push(TrackPoint { points.push(TrackPoint {
lon: point.x(), lon: point.x(),
lat: point.y(), lat: point.y(),
elevation: waypoint.elevation,
}); });
} }
} }
@ -67,13 +68,13 @@ pub fn extract_track_points(gpx: &Gpx) -> Result<Vec<TrackPoint>> {
Ok(points) 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() { for crossing in crossings.iter() {
let mut waypoint = Waypoint::new(crossing.point); let mut waypoint = Waypoint::new(crossing.point);
waypoint.name = Some(crossing.to.name.clone()); waypoint.name = Some(crossing.to.name.clone());
waypoint.elevation = Some(0.0); waypoint.elevation = Some(crossing.elevation);
waypoint.description = Some(format!( waypoint.description = Some(format!(
"Administrative border crossing: {} [{}] -> {} [{}]", "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.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); gpx.waypoints.push(waypoint);
} }

View file

@ -83,6 +83,10 @@ fn main() -> Result<()> {
let mut output_gpx = gpx; 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 output_gpx
.metadata .metadata
.get_or_insert_with(Default::default) .get_or_insert_with(Default::default)
@ -91,6 +95,7 @@ fn main() -> Result<()> {
append_crossing_waypoints( append_crossing_waypoints(
&mut output_gpx, &mut output_gpx,
&crossings, &crossings,
&args.waypoint_type,
); );
write_gpx( write_gpx(