Skip to content

Instantly share code, notes, and snippets.

@gshaw
Created March 27, 2026 19:38
Show Gist options
  • Select an option

  • Save gshaw/991dd40d872ba08341464a9f8e19b26d to your computer and use it in GitHub Desktop.

Select an option

Save gshaw/991dd40d872ba08341464a9f8e19b26d to your computer and use it in GitHub Desktop.

Review: NavigationInfo, NavigateToState, and CurrentRouteState

Current Structure

NavigationInfo (value type)

A struct on NavigateToState.currentInfo. Rebuilt on every GPS tick by BuildNavigationInfo during active navigation. Set to nil on stop.

Fields:

  • nextWaypoint: Waypoint — the waypoint being navigated toward
  • currentCoordinate: Coordinate — boat position snapshot
  • trackCoordinate: Coordinate — closest point on route to boat
  • steerCoordinate: Coordinate — intercept point for smooth course-to-steer
  • completedLine: LineString? — start → trackCoordinate
  • crossTrackErrorLine: LineString? — boat → trackCoordinate (straight)
  • crossTrackErrorDirection: CrossTrackErrorDirection? — left/right of track
  • steerLine: LineString? — boat → steerCoordinate (smooth curve)
  • toEndLine: LineString? — steerLine + (steerCoordinate → end)
  • toNextLine: LineString? — steerLine + (steerCoordinate → next waypoint)
  • courseToSteerInDegrees: Double
  • distanceToEndInMeters: Double
  • routeCrossTrackDistance: Double
  • currentPositionIndex: Int — index of nextWaypoint in waypoints array

NavigateToState (ObservableObject)

The navigation engine's state.

final class NavigateToState: ObservableObject {
    @Published var currentInfo: NavigationInfo?
    @Published var phase = Phase.stop          // .start / .pause / .stop
    @Published var nextWaypointID: UUID?       // ⚠️ duplicated

    @Published var routedSteerLine: LineString?
    @Published var isLoadingSteerLine = false
    var routedSteerTargetCoordinate: Coordinate?
    var routedSteerOriginCoordinate: Coordinate?

    var arrivalConfirmationCount = 0
    var lastNotifiedWaypointID: UUID?

    var isNavigating: Bool { phase == .start }
}

CurrentRouteState (ObservableObject)

The route definition and editing state. Persisted to disk.

class CurrentRouteState: ObservableObject {
    var points: [DrawingPoint] = []            // triggers waypoint rebuild on didSet
    @Published var nextWaypointID: UUID?       // ⚠️ duplicated
    @Published var selectedPointID: UUID?
    @Published var isSaved = true
    @Published var routeMode: RouteMode = .auto
    @Published var draft = Measurement(value: 2, unit: UnitLength.meters)
    @Published var isLoadingSegments = false
    @Published var segments: [TrackSegment] = []
    @Published var unnavigableSegments: [TrackSegment] = []
    @Published var navigableLine: LineString?

    private(set) var waypoints: [Waypoint] = []

    var nextWaypoint: Waypoint? { waypoints.first { $0.id == nextWaypointID } }
    var nextWaypointIndex: Int? { ... } // proposed, currently recomputed in 3 views
}

Issues Found

1. nextWaypointID is duplicated with manual sync

Both NavigateToState.nextWaypointID and CurrentRouteState.nextWaypointID hold the same value during navigation, kept in sync by multiple operations:

Write site Writes navState Writes routeState
SetNextWaypoint.call
UpdateNavigation (.start phase)
SetNextWaypointOnRoute.call ✅ (if navigating)
DetermineNextWaypoint.call
UpdateNextWaypointOnRoute.call
ReverseWaypoints ✅ (set to nil)
ConfirmClearRoute ✅ (set to nil)
RestoreCurrentRouteState

CurrentRouteState.nextWaypointID is the real source of truth. It's persisted to disk, used by all views, survives navigation stop, and is written in non-navigation contexts.

NavigateToState.nextWaypointID is redundant. Only read in one place: BuildNavigationInfo.findNextWaypoint, which resolves it to a Waypoint. This could read from currentRouteState instead.

2. NavigationInfo.currentPositionIndex is unused by views

Three views independently recompute currentPositionIndex with identical logic:

// CurrentRouteBottomSheet, MiniMapContent, WaypointsTableSection — all identical
private var currentPositionIndex: Int? {
    guard let nextID = currentRouteState.nextWaypointID else { return nil }
    return currentRouteState.waypoints.firstIndex(where: { $0.id == nextID })
}

None read NavigationInfo.currentPositionIndex. The only consumer is InsertWaypointAtCurrentPosition (an Operation), which uses it as a cached value — but it's a trivial firstIndex lookup that doesn't need caching.

3. NavigationInfo.nextWaypoint overlaps with CurrentRouteState.nextWaypoint

During navigation these resolve to the same Waypoint. Views fork on isNavigating:

// Pattern in DashboardStack, GaugeSlotContent, GaugeCellBuilder
private var waypointLabel: String? {
    if isNavigating {
        return navigateToState.currentInfo?.nextWaypoint.pinLabel
    }
    return currentRouteState.nextWaypoint?.pinLabel
}

With a single nextWaypointID, currentRouteState.nextWaypoint is always correct regardless of navigation state, eliminating these forks.

4. NavigationInfo mixes geometry with identity concerns

NavigationInfo bundles two kinds of data:

  • Navigation geometry (core value): trackCoordinate, steerCoordinate, all LineStrings, courseToSteerInDegrees, distanceToEndInMeters, routeCrossTrackDistance
  • Identity/context (available elsewhere): nextWaypoint, currentCoordinate, currentPositionIndex

currentCoordinate is already available from locationState/locationPuckState.

5. Steer-line caching state mixed into NavigateToState

routedSteerLine, isLoadingSteerLine, routedSteerTargetCoordinate, routedSteerOriginCoordinate are async caching bookkeeping for the routed steer line — implementation detail of UpdateNavigation, not broadly needed state.


Suggested Changes

Priority 1: Remove NavigateToState.nextWaypointID (low risk, high value)

  • Delete @Published var nextWaypointID: UUID? from NavigateToState
  • Change BuildNavigationInfo.findNextWaypoint to read currentRouteState.nextWaypointID
  • Remove all sync writes from SetNextWaypoint, UpdateNavigation, etc.
  • SetNextWaypoint.call simplifies to only writing currentRouteState

Priority 2: Add CurrentRouteState.nextWaypointIndex (low risk)

var nextWaypointIndex: Int? {
    guard let nextWaypointID else { return nil }
    return waypoints.firstIndex { $0.id == nextWaypointID }
}

Replace the duplicated firstIndex(where:) in CurrentRouteBottomSheet, MiniMapContent, and WaypointsTableSection.

Priority 3: Remove identity fields from NavigationInfo (medium risk)

Remove nextWaypoint, currentPositionIndex, and currentCoordinate. The struct becomes a focused geometry container:

struct NavigationInfo {
    let trackCoordinate: Coordinate
    let steerCoordinate: Coordinate

    let completedLine: LineString?
    let crossTrackErrorLine: LineString?
    let crossTrackErrorDirection: CrossTrackErrorDirection?
    let steerLine: LineString?
    let toEndLine: LineString?
    let toNextLine: LineString?

    let courseToSteerInDegrees: Double
    let distanceToEndInMeters: Double
    let routeCrossTrackDistance: Double
}

Views that had isNavigating forks for nextWaypoint properties simplify to always reading currentRouteState.nextWaypoint.

Priority 4: Extract steer-line caching (low risk, minor)

Move routedSteerLine, isLoadingSteerLine, routedSteerTargetCoordinate, routedSteerOriginCoordinate into a small dedicated struct or localize to the operation.


Summary

Change Risk Impact
Remove NavigateToState.nextWaypointID Low Eliminates all manual sync code
Add CurrentRouteState.nextWaypointIndex Low Removes duplicated logic in 3 views
Remove identity fields from NavigationInfo Medium Cleaner struct, simpler view logic
Remove NavigationInfo.currentCoordinate Medium Views already have locationState
Extract steer-line caching Low Cleaner separation of concerns
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment