diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index e732dca1a..68da8d111 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -13,7 +13,7 @@ jobs: cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true @@ -53,7 +53,7 @@ jobs: packages: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true @@ -99,7 +99,7 @@ jobs: packages: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true @@ -146,7 +146,7 @@ jobs: packages: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true diff --git a/.github/workflows/cargo-publish.yml b/.github/workflows/cargo-publish.yml index 27a53d68c..7c70362df 100644 --- a/.github/workflows/cargo-publish.yml +++ b/.github/workflows/cargo-publish.yml @@ -11,7 +11,7 @@ jobs: publish_crate: runs-on: macos-15 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Cargo Publish run: cargo publish -p ferrostar working-directory: common diff --git a/.github/workflows/gradle-publish.yml b/.github/workflows/gradle-publish.yml index 64a863100..4f062537c 100644 --- a/.github/workflows/gradle-publish.yml +++ b/.github/workflows/gradle-publish.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up JDK 21 uses: actions/setup-java@v4 diff --git a/.github/workflows/ios-release.yml b/.github/workflows/ios-release.yml index 4ec5c1f83..d59543d4a 100644 --- a/.github/workflows/ios-release.yml +++ b/.github/workflows/ios-release.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 # Ensure that we can operate on the full history ref: main diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 2bb168b56..31e8471fa 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Check format id: check-format @@ -37,7 +37,7 @@ jobs: steps: - name: Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Ensure that Package.swift is NOT configured for local development at this stage (easy to forget) run: | @@ -100,7 +100,7 @@ jobs: steps: - name: Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Configure Package.swift for local development run: sed -i '' 's/let useLocalFramework = false/let useLocalFramework = true/' Package.swift @@ -155,7 +155,7 @@ jobs: steps: - name: Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Configure Package.swift for local development run: sed -i '' 's/let useLocalFramework = false/let useLocalFramework = true/' Package.swift diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 7478214a9..40a261367 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install wasm-pack run: cargo install wasm-pack diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 921ab95e9..bbe680b45 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -12,7 +12,7 @@ jobs: pages: write # To push to a GitHub Pages site id-token: write # To update the deployment status steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Install latest mdbook diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index d0bf8bb1e..4b89be7d1 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -15,7 +15,7 @@ jobs: group: ${{ github.workflow }}-${{ github.ref }}-rust-msrv cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: taiki-e/install-action@v2 with: tool: cargo-hack @@ -31,7 +31,7 @@ jobs: cancel-in-progress: true steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Check semver uses: obi1kenobi/cargo-semver-checks-action@v2 with: @@ -45,7 +45,7 @@ jobs: cancel-in-progress: true steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: rustfmt checks run: cargo fmt --check working-directory: common @@ -56,7 +56,7 @@ jobs: group: ${{ github.workflow }}-${{ github.ref }}-rust-build cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: taiki-e/install-action@nextest - name: Cargo Build diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml index 794057b3f..e86eb34be 100644 --- a/.github/workflows/typos.yml +++ b/.github/workflows/typos.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Run typos uses: crate-ci/typos@master diff --git a/.github/workflows/wasm-js.yml b/.github/workflows/wasm-js.yml index 40caa248b..dbfeed4d4 100644 --- a/.github/workflows/wasm-js.yml +++ b/.github/workflows/wasm-js.yml @@ -15,7 +15,7 @@ jobs: group: ${{ github.workflow }}-${{ github.ref }}-wasm-build cancel-in-progress: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install wasm-pack run: cargo install wasm-pack diff --git a/Package.swift b/Package.swift index df5af6eb8..88f4fb1dd 100644 --- a/Package.swift +++ b/Package.swift @@ -16,7 +16,7 @@ if useLocalFramework { path: "./common/target/ios/libferrostar-rs.xcframework" ) } else { - let releaseTag = "0.49.0" + let releaseTag = "0.50.0" let releaseChecksum = "7b201e32a4e55ea127ccb9c72d8fdbf09540755471b7794de90ca369b10ed314" binaryTarget = .binaryTarget( name: "FerrostarCoreRS", diff --git a/android/build.gradle b/android/build.gradle index 88d09ec43..87e36efed 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -29,5 +29,5 @@ ext { allprojects { group = "com.stadiamaps.ferrostar" - version = "0.49.0" + version = "0.50.0" } diff --git a/android/core/src/androidTest/java/com/stadiamaps/ferrostar/core/FerrostarCoreTest.kt b/android/core/src/androidTest/java/com/stadiamaps/ferrostar/core/FerrostarCoreTest.kt index 266eae42e..cbc79e379 100644 --- a/android/core/src/androidTest/java/com/stadiamaps/ferrostar/core/FerrostarCoreTest.kt +++ b/android/core/src/androidTest/java/com/stadiamaps/ferrostar/core/FerrostarCoreTest.kt @@ -41,6 +41,8 @@ import uniffi.ferrostar.VisualInstructionContent import uniffi.ferrostar.Waypoint import uniffi.ferrostar.WaypointAdvanceMode import uniffi.ferrostar.WaypointKind +import uniffi.ferrostar.DeviationCalculationPolicy +import uniffi.ferrostar.DeviationKind import uniffi.ferrostar.stepAdvanceDistanceFromStep import uniffi.ferrostar.stepAdvanceDistanceToEndOfStep import uniffi.ferrostar.stepAdvanceManual @@ -130,14 +132,16 @@ class FerrostarCoreTest { secondaryContent = null, subContent = null, triggerDistanceBeforeManeuver = 42.0, - )), + ), + ), spokenInstructions = listOf(), duration = 0.0, annotations = null, incidents = listOf(), drivingSide = DrivingSide.RIGHT, roundaboutExitNumber = null, - )), + ), + ), ) @Test @@ -191,7 +195,8 @@ class FerrostarCoreTest { Waypoint( coordinate = GeographicCoordinate(60.5349908, -149.5485806), kind = WaypointKind.BREAK, - )), + ), + ), ) fail("Expected the request to fail") } catch (e: InvalidStatusCodeException) { @@ -247,7 +252,8 @@ class FerrostarCoreTest { Waypoint( coordinate = GeographicCoordinate(lat = 60.5349908, lng = -149.5485806), kind = WaypointKind.BREAK, - )), + ), + ), ) assertEquals(listOf(mockRoute), routes) @@ -301,7 +307,8 @@ class FerrostarCoreTest { Waypoint( coordinate = GeographicCoordinate(lat = 60.5349908, lng = -149.5485806), kind = WaypointKind.BREAK, - )), + ), + ), ) assertEquals(listOf(mockRoute), routes) @@ -362,7 +369,8 @@ class FerrostarCoreTest { Waypoint( coordinate = GeographicCoordinate(lat = 60.5349908, lng = -149.5485806), kind = WaypointKind.BREAK, - )), + ), + ), ) assertEquals(listOf(mockRoute), routes) @@ -385,11 +393,11 @@ class FerrostarCoreTest { override fun correctiveActionForDeviation( core: FerrostarCore, - deviationInMeters: Double, - remainingWaypoints: List, + deviation: DeviationKind, + remainingWaypoints: List ): CorrectiveAction { called = true - assertEquals(42.0, deviationInMeters, Double.MIN_VALUE) + assertEquals(DeviationKind.CompletelyOffRoute(42.0), deviation) return CorrectiveAction.GetNewRoutes(remainingWaypoints) } } @@ -451,14 +459,19 @@ class FerrostarCoreTest { Waypoint( coordinate = GeographicCoordinate(lat = 60.5349908, lng = -149.5485806), kind = WaypointKind.BREAK, - )), + ), + ), ) core.startNavigation( routes.first(), NavigationControllerConfig( WaypointAdvanceMode.WaypointWithinRange(100.0), - stepAdvanceDistanceFromStep(16u, 32u, true), + stepAdvanceDistanceFromStep( + distance = 16u, + minimumHorizontalAccuracy = 32u, + calculationPolicy = DeviationCalculationPolicy.ALWAYS, + ), stepAdvanceDistanceToEndOfStep(16u, 32u), routeDeviationTracking = RouteDeviationTracking.Custom( @@ -468,9 +481,10 @@ class FerrostarCoreTest { route: Route, tripState: TripState, ): RouteDeviation { - return RouteDeviation.OffRoute(42.0) + return RouteDeviation.Deviation(DeviationKind.CompletelyOffRoute(42.0)) } - }), + }, + ), CourseFiltering.RAW, ), ) diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/FerrostarCore.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/FerrostarCore.kt index a00450943..1586d2246 100644 --- a/android/core/src/main/java/com/stadiamaps/ferrostar/core/FerrostarCore.kt +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/FerrostarCore.kt @@ -366,14 +366,15 @@ class FerrostarCore( private fun handleStateUpdate(newState: NavState, location: UserLocation) { val tripState = newState.tripState if (tripState is TripState.Navigating) { - if (tripState.deviation is RouteDeviation.OffRoute) { + val deviation = tripState.deviation + if (deviation is RouteDeviation.Deviation) { if (!_routeRequestInFlight && // We can't have a request in flight already hasWaitedMinimumRecalculationDelay( _lastAutomaticRecalculation, minimumTimeBeforeRecalculation) && hasUserMovedSignificantlySinceLastRecalc(location)) { val action = deviationHandler?.correctiveActionForDeviation( - this, tripState.deviation.deviationFromRouteLine, tripState.remainingWaypoints) + this, deviation.kind, tripState.remainingWaypoints) ?: CorrectiveAction.GetNewRoutes(tripState.remainingWaypoints) when (action) { is CorrectiveAction.DoNothing -> { @@ -391,7 +392,7 @@ class FerrostarCore( val state = _state.value // Make sure we are still navigating and the new route is still relevant if (state.tripState is TripState.Navigating && - state.tripState.deviation is RouteDeviation.OffRoute) { + state.tripState.deviation is RouteDeviation.Deviation) { if (processor != null) { processor.loadedAlternativeRoutes(this@FerrostarCore, routes) } else if (routes.isNotEmpty()) { diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/RouteDeviationHandler.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/RouteDeviationHandler.kt index 903e0303f..cfe5869a6 100644 --- a/android/core/src/main/java/com/stadiamaps/ferrostar/core/RouteDeviationHandler.kt +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/RouteDeviationHandler.kt @@ -1,5 +1,6 @@ package com.stadiamaps.ferrostar.core +import uniffi.ferrostar.DeviationKind import uniffi.ferrostar.Waypoint /** Corrective action to take when the user deviates from the route. */ @@ -26,7 +27,7 @@ sealed class CorrectiveAction { fun interface RouteDeviationHandler { fun correctiveActionForDeviation( core: FerrostarCore, - deviationInMeters: Double, + deviation: DeviationKind, remainingWaypoints: List, ): CorrectiveAction } diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/extensions/TripStateExtensions.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/extensions/TripStateExtensions.kt index b6a7e4946..501ee46a0 100644 --- a/android/core/src/main/java/com/stadiamaps/ferrostar/core/extensions/TripStateExtensions.kt +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/extensions/TripStateExtensions.kt @@ -132,7 +132,7 @@ fun TripState.preferredUserLocation() = is TripState.Navigating -> { when (this.deviation) { is RouteDeviation.NoDeviation -> this.snappedUserLocation - is RouteDeviation.OffRoute -> this.userLocation + is RouteDeviation.Deviation -> this.userLocation } } is TripState.Idle -> this.userLocation diff --git a/apple/DemoApp/Demo/NavigationDelegate.swift b/apple/DemoApp/Demo/NavigationDelegate.swift index d51206d97..a7ca3f0e0 100644 --- a/apple/DemoApp/Demo/NavigationDelegate.swift +++ b/apple/DemoApp/Demo/NavigationDelegate.swift @@ -10,7 +10,7 @@ class NavigationDelegate: FerrostarCoreDelegate { // TODO: Create defaults extension on FerrostarCoreDelegate } - func core(_: FerrostarCore, correctiveActionForDeviation _: Double, + func core(_: FerrostarCore, correctiveActionForDeviation _: DeviationKind, remainingWaypoints waypoints: [Waypoint]) -> CorrectiveAction { // If the user is off course, we'll try to calculate a new route using the remaining waypoints. diff --git a/apple/Sources/FerrostarCore/FerrostarCore.swift b/apple/Sources/FerrostarCore/FerrostarCore.swift index ed0a1f22c..bba815ef1 100644 --- a/apple/Sources/FerrostarCore/FerrostarCore.swift +++ b/apple/Sources/FerrostarCore/FerrostarCore.swift @@ -43,7 +43,7 @@ public protocol FerrostarCoreDelegate: AnyObject { /// This hook enables app developers to take the most appropriate corrective action. func core( _ core: FerrostarCore, - correctiveActionForDeviation deviationInMeters: Double, + correctiveActionForDeviation deviation: DeviationKind, remainingWaypoints waypoints: [Waypoint] ) -> CorrectiveAction @@ -437,7 +437,7 @@ public protocol FerrostarCoreDelegate: AnyObject { case .noDeviation: // No action break - case let .offRoute(deviationFromRouteLine: deviationFromRouteLine): + case let .deviation(kind: kind): guard !self.routeRequestInFlight, // We can't have a request in flight already // Ensure a minimum cool down before a new route fetch self.lastAutomaticRecalculation?.timeIntervalSinceNow ?? -TimeInterval @@ -454,7 +454,7 @@ public protocol FerrostarCoreDelegate: AnyObject { switch self.delegate?.core( self, - correctiveActionForDeviation: deviationFromRouteLine, + correctiveActionForDeviation: kind, remainingWaypoints: remainingWaypoints ) ?? .getNewRoutes(waypoints: remainingWaypoints) { case .doNothing: diff --git a/apple/Sources/FerrostarCore/NavigationState.swift b/apple/Sources/FerrostarCore/NavigationState.swift index cdc024445..57ce674a2 100644 --- a/apple/Sources/FerrostarCore/NavigationState.swift +++ b/apple/Sources/FerrostarCore/NavigationState.swift @@ -144,7 +144,7 @@ public struct NavigationState: Hashable { switch deviation { case .noDeviation: snappedUserLocation - case .offRoute: + case .deviation: userLocation } } diff --git a/apple/Sources/FerrostarMapLibreUI/Views/Overylays/LandscapeNavigationOverlayView.swift b/apple/Sources/FerrostarMapLibreUI/Views/Overylays/LandscapeNavigationOverlayView.swift index 342c10cb9..6a14eed99 100644 --- a/apple/Sources/FerrostarMapLibreUI/Views/Overylays/LandscapeNavigationOverlayView.swift +++ b/apple/Sources/FerrostarMapLibreUI/Views/Overylays/LandscapeNavigationOverlayView.swift @@ -68,7 +68,9 @@ struct LandscapeNavigationOverlayView: View { } } - if case .offRoute = navigationState?.currentDeviation { + // Show the off-route overlay if and only if the user is completely off the route + // (e.g. taking a quick shortcut and rejoining the route doesn't show the banner). + if case .deviation(kind: .completelyOffRoute) = navigationState?.currentDeviation { componentsConfig.getOffRouteView( navigationState, size: .constant(.zero) diff --git a/apple/Sources/FerrostarMapLibreUI/Views/Overylays/PortraitNavigationOverlayView.swift b/apple/Sources/FerrostarMapLibreUI/Views/Overylays/PortraitNavigationOverlayView.swift index c1720cece..ee0f72ad8 100644 --- a/apple/Sources/FerrostarMapLibreUI/Views/Overylays/PortraitNavigationOverlayView.swift +++ b/apple/Sources/FerrostarMapLibreUI/Views/Overylays/PortraitNavigationOverlayView.swift @@ -105,7 +105,7 @@ struct PortraitNavigationOverlayView: View { } .padding(.top, topPadding + 16) - if case .offRoute = navigationState?.currentDeviation { + if showOffRouteView { componentsConfig.getOffRouteView( navigationState, size: $offRouteViewSizeWhenNotExpanded @@ -120,12 +120,22 @@ struct PortraitNavigationOverlayView: View { } } + private var showOffRouteView: Bool { + // Show the off-route overlay if and only if the user is completely off the route + // (e.g. taking a quick shortcut and rejoining the route doesn't show the banner). + if case .deviation(kind: .completelyOffRoute) = navigationState?.currentDeviation { + true + } else { + false + } + } + private var topPadding: CGFloat { guard case .navigating = navigationState?.tripState else { return 0 } - if case .offRoute = navigationState?.currentDeviation { + if showOffRouteView { return offRouteViewSizeWhenNotExpanded.height } diff --git a/apple/Sources/UniFFI/ferrostar.swift b/apple/Sources/UniFFI/ferrostar.swift index 20bea4fcb..e470fb36a 100644 --- a/apple/Sources/UniFFI/ferrostar.swift +++ b/apple/Sources/UniFFI/ferrostar.swift @@ -1007,31 +1007,31 @@ public func FfiConverterTypeDistanceEntryAndSnappedExitCondition_lower(_ value: /** - * Requires that the user be at least this far from the current route step. + * Advances once the user is at least [`Self::distance`] meters from the current step's polyline. * * This results in *delayed* advance, * but is more robust to spurious / unwanted step changes in scenarios including - * self-intersecting routes (sudden jump to the next step) + * self-intersecting routes (sudden jumps to the next step) * and pauses at intersections (advancing too soon before the maneuver is complete). * - * NOTE! This may be less robust to things like short steps, out and backs and U-turns, - * where this may eagerly exit a current step before the user has traversed it if the start - * the step within range of the end. + * NOTE! This may be less robust to things like short steps, out-and-backs, and U-turns, + * where this may eagerly exit a current step before the user has traversed it + * if the start of the step is within range of the end. */ public protocol DistanceFromStepConditionProtocol: AnyObject, Sendable { } /** - * Requires that the user be at least this far from the current route step. + * Advances once the user is at least [`Self::distance`] meters from the current step's polyline. * * This results in *delayed* advance, * but is more robust to spurious / unwanted step changes in scenarios including - * self-intersecting routes (sudden jump to the next step) + * self-intersecting routes (sudden jumps to the next step) * and pauses at intersections (advancing too soon before the maneuver is complete). * - * NOTE! This may be less robust to things like short steps, out and backs and U-turns, - * where this may eagerly exit a current step before the user has traversed it if the start - * the step within range of the end. + * NOTE! This may be less robust to things like short steps, out-and-backs, and U-turns, + * where this may eagerly exit a current step before the user has traversed it + * if the start of the step is within range of the end. */ open class DistanceFromStepCondition: DistanceFromStepConditionProtocol, @unchecked Sendable { fileprivate let handle: UInt64 @@ -6921,6 +6921,192 @@ public func FfiConverterTypeCourseFiltering_lower(_ value: CourseFiltering) -> R } +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. +/** + * Controls when a deviation-aware step-advance condition is allowed to evaluate, + * based on the user's current + * [`RouteDeviation`](crate::deviation_detection::RouteDeviation) status. + */ + +public enum DeviationCalculationPolicy: Equatable, Hashable, Codable { + + /** + * Always evaluate, regardless of deviation status. + */ + case always + /** + * Evaluate while the user is still somewhere on the route polyline + * (current step or any future step), + * but suspend if the user is completely off the route. + */ + case whileOnRoute + /** + * Evaluate only while the user is within an acceptable deviation of the current step's polyline. + * + * Suspend on any deviation + * (whether off-step-but-on-route, or completely off-route). + */ + case whileOnCurrentStep + + + + + +} + +#if compiler(>=6) +extension DeviationCalculationPolicy: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeDeviationCalculationPolicy: FfiConverterRustBuffer { + typealias SwiftType = DeviationCalculationPolicy + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> DeviationCalculationPolicy { + let variant: Int32 = try readInt(&buf) + switch variant { + + case 1: return .always + + case 2: return .whileOnRoute + + case 3: return .whileOnCurrentStep + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: DeviationCalculationPolicy, into buf: inout [UInt8]) { + switch value { + + + case .always: + writeInt(&buf, Int32(1)) + + + case .whileOnRoute: + writeInt(&buf, Int32(2)) + + + case .whileOnCurrentStep: + writeInt(&buf, Int32(3)) + + } + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeDeviationCalculationPolicy_lift(_ buf: RustBuffer) throws -> DeviationCalculationPolicy { + return try FfiConverterTypeDeviationCalculationPolicy.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeDeviationCalculationPolicy_lower(_ value: DeviationCalculationPolicy) -> RustBuffer { + return FfiConverterTypeDeviationCalculationPolicy.lower(value) +} + + +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. +/** + * The kind of deviation from the expected route. + */ + +public enum DeviationKind: Equatable, Hashable, Codable { + + /** + * The user is off the current step, but still on the route polyline (on a future step). + * + * This can happen when the user takes a shortcut, GPS noise places them on a future step, + * or the route self-intersects. Step advance conditions can use this to limit advancement. + */ + case offStepOnRoute( + /** + * The deviation from the current step line, in meters. + */deviationFromStepLine: Double + ) + /** + * The user is off the expected route entirely + * (not within threshold of any remaining step, + * neither the current one nor any future one). + */ + case completelyOffRoute( + /** + * The deviation from the route line, in meters. + */deviationFromRouteLine: Double + ) + + + + + +} + +#if compiler(>=6) +extension DeviationKind: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeDeviationKind: FfiConverterRustBuffer { + typealias SwiftType = DeviationKind + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> DeviationKind { + let variant: Int32 = try readInt(&buf) + switch variant { + + case 1: return .offStepOnRoute(deviationFromStepLine: try FfiConverterDouble.read(from: &buf) + ) + + case 2: return .completelyOffRoute(deviationFromRouteLine: try FfiConverterDouble.read(from: &buf) + ) + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: DeviationKind, into buf: inout [UInt8]) { + switch value { + + + case let .offStepOnRoute(deviationFromStepLine): + writeInt(&buf, Int32(1)) + FfiConverterDouble.write(deviationFromStepLine, into: &buf) + + + case let .completelyOffRoute(deviationFromRouteLine): + writeInt(&buf, Int32(2)) + FfiConverterDouble.write(deviationFromRouteLine, into: &buf) + + } + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeDeviationKind_lift(_ buf: RustBuffer) throws -> DeviationKind { + return try FfiConverterTypeDeviationKind.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeDeviationKind_lower(_ value: DeviationKind) -> RustBuffer { + return FfiConverterTypeDeviationKind.lower(value) +} + + // Note that we don't yet support `indirect` for enums. // See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. /** @@ -8134,12 +8320,12 @@ public enum RouteDeviation: Equatable, Hashable, Codable { */ case noDeviation /** - * The user is off the expected route. + * The user has deviated from the expected route. + * + * Check the [`DeviationKind`] to determine if the user is still on the route polyline + * (off the current step but on a future step) or off the route entirely. */ - case offRoute( - /** - * The deviation from the route line, in meters. - */deviationFromRouteLine: Double + case deviation(kind: DeviationKind ) @@ -8164,7 +8350,7 @@ public struct FfiConverterTypeRouteDeviation: FfiConverterRustBuffer { case 1: return .noDeviation - case 2: return .offRoute(deviationFromRouteLine: try FfiConverterDouble.read(from: &buf) + case 2: return .deviation(kind: try FfiConverterTypeDeviationKind.read(from: &buf) ) default: throw UniffiInternalError.unexpectedEnumCase @@ -8179,9 +8365,9 @@ public struct FfiConverterTypeRouteDeviation: FfiConverterRustBuffer { writeInt(&buf, Int32(1)) - case let .offRoute(deviationFromRouteLine): + case let .deviation(kind): writeInt(&buf, Int32(2)) - FfiConverterDouble.write(deviationFromRouteLine, into: &buf) + FfiConverterTypeDeviationKind.write(kind, into: &buf) } } @@ -8476,7 +8662,7 @@ public enum SerializableStepAdvanceCondition: Equatable, Hashable, Codable { case manual case distanceToEndOfStep(distance: UInt16, minimumHorizontalAccuracy: UInt16 ) - case distanceFromStep(distance: UInt16, minimumHorizontalAccuracy: UInt16, calculateWhileOffRoute: Bool + case distanceFromStep(distance: UInt16, minimumHorizontalAccuracy: UInt16, calculationPolicy: DeviationCalculationPolicy ) case distanceEntryExit(distanceToEndOfStep: UInt16, distanceAfterEndStep: UInt16, minimumHorizontalAccuracy: UInt16, hasReachedEndOfCurrentStep: Bool ) @@ -8512,7 +8698,7 @@ public struct FfiConverterTypeSerializableStepAdvanceCondition: FfiConverterRust case 2: return .distanceToEndOfStep(distance: try FfiConverterUInt16.read(from: &buf), minimumHorizontalAccuracy: try FfiConverterUInt16.read(from: &buf) ) - case 3: return .distanceFromStep(distance: try FfiConverterUInt16.read(from: &buf), minimumHorizontalAccuracy: try FfiConverterUInt16.read(from: &buf), calculateWhileOffRoute: try FfiConverterBool.read(from: &buf) + case 3: return .distanceFromStep(distance: try FfiConverterUInt16.read(from: &buf), minimumHorizontalAccuracy: try FfiConverterUInt16.read(from: &buf), calculationPolicy: try FfiConverterTypeDeviationCalculationPolicy.read(from: &buf) ) case 4: return .distanceEntryExit(distanceToEndOfStep: try FfiConverterUInt16.read(from: &buf), distanceAfterEndStep: try FfiConverterUInt16.read(from: &buf), minimumHorizontalAccuracy: try FfiConverterUInt16.read(from: &buf), hasReachedEndOfCurrentStep: try FfiConverterBool.read(from: &buf) @@ -8545,11 +8731,11 @@ public struct FfiConverterTypeSerializableStepAdvanceCondition: FfiConverterRust FfiConverterUInt16.write(minimumHorizontalAccuracy, into: &buf) - case let .distanceFromStep(distance,minimumHorizontalAccuracy,calculateWhileOffRoute): + case let .distanceFromStep(distance,minimumHorizontalAccuracy,calculationPolicy): writeInt(&buf, Int32(3)) FfiConverterUInt16.write(distance, into: &buf) FfiConverterUInt16.write(minimumHorizontalAccuracy, into: &buf) - FfiConverterBool.write(calculateWhileOffRoute, into: &buf) + FfiConverterTypeDeviationCalculationPolicy.write(calculationPolicy, into: &buf) case let .distanceEntryExit(distanceToEndOfStep,distanceAfterEndStep,minimumHorizontalAccuracy,hasReachedEndOfCurrentStep): @@ -10715,15 +10901,20 @@ public func stepAdvanceDistanceEntryAndSnappedExit(distanceToEndOfStep: UInt16, /** * Convenience function for creating a [`DistanceFromStepCondition`]. * - * This advances to the next step when the user is at least `distance` meters away _from_ any point on the current route step geometry. - * Does not advance unless the reported location accuracy is `minimum_horizontal_accuracy` meters or better. + * This advances to the next step when the user is at least `distance` meters away from any point + * on the current route step geometry. + * Does not advance unless the reported location accuracy is `minimum_horizontal_accuracy` + * meters or better. + * + * `calculation_policy` controls when this condition is permitted to evaluate + * based on the user's current route deviation status. */ -public func stepAdvanceDistanceFromStep(distance: UInt16, minimumHorizontalAccuracy: UInt16, calculateWhileOffRoute: Bool) -> StepAdvanceCondition { +public func stepAdvanceDistanceFromStep(distance: UInt16, minimumHorizontalAccuracy: UInt16, calculationPolicy: DeviationCalculationPolicy) -> StepAdvanceCondition { return try! FfiConverterTypeStepAdvanceCondition_lift(try! rustCall() { uniffi_ferrostar_fn_func_step_advance_distance_from_step( FfiConverterUInt16.lower(distance), FfiConverterUInt16.lower(minimumHorizontalAccuracy), - FfiConverterBool.lower(calculateWhileOffRoute),$0 + FfiConverterTypeDeviationCalculationPolicy_lower(calculationPolicy),$0 ) }) } @@ -10903,7 +11094,7 @@ private let initializationResult: InitializationResult = { if (uniffi_ferrostar_checksum_func_step_advance_distance_entry_and_snapped_exit() != 17502) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ferrostar_checksum_func_step_advance_distance_from_step() != 24638) { + if (uniffi_ferrostar_checksum_func_step_advance_distance_from_step() != 14034) { return InitializationResult.apiChecksumMismatch } if (uniffi_ferrostar_checksum_func_step_advance_distance_to_end_of_step() != 37822) { diff --git a/apple/Tests/FerrostarCoreTests/FerrostarCoreTests.swift b/apple/Tests/FerrostarCoreTests/FerrostarCoreTests.swift index 9e6583813..a4aca8929 100644 --- a/apple/Tests/FerrostarCoreTests/FerrostarCoreTests.swift +++ b/apple/Tests/FerrostarCoreTests/FerrostarCoreTests.swift @@ -367,10 +367,14 @@ final class FerrostarCoreTests: XCTestCase { func core( _: FerrostarCore, - correctiveActionForDeviation deviationInMeters: Double, + correctiveActionForDeviation deviation: DeviationKind, remainingWaypoints waypoints: [Waypoint] ) -> CorrectiveAction { - XCTAssertEqual(deviationInMeters, 42) + if case let .completelyOffRoute(deviationFromRouteLine: meters) = deviation { + XCTAssertEqual(meters, 42) + } else { + XCTFail("Expected completelyOffRoute deviation") + } routeDeviationCallbackExp.fulfill() return .getNewRoutes(waypoints: waypoints) } @@ -412,7 +416,7 @@ final class FerrostarCoreTests: XCTestCase { arrivalStepAdvanceCondition: stepAdvanceDistanceToEndOfStep(distance: 10, minimumHorizontalAccuracy: 32), routeDeviationTracking: .custom(detector: { _, _ in // Pretend that the user is always off route - .offRoute(deviationFromRouteLine: 42) + .deviation(kind: .completelyOffRoute(deviationFromRouteLine: 42)) }), snappedLocationCourseFiltering: .raw ) diff --git a/common/Cargo.lock b/common/Cargo.lock index 9dd81e480..e63a8ffc6 100644 --- a/common/Cargo.lock +++ b/common/Cargo.lock @@ -484,7 +484,7 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "ferrostar" -version = "0.49.0" +version = "0.50.0" dependencies = [ "android_logger", "assert-json-diff", diff --git a/common/ferrostar/Cargo.toml b/common/ferrostar/Cargo.toml index 4a74f1168..2e49e3618 100644 --- a/common/ferrostar/Cargo.toml +++ b/common/ferrostar/Cargo.toml @@ -2,7 +2,7 @@ lints.workspace = true [package] name = "ferrostar" -version = "0.49.0" +version = "0.50.0" readme = "README.md" description = "The core of modern turn-by-turn navigation." keywords = ["navigation", "routing", "valhalla", "osrm"] diff --git a/common/ferrostar/src/deviation_detection.rs b/common/ferrostar/src/deviation_detection.rs index 99cd81f9f..0c21574d6 100644 --- a/common/ferrostar/src/deviation_detection.rs +++ b/common/ferrostar/src/deviation_detection.rs @@ -19,7 +19,7 @@ use crate::navigation_controller::models::TripState; use crate::{models::UserLocation, navigation_controller::test_helpers::get_navigating_trip_state}; #[cfg(feature = "alloc")] use alloc::sync::Arc; -use geo::{LineString, Point}; +use geo::Point; use serde::{Deserialize, Serialize}; #[cfg(feature = "wasm-bindgen")] use tsify::Tsify; @@ -87,29 +87,46 @@ impl RouteDeviationTracking { remaining_steps, .. } => { + // Short-circuit in case the GPS is going wild (e.g. in a tunnel or deep urban canyon). if user_location.horizontal_accuracy > f64::from(*minimum_horizontal_accuracy) { return RouteDeviation::NoDeviation; } - let mut first_step_deviation = None; + let user_point = Point::from(*user_location); - 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(), - ); + // No steps remain, so... ;) + let Some(current_step) = remaining_steps.first() else { + return RouteDeviation::NoDeviation; + }; - if index == 0 { - first_step_deviation = Some(step_deviation.clone()); - } + let current_step_distance = + deviation_from_line(&user_point, ¤t_step.get_linestring()) + .unwrap_or(0.0); - if matches!(step_deviation, RouteDeviation::NoDeviation) { - return RouteDeviation::NoDeviation; + // Check if the user is on the current step. + if current_step_distance <= *max_acceptable_deviation { + return RouteDeviation::NoDeviation; + } + + // User is off the current step. Check if they're on any future step. + for step in remaining_steps.iter().skip(1) { + let distance = deviation_from_line(&user_point, &step.get_linestring()) + .unwrap_or(f64::MAX); + if distance <= *max_acceptable_deviation { + return RouteDeviation::Deviation { + kind: DeviationKind::OffStepOnRoute { + deviation_from_step_line: current_step_distance, + }, + }; } } - first_step_deviation.unwrap_or(RouteDeviation::NoDeviation) + // Not within the distance threshold of *any* step, so we're off the route. + RouteDeviation::Deviation { + kind: DeviationKind::CompletelyOffRoute { + deviation_from_route_line: current_step_distance, + }, + } } }, RouteDeviationTracking::Custom { detector } => { @@ -117,25 +134,31 @@ impl RouteDeviationTracking { } } } +} - /// Get the `RouteDeviation` status for a given location on a line string. - /// This can be used with a Route or `RouteStep`. - 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 - } - }) - } +/// The kind of deviation from the expected route. +#[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 DeviationKind { + /// The user is off the current step, but still on the route polyline (on a future step). + /// + /// This can happen when the user takes a shortcut, GPS noise places them on a future step, + /// or the route self-intersects. Step advance conditions can use this to limit advancement. + #[cfg_attr(feature = "wasm-bindgen", serde(rename_all = "camelCase"))] + OffStepOnRoute { + /// The deviation from the current step line, in meters. + deviation_from_step_line: f64, + }, + /// The user is off the expected route entirely + /// (not within threshold of any remaining step, + /// neither the current one nor any future one). + #[cfg_attr(feature = "wasm-bindgen", serde(rename_all = "camelCase"))] + CompletelyOffRoute { + /// The deviation from the route line, in meters. + deviation_from_route_line: f64, + }, } /// Status information that describes whether the user is proceeding according to the route or not. @@ -149,12 +172,37 @@ impl RouteDeviationTracking { pub enum RouteDeviation { /// The user is proceeding on course within the expected tolerances; everything is normal. NoDeviation, - /// The user is off the expected route. - #[cfg_attr(feature = "wasm-bindgen", serde(rename_all = "camelCase"))] - OffRoute { - /// The deviation from the route line, in meters. - deviation_from_route_line: f64, - }, + /// The user has deviated from the expected route. + /// + /// Check the [`DeviationKind`] to determine if the user is still on the route polyline + /// (off the current step but on a future step) or off the route entirely. + Deviation { kind: DeviationKind }, +} + +impl RouteDeviation { + /// Whether the user is no longer on the current step's polyline. + /// + /// This can happen either because the user has moved onto a future step + /// without triggering advance, or because they have left the route entirely. + /// In other words: **any deviation from the current step**. + #[must_use] + pub fn is_deviated_from_current_step(&self) -> bool { + matches!(self, RouteDeviation::Deviation { .. }) + } + + /// Whether the user has deviated from the route entirely. + /// + /// This is true when the user is not just deviated from the current step, + /// but also from the route as a whole (they didn't just take a shortcut). + #[must_use] + pub fn is_completely_off_route(&self) -> bool { + matches!( + self, + RouteDeviation::Deviation { + kind: DeviationKind::CompletelyOffRoute { .. } + } + ) + } } /// A custom deviation detector (for extending the behavior of [`RouteDeviationTracking`]). @@ -326,8 +374,10 @@ proptest! { _route: Route, _trip_state: TripState, ) -> RouteDeviation { - return RouteDeviation::OffRoute { - deviation_from_route_line: 7.0 + RouteDeviation::Deviation { + kind: DeviationKind::CompletelyOffRoute { + deviation_from_route_line: 7.0, + }, } } } @@ -358,8 +408,10 @@ proptest! { ); prop_assert_eq!( tracking.check_route_deviation(&route, &trip_state_on_route), - RouteDeviation::OffRoute { - deviation_from_route_line: 7.0 + RouteDeviation::Deviation { + kind: DeviationKind::CompletelyOffRoute { + deviation_from_route_line: 7.0, + }, } ); @@ -383,8 +435,10 @@ proptest! { ); prop_assert_eq!( tracking.check_route_deviation(&route, &trip_state_random), - RouteDeviation::OffRoute { - deviation_from_route_line: 7.0 + RouteDeviation::Deviation { + kind: DeviationKind::CompletelyOffRoute { + deviation_from_route_line: 7.0, + }, } ); } @@ -457,7 +511,11 @@ proptest! { prop_assert!(calculated <= max_acceptable_deviation); } } - RouteDeviation::OffRoute{ deviation_from_route_line } => { + RouteDeviation::Deviation { kind: DeviationKind::OffStepOnRoute { .. } } => { + // Cannot happen with a single-step route, but handle for exhaustiveness. + prop_assert!(false, "OffStep should not occur with a single-step route"); + } + RouteDeviation::Deviation { kind: DeviationKind::CompletelyOffRoute { deviation_from_route_line } } => { prop_assert_eq!( deviation_from_route_line, deviation.unwrap() @@ -506,3 +564,158 @@ proptest! { ); } } + +#[cfg(test)] +mod off_step_tests { + use super::*; + use crate::models::GeographicCoordinate; + use crate::navigation_controller::test_helpers::{gen_dummy_route_step, gen_route_from_steps}; + + #[cfg(all(feature = "std", not(feature = "web-time")))] + use std::time::SystemTime; + + #[cfg(feature = "web-time")] + use web_time::SystemTime; + + fn make_location(lng: f64, lat: f64) -> UserLocation { + UserLocation { + coordinates: GeographicCoordinate { lng, lat }, + horizontal_accuracy: 5.0, + course_over_ground: None, + timestamp: SystemTime::now(), + speed: None, + } + } + + #[test] + fn test_off_step_detected_when_on_future_step() { + // Route: Step 1 goes east, Step 2 goes north + let step1 = gen_dummy_route_step(0.0, 0.0, 0.001, 0.0); // ~111m east + let step2 = gen_dummy_route_step(0.001, 0.0, 0.001, 0.001); // ~111m north + + let route = gen_route_from_steps(vec![step1.clone(), step2.clone()]); + let tracking = RouteDeviationTracking::StaticThreshold { + minimum_horizontal_accuracy: 10, + max_acceptable_deviation: 50.0, + }; + + // User is on step 2's geometry (far from step 1) + let user_on_step2 = make_location(0.001, 0.0005); + let trip_state = get_navigating_trip_state( + user_on_step2, + vec![step1.clone(), step2.clone()], + vec![], + RouteDeviation::NoDeviation, + ); + + let result = tracking.check_route_deviation(&route, &trip_state); + assert!( + matches!( + result, + RouteDeviation::Deviation { + kind: DeviationKind::OffStepOnRoute { .. } + } + ), + "Should detect OffStep when user is on a future step, got: {result:?}" + ); + } + + #[test] + fn test_no_deviation_when_on_current_step() { + let step1 = gen_dummy_route_step(0.0, 0.0, 0.001, 0.0); + let step2 = gen_dummy_route_step(0.001, 0.0, 0.001, 0.001); + + let route = gen_route_from_steps(vec![step1.clone(), step2.clone()]); + let tracking = RouteDeviationTracking::StaticThreshold { + minimum_horizontal_accuracy: 10, + max_acceptable_deviation: 50.0, + }; + + // User is on step 1's geometry + let user_on_step1 = make_location(0.0005, 0.0); + let trip_state = get_navigating_trip_state( + user_on_step1, + vec![step1, step2], + vec![], + RouteDeviation::NoDeviation, + ); + + let result = tracking.check_route_deviation(&route, &trip_state); + assert_eq!( + result, + RouteDeviation::NoDeviation, + "Should be NoDeviation when user is on current step" + ); + } + + #[test] + fn test_off_route_when_not_on_any_step() { + let step1 = gen_dummy_route_step(0.0, 0.0, 0.001, 0.0); + let step2 = gen_dummy_route_step(0.001, 0.0, 0.001, 0.001); + + let route = gen_route_from_steps(vec![step1.clone(), step2.clone()]); + let tracking = RouteDeviationTracking::StaticThreshold { + minimum_horizontal_accuracy: 10, + max_acceptable_deviation: 50.0, + }; + + // User is far from both steps (~5.5km north) + let user_off_route = make_location(0.0005, 0.05); + let trip_state = get_navigating_trip_state( + user_off_route, + vec![step1, step2], + vec![], + RouteDeviation::NoDeviation, + ); + + let result = tracking.check_route_deviation(&route, &trip_state); + assert!( + matches!( + result, + RouteDeviation::Deviation { + kind: DeviationKind::CompletelyOffRoute { .. } + } + ), + "Should be CompletelyOffRoute when user is not on any step, got: {result:?}" + ); + } + + #[test] + fn test_off_step_has_deviation_distance() { + // Step 1 goes east, step 2 goes north — perpendicular + let step1 = gen_dummy_route_step(0.0, 0.0, 0.001, 0.0); // ~111m east + let step2 = gen_dummy_route_step(0.001, 0.0, 0.001, 0.002); // ~222m north + + let route = gen_route_from_steps(vec![step1.clone(), step2.clone()]); + let tracking = RouteDeviationTracking::StaticThreshold { + minimum_horizontal_accuracy: 10, + max_acceptable_deviation: 50.0, + }; + + // User is clearly on step 2's geometry, far from step 1 + // (~111m north of step 1's end, on step 2's line) + let user_on_step2 = make_location(0.001, 0.001); + let trip_state = get_navigating_trip_state( + user_on_step2, + vec![step1, step2], + vec![], + RouteDeviation::NoDeviation, + ); + + let result = tracking.check_route_deviation(&route, &trip_state); + match result { + RouteDeviation::Deviation { + kind: + DeviationKind::OffStepOnRoute { + deviation_from_step_line, + }, + } => { + assert!( + deviation_from_step_line > 0.0, + "OffStep should have a positive deviation distance" + ); + } + _ => panic!("Expected OffStep, got: {result:?}"), + } + } +} diff --git a/common/ferrostar/src/navigation_controller/mod.rs b/common/ferrostar/src/navigation_controller/mod.rs index c03925e85..504838302 100644 --- a/common/ferrostar/src/navigation_controller/mod.rs +++ b/common/ferrostar/src/navigation_controller/mod.rs @@ -169,16 +169,17 @@ impl Navigator for NavigationController { .. } = initial_trip_state { - // If the user starts off-route, suppress instructions for the same reason as - // in `create_intermediate_trip_state`: the snap-derived distance to the next - // maneuver is geometrically unsound, so any countdown surfaced from it would - // mislead the user. - let (visual_instruction, spoken_instruction) = - if deviation == RouteDeviation::NoDeviation { - (visual_instruction, spoken_instruction) - } else { - (None, None) - }; + // If the user starts completely off the route, suppress instructions for the + // same reason as in `create_intermediate_trip_state`: the snap-derived distance + // to the next maneuver is geometrically unsound, so any countdown surfaced + // from it would mislead the user. `OffStepOnRoute` is intentionally not + // suppressed here — the user is still on the route polyline (just on a future + // step), and the step-advance flow will reconcile shortly. + let (visual_instruction, spoken_instruction) = if deviation.is_completely_off_route() { + (None, None) + } else { + (visual_instruction, spoken_instruction) + }; TripState::Navigating { current_step_geometry_index, user_location, @@ -383,17 +384,22 @@ impl NavigationController { ); // Visual + spoken instructions are derived from the *snapped* distance to - // the next maneuver. While off-route, that snap is geometrically unsound: - // a user laterally far from the route still projects onto it somewhere, and - // the resulting "distance to next maneuver" is the phantom snap's distance, - // not the user's. Surfacing instructions paced off that phantom distance - // produces wrong countdowns to maneuvers the user can't take from their - // current position. Suppress both while off-route — the off-route signal - // (deviation + the consumer's own alert) is the right cue to surface; - // resume normal instruction emission on return. Apps that don't want this - // policy can configure `RouteDeviationTracking::None`. + // the next maneuver. When the user is completely off the route, that snap + // is geometrically unsound: a user laterally far from the route still + // projects onto it somewhere, and the resulting "distance to next maneuver" + // is the phantom snap's distance, not the user's. Surfacing instructions + // paced off that phantom distance produces wrong countdowns to maneuvers + // the user can't take from their current position. Suppress both while + // completely off-route — the deviation flag (and the consumer's own alert) + // is the right cue to surface; resume normal instruction emission on + // return. `OffStepOnRoute` is intentionally not suppressed: the user is + // still on the route polyline, and the step-advance flow will reconcile + // shortly. Apps that don't want any of this policy can configure + // `RouteDeviationTracking::None`. let (visual_instruction, spoken_instruction) = - if deviation == RouteDeviation::NoDeviation { + if deviation.is_completely_off_route() { + (None, None) + } else { ( current_step .get_active_visual_instruction(progress.distance_to_next_maneuver) @@ -402,8 +408,6 @@ impl NavigationController { .get_current_spoken_instruction(progress.distance_to_next_maneuver) .cloned(), ) - } else { - (None, None) }; let annotation_json = current_step_geometry_index .and_then(|index| current_step.get_annotation_at_current_index(index)); @@ -534,7 +538,6 @@ impl JsNavigationController { mod tests { use super::step_advance::StepAdvanceCondition; use super::*; - use crate::deviation_detection::RouteDeviation; use crate::navigation_controller::step_advance::conditions::{ DistanceEntryAndExitCondition, DistanceToEndOfStepCondition, }; @@ -587,10 +590,14 @@ mod tests { ); } - // Regression test that we are never marked as off the route. + // Regression test that we are never marked as completely off the route. // We used to encounter this with relative step advance on self-intersecting - // routes, for example. - assert_eq!(deviation, &RouteDeviation::NoDeviation); + // routes, for example. OffStepOnRoute is acceptable + // (on the route, just a different step). + assert!( + !deviation.is_completely_off_route(), + "User should never be completely off route during simulation, got: {deviation:?}" + ); } TripState::Complete { .. } => { states.push(new_state); @@ -787,7 +794,8 @@ mod tests { // Note: `update_user_location` computes deviation against the *previous* trip // state's user_location (mod.rs:289), so the deviation flag lags one tick. The // first off-route update therefore still reports NoDeviation; the second flips it - // to OffRoute, and that is when instruction suppression activates. + // to `Deviation { kind: CompletelyOffRoute }` (the user is 55km from every step), + // and that is when instruction suppression activates. let off_route_loc = make_user_location( coord!(x: start_coord.lng + 0.5, y: start_coord.lat + 0.5), 5.0, @@ -802,24 +810,24 @@ mod tests { .. } => { assert!( - matches!(deviation, RouteDeviation::OffRoute { .. }), - "expected OffRoute on second off-route tick, got {deviation:?}" + deviation.is_completely_off_route(), + "expected CompletelyOffRoute on second off-route tick, got {deviation:?}" ); assert!( visual_instruction.is_none(), - "visual_instruction must be None while off-route, got {visual_instruction:?}" + "visual_instruction must be None while completely off-route, got {visual_instruction:?}" ); assert!( spoken_instruction.is_none(), - "spoken_instruction must be None while off-route, got {spoken_instruction:?}" + "spoken_instruction must be None while completely off-route, got {spoken_instruction:?}" ); } other => panic!("expected Navigating, got {other:?}"), }; // 3. Update back to the on-route location. The first return tick still carries - // the lagged OffRoute flag (deviation is computed from the previous user_location, - // which was off-route), so update twice to clear the lag. + // the lagged CompletelyOffRoute flag (deviation is computed from the previous + // user_location, which was off-route), so update twice to clear the lag. let recovered_lagged = controller.update_user_location(on_route_loc.clone(), off_state); let recovered = controller.update_user_location(on_route_loc, recovered_lagged); match recovered.trip_state() { @@ -890,8 +898,8 @@ mod tests { .. } => { assert!( - matches!(deviation, RouteDeviation::OffRoute { .. }), - "expected OffRoute on initial state at off-route location, got {deviation:?}" + deviation.is_completely_off_route(), + "expected CompletelyOffRoute on initial state at off-route location, got {deviation:?}" ); assert!( visual_instruction.is_none(), diff --git a/common/ferrostar/src/navigation_controller/step_advance/conditions.rs b/common/ferrostar/src/navigation_controller/step_advance/conditions.rs index dd69367c4..7cc28dca7 100644 --- a/common/ferrostar/src/navigation_controller/step_advance/conditions.rs +++ b/common/ferrostar/src/navigation_controller/step_advance/conditions.rs @@ -6,16 +6,20 @@ use crate::{ deviation_from_line, get_linestring, is_within_threshold_to_end_of_linestring, snap_user_location_to_line, }, - deviation_detection::RouteDeviation, 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}, }; @@ -112,26 +116,49 @@ impl StepAdvanceConditionSerializable for DistanceToEndOfStepCondition { } } -/// Requires that the user be at least this far from the current route step. +/// Controls when a deviation-aware step-advance condition is allowed to evaluate, +/// based on the user's current +/// [`RouteDeviation`](crate::deviation_detection::RouteDeviation) status. +#[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 evaluate, regardless of deviation status. + Always, + /// Evaluate while the user is still somewhere on the route polyline + /// (current step or any future step), + /// but suspend if the user is completely off the route. + WhileOnRoute, + /// Evaluate only while the user is within an acceptable deviation of the current step's polyline. + /// + /// Suspend on any deviation + /// (whether off-step-but-on-route, or completely off-route). + WhileOnCurrentStep, +} + +/// Advances once the user is at least [`Self::distance`] meters from the current step's polyline. /// /// This results in *delayed* advance, /// but is more robust to spurious / unwanted step changes in scenarios including -/// self-intersecting routes (sudden jump to the next step) +/// self-intersecting routes (sudden jumps to the next step) /// and pauses at intersections (advancing too soon before the maneuver is complete). /// -/// NOTE! This may be less robust to things like short steps, out and backs and U-turns, -/// where this may eagerly exit a current step before the user has traversed it if the start -/// the step within range of the end. +/// NOTE! This may be less robust to things like short steps, out-and-backs, and U-turns, +/// where this may eagerly exit a current step before the user has traversed it +/// if the start of the step is within range of the end. #[derive(Debug, Copy, Clone)] #[cfg_attr(feature = "uniffi", derive(uniffi::Object))] pub struct DistanceFromStepCondition { - /// The minimum the distance the user must have travelled from the step's polyline. + /// The minimum distance, in meters, + /// from the current step's polyline at which this condition advances. pub distance: u16, /// The minimum required horizontal accuracy of the user location, in meters. - /// Values larger than this cannot ever trigger a step advance. + /// Locations with reported accuracy worse than this cannot trigger a step advance. pub minimum_horizontal_accuracy: u16, - /// Whether the condition can succeed when the user is off route. - pub calculate_while_off_route: bool, + /// Controls when this condition is permitted to evaluate based on the user's current + /// [`RouteDeviation`](crate::deviation_detection::RouteDeviation) status. + pub calculation_policy: DeviationCalculationPolicy, } impl StepAdvanceCondition for DistanceFromStepCondition { @@ -141,11 +168,8 @@ impl StepAdvanceCondition for DistanceFromStepCondition { } fn new_instance(&self) -> Arc { - Arc::new(DistanceFromStepCondition { - distance: self.distance, - minimum_horizontal_accuracy: self.minimum_horizontal_accuracy, - calculate_while_off_route: self.calculate_while_off_route, - }) + // Simple Arc::new here; there is no internal state to reset for this condition. + Arc::new(*self) } } @@ -155,19 +179,26 @@ impl DistanceFromStepCondition { let user_location = trip_state.user_location()?; let current_step = trip_state.current_step()?; - let should_advance = - // If the user is not on route and we don't allow calculating while off route, don't advance - if (!self.calculate_while_off_route && deviation != RouteDeviation::NoDeviation) - // If the user location isn't accurate enough, don't advance - || (user_location.horizontal_accuracy > self.minimum_horizontal_accuracy.into()){ - false - } else { - let current_position: Point = user_location.into(); - let current_step_linestring = current_step.get_linestring(); + 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(); - deviation_from_line(¤t_position, ¤t_step_linestring) - .is_some_and(|deviation| deviation > self.distance.into()) - }; + let should_advance = if !permits_calculation || location_too_inaccurate { + // Bail early + 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) @@ -184,7 +215,7 @@ impl StepAdvanceConditionSerializable for DistanceFromStepCondition { SerializableStepAdvanceCondition::DistanceFromStep { distance: self.distance, minimum_horizontal_accuracy: self.minimum_horizontal_accuracy, - calculate_while_off_route: self.calculate_while_off_route, + calculation_policy: self.calculation_policy, } } } @@ -344,10 +375,18 @@ impl DistanceEntryAndExitCondition { impl StepAdvanceCondition for DistanceEntryAndExitCondition { fn should_advance_step(&self, trip_state: TripState) -> StepAdvanceResult { if self.has_reached_end_of_current_step { + // This inner check fires once the user is far enough from the current step's + // polyline that we treat them as having moved past the end of the step. + // + // `WhileOnRoute` is the policy we want here: + // - On `OffStepOnRoute` (user has progressed onto a future step): + // evaluate. That is the normal success case for an exit check. + // - On `CompletelyOffRoute` (user is lost): suspend. + // The rerouter, not this condition, should drive what happens next. let distance_from_end = DistanceFromStepCondition { minimum_horizontal_accuracy: self.minimum_horizontal_accuracy, distance: self.distance_after_end_of_step, - calculate_while_off_route: false, + calculation_policy: DeviationCalculationPolicy::WhileOnRoute, }; let should_advance = distance_from_end @@ -673,15 +712,17 @@ mod tests { let condition = DistanceFromStepCondition { minimum_horizontal_accuracy: 10, distance: 100, // Must be at least 100 meters from route to advance - calculate_while_off_route: true, + calculation_policy: DeviationCalculationPolicy::Always, }; let trip_state = get_navigating_trip_state( user_location, vec![STRAIGHT_LINE_SHORT_ROUTE_STEP.clone()], vec![], - RouteDeviation::OffRoute { - deviation_from_route_line: 10.0, + RouteDeviation::Deviation { + kind: DeviationKind::CompletelyOffRoute { + deviation_from_route_line: 10.0, + }, }, ); @@ -695,7 +736,7 @@ mod tests { } #[test] - fn test_distance_from_step_advance_with_deviation_off() { + fn test_distance_from_step_no_advance_when_completely_off_route() { // Create a location that's far from the route (500+ meters north) // At the equator, 0.005° latitude is approximately 555 meters let user_location = make_user_location(coord!(x: 0.005, y: 0.0005), 5.0); @@ -704,15 +745,17 @@ mod tests { let condition = DistanceFromStepCondition { minimum_horizontal_accuracy: 10, distance: 100, // Must be at least 100 meters from route to advance - calculate_while_off_route: false, + calculation_policy: DeviationCalculationPolicy::WhileOnRoute, }; let trip_state = get_navigating_trip_state( user_location, vec![STRAIGHT_LINE_SHORT_ROUTE_STEP.clone()], vec![], - RouteDeviation::OffRoute { - deviation_from_route_line: 10.0, + RouteDeviation::Deviation { + kind: DeviationKind::CompletelyOffRoute { + deviation_from_route_line: 10.0, + }, }, ); @@ -721,7 +764,7 @@ mod tests { assert!( !result.should_advance, - "Should not advance when far from the route" + "WhileOnRoute should not advance when the user is completely off the route" ); } @@ -735,7 +778,7 @@ mod tests { let condition = DistanceFromStepCondition { minimum_horizontal_accuracy: 10, distance: 100, // Must be at least 100 meters from route to advance - calculate_while_off_route: false, + calculation_policy: DeviationCalculationPolicy::WhileOnRoute, }; let trip_state = get_navigating_trip_state( @@ -1597,3 +1640,218 @@ proptest! { // TODO: Similar to the above, but with, say, 5 random updates where the user is *always* within the distance threshold, so they never advance to the next step } + +#[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 = LazyLock::new(|| { + gen_route_step_with_coords(vec![ + coord!(x: 0.0, y: 0.0), + coord!(x: 0.001, y: 0.0), // 111 meters east + ]) + }); + + static LOCATION_FAR_FROM_STEP: LazyLock = + 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() { + // WhileOnRoute should permit advance when the user is off the current step + // but still on a future step on the route polyline. + 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, + // `WhileOnCurrentStep` blocks advancement on *any* + // deviation, including `CompletelyOffRoute`. + // Being completely off the route is itself a (stronger) form of being off the current step. + 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, + }; + + // OffStepOnRoute: blocked + 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" + ); + + // CompletelyOffRoute: blocked + 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" + ); + + // NoDeviation: allowed + 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" + ); + } +} diff --git a/common/ferrostar/src/navigation_controller/step_advance/mod.rs b/common/ferrostar/src/navigation_controller/step_advance/mod.rs index d82403a1d..344608379 100644 --- a/common/ferrostar/src/navigation_controller/step_advance/mod.rs +++ b/common/ferrostar/src/navigation_controller/step_advance/mod.rs @@ -2,9 +2,9 @@ use crate::navigation_controller::{ models::TripState, step_advance::conditions::{ - AndAdvanceConditions, DistanceEntryAndExitCondition, DistanceEntryAndSnappedExitCondition, - DistanceFromStepCondition, DistanceToEndOfStepCondition, ManualStepCondition, - OrAdvanceConditions, + AndAdvanceConditions, DeviationCalculationPolicy, DistanceEntryAndExitCondition, + DistanceEntryAndSnappedExitCondition, DistanceFromStepCondition, + DistanceToEndOfStepCondition, ManualStepCondition, OrAdvanceConditions, }, }; use serde::{Deserialize, Serialize}; @@ -114,7 +114,7 @@ pub enum SerializableStepAdvanceCondition { DistanceFromStep { distance: u16, minimum_horizontal_accuracy: u16, - calculate_while_off_route: bool, + calculation_policy: DeviationCalculationPolicy, }, #[cfg_attr(feature = "wasm-bindgen", serde(rename_all = "camelCase"))] DistanceEntryExit { @@ -154,11 +154,11 @@ impl From for Arc { SerializableStepAdvanceCondition::DistanceFromStep { distance, minimum_horizontal_accuracy, - calculate_while_off_route, + calculation_policy, } => Arc::new(DistanceFromStepCondition { distance, minimum_horizontal_accuracy, - calculate_while_off_route, + calculation_policy, }), SerializableStepAdvanceCondition::DistanceEntryExit { minimum_horizontal_accuracy, @@ -225,19 +225,24 @@ pub fn step_advance_distance_to_end_of_step( /// Convenience function for creating a [`DistanceFromStepCondition`]. /// -/// This advances to the next step when the user is at least `distance` meters away _from_ any point on the current route step geometry. -/// Does not advance unless the reported location accuracy is `minimum_horizontal_accuracy` meters or better. +/// This advances to the next step when the user is at least `distance` meters away from any point +/// on the current route step geometry. +/// Does not advance unless the reported location accuracy is `minimum_horizontal_accuracy` +/// meters or better. +/// +/// `calculation_policy` controls when this condition is permitted to evaluate +/// based on the user's current route deviation status. #[cfg(feature = "uniffi")] #[uniffi::export] pub fn step_advance_distance_from_step( distance: u16, minimum_horizontal_accuracy: u16, - calculate_while_off_route: bool, + calculation_policy: DeviationCalculationPolicy, ) -> Arc { Arc::new(DistanceFromStepCondition { distance, minimum_horizontal_accuracy, - calculate_while_off_route, + calculation_policy, }) } diff --git a/react-native/.github/workflows/ci.yml b/react-native/.github/workflows/ci.yml index 0c7ff3c96..94549187c 100644 --- a/react-native/.github/workflows/ci.yml +++ b/react-native/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Setup uses: ./.github/actions/setup @@ -30,7 +30,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Setup uses: ./.github/actions/setup @@ -42,7 +42,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Setup uses: ./.github/actions/setup @@ -56,7 +56,7 @@ jobs: TURBO_CACHE_DIR: .turbo/android steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Setup uses: ./.github/actions/setup @@ -112,7 +112,7 @@ jobs: TURBO_CACHE_DIR: .turbo/ios steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Setup uses: ./.github/actions/setup diff --git a/react-native/core/package.json b/react-native/core/package.json index 18b7b8c22..ced8ab7c5 100644 --- a/react-native/core/package.json +++ b/react-native/core/package.json @@ -1,6 +1,6 @@ { "name": "@stadiamaps/ferrostar-core-react-native", - "version": "0.49.0", + "version": "0.50.0", "description": "Ferrostar React Native Core", "main": "./lib/index.js", "types": "./src/index.ts", diff --git a/react-native/core/src/FerrostarCore.ts b/react-native/core/src/FerrostarCore.ts index 497e37acc..6a6267046 100644 --- a/react-native/core/src/FerrostarCore.ts +++ b/react-native/core/src/FerrostarCore.ts @@ -296,7 +296,7 @@ export class FerrostarCore implements LocationUpdateListener { } // If we're not recalculating a new route, we don't care about state changes. - if (RouteDeviation.OffRoute.instanceOf(newState.inner.deviation)) { + if (RouteDeviation.Deviation.instanceOf(newState.inner.deviation)) { // Check that the last automatic recalculation wasn't too recent. // We have to do some weird thing here with hrTime since JavaScript doesn't have a nice nanoseoncds method. const isGreaterThanMinimumTime = this._lastAutomaticRecalculation @@ -311,7 +311,7 @@ export class FerrostarCore implements LocationUpdateListener { const action = this.deviationHandler?.correctiveActionForDeviation( this, - newState.inner.deviation.inner.deviationFromRouteLine, + newState.inner.deviation.inner.kind, newState.inner.remainingWaypoints ) ?? CorrectiveAction.GetNewRoutes; @@ -331,7 +331,7 @@ export class FerrostarCore implements LocationUpdateListener { // Make sure we are still navigating and the new route is still relevant. if ( TripState.Navigating.instanceOf(state.tripState) && - RouteDeviation.OffRoute.instanceOf( + RouteDeviation.Deviation.instanceOf( state.tripState.inner.deviation ) ) { diff --git a/react-native/core/src/RouteDeviationHandler.ts b/react-native/core/src/RouteDeviationHandler.ts index c7a256bc6..b6a2bdd11 100644 --- a/react-native/core/src/RouteDeviationHandler.ts +++ b/react-native/core/src/RouteDeviationHandler.ts @@ -1,4 +1,7 @@ -import type { Waypoint } from '@stadiamaps/ferrostar-uniffi-react-native'; +import type { + DeviationKind, + Waypoint, +} from '@stadiamaps/ferrostar-uniffi-react-native'; import type { FerrostarCore } from './FerrostarCore'; export enum CorrectiveAction { @@ -9,7 +12,7 @@ export enum CorrectiveAction { export interface RouteDeviationHandler { correctiveActionForDeviation( core: FerrostarCore, - deviationInMeters: number, + deviation: DeviationKind, remainingWaypoints: Array ): CorrectiveAction; } diff --git a/react-native/maplibreui/package.json b/react-native/maplibreui/package.json index 172fc6e13..2441d86d4 100644 --- a/react-native/maplibreui/package.json +++ b/react-native/maplibreui/package.json @@ -1,6 +1,6 @@ { "name": "@stadiamaps/ferrostar-maplibre-react-native", - "version": "0.49.0", + "version": "0.50.0", "description": "Ferrostar React Native MapLibre UI", "main": "./lib/index.js", "types": "./src/index.ts", diff --git a/react-native/package.json b/react-native/package.json index 67298a753..fe3116ab7 100644 --- a/react-native/package.json +++ b/react-native/package.json @@ -1,6 +1,6 @@ { "name": "react-native-ferrostar", - "version": "0.49.0", + "version": "0.50.0", "description": "Ferrostar React Native", "private": true, "scripts": { diff --git a/react-native/uniffi/package.json b/react-native/uniffi/package.json index b262d1b57..cb622cacd 100644 --- a/react-native/uniffi/package.json +++ b/react-native/uniffi/package.json @@ -1,6 +1,6 @@ { "name": "@stadiamaps/ferrostar-uniffi-react-native", - "version": "0.49.0", + "version": "0.50.0", "description": "Ferrostar React Native Binding", "source": "./src/index.tsx", "main": "./lib/commonjs/index.js", diff --git a/web/package-lock.json b/web/package-lock.json index 6a9c238d8..2d89dc810 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "@stadiamaps/ferrostar-webcomponents", - "version": "0.49.0", + "version": "0.50.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@stadiamaps/ferrostar-webcomponents", - "version": "0.49.0", + "version": "0.50.0", "license": "BSD-3-Clause", "dependencies": { "@maptimy/platform-formatters": "^0.6.0", diff --git a/web/package.json b/web/package.json index 252972b60..c4c0ee5e9 100644 --- a/web/package.json +++ b/web/package.json @@ -10,7 +10,7 @@ "type": "git", "url": "https://github.com/stadiamaps/ferrostar" }, - "version": "0.49.0", + "version": "0.50.0", "license": "BSD-3-Clause", "type": "module", "main": "./dist/ferrostar-webcomponents.js",