use std::sync::Arc;
use super::{StepAdvanceCondition, StepAdvanceConditionSerializable, StepAdvanceResult};
use crate::{
algorithms::{
deviation_from_line, get_linestring, is_within_threshold_to_end_of_linestring,
snap_user_location_to_line,
},
navigation_controller::models::TripState,
};
use geo::Point;
use serde::{Deserialize, Serialize};
#[cfg(feature = "wasm-bindgen")]
use tsify::Tsify;
#[cfg(test)]
use proptest::prelude::*;
#[cfg(test)]
use crate::{
deviation_detection::{DeviationKind, RouteDeviation},
navigation_controller::test_helpers::get_navigating_trip_state,
test_utils::{arb_coord, make_user_location},
};
use super::SerializableStepAdvanceCondition;
#[derive(Debug, Copy, Clone)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
pub struct ManualStepCondition;
impl StepAdvanceCondition for ManualStepCondition {
#[allow(unused_variables)]
fn should_advance_step(&self, trip_state: TripState) -> StepAdvanceResult {
StepAdvanceResult::continue_with_state(Arc::new(ManualStepCondition))
}
fn new_instance(&self) -> Arc<dyn StepAdvanceCondition> {
Arc::new(ManualStepCondition)
}
}
impl StepAdvanceConditionSerializable for ManualStepCondition {
fn to_js(&self) -> SerializableStepAdvanceCondition {
SerializableStepAdvanceCondition::Manual
}
}
#[derive(Debug, Copy, Clone)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
pub struct DistanceToEndOfStepCondition {
pub distance: u16,
pub minimum_horizontal_accuracy: u16,
}
impl StepAdvanceCondition for DistanceToEndOfStepCondition {
fn should_advance_step(&self, trip_state: TripState) -> StepAdvanceResult {
self.should_advance_inner(&trip_state)
.unwrap_or(StepAdvanceResult::continue_with_state(self.new_instance()))
}
fn new_instance(&self) -> Arc<dyn StepAdvanceCondition> {
Arc::new(DistanceToEndOfStepCondition {
distance: self.distance,
minimum_horizontal_accuracy: self.minimum_horizontal_accuracy,
})
}
}
impl DistanceToEndOfStepCondition {
fn should_advance_inner(&self, trip_state: &TripState) -> Option<StepAdvanceResult> {
let user_location = trip_state.user_location()?;
let current_step = trip_state.current_step()?;
let should_advance =
if user_location.horizontal_accuracy > self.minimum_horizontal_accuracy.into() {
false
} else {
is_within_threshold_to_end_of_linestring(
&user_location.into(),
¤t_step.get_linestring(),
f64::from(self.distance),
)
};
let result = if should_advance {
StepAdvanceResult::advance_to_new_instance(self)
} else {
StepAdvanceResult::continue_with_state(self.new_instance())
};
Some(result)
}
}
impl StepAdvanceConditionSerializable for DistanceToEndOfStepCondition {
fn to_js(&self) -> SerializableStepAdvanceCondition {
SerializableStepAdvanceCondition::DistanceToEndOfStep {
distance: self.distance,
minimum_horizontal_accuracy: self.minimum_horizontal_accuracy,
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, 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 DeviationCalculationPolicy {
Always,
WhileOnRoute,
WhileOnCurrentStep,
}
#[derive(Debug, Copy, Clone)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
pub struct DistanceFromStepCondition {
pub distance: u16,
pub minimum_horizontal_accuracy: u16,
pub calculation_policy: DeviationCalculationPolicy,
}
impl StepAdvanceCondition for DistanceFromStepCondition {
fn should_advance_step(&self, trip_state: TripState) -> StepAdvanceResult {
self.should_advance_inner(&trip_state)
.unwrap_or(StepAdvanceResult::continue_with_state(self.new_instance()))
}
fn new_instance(&self) -> Arc<dyn StepAdvanceCondition> {
Arc::new(*self)
}
}
impl DistanceFromStepCondition {
fn should_advance_inner(&self, trip_state: &TripState) -> Option<StepAdvanceResult> {
let deviation = trip_state.deviation()?;
let user_location = trip_state.user_location()?;
let current_step = trip_state.current_step()?;
let permits_calculation = match self.calculation_policy {
DeviationCalculationPolicy::Always => true,
DeviationCalculationPolicy::WhileOnRoute => !deviation.is_completely_off_route(),
DeviationCalculationPolicy::WhileOnCurrentStep => {
!deviation.is_deviated_from_current_step()
}
};
let location_too_inaccurate =
user_location.horizontal_accuracy > self.minimum_horizontal_accuracy.into();
let should_advance = if !permits_calculation || location_too_inaccurate {
false
} else {
let current_position: Point = user_location.into();
let current_step_linestring = current_step.get_linestring();
deviation_from_line(¤t_position, ¤t_step_linestring)
.is_some_and(|deviation| deviation > self.distance.into())
};
let result = if should_advance {
StepAdvanceResult::advance_to_new_instance(self)
} else {
StepAdvanceResult::continue_with_state(self.new_instance())
};
Some(result)
}
}
impl StepAdvanceConditionSerializable for DistanceFromStepCondition {
fn to_js(&self) -> SerializableStepAdvanceCondition {
SerializableStepAdvanceCondition::DistanceFromStep {
distance: self.distance,
minimum_horizontal_accuracy: self.minimum_horizontal_accuracy,
calculation_policy: self.calculation_policy,
}
}
}
#[derive(Clone)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
pub struct OrAdvanceConditions {
pub conditions: Vec<Arc<dyn StepAdvanceCondition>>,
}
impl StepAdvanceCondition for OrAdvanceConditions {
fn should_advance_step(&self, trip_state: TripState) -> StepAdvanceResult {
let mut should_advance = false;
let mut next_conditions = Vec::with_capacity(self.conditions.len());
for condition in &self.conditions {
let result = condition.should_advance_step(trip_state.clone());
should_advance = should_advance || result.should_advance;
next_conditions.push(result.next_iteration);
}
StepAdvanceResult {
should_advance,
next_iteration: if should_advance {
self.new_instance()
} else {
Arc::new(OrAdvanceConditions {
conditions: next_conditions,
})
},
}
}
fn new_instance(&self) -> Arc<dyn StepAdvanceCondition> {
Arc::new(OrAdvanceConditions {
conditions: self
.conditions
.iter()
.map(|condition| condition.new_instance())
.collect(),
})
}
}
impl StepAdvanceConditionSerializable for OrAdvanceConditions {
fn to_js(&self) -> SerializableStepAdvanceCondition {
SerializableStepAdvanceCondition::OrAdvanceConditions {
conditions: self.conditions.iter().map(|c| c.to_js()).collect(),
}
}
}
#[derive(Clone)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
pub struct AndAdvanceConditions {
pub conditions: Vec<Arc<dyn StepAdvanceCondition>>,
}
impl StepAdvanceCondition for AndAdvanceConditions {
fn should_advance_step(&self, trip_state: TripState) -> StepAdvanceResult {
let mut should_advance = true;
let mut next_conditions = Vec::with_capacity(self.conditions.len());
for condition in &self.conditions {
let result = condition.should_advance_step(trip_state.clone());
should_advance = should_advance && result.should_advance;
next_conditions.push(result.next_iteration);
}
StepAdvanceResult {
should_advance,
next_iteration: if should_advance {
self.new_instance()
} else {
Arc::new(AndAdvanceConditions {
conditions: next_conditions,
})
},
}
}
fn new_instance(&self) -> Arc<dyn StepAdvanceCondition> {
Arc::new(AndAdvanceConditions {
conditions: self
.conditions
.iter()
.map(|condition| condition.new_instance())
.collect(),
})
}
}
impl StepAdvanceConditionSerializable for AndAdvanceConditions {
fn to_js(&self) -> SerializableStepAdvanceCondition {
SerializableStepAdvanceCondition::AndAdvanceConditions {
conditions: self.conditions.iter().map(|c| c.to_js()).collect(),
}
}
}
#[derive(Debug, Copy, Clone)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
pub struct DistanceEntryAndExitCondition {
pub(super) distance_to_end_of_step: u16,
pub(super) distance_after_end_of_step: u16,
pub(super) minimum_horizontal_accuracy: u16,
pub(super) has_reached_end_of_current_step: bool,
}
impl Default for DistanceEntryAndExitCondition {
fn default() -> Self {
Self {
distance_to_end_of_step: 20,
distance_after_end_of_step: 5,
minimum_horizontal_accuracy: 25,
has_reached_end_of_current_step: false,
}
}
}
#[cfg(test)]
impl DistanceEntryAndExitCondition {
pub fn exact() -> Self {
Self {
distance_to_end_of_step: 0,
distance_after_end_of_step: 0,
minimum_horizontal_accuracy: 0,
has_reached_end_of_current_step: false,
}
}
}
impl StepAdvanceCondition for DistanceEntryAndExitCondition {
fn should_advance_step(&self, trip_state: TripState) -> StepAdvanceResult {
if self.has_reached_end_of_current_step {
let distance_from_end = DistanceFromStepCondition {
minimum_horizontal_accuracy: self.minimum_horizontal_accuracy,
distance: self.distance_after_end_of_step,
calculation_policy: DeviationCalculationPolicy::WhileOnRoute,
};
let should_advance = distance_from_end
.should_advance_step(trip_state)
.should_advance;
if should_advance {
StepAdvanceResult::advance_to_new_instance(self)
} else {
StepAdvanceResult::continue_with_state(Arc::new(DistanceEntryAndExitCondition {
distance_to_end_of_step: self.distance_to_end_of_step,
distance_after_end_of_step: self.distance_after_end_of_step,
minimum_horizontal_accuracy: self.minimum_horizontal_accuracy,
has_reached_end_of_current_step: true,
}))
}
} else {
let distance_to_end = DistanceToEndOfStepCondition {
minimum_horizontal_accuracy: self.minimum_horizontal_accuracy,
distance: self.distance_to_end_of_step,
};
let next_iteration = DistanceEntryAndExitCondition {
minimum_horizontal_accuracy: self.minimum_horizontal_accuracy,
distance_to_end_of_step: self.distance_to_end_of_step,
distance_after_end_of_step: self.distance_after_end_of_step,
has_reached_end_of_current_step: distance_to_end
.should_advance_step(trip_state)
.should_advance,
};
StepAdvanceResult::continue_with_state(Arc::new(next_iteration))
}
}
fn new_instance(&self) -> Arc<dyn StepAdvanceCondition> {
Arc::new(DistanceEntryAndExitCondition {
distance_to_end_of_step: self.distance_to_end_of_step,
distance_after_end_of_step: self.distance_after_end_of_step,
minimum_horizontal_accuracy: self.minimum_horizontal_accuracy,
has_reached_end_of_current_step: false, })
}
}
impl StepAdvanceConditionSerializable for DistanceEntryAndExitCondition {
fn to_js(&self) -> SerializableStepAdvanceCondition {
SerializableStepAdvanceCondition::DistanceEntryExit {
minimum_horizontal_accuracy: self.minimum_horizontal_accuracy,
distance_to_end_of_step: self.distance_to_end_of_step,
distance_after_end_step: self.distance_after_end_of_step,
has_reached_end_of_current_step: self.has_reached_end_of_current_step,
}
}
}
#[derive(Debug, Copy, Clone)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
pub struct DistanceEntryAndSnappedExitCondition {
pub(super) distance_to_end_of_step: u16,
pub(super) distance_after_end_of_step: u16,
pub(super) minimum_horizontal_accuracy: u16,
pub(super) has_reached_end_of_current_step: bool,
}
impl Default for DistanceEntryAndSnappedExitCondition {
fn default() -> Self {
Self {
distance_to_end_of_step: 20,
distance_after_end_of_step: 2,
minimum_horizontal_accuracy: 25,
has_reached_end_of_current_step: false,
}
}
}
#[cfg(test)]
impl DistanceEntryAndSnappedExitCondition {
pub fn exact() -> Self {
Self {
distance_to_end_of_step: 0,
distance_after_end_of_step: 0,
minimum_horizontal_accuracy: 0,
has_reached_end_of_current_step: false,
}
}
}
impl StepAdvanceCondition for DistanceEntryAndSnappedExitCondition {
#[allow(unused_variables)]
fn should_advance_step(&self, trip_state: TripState) -> StepAdvanceResult {
let result = if self.has_reached_end_of_current_step {
self.check_exit_result(&trip_state)
} else {
self.check_entry_result(&trip_state)
};
result.unwrap_or(StepAdvanceResult::continue_with_state(self.new_instance()))
}
fn new_instance(&self) -> Arc<dyn StepAdvanceCondition> {
Arc::new(DistanceEntryAndSnappedExitCondition {
has_reached_end_of_current_step: false, ..*self
})
}
}
impl DistanceEntryAndSnappedExitCondition {
fn check_exit_result(&self, trip_state: &TripState) -> Option<StepAdvanceResult> {
let user_location = trip_state.user_location()?;
let current_step = trip_state.current_step()?;
let next_step = trip_state.next_step();
let should_advance = if user_location.horizontal_accuracy
> self.minimum_horizontal_accuracy.into()
{
false
} else if let Some(next) = next_step {
let mut combined_coords = current_step.geometry.clone();
let mut accumulated_distance = next.distance;
let mut step_index = 1;
combined_coords.extend(next.geometry.clone());
let target_distance = (self.distance_after_end_of_step as f64) * 2.0;
while accumulated_distance < target_distance {
if let Some(future_step) = trip_state.get_step(step_index + 1) {
combined_coords.extend(future_step.geometry.clone());
accumulated_distance += future_step.distance;
step_index += 1;
} else {
break;
}
}
let combined_linestring = get_linestring(&combined_coords);
let snapped_to_route = snap_user_location_to_line(user_location, &combined_linestring);
let current_step_linestring = current_step.get_linestring();
let deviation =
deviation_from_line(&Point::from(snapped_to_route), ¤t_step_linestring)
.unwrap_or(0.0);
let effective_exit_distance =
(self.distance_after_end_of_step as f64).min(accumulated_distance);
deviation >= effective_exit_distance
} else {
true
};
if should_advance {
Some(StepAdvanceResult::advance_to_new_instance(self))
} else {
None
}
}
fn check_entry_result(&self, trip_state: &TripState) -> Option<StepAdvanceResult> {
let distance_to_end = DistanceToEndOfStepCondition {
minimum_horizontal_accuracy: self.minimum_horizontal_accuracy,
distance: self.distance_to_end_of_step,
};
let next_iteration = DistanceEntryAndSnappedExitCondition {
has_reached_end_of_current_step: distance_to_end
.should_advance_step(trip_state.clone())
.should_advance,
..*self
};
let result = StepAdvanceResult::continue_with_state(Arc::new(next_iteration));
Some(result)
}
}
impl StepAdvanceConditionSerializable for DistanceEntryAndSnappedExitCondition {
fn to_js(&self) -> SerializableStepAdvanceCondition {
SerializableStepAdvanceCondition::DistanceEntryAndSnappedExit {
minimum_horizontal_accuracy: self.minimum_horizontal_accuracy,
distance_to_end_of_step: self.distance_to_end_of_step,
distance_after_end_step: self.distance_after_end_of_step,
has_reached_end_of_current_step: self.has_reached_end_of_current_step,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::{RouteStep, UserLocation};
use crate::navigation_controller::test_helpers::{
gen_route_step_with_coords, get_navigating_trip_state,
};
use crate::test_utils::make_user_location;
use geo::coord;
use std::sync::LazyLock;
static STRAIGHT_LINE_SHORT_ROUTE_STEP: LazyLock<RouteStep> = LazyLock::new(|| {
gen_route_step_with_coords(vec![
coord!(x: 0.0, y: 0.0), coord!(x: 0.001, y: 0.0), ])
});
static LOCATION_NEAR_START_OF_STEP: LazyLock<UserLocation> =
LazyLock::new(|| make_user_location(coord!(x: 0.0001, y: 0.0), 5.0));
static LOCATION_NEAR_END_OF_STEP: LazyLock<UserLocation> =
LazyLock::new(|| make_user_location(coord!(x: 0.00099, y: 0.0), 5.0));
#[test]
fn test_manual_step_advance() {
let condition = ManualStepCondition;
let trip_state = get_navigating_trip_state(
*LOCATION_NEAR_START_OF_STEP,
vec![STRAIGHT_LINE_SHORT_ROUTE_STEP.clone()],
vec![],
RouteDeviation::NoDeviation,
);
let result = condition.should_advance_step(trip_state);
assert!(
!result.should_advance,
"Should not advance with the manual condition"
);
}
#[test]
fn test_distance_to_end_of_step_doesnt_advance() {
let condition = DistanceToEndOfStepCondition {
minimum_horizontal_accuracy: 10,
distance: 20, };
let trip_state = get_navigating_trip_state(
*LOCATION_NEAR_START_OF_STEP,
vec![STRAIGHT_LINE_SHORT_ROUTE_STEP.clone()],
vec![],
RouteDeviation::NoDeviation,
);
let result = condition.should_advance_step(trip_state);
assert!(
!result.should_advance,
"Should not advance when far from the end of the step"
);
}
#[test]
fn test_distance_to_end_of_step_advance() {
let condition = DistanceToEndOfStepCondition {
minimum_horizontal_accuracy: 10,
distance: 20, };
let trip_state = get_navigating_trip_state(
*LOCATION_NEAR_END_OF_STEP,
vec![STRAIGHT_LINE_SHORT_ROUTE_STEP.clone()],
vec![],
RouteDeviation::NoDeviation,
);
let result = condition.should_advance_step(trip_state);
assert!(
result.should_advance,
"Should advance when close to the end of the step"
);
}
#[test]
fn test_distance_from_step_advance_with_deviation() {
let user_location = make_user_location(coord!(x: 0.005, y: 0.0005), 5.0);
let condition = DistanceFromStepCondition {
minimum_horizontal_accuracy: 10,
distance: 100, calculation_policy: DeviationCalculationPolicy::Always,
};
let trip_state = get_navigating_trip_state(
user_location,
vec![STRAIGHT_LINE_SHORT_ROUTE_STEP.clone()],
vec![],
RouteDeviation::Deviation {
kind: DeviationKind::CompletelyOffRoute {
deviation_from_route_line: 10.0,
},
},
);
let result = condition.should_advance_step(trip_state);
assert!(
result.should_advance,
"Should advance when far from the route"
);
}
#[test]
fn test_distance_from_step_no_advance_when_completely_off_route() {
let user_location = make_user_location(coord!(x: 0.005, y: 0.0005), 5.0);
let condition = DistanceFromStepCondition {
minimum_horizontal_accuracy: 10,
distance: 100, calculation_policy: DeviationCalculationPolicy::WhileOnRoute,
};
let trip_state = get_navigating_trip_state(
user_location,
vec![STRAIGHT_LINE_SHORT_ROUTE_STEP.clone()],
vec![],
RouteDeviation::Deviation {
kind: DeviationKind::CompletelyOffRoute {
deviation_from_route_line: 10.0,
},
},
);
let result = condition.should_advance_step(trip_state);
assert!(
!result.should_advance,
"WhileOnRoute should not advance when the user is completely off the route"
);
}
#[test]
fn test_distance_from_step_advance() {
let user_location = make_user_location(coord!(x: 0.005, y: 0.0005), 5.0);
let condition = DistanceFromStepCondition {
minimum_horizontal_accuracy: 10,
distance: 100, calculation_policy: DeviationCalculationPolicy::WhileOnRoute,
};
let trip_state = get_navigating_trip_state(
user_location,
vec![STRAIGHT_LINE_SHORT_ROUTE_STEP.clone()],
vec![],
RouteDeviation::NoDeviation,
);
let result = condition.should_advance_step(trip_state);
assert!(
result.should_advance,
"Should advance when far from the route"
);
}
#[test]
fn test_or_condition_doesnt_advance() {
let manual_condition1 = ManualStepCondition;
let manual_condition2 = ManualStepCondition;
let or_condition = OrAdvanceConditions {
conditions: vec![Arc::new(manual_condition1), Arc::new(manual_condition2)],
};
let trip_state = get_navigating_trip_state(
*LOCATION_NEAR_START_OF_STEP,
vec![STRAIGHT_LINE_SHORT_ROUTE_STEP.clone()],
vec![],
RouteDeviation::NoDeviation,
);
let result = or_condition.should_advance_step(trip_state);
assert!(
!result.should_advance,
"Should not advance when all OR conditions are false"
);
}
#[test]
fn test_or_condition_advance() {
let manual_condition = ManualStepCondition;
let distance_condition = DistanceToEndOfStepCondition {
minimum_horizontal_accuracy: 10,
distance: 20, };
let or_condition = OrAdvanceConditions {
conditions: vec![Arc::new(manual_condition), Arc::new(distance_condition)],
};
let trip_state = get_navigating_trip_state(
*LOCATION_NEAR_END_OF_STEP,
vec![STRAIGHT_LINE_SHORT_ROUTE_STEP.clone()],
vec![],
RouteDeviation::NoDeviation,
);
let result = or_condition.should_advance_step(trip_state);
assert!(
result.should_advance,
"Should advance when at least one OR condition is true"
);
}
#[test]
fn test_and_condition_doesnt_advance() {
let manual_condition = ManualStepCondition;
let distance_condition = DistanceToEndOfStepCondition {
minimum_horizontal_accuracy: 10,
distance: 20, };
let and_condition = AndAdvanceConditions {
conditions: vec![
Arc::new(manual_condition), Arc::new(distance_condition), ],
};
let trip_state = get_navigating_trip_state(
*LOCATION_NEAR_END_OF_STEP,
vec![STRAIGHT_LINE_SHORT_ROUTE_STEP.clone()],
vec![],
RouteDeviation::NoDeviation,
);
let result = and_condition.should_advance_step(trip_state);
assert!(
!result.should_advance,
"Should not advance when at least one AND condition is false"
);
}
#[test]
fn test_and_condition_advance() {
let distance_condition1 = DistanceToEndOfStepCondition {
minimum_horizontal_accuracy: 10,
distance: 30, };
let distance_condition2 = DistanceToEndOfStepCondition {
minimum_horizontal_accuracy: 10,
distance: 20, };
let and_condition = AndAdvanceConditions {
conditions: vec![Arc::new(distance_condition1), Arc::new(distance_condition2)],
};
let trip_state = get_navigating_trip_state(
*LOCATION_NEAR_END_OF_STEP,
vec![STRAIGHT_LINE_SHORT_ROUTE_STEP.clone()],
vec![],
RouteDeviation::NoDeviation,
);
let result = and_condition.should_advance_step(trip_state);
assert!(
result.should_advance,
"Should advance when all AND conditions are true"
);
}
#[test]
fn test_entry_and_exit_condition_doesnt_advance() {
let condition = DistanceEntryAndExitCondition {
distance_to_end_of_step: 10,
distance_after_end_of_step: 20,
minimum_horizontal_accuracy: 5,
has_reached_end_of_current_step: false,
};
let trip_state = get_navigating_trip_state(
*LOCATION_NEAR_END_OF_STEP,
vec![STRAIGHT_LINE_SHORT_ROUTE_STEP.clone()],
vec![],
RouteDeviation::NoDeviation,
);
let result1 = condition.should_advance_step(trip_state);
assert!(
!result1.should_advance,
"Should not advance on first update even when near end of step"
);
let user_location_still_close = make_user_location(coord!(x: 0.001, y: 0.00002), 5.0);
let next_condition = result1.next_iteration;
let trip_state2 = get_navigating_trip_state(
user_location_still_close,
vec![STRAIGHT_LINE_SHORT_ROUTE_STEP.clone()],
vec![],
RouteDeviation::NoDeviation,
);
let result2 = next_condition.should_advance_step(trip_state2);
assert!(
!result2.should_advance,
"Should not advance when user hasn't moved far enough from the route"
);
}
#[test]
fn test_entry_and_exit_condition_advance() {
let condition = DistanceEntryAndExitCondition {
distance_to_end_of_step: 10,
distance_after_end_of_step: 20,
minimum_horizontal_accuracy: 5,
has_reached_end_of_current_step: false,
};
let trip_state = get_navigating_trip_state(
*LOCATION_NEAR_END_OF_STEP,
vec![STRAIGHT_LINE_SHORT_ROUTE_STEP.clone()],
vec![],
RouteDeviation::NoDeviation,
);
let result1 = condition.should_advance_step(trip_state);
assert!(
!result1.should_advance,
"Should not advance on first update even when near end of step"
);
let next_condition = result1.next_iteration;
let user_location_far = make_user_location(coord!(x: 0.001, y: 0.0005), 5.0);
let trip_state2 = get_navigating_trip_state(
user_location_far,
vec![STRAIGHT_LINE_SHORT_ROUTE_STEP.clone()],
vec![],
RouteDeviation::NoDeviation,
);
let result2 = next_condition.should_advance_step(trip_state2);
assert!(
result2.should_advance,
"Should advance when user has first reached end of step and then moved away"
);
}
#[test]
fn test_and_condition_preserves_state() {
let entry_exit_condition = DistanceEntryAndExitCondition {
distance_to_end_of_step: 10,
distance_after_end_of_step: 5,
minimum_horizontal_accuracy: 5,
has_reached_end_of_current_step: false,
};
let and_condition = AndAdvanceConditions {
conditions: vec![Arc::new(entry_exit_condition)],
};
let trip_state = get_navigating_trip_state(
*LOCATION_NEAR_END_OF_STEP,
vec![STRAIGHT_LINE_SHORT_ROUTE_STEP.clone()],
vec![],
RouteDeviation::NoDeviation,
);
let result1 = and_condition.should_advance_step(trip_state);
assert!(
!result1.should_advance,
"Should not advance on first update"
);
let next_and_condition = result1.next_iteration;
let user_location_far = make_user_location(coord!(x: 0.001, y: 0.0005), 5.0);
let trip_state2 = get_navigating_trip_state(
user_location_far,
vec![STRAIGHT_LINE_SHORT_ROUTE_STEP.clone()],
vec![],
RouteDeviation::NoDeviation,
);
let result2 = next_and_condition.should_advance_step(trip_state2);
assert!(
result2.should_advance,
"Should advance when stateful condition completes in AND"
);
}
#[test]
fn test_entry_and_exit_condition_in_and_composite_advance() {
let entry_exit_condition = DistanceEntryAndExitCondition {
distance_to_end_of_step: 10,
distance_after_end_of_step: 5,
minimum_horizontal_accuracy: 5,
has_reached_end_of_current_step: false,
};
let distance_condition = DistanceToEndOfStepCondition {
minimum_horizontal_accuracy: 10,
distance: 100, };
let and_condition = AndAdvanceConditions {
conditions: vec![Arc::new(entry_exit_condition), Arc::new(distance_condition)],
};
let trip_state = get_navigating_trip_state(
*LOCATION_NEAR_END_OF_STEP,
vec![STRAIGHT_LINE_SHORT_ROUTE_STEP.clone()],
vec![],
RouteDeviation::NoDeviation,
);
let result1 = and_condition.should_advance_step(trip_state);
assert!(
!result1.should_advance,
"Should not advance on first update even when near end of step"
);
let next_condition = result1.next_iteration;
let user_location_far = make_user_location(coord!(x: 0.001, y: 0.0005), 5.0);
let trip_state2 = get_navigating_trip_state(
user_location_far,
vec![STRAIGHT_LINE_SHORT_ROUTE_STEP.clone()],
vec![],
RouteDeviation::NoDeviation,
);
let result2 = next_condition.should_advance_step(trip_state2);
assert!(
result2.should_advance,
"Should advance when stateful condition completes within AND composite"
);
}
#[test]
fn test_entry_and_exit_condition_in_or_composite_advance() {
let entry_exit_condition = DistanceEntryAndExitCondition {
distance_to_end_of_step: 10,
distance_after_end_of_step: 5,
minimum_horizontal_accuracy: 5,
has_reached_end_of_current_step: false,
};
let manual_condition = ManualStepCondition;
let or_condition = OrAdvanceConditions {
conditions: vec![Arc::new(entry_exit_condition), Arc::new(manual_condition)],
};
let trip_state = get_navigating_trip_state(
*LOCATION_NEAR_END_OF_STEP,
vec![STRAIGHT_LINE_SHORT_ROUTE_STEP.clone()],
vec![],
RouteDeviation::NoDeviation,
);
let result1 = or_condition.should_advance_step(trip_state);
assert!(
!result1.should_advance,
"Should not advance on first update even when near end of step"
);
let next_condition = result1.next_iteration;
let user_location_far = make_user_location(coord!(x: 0.001, y: 0.0005), 5.0);
let trip_state2 = get_navigating_trip_state(
user_location_far,
vec![STRAIGHT_LINE_SHORT_ROUTE_STEP.clone()],
vec![],
RouteDeviation::NoDeviation,
);
let result2 = next_condition.should_advance_step(trip_state2);
assert!(
result2.should_advance,
"Should advance when stateful condition completes within OR composite"
);
}
#[test]
fn test_entry_and_exit_condition_resets_in_and_composite_when_advancing() {
let entry_exit_condition = DistanceEntryAndExitCondition {
distance_to_end_of_step: 10,
distance_after_end_of_step: 5,
minimum_horizontal_accuracy: 5,
has_reached_end_of_current_step: false,
};
let distance_condition = DistanceToEndOfStepCondition {
minimum_horizontal_accuracy: 10,
distance: 100, };
let and_condition = AndAdvanceConditions {
conditions: vec![Arc::new(entry_exit_condition), Arc::new(distance_condition)],
};
let trip_state = get_navigating_trip_state(
*LOCATION_NEAR_END_OF_STEP,
vec![STRAIGHT_LINE_SHORT_ROUTE_STEP.clone()],
vec![],
RouteDeviation::NoDeviation,
);
let result1 = and_condition.should_advance_step(trip_state);
assert!(
!result1.should_advance,
"Should not advance on first update even when near end of step"
);
let next_condition = result1.next_iteration;
let user_location_far = make_user_location(coord!(x: 0.001, y: 0.0005), 5.0);
let trip_state2 = get_navigating_trip_state(
user_location_far,
vec![STRAIGHT_LINE_SHORT_ROUTE_STEP.clone()],
vec![],
RouteDeviation::NoDeviation,
);
let result2 = next_condition.should_advance_step(trip_state2);
assert!(
result2.should_advance,
"Should advance when stateful condition completes within AND composite"
);
let reset_condition = result2.next_iteration;
let trip_state3 = get_navigating_trip_state(
*LOCATION_NEAR_END_OF_STEP,
vec![STRAIGHT_LINE_SHORT_ROUTE_STEP.clone()],
vec![],
RouteDeviation::NoDeviation,
);
let result3 = reset_condition.should_advance_step(trip_state3);
assert!(
!result3.should_advance,
"Should not advance immediately after reset - entry/exit condition should restart its two-phase process"
);
}
#[test]
fn test_route_snapped_entry_and_exit_condition_advance() {
let step1 = gen_route_step_with_coords(vec![
coord!(x: 0.0, y: 0.0), coord!(x: 0.001, y: 0.0), ]);
let step2 = gen_route_step_with_coords(vec![
coord!(x: 0.001, y: 0.0), coord!(x: 0.001, y: 0.001), ]);
let condition = DistanceEntryAndSnappedExitCondition {
distance_to_end_of_step: 10,
distance_after_end_of_step: 5,
minimum_horizontal_accuracy: 10,
has_reached_end_of_current_step: false,
};
let location_near_end = make_user_location(coord!(x: 0.00099, y: 0.0), 5.0);
let trip_state = get_navigating_trip_state(
location_near_end,
vec![step1],
vec![],
RouteDeviation::NoDeviation,
);
let result1 = condition.should_advance_step(trip_state);
assert!(
!result1.should_advance,
"Should not advance on first update when entering end zone"
);
let location_on_step2 = make_user_location(coord!(x: 0.001, y: 0.0001), 5.0);
let next_condition = result1.next_iteration;
let trip_state2 = get_navigating_trip_state(
location_on_step2,
vec![step2],
vec![],
RouteDeviation::NoDeviation,
);
let result2 = next_condition.should_advance_step(trip_state2);
assert!(
result2.should_advance,
"Should advance when route-snapped position has moved onto next step"
);
}
#[test]
fn test_route_snapped_entry_and_exit_with_short_next_step() {
let step1 = gen_route_step_with_coords(vec![
coord!(x: 0.0, y: 0.0), coord!(x: 0.001, y: 0.0), ]);
let step2 = gen_route_step_with_coords(vec![
coord!(x: 0.001, y: 0.0), coord!(x: 0.001, y: 0.000027), ]);
let step3 = gen_route_step_with_coords(vec![
coord!(x: 0.001, y: 0.000027), coord!(x: 0.001, y: 0.000054), ]);
let condition = DistanceEntryAndSnappedExitCondition {
distance_to_end_of_step: 10,
distance_after_end_of_step: 10, minimum_horizontal_accuracy: 10,
has_reached_end_of_current_step: false,
};
let location_near_end = make_user_location(coord!(x: 0.00099, y: 0.0), 5.0);
let trip_state = get_navigating_trip_state(
location_near_end,
vec![step1, step2.clone(), step3.clone()],
vec![],
RouteDeviation::NoDeviation,
);
let result1 = condition.should_advance_step(trip_state);
assert!(
!result1.should_advance,
"Should not advance on first update when entering end zone"
);
let location_on_step2 = make_user_location(coord!(x: 0.001, y: 0.000072), 5.0);
let next_condition = result1.next_iteration;
let trip_state2 = get_navigating_trip_state(
location_on_step2,
vec![step2, step3],
vec![],
RouteDeviation::NoDeviation,
);
let result2 = next_condition.should_advance_step(trip_state2);
assert!(
result2.should_advance,
"Should advance with short next steps using min(configured, accumulated) distance"
);
}
#[test]
fn test_route_snapped_entry_and_exit_with_zero_length_via_waypoint() {
let step1 = gen_route_step_with_coords(vec![
coord!(x: 0.0, y: 0.0), coord!(x: 0.001, y: 0.0), ]);
let mut step2 = gen_route_step_with_coords(vec![
coord!(x: 0.001, y: 0.0), coord!(x: 0.001, y: 0.0), ]);
step2.distance = 0.0;
let step3 = gen_route_step_with_coords(vec![
coord!(x: 0.001, y: 0.0), coord!(x: 0.001, y: 0.001), ]);
let condition = DistanceEntryAndSnappedExitCondition {
distance_to_end_of_step: 10,
distance_after_end_of_step: 5,
minimum_horizontal_accuracy: 10,
has_reached_end_of_current_step: false,
};
let location_near_end = make_user_location(coord!(x: 0.00099, y: 0.0), 5.0);
let trip_state = get_navigating_trip_state(
location_near_end,
vec![step1, step2.clone(), step3.clone()],
vec![],
RouteDeviation::NoDeviation,
);
let result1 = condition.should_advance_step(trip_state);
assert!(
!result1.should_advance,
"Should not advance on first update when entering end zone"
);
let location_on_step3 = make_user_location(coord!(x: 0.001, y: 0.0001), 5.0);
let next_condition = result1.next_iteration;
let trip_state2 = get_navigating_trip_state(
location_on_step3,
vec![step2, step3],
vec![],
RouteDeviation::NoDeviation,
);
let result2 = next_condition.should_advance_step(trip_state2);
assert!(
result2.should_advance,
"Should advance when route-snapped position has moved onto step after 0-length via waypoint"
);
}
#[test]
fn test_entry_and_exit_condition_resets_in_or_composite_when_advancing() {
let entry_exit_condition = DistanceEntryAndExitCondition {
distance_to_end_of_step: 10,
distance_after_end_of_step: 5,
minimum_horizontal_accuracy: 5,
has_reached_end_of_current_step: false,
};
let manual_condition = ManualStepCondition;
let or_condition = OrAdvanceConditions {
conditions: vec![Arc::new(entry_exit_condition), Arc::new(manual_condition)],
};
let trip_state = get_navigating_trip_state(
*LOCATION_NEAR_END_OF_STEP,
vec![STRAIGHT_LINE_SHORT_ROUTE_STEP.clone()],
vec![],
RouteDeviation::NoDeviation,
);
let result1 = or_condition.should_advance_step(trip_state);
assert!(
!result1.should_advance,
"Should not advance on first update even when near end of step"
);
let next_condition = result1.next_iteration;
let user_location_far = make_user_location(coord!(x: 0.001, y: 0.0005), 5.0);
let trip_state2 = get_navigating_trip_state(
user_location_far,
vec![STRAIGHT_LINE_SHORT_ROUTE_STEP.clone()],
vec![],
RouteDeviation::NoDeviation,
);
let result2 = next_condition.should_advance_step(trip_state2);
assert!(
result2.should_advance,
"Should advance when stateful condition completes within OR composite"
);
let reset_condition = result2.next_iteration;
let trip_state3 = get_navigating_trip_state(
*LOCATION_NEAR_END_OF_STEP,
vec![STRAIGHT_LINE_SHORT_ROUTE_STEP.clone()],
vec![],
RouteDeviation::NoDeviation,
);
let result3 = reset_condition.should_advance_step(trip_state3);
assert!(
!result3.should_advance,
"Should not advance immediately after reset - entry/exit condition should restart its two-phase process"
);
}
}
#[cfg(test)]
proptest! {
#[test]
fn manual_step_never_advances(
c1 in arb_coord(),
c2 in arb_coord(),
accuracy: f64
) {
let route_step =
crate::navigation_controller::test_helpers::gen_route_step_with_coords(vec![c1, c2]);
let user_location = make_user_location(c2, accuracy);
let condition = ManualStepCondition;
let trip_state = get_navigating_trip_state(
user_location,
vec![route_step],
vec![],
RouteDeviation::NoDeviation,
);
let result = condition.should_advance_step(trip_state);
prop_assert!(
!result.should_advance,
"Should not advance with the manual condition"
);
}
#[test]
fn entry_and_exit_never_advances_on_zero_movement(
c1 in arb_coord(),
c2 in arb_coord(),
) {
let route_step =
crate::navigation_controller::test_helpers::gen_route_step_with_coords(vec![c1, c2]);
let user_location = make_user_location(c2, 5.0);
let condition = DistanceEntryAndExitCondition {
distance_to_end_of_step: 10,
distance_after_end_of_step: 20,
minimum_horizontal_accuracy: 5,
has_reached_end_of_current_step: false,
};
let trip_state = get_navigating_trip_state(
user_location,
vec![route_step.clone()],
vec![],
RouteDeviation::NoDeviation,
);
let result1 = condition.should_advance_step(trip_state);
prop_assert!(
!result1.should_advance,
"Should not advance on first update even when at the end of the step"
);
let next_condition = result1.next_iteration;
let trip_state2 = get_navigating_trip_state(
user_location,
vec![route_step],
vec![],
RouteDeviation::NoDeviation,
);
let result2 = next_condition.should_advance_step(trip_state2);
prop_assert!(
!result2.should_advance,
"Should not advance when user hasn't moved far enough from the route"
);
}
}
#[cfg(test)]
mod off_step_tests {
use super::*;
use crate::models::UserLocation;
use crate::navigation_controller::test_helpers::get_navigating_trip_state;
use crate::test_utils::make_user_location;
use geo::coord;
use std::sync::LazyLock;
use crate::models::RouteStep;
use crate::navigation_controller::test_helpers::gen_route_step_with_coords;
static STRAIGHT_LINE_STEP: LazyLock<RouteStep> = LazyLock::new(|| {
gen_route_step_with_coords(vec![
coord!(x: 0.0, y: 0.0),
coord!(x: 0.001, y: 0.0), ])
});
static LOCATION_FAR_FROM_STEP: LazyLock<UserLocation> =
LazyLock::new(|| make_user_location(coord!(x: 0.005, y: 0.0005), 5.0));
#[test]
fn test_always_policy_advances_when_off_step_on_route() {
let condition = DistanceFromStepCondition {
minimum_horizontal_accuracy: 10,
distance: 100,
calculation_policy: DeviationCalculationPolicy::Always,
};
let trip_state = get_navigating_trip_state(
*LOCATION_FAR_FROM_STEP,
vec![STRAIGHT_LINE_STEP.clone()],
vec![],
RouteDeviation::Deviation {
kind: DeviationKind::OffStepOnRoute {
deviation_from_step_line: 50.0,
},
},
);
let result = condition.should_advance_step(trip_state);
assert!(
result.should_advance,
"Always should advance regardless of deviation status"
);
}
#[test]
fn test_while_on_current_step_policy_blocks_advance_when_off_step_on_route() {
let condition = DistanceFromStepCondition {
minimum_horizontal_accuracy: 10,
distance: 100,
calculation_policy: DeviationCalculationPolicy::WhileOnCurrentStep,
};
let trip_state = get_navigating_trip_state(
*LOCATION_FAR_FROM_STEP,
vec![STRAIGHT_LINE_STEP.clone()],
vec![],
RouteDeviation::Deviation {
kind: DeviationKind::OffStepOnRoute {
deviation_from_step_line: 50.0,
},
},
);
let result = condition.should_advance_step(trip_state);
assert!(
!result.should_advance,
"WhileOnCurrentStep should block advance once the user is off the current step"
);
}
#[test]
fn test_while_on_route_policy_advances_when_off_step_on_route() {
let condition = DistanceFromStepCondition {
minimum_horizontal_accuracy: 10,
distance: 100,
calculation_policy: DeviationCalculationPolicy::WhileOnRoute,
};
let trip_state = get_navigating_trip_state(
*LOCATION_FAR_FROM_STEP,
vec![STRAIGHT_LINE_STEP.clone()],
vec![],
RouteDeviation::Deviation {
kind: DeviationKind::OffStepOnRoute {
deviation_from_step_line: 50.0,
},
},
);
let result = condition.should_advance_step(trip_state);
assert!(
result.should_advance,
"WhileOnRoute should not block advancement when only off the current step"
);
}
#[test]
fn test_while_on_current_step_policy_blocks_advance_when_completely_off_route() {
let condition = DistanceFromStepCondition {
minimum_horizontal_accuracy: 10,
distance: 100,
calculation_policy: DeviationCalculationPolicy::WhileOnCurrentStep,
};
let trip_state = get_navigating_trip_state(
*LOCATION_FAR_FROM_STEP,
vec![STRAIGHT_LINE_STEP.clone()],
vec![],
RouteDeviation::Deviation {
kind: DeviationKind::CompletelyOffRoute {
deviation_from_route_line: 200.0,
},
},
);
let result = condition.should_advance_step(trip_state);
assert!(
!result.should_advance,
"WhileOnCurrentStep must block advancement even when CompletelyOffRoute, \
since being off the route is itself being off the current step"
);
}
#[test]
fn test_while_on_route_policy_blocks_advance_when_completely_off_route() {
let condition = DistanceFromStepCondition {
minimum_horizontal_accuracy: 10,
distance: 100,
calculation_policy: DeviationCalculationPolicy::WhileOnRoute,
};
let trip_state = get_navigating_trip_state(
*LOCATION_FAR_FROM_STEP,
vec![STRAIGHT_LINE_STEP.clone()],
vec![],
RouteDeviation::Deviation {
kind: DeviationKind::CompletelyOffRoute {
deviation_from_route_line: 200.0,
},
},
);
let result = condition.should_advance_step(trip_state);
assert!(
!result.should_advance,
"WhileOnRoute should block advancement when the user is completely off the route"
);
}
#[test]
fn test_while_on_current_step_policy_handles_all_deviation_states() {
let condition = DistanceFromStepCondition {
minimum_horizontal_accuracy: 10,
distance: 100,
calculation_policy: DeviationCalculationPolicy::WhileOnCurrentStep,
};
let trip_state_off_step = get_navigating_trip_state(
*LOCATION_FAR_FROM_STEP,
vec![STRAIGHT_LINE_STEP.clone()],
vec![],
RouteDeviation::Deviation {
kind: DeviationKind::OffStepOnRoute {
deviation_from_step_line: 50.0,
},
},
);
let result = condition.should_advance_step(trip_state_off_step);
assert!(
!result.should_advance,
"WhileOnCurrentStep should block on OffStepOnRoute"
);
let trip_state_off_route = get_navigating_trip_state(
*LOCATION_FAR_FROM_STEP,
vec![STRAIGHT_LINE_STEP.clone()],
vec![],
RouteDeviation::Deviation {
kind: DeviationKind::CompletelyOffRoute {
deviation_from_route_line: 200.0,
},
},
);
let result = condition.should_advance_step(trip_state_off_route);
assert!(
!result.should_advance,
"WhileOnCurrentStep should block on CompletelyOffRoute"
);
let trip_state_on_route = get_navigating_trip_state(
*LOCATION_FAR_FROM_STEP,
vec![STRAIGHT_LINE_STEP.clone()],
vec![],
RouteDeviation::NoDeviation,
);
let result = condition.should_advance_step(trip_state_on_route);
assert!(
result.should_advance,
"WhileOnCurrentStep should still allow advance when NoDeviation"
);
}
}