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 towardcurrentCoordinate: Coordinate— boat position snapshottrackCoordinate: Coordinate— closest point on route to boatsteerCoordinate: Coordinate— intercept point for smooth course-to-steercompletedLine: LineString?— start → trackCoordinatecrossTrackErrorLine: LineString?— boat → trackCoordinate (straight)crossTrackErrorDirection: CrossTrackErrorDirection?— left/right of tracksteerLine: LineString?— boat → steerCoordinate (smooth curve)toEndLine: LineString?— steerLine + (steerCoordinate → end)toNextLine: LineString?— steerLine + (steerCoordinate → next waypoint)courseToSteerInDegrees: DoubledistanceToEndInMeters: DoublerouteCrossTrackDistance: DoublecurrentPositionIndex: Int— index of nextWaypoint in waypoints array
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 }
}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
}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.
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.
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.
NavigationInfo bundles two kinds of data:
- Navigation geometry (core value):
trackCoordinate,steerCoordinate, allLineStrings,courseToSteerInDegrees,distanceToEndInMeters,routeCrossTrackDistance - Identity/context (available elsewhere):
nextWaypoint,currentCoordinate,currentPositionIndex
currentCoordinate is already available from locationState/locationPuckState.
routedSteerLine, isLoadingSteerLine, routedSteerTargetCoordinate, routedSteerOriginCoordinate are async caching bookkeeping for the routed steer line — implementation detail of UpdateNavigation, not broadly needed state.
- Delete
@Published var nextWaypointID: UUID?fromNavigateToState - Change
BuildNavigationInfo.findNextWaypointto readcurrentRouteState.nextWaypointID - Remove all sync writes from
SetNextWaypoint,UpdateNavigation, etc. SetNextWaypoint.callsimplifies to only writingcurrentRouteState
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.
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.
Move routedSteerLine, isLoadingSteerLine, routedSteerTargetCoordinate, routedSteerOriginCoordinate into a small dedicated struct or localize to the operation.
| 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 |