waypoint and track names
This commit is contained in:
parent
8e79bef1d9
commit
aa2dcbe16c
4 changed files with 115 additions and 29 deletions
|
|
@ -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 {
|
||||||
|
|
|
||||||
120
src/crossings.rs
120
src/crossings.rs
|
|
@ -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,16 +276,23 @@ 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
|
||||||
|
.cmp(&b.segment_index)
|
||||||
|
.then_with(|| {
|
||||||
a.position
|
a.position
|
||||||
.partial_cmp(&b.position)
|
.partial_cmp(&b.position)
|
||||||
.unwrap_or(Ordering::Equal)
|
.unwrap_or(Ordering::Equal)
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue