use crate::annotation_system::annotation_support::{
Annotation, ClosestPoint, DirectionFlag, LabelPosition, MercatorRectangle, RenderingInformation,
};
use crate::gui_system::internal_math::MercatorPoint;
use crate::gui_system::latitude_longitude::LatitudeLongitude;
use geo::{Densify, Haversine, LineString};
use iced::Color;
use slotmap::{SlotMap, new_key_type};
use std::sync::Arc;
const MAX_SEGMENT_METER: f64 = 10_000.0;
const PATH_HOVER_PADDING: f64 = 8.0;
new_key_type! {
pub struct PathKey;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PathError {
KeyNotContained,
TooFewPositions,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PathLocation {
pub position: LatitudeLongitude,
pub distance_from_start_meter: f64,
}
#[derive(Debug)]
struct PathGeometry {
dense_positions: Vec<LatitudeLongitude>,
mercator: Vec<MercatorPoint>,
distance_to_vertex: Vec<f64>,
bounds: MercatorRectangle,
center: LatitudeLongitude,
}
struct Projection {
segment: usize,
point: MercatorPoint,
}
impl PathGeometry {
fn new(positions: Vec<LatitudeLongitude>) -> Result<Self, PathError> {
if positions.len() < 2 {
return Err(PathError::TooFewPositions);
}
let line: LineString = positions
.iter()
.map(|pos| (pos.longitude, pos.latitude))
.collect::<Vec<_>>()
.into();
let dense_positions: Vec<_> = Haversine
.densify(&line, MAX_SEGMENT_METER)
.coords()
.map(|coord| LatitudeLongitude::new(coord.y, coord.x))
.collect();
let mercator = dense_positions.iter().map(|&pos| pos.into()).collect();
let distance_to_vertex = std::iter::once(0.0)
.chain(dense_positions.windows(2).scan(0.0, |sum, pair| {
*sum += pair[0].get_distance_to(pair[1]);
Some(*sum)
}))
.collect();
let bounds = MercatorRectangle::create_from_position_array(
dense_positions.iter().copied(),
PATH_HOVER_PADDING,
)
.expect("a path has at least two positions");
let mut geometry = Self {
dense_positions,
mercator,
distance_to_vertex,
center: bounds.get_center(),
bounds,
};
geometry.center = geometry.project(geometry.center.into()).point.into();
Ok(geometry)
}
fn project(&self, position: MercatorPoint) -> Projection {
self.mercator
.windows(2)
.enumerate()
.map(|(segment, pair)| {
let (start, end) = (pair[0], pair[1]);
let (dx, dy) = (end.x - start.x, end.y - start.y);
let length_squared = dx * dx + dy * dy;
let fraction = if length_squared > 0.0 {
(((position.x - start.x) * dx + (position.y - start.y) * dy) / length_squared)
.clamp(0.0, 1.0)
} else {
0.0
};
let point = MercatorPoint {
x: start.x + fraction * dx,
y: start.y + fraction * dy,
};
let distance = (point.x - position.x).hypot(point.y - position.y);
(distance, Projection { segment, point })
})
.min_by(|a, b| a.0.total_cmp(&b.0))
.map(|(_, projection)| projection)
.expect("a path has at least two positions")
}
fn locate(&self, position: LatitudeLongitude) -> PathLocation {
let Projection { segment, point } = self.project(position.into());
let position = LatitudeLongitude::from(point);
PathLocation {
position,
distance_from_start_meter: self.distance_to_vertex[segment]
+ self.dense_positions[segment].get_distance_to(position),
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct PathInfo {
geometry: Arc<PathGeometry>,
color: Color,
pub(crate) description: Option<String>,
pub(crate) flag: Option<DirectionFlag>,
}
impl Annotation for PathInfo {
fn description(&self, _cursor: LatitudeLongitude) -> Option<String> {
self.description.clone()
}
fn cull_bounds(&self) -> &MercatorRectangle {
&self.geometry.bounds
}
fn get_center(&self) -> LatitudeLongitude {
self.geometry.center
}
fn closest_point(&self, position: MercatorPoint) -> ClosestPoint {
ClosestPoint::Nearest(self.geometry.project(position).point)
}
fn label_anchor(&self, cursor: LatitudeLongitude) -> LabelPosition {
LabelPosition::FixGeoLocation(self.geometry.project(cursor.into()).point.into())
}
fn get_flag(&self) -> Option<DirectionFlag> {
self.flag
}
fn get_render_information(&self) -> RenderingInformation<'_> {
RenderingInformation::Line {
positions: &self.geometry.dense_positions,
color: self.color,
}
}
fn clone_box(&self) -> Box<dyn Annotation> {
Box::new(self.clone())
}
}
#[derive(Debug, Clone, Default)]
pub struct PathSystem {
pub(crate) path_collection: SlotMap<PathKey, PathInfo>,
}
impl PathSystem {
pub fn add_path(
&mut self,
positions: Vec<LatitudeLongitude>,
color: Color,
description: Option<String>,
) -> Result<PathKey, PathError> {
let path = PathInfo {
geometry: Arc::new(PathGeometry::new(positions)?),
color,
description,
flag: None,
};
Ok(self.path_collection.insert(path))
}
pub fn closest_point_on_path(
&self,
key: PathKey,
position: LatitudeLongitude,
) -> Result<PathLocation, PathError> {
Ok(self
.path_collection
.get(key)
.ok_or(PathError::KeyNotContained)?
.geometry
.locate(position))
}
pub fn center(&self, key: PathKey) -> Result<LatitudeLongitude, PathError> {
Ok(self
.path_collection
.get(key)
.ok_or(PathError::KeyNotContained)?
.geometry
.center)
}
pub fn update_positions(
&mut self,
key: PathKey,
positions: Vec<LatitudeLongitude>,
) -> Result<(), PathError> {
let path = self
.path_collection
.get_mut(key)
.ok_or(PathError::KeyNotContained)?;
path.geometry = Arc::new(PathGeometry::new(positions)?);
Ok(())
}
pub fn update_color(&mut self, key: PathKey, color: Color) -> Result<(), PathError> {
self.path_collection
.get_mut(key)
.ok_or(PathError::KeyNotContained)?
.color = color;
Ok(())
}
pub fn update_description(
&mut self,
key: PathKey,
description: Option<String>,
) -> Result<(), PathError> {
self.path_collection
.get_mut(key)
.ok_or(PathError::KeyNotContained)?
.description = description;
Ok(())
}
pub fn set_flag(&mut self, key: PathKey, flag: Option<DirectionFlag>) -> Result<(), PathError> {
self.path_collection
.get_mut(key)
.ok_or(PathError::KeyNotContained)?
.flag = flag;
Ok(())
}
pub fn delete_path(&mut self, key: PathKey) -> Result<(), PathError> {
self.path_collection
.remove(key)
.map(|_| ())
.ok_or(PathError::KeyNotContained)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn geometry(points: &[(f64, f64)]) -> PathGeometry {
PathGeometry::new(
points
.iter()
.map(|&(lat, long)| LatitudeLongitude::new(lat, long))
.collect(),
)
.expect("at least two positions")
}
#[test]
fn long_segments_get_densified_on_the_great_circle() {
let path = geometry(&[(50.0, 0.0), (50.0, 10.0)]);
assert!(path.dense_positions.len() > 70);
let middle = path.dense_positions[path.dense_positions.len() / 2];
assert!((middle.latitude - 50.107).abs() < 0.002, "{middle:?}");
assert!(path.bounds.min_max_lat.1 > 50.1);
for pair in path.dense_positions.windows(2) {
assert!(pair[0].get_distance_to(pair[1]) <= MAX_SEGMENT_METER * 1.01);
}
let total = *path.distance_to_vertex.last().expect("not empty");
let direct =
LatitudeLongitude::new(50.0, 0.0).get_distance_to(LatitudeLongitude::new(50.0, 10.0));
assert!((total - direct).abs() < 1.0, "{total} vs {direct}");
}
#[test]
fn short_segments_stay_as_they_are() {
let points = [(49.75, 6.63), (49.755, 6.64), (49.76, 6.645)];
let path = geometry(&points);
let expected: Vec<_> = points
.iter()
.map(|&(lat, long)| LatitudeLongitude::new(lat, long))
.collect();
assert_eq!(path.dense_positions, expected);
}
#[test]
fn locate_measures_along_the_path() {
let path = geometry(&[(50.0, 7.0), (50.0, 7.01), (50.01, 7.01)]);
let first_leg = path.dense_positions[0].get_distance_to(path.dense_positions[1]);
let location = path.locate(LatitudeLongitude::new(50.005, 7.02));
assert!((location.position.longitude - 7.01).abs() < 1e-9);
let up = path.dense_positions[1].get_distance_to(location.position);
assert!((location.distance_from_start_meter - (first_leg + up)).abs() < 1e-6);
let location = path.locate(LatitudeLongitude::new(50.0, 6.9));
assert!(location.distance_from_start_meter.abs() < 1e-6);
assert!((location.position.longitude - 7.0).abs() < 1e-9);
}
#[test]
fn repeated_positions_do_not_break_the_projection() {
let path = geometry(&[(50.0, 7.0), (50.0, 7.0), (50.0, 7.01)]);
let location = path.locate(LatitudeLongitude::new(50.001, 7.005));
assert!(location.distance_from_start_meter.is_finite());
assert!((location.position.longitude - 7.005).abs() < 1e-9);
}
#[test]
fn too_few_positions_give_no_path() {
let mut system = PathSystem::default();
assert_eq!(
system.add_path(vec![], Color::BLACK, None),
Err(PathError::TooFewPositions)
);
let key = system
.add_path(
vec![
LatitudeLongitude::new(50.0, 7.0),
LatitudeLongitude::new(50.0, 7.1),
],
Color::BLACK,
None,
)
.expect("two positions");
assert_eq!(
system.update_positions(key, vec![LatitudeLongitude::new(50.0, 7.0)]),
Err(PathError::TooFewPositions)
);
assert_eq!(
system.path_collection[key].geometry.dense_positions.last(),
Some(&LatitudeLongitude::new(50.0, 7.1))
);
}
}