use crate::algorithms::deviation_from_line;
use crate::models::Route;
use crate::navigation_controller::models::TripState;
#[cfg(test)]
use crate::{models::UserLocation, navigation_controller::test_helpers::get_navigating_trip_state};
#[cfg(feature = "alloc")]
use alloc::sync::Arc;
use geo::{LineString, Point};
use serde::{Deserialize, Serialize};
#[cfg(feature = "wasm-bindgen")]
use tsify::Tsify;
#[cfg(test)]
use {
crate::{
models::GeographicCoordinate,
navigation_controller::test_helpers::{gen_dummy_route_step, gen_route_from_steps},
},
proptest::prelude::*,
};
#[cfg(all(test, feature = "std", not(feature = "web-time")))]
use std::time::SystemTime;
#[cfg(all(test, feature = "web-time"))]
use web_time::SystemTime;
#[derive(Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
#[cfg_attr(feature = "wasm-bindgen", derive(Tsify))]
#[cfg_attr(feature = "wasm-bindgen", tsify(from_wasm_abi))]
pub enum RouteDeviationTracking {
None,
#[cfg_attr(feature = "wasm-bindgen", serde(rename_all = "camelCase"))]
StaticThreshold {
minimum_horizontal_accuracy: u16,
max_acceptable_deviation: f64,
},
#[serde(skip)]
Custom {
detector: Arc<dyn RouteDeviationDetector>,
},
}
impl RouteDeviationTracking {
#[must_use]
pub(crate) fn check_route_deviation(
&self,
route: &Route,
trip_state: &TripState,
) -> RouteDeviation {
match self {
RouteDeviationTracking::None => RouteDeviation::NoDeviation,
RouteDeviationTracking::StaticThreshold {
minimum_horizontal_accuracy,
max_acceptable_deviation,
} => match trip_state {
TripState::Idle { .. } | TripState::Complete { .. } => RouteDeviation::NoDeviation,
TripState::Navigating {
user_location,
remaining_steps,
..
} => {
if user_location.horizontal_accuracy > f64::from(*minimum_horizontal_accuracy) {
return RouteDeviation::NoDeviation;
}
let mut first_step_deviation = None;
for (index, step) in remaining_steps.iter().enumerate() {
let step_deviation = self.static_threshold_deviation_from_line(
&Point::from(*user_location),
&step.get_linestring(),
max_acceptable_deviation.clone(),
);
if index == 0 {
first_step_deviation = Some(step_deviation.clone());
}
if matches!(step_deviation, RouteDeviation::NoDeviation) {
return RouteDeviation::NoDeviation;
}
}
first_step_deviation.unwrap_or(RouteDeviation::NoDeviation)
}
},
RouteDeviationTracking::Custom { detector } => {
detector.check_route_deviation(route.clone(), trip_state.clone())
}
}
}
fn static_threshold_deviation_from_line(
&self,
point: &Point,
line: &LineString,
max_acceptable_deviation: f64,
) -> RouteDeviation {
deviation_from_line(point, line).map_or(RouteDeviation::NoDeviation, |deviation| {
if deviation > 0.0 && deviation > max_acceptable_deviation {
RouteDeviation::OffRoute {
deviation_from_route_line: deviation,
}
} else {
RouteDeviation::NoDeviation
}
})
}
}
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
#[cfg_attr(feature = "wasm-bindgen", derive(Tsify))]
#[cfg_attr(feature = "wasm-bindgen", tsify(into_wasm_abi, from_wasm_abi))]
pub enum RouteDeviation {
NoDeviation,
#[cfg_attr(feature = "wasm-bindgen", serde(rename_all = "camelCase"))]
OffRoute {
deviation_from_route_line: f64,
},
}
#[cfg_attr(feature = "uniffi", uniffi::export(with_foreign))]
pub trait RouteDeviationDetector: Send + Sync {
#[must_use]
fn check_route_deviation(&self, route: Route, trip_state: TripState) -> RouteDeviation;
}
#[cfg(test)]
proptest! {
#[test]
fn no_deviation_tracking(
x1: f64, y1: f64,
x2: f64, y2: f64,
x3: f64, y3: f64,
) {
let tracking = RouteDeviationTracking::None;
let current_route_step = gen_dummy_route_step(x1, y1, x2, y2);
let route = gen_route_from_steps(vec![current_route_step.clone()]);
let user_location_on_route = UserLocation {
coordinates: GeographicCoordinate {
lng: x1,
lat: y1,
},
horizontal_accuracy: 0.0,
course_over_ground: None,
timestamp: SystemTime::now(),
speed: None
};
let trip_state = get_navigating_trip_state(
user_location_on_route.clone(),
vec![current_route_step.clone()],
vec![],
RouteDeviation::NoDeviation
);
prop_assert_eq!(
tracking.check_route_deviation(&route, &trip_state),
RouteDeviation::NoDeviation
);
let user_location_random = UserLocation {
coordinates: GeographicCoordinate {
lng: x3,
lat: y3,
},
horizontal_accuracy: 0.0,
course_over_ground: None,
timestamp: SystemTime::now(),
speed: None
};
let trip_state_random = get_navigating_trip_state(
user_location_random.clone(),
vec![current_route_step.clone()],
vec![],
RouteDeviation::NoDeviation
);
prop_assert_eq!(
tracking.check_route_deviation(&route, &trip_state_random),
RouteDeviation::NoDeviation
);
}
#[test]
fn custom_no_deviation_mode(
x1: f64, y1: f64,
x2: f64, y2: f64,
x3: f64, y3: f64,
) {
struct NeverDetector {}
impl RouteDeviationDetector for NeverDetector {
fn check_route_deviation(
&self,
_route: Route,
_trip_state: TripState,
) -> RouteDeviation {
return RouteDeviation::NoDeviation
}
}
let tracking = RouteDeviationTracking::Custom {
detector: Arc::new(NeverDetector {})
};
let current_route_step = gen_dummy_route_step(x1, y1, x2, y2);
let route = gen_route_from_steps(vec![current_route_step.clone()]);
let user_location_on_route = UserLocation {
coordinates: GeographicCoordinate {
lng: x1,
lat: y1,
},
horizontal_accuracy: 0.0,
course_over_ground: None,
timestamp: SystemTime::now(),
speed: None
};
let trip_state_on_route = get_navigating_trip_state(
user_location_on_route.clone(),
vec![current_route_step.clone()],
vec![],
RouteDeviation::NoDeviation
);
prop_assert_eq!(
tracking.check_route_deviation(&route, &trip_state_on_route),
RouteDeviation::NoDeviation
);
let user_location_random = UserLocation {
coordinates: GeographicCoordinate {
lng: x3,
lat: y3,
},
horizontal_accuracy: 0.0,
course_over_ground: None,
timestamp: SystemTime::now(),
speed: None
};
let trip_state_random = get_navigating_trip_state(
user_location_random.clone(),
vec![current_route_step.clone()],
vec![],
RouteDeviation::NoDeviation
);
prop_assert_eq!(
tracking.check_route_deviation(&route, &trip_state_random),
RouteDeviation::NoDeviation
);
}
#[test]
fn custom_always_off_route(
x1: f64, y1: f64,
x2: f64, y2: f64,
x3: f64, y3: f64,
) {
struct NeverDetector {}
impl RouteDeviationDetector for NeverDetector {
fn check_route_deviation(
&self,
_route: Route,
_trip_state: TripState,
) -> RouteDeviation {
return RouteDeviation::OffRoute {
deviation_from_route_line: 7.0
}
}
}
let tracking = RouteDeviationTracking::Custom {
detector: Arc::new(NeverDetector {})
};
let current_route_step = gen_dummy_route_step(x1, y1, x2, y2);
let route = gen_route_from_steps(vec![current_route_step.clone()]);
let user_location_on_route = UserLocation {
coordinates: GeographicCoordinate {
lng: x1,
lat: y1,
},
horizontal_accuracy: 0.0,
course_over_ground: None,
timestamp: SystemTime::now(),
speed: None
};
let trip_state_on_route = get_navigating_trip_state(
user_location_on_route.clone(),
vec![current_route_step.clone()],
vec![],
RouteDeviation::NoDeviation
);
prop_assert_eq!(
tracking.check_route_deviation(&route, &trip_state_on_route),
RouteDeviation::OffRoute {
deviation_from_route_line: 7.0
}
);
let user_location_random = UserLocation {
coordinates: GeographicCoordinate {
lng: x3,
lat: y3,
},
horizontal_accuracy: 0.0,
course_over_ground: None,
timestamp: SystemTime::now(),
speed: None
};
let trip_state_random = get_navigating_trip_state(
user_location_random.clone(),
vec![current_route_step.clone()],
vec![],
RouteDeviation::NoDeviation
);
prop_assert_eq!(
tracking.check_route_deviation(&route, &trip_state_random),
RouteDeviation::OffRoute {
deviation_from_route_line: 7.0
}
);
}
#[test]
fn static_threshold_oracle_test(
x1: f64, y1: f64,
x2: f64, y2: f64,
x3: f64, y3: f64,
minimum_horizontal_accuracy: u16,
horizontal_accuracy: f64,
max_acceptable_deviation in 0f64..,
) {
let tracking = RouteDeviationTracking::StaticThreshold {
minimum_horizontal_accuracy,
max_acceptable_deviation
};
let current_route_step = gen_dummy_route_step(x1, y1, x2, y2);
let route = gen_route_from_steps(vec![current_route_step.clone()]);
let user_location_on_route = UserLocation {
coordinates: GeographicCoordinate {
lng: x1,
lat: y1,
},
horizontal_accuracy,
course_over_ground: None,
timestamp: SystemTime::now(),
speed: None
};
let trip_state = get_navigating_trip_state(
user_location_on_route.clone(),
vec![current_route_step.clone()],
vec![],
RouteDeviation::NoDeviation
);
prop_assert_eq!(
tracking.check_route_deviation(&route, &trip_state),
RouteDeviation::NoDeviation
);
let coordinates = GeographicCoordinate {
lng: x3,
lat: y3,
};
let user_location_random = UserLocation {
coordinates,
horizontal_accuracy: 0.0,
course_over_ground: None,
timestamp: SystemTime::now(),
speed: None
};
let trip_state_random = get_navigating_trip_state(
user_location_random.clone(),
vec![current_route_step.clone()],
vec![],
RouteDeviation::NoDeviation
);
let deviation = deviation_from_line(&Point::from(coordinates), ¤t_route_step.get_linestring());
match tracking.check_route_deviation(&route, &trip_state_random) {
RouteDeviation::NoDeviation => {
if let Some(calculated) = deviation {
prop_assert!(calculated <= max_acceptable_deviation);
}
}
RouteDeviation::OffRoute{ deviation_from_route_line } => {
prop_assert_eq!(
deviation_from_route_line,
deviation.unwrap()
);
}
}
}
#[test]
fn static_threshold_ignores_inaccurate_location_updates(
x1 in -180f64..=180f64, y1 in -90f64..=90f64,
x2 in -180f64..=180f64, y2 in -90f64..=90f64,
x3 in -180f64..=180f64, y3 in -90f64..=90f64,
horizontal_accuracy in 1u16..,
max_acceptable_deviation: f64,
) {
let tracking = RouteDeviationTracking::StaticThreshold {
minimum_horizontal_accuracy: horizontal_accuracy - 1,
max_acceptable_deviation
};
let current_route_step = gen_dummy_route_step(x1, y1, x2, y2);
let route = gen_route_from_steps(vec![current_route_step.clone()]);
let coordinates = GeographicCoordinate {
lng: x3,
lat: y3,
};
let user_location_random = UserLocation {
coordinates,
horizontal_accuracy: horizontal_accuracy as f64,
course_over_ground: None,
timestamp: SystemTime::now(),
speed: None
};
let trip_state_random = get_navigating_trip_state(
user_location_random.clone(),
vec![current_route_step.clone()],
vec![],
RouteDeviation::NoDeviation
);
prop_assert_eq!(
tracking.check_route_deviation(&route, &trip_state_random),
RouteDeviation::NoDeviation
);
}
}