use crate::{
models::CourseOverGround,
navigation_controller::models::StepAdvanceStatus::{self, Advanced, EndOfRoute},
};
use crate::{
models::{GeographicCoordinate, RouteStep, UserLocation},
navigation_controller::models::TripProgress,
};
use geo::{
Bearing, Closest, Coord, Distance, Euclidean, Geodesic, Haversine, HaversineClosestPoint,
Length, LineLocatePoint, LineString, Point,
};
#[cfg(test)]
use {
crate::navigation_controller::test_helpers::gen_dummy_route_step,
geo::{CoordsIter, coord, point},
proptest::{collection::vec, prelude::*},
};
#[cfg(test)]
use crate::test_utils::{arb_coord, arb_user_loc, make_user_location};
pub fn index_of_closest_segment_origin(location: UserLocation, line: &LineString) -> Option<u64> {
let point = Point::from(location.coordinates);
line.lines()
.enumerate()
.min_by(|(_, line_segment_1), (_, line_segment_2)| {
let dist1 = Euclidean.distance(line_segment_1, &point);
let dist2 = Euclidean.distance(line_segment_2, &point);
dist1.total_cmp(&dist2)
})
.map(|(index, _)| index as u64)
}
fn get_bearing_to_next_point(
index_along_line: usize,
line: &LineString,
) -> Option<CourseOverGround> {
let mut points = line.points().skip(index_along_line);
let current = points.next()?;
let next = points.next()?;
let degrees = Geodesic.bearing(current, next);
Some(CourseOverGround::new(degrees, None))
}
pub fn apply_snapped_course(
location: UserLocation,
index_along_line: Option<u64>,
line: &LineString,
) -> UserLocation {
let snapped_course =
index_along_line.and_then(|index| get_bearing_to_next_point(index as usize, line));
let course_over_ground = snapped_course.or(location.course_over_ground);
UserLocation {
course_over_ground,
..location
}
}
pub fn snap_user_location_to_line(location: UserLocation, line: &LineString) -> UserLocation {
let original_point = Point::from(location);
snap_point_to_line(&original_point, line).map_or_else(
|| location,
|snapped| UserLocation {
coordinates: GeographicCoordinate {
lng: snapped.x(),
lat: snapped.y(),
},
..location
},
)
}
pub(crate) fn trunc_float(value: f64, decimal_digits: u32) -> f64 {
let factor = 10_i64.pow(decimal_digits) as f64;
(value * factor).round() / factor
}
fn is_valid_float(value: f64) -> bool {
!value.is_nan() && !value.is_subnormal() && !value.is_infinite()
}
fn snap_point_to_line(point: &Point, line: &LineString) -> Option<Point> {
if Euclidean.distance(line, point) < 0.000_001 {
return Some(*point);
}
if !is_valid_float(point.x()) || !is_valid_float(point.y()) {
return None;
}
match line.haversine_closest_point(point) {
Closest::Intersection(snapped) | Closest::SinglePoint(snapped) => {
let (x, y) = (snapped.x(), snapped.y());
if is_valid_float(x) && is_valid_float(y) {
Some(snapped)
} else {
None
}
}
Closest::Indeterminate => None,
}
}
pub fn deviation_from_line(point: &Point, line: &LineString) -> Option<f64> {
snap_point_to_line(point, line).and_then(|snapped| {
let distance = Haversine.distance(snapped, *point);
if distance.is_nan() || distance.is_infinite() {
None
} else {
Some(distance)
}
})
}
pub(crate) fn is_within_threshold_to_end_of_linestring(
current_position: &Point,
current_step_linestring: &LineString,
threshold: f64,
) -> bool {
current_step_linestring
.coords()
.last()
.is_some_and(|end_coord| {
let end_point = Point::from(*end_coord);
let distance_to_end = Haversine.distance(end_point, *current_position);
distance_to_end <= threshold
})
}
pub(crate) fn advance_step(remaining_steps: &[RouteStep]) -> StepAdvanceStatus {
match remaining_steps.get(1) {
Some(new_step) => Advanced {
step: new_step.clone(),
},
None => EndOfRoute,
}
}
fn distance_along(point: &Point, linestring: &LineString) -> Option<f64> {
let total_length = Haversine.length(linestring);
if total_length == 0.0 {
return Some(0.0);
}
let (_, _, traversed) = linestring.lines().try_fold(
(0f64, f64::INFINITY, 0f64),
|(cum_length, closest_dist_to_point, traversed), segment| {
let segment_linestring = LineString::from(segment);
let segment_distance_to_point = Euclidean.distance(&segment, point);
let segment_length = Haversine.length(&segment_linestring);
if segment_distance_to_point < closest_dist_to_point {
let segment_fraction = segment.line_locate_point(point)?;
Some((
cum_length + segment_length,
segment_distance_to_point,
cum_length + segment_fraction * segment_length,
))
} else {
Some((
cum_length + segment_length,
closest_dist_to_point,
traversed,
))
}
},
)?;
Some(traversed)
}
fn travel_distance_to_end_of_step(
snapped_location: &Point,
current_step_linestring: &LineString,
) -> Option<f64> {
let step_length = Haversine.length(current_step_linestring);
distance_along(snapped_location, current_step_linestring)
.map(|traversed| step_length - traversed)
}
pub(crate) fn distance_between_locations(
previous_location: &UserLocation,
current_location: &UserLocation,
) -> f64 {
let prev_point: Point = previous_location.coordinates.into();
let current_point: Point = current_location.coordinates.into();
Haversine.distance(prev_point, current_point)
}
pub fn calculate_trip_progress(
snapped_location: &Point,
current_step_linestring: &LineString,
remaining_steps: &[RouteStep],
) -> TripProgress {
let Some(current_step) = remaining_steps.first() else {
return TripProgress {
distance_to_next_maneuver: 0.0,
distance_remaining: 0.0,
duration_remaining: 0.0,
};
};
let distance_to_next_maneuver =
travel_distance_to_end_of_step(snapped_location, current_step_linestring)
.unwrap_or(current_step.distance);
let pct_remaining_current_step = if current_step.distance > 0f64 {
distance_to_next_maneuver / current_step.distance
} else {
0f64
};
let duration_to_next_maneuver = pct_remaining_current_step * current_step.duration;
if remaining_steps.len() == 1 {
return TripProgress {
distance_to_next_maneuver,
distance_remaining: distance_to_next_maneuver,
duration_remaining: duration_to_next_maneuver,
};
}
let steps_after_current = &remaining_steps[1..];
let distance_remaining = distance_to_next_maneuver
+ steps_after_current
.iter()
.map(|step| step.distance)
.sum::<f64>();
let duration_remaining = duration_to_next_maneuver
+ steps_after_current
.iter()
.map(|step| step.duration)
.sum::<f64>();
TripProgress {
distance_to_next_maneuver,
distance_remaining,
duration_remaining,
}
}
pub(crate) fn get_linestring(geometry: &[GeographicCoordinate]) -> LineString {
geometry
.iter()
.map(|coord| Coord {
x: coord.lng,
y: coord.lat,
})
.collect()
}
#[cfg(test)]
proptest! {
#[test]
fn snap_point_to_line_intersection(
x1: f64, y1: f64,
x2: f64, y2: f64,
) {
let point = point! {
x: x1,
y: y1,
};
let line = LineString::new(vec! {
coord! {
x: x1,
y: y1,
},
coord! {
x: x2,
y: y2,
},
});
if let Some(snapped) = snap_point_to_line(&point, &line) {
let x = snapped.x();
let y = snapped.y();
prop_assert!(is_valid_float(x) || (!is_valid_float(x1) && x == x1));
prop_assert!(is_valid_float(y) || (!is_valid_float(y1) && y == y1));
prop_assert!(Euclidean.distance(&line, &snapped) < 0.000001);
} else {
let is_miniscule_difference = (x1 - x2).abs() < 0.00000001 || (y1 - y2).abs() < 0.00000001;
let is_non_wgs84 = (x1 - x2).abs() > 180.0 || (y1 - y2).abs() > 90.0;
prop_assert!(is_miniscule_difference || is_non_wgs84);
}
}
#[test]
fn test_end_of_step_progress(
x1 in -180f64..180f64, y1 in -90f64..90f64,
x2 in -180f64..180f64, y2 in -90f64..90f64,
) {
let current_route_step = gen_dummy_route_step(x1, y1, x2, y2);
let linestring = current_route_step.get_linestring();
let end = linestring.points().last().expect("Expected at least one point");
let progress = calculate_trip_progress(&end, &linestring, &[current_route_step]);
prop_assert_eq!(progress.distance_to_next_maneuver, 0f64);
prop_assert_eq!(progress.distance_remaining, 0f64);
prop_assert_eq!(progress.duration_remaining, 0f64);
}
#[test]
fn test_end_of_trip_progress_valhalla_arrival(
x1: f64, y1: f64,
) {
let current_route_step = gen_dummy_route_step(x1, y1, x1, y1);
let linestring = current_route_step.get_linestring();
let end = linestring.points().last().expect("Expected at least one point");
let progress = calculate_trip_progress(&end, &linestring, &[current_route_step]);
prop_assert_eq!(progress.distance_to_next_maneuver, 0f64);
prop_assert_eq!(progress.distance_remaining, 0f64);
prop_assert_eq!(progress.duration_remaining, 0f64);
}
#[test]
fn test_geometry_index_empty_linestring(
user_loc in arb_user_loc(0.0)
) {
let index = index_of_closest_segment_origin(user_loc, &LineString::new(vec![]));
prop_assert_eq!(index, None);
}
#[test]
fn test_geometry_index_single_coord_invalid_linestring(
coord in arb_coord(),
) {
let index = index_of_closest_segment_origin(make_user_location(coord, 0.0), &LineString::new(vec![coord]));
prop_assert_eq!(index, None);
}
#[test]
fn test_geometry_index_is_some_for_reasonable_linestrings(
user_coord in arb_coord(),
coords in vec(arb_coord(), 2..500)
) {
let index = index_of_closest_segment_origin(make_user_location(user_coord, 0.0), &LineString::new(coords));
prop_assert_ne!(index, None);
}
#[test]
fn test_geometry_index_at_terminal_coord(
coords in vec(arb_coord(), 2..500)
) {
let last_coord = coords.last().unwrap();
let coord_len = coords.len();
let user_location = make_user_location(*last_coord, 0.0);
let index = index_of_closest_segment_origin(user_location, &LineString::new(coords));
prop_assert_ne!(index, None);
let index = index.unwrap();
prop_assert!(index < (coord_len - 1) as u64);
}
#[test]
fn test_bearing_fuzz(coords in vec(arb_coord(), 2..500), index in 0usize..1_000usize) {
let line = LineString::new(coords);
let result = get_bearing_to_next_point(index, &line);
if index < line.coords_count() - 1 {
prop_assert!(result.is_some());
} else {
prop_assert!(result.is_none());
}
}
#[test]
fn test_bearing_end_of_line(coords in vec(arb_coord(), 2..500)) {
let line = LineString::new(coords);
prop_assert!(get_bearing_to_next_point(line.coords_count(), &line).is_none());
prop_assert!(get_bearing_to_next_point(line.coords_count() - 1, &line).is_none());
prop_assert!(get_bearing_to_next_point(line.coords_count() - 2, &line).is_some());
}
}
#[cfg(test)]
mod linestring_based_tests {
use super::*;
static COORDS: [Coord; 5] = [
coord!(x: 0.0, y: 0.0),
coord!(x: 1.0, y: 1.0),
coord!(x: 2.0, y: 2.0),
coord!(x: 3.0, y: 3.0),
coord!(x: 4.0, y: 4.0),
];
#[test]
fn test_geometry_index_at_point() {
let line = LineString::new(COORDS.to_vec());
let index = index_of_closest_segment_origin(make_user_location(COORDS[2], 2.0), &line);
assert_eq!(index, Some(1));
}
#[test]
fn test_geometry_index_near_point() {
let line = LineString::new(COORDS.to_vec());
let index =
index_of_closest_segment_origin(make_user_location(coord!(x: 1.1, y: 1.1), 0.0), &line);
assert_eq!(index, Some(1));
let index = index_of_closest_segment_origin(
make_user_location(coord!(x: 1.99, y: 1.99), 0.0),
&line,
);
assert_eq!(index, Some(1));
}
#[test]
fn test_geometry_index_far_from_point() {
let line = LineString::new(COORDS.to_vec());
let index = index_of_closest_segment_origin(
make_user_location(coord!(x: -1.1, y: -1.1), 0.0),
&line,
);
assert_eq!(index, Some(0));
let index = index_of_closest_segment_origin(
make_user_location(coord!(x: 10.0, y: 10.0), 0.0),
&line,
);
assert_eq!(index, Some(3));
}
}
#[cfg(test)]
mod bearing_snapping_tests {
use super::*;
static COORDS: [Coord; 6] = [
coord!(x: 0.0, y: 0.0),
coord!(x: 1.0, y: 1.0),
coord!(x: 2.0, y: 1.0),
coord!(x: 2.0, y: 2.0),
coord!(x: 2.0, y: 1.0),
coord!(x: 1.0, y: 1.0),
];
#[test]
fn test_bearing_to_next_point() {
let line = LineString::new(COORDS.to_vec());
let bearing = get_bearing_to_next_point(0, &line);
assert_eq!(
bearing,
Some(CourseOverGround {
degrees: 45,
accuracy: None
})
);
let bearing = get_bearing_to_next_point(1, &line);
assert_eq!(
bearing,
Some(CourseOverGround {
degrees: 90,
accuracy: None
})
);
let bearing = get_bearing_to_next_point(2, &line);
assert_eq!(
bearing,
Some(CourseOverGround {
degrees: 0,
accuracy: None
})
);
let bearing = get_bearing_to_next_point(3, &line);
assert_eq!(
bearing,
Some(CourseOverGround {
degrees: 180,
accuracy: None
})
);
let bearing = get_bearing_to_next_point(4, &line);
assert_eq!(
bearing,
Some(CourseOverGround {
degrees: 270,
accuracy: None
})
);
let bearing = get_bearing_to_next_point(5, &line);
assert_eq!(bearing, None);
}
#[test]
fn test_apply_snapped_course() {
let line = LineString::new(COORDS.to_vec());
let user_location = make_user_location(coord!(x: 5.0, y: 1.0), 0.0);
let updated_location = apply_snapped_course(user_location, Some(1), &line);
assert_eq!(
updated_location.course_over_ground,
Some(CourseOverGround {
degrees: 90,
accuracy: None
})
);
}
}