Skip to content

Use Case 025: Control Points Persistence & Orphaned Cues

1. Objective

To stabilize the route planning experience by separating the "Skeleton" (user-defined control points/clicks) from the "Meat" (high-resolution track geometry), ensuring long-term track editability. Additionally, to implement a "Pro-level" UX for track modifications where absolute POIs remain stationary, while track-dependent Cue Points intelligently snap to micro-shifts or visually warn the user (Orphan State) upon macro-shifts.

Target Users: Advanced route planners who design complex routes and later return to tweak specific segments without destroying the original routing intent.

2. Core Features & Acceptance Criteria

Block 1: "Skeleton and Meat" (Routing Control Points)

  • Persistence: Start, Finish, and all intermediate control points MUST be explicitly serialized and saved to the database alongside the track's GeoJSON geometry.
  • Visibility by State:
    • Edit Mode: Control points are visible as white markers. The user can drag, delete, or add them.
    • Read-Only Mode: Control points are completely hidden. Only the track line, Start/Finish, and POIs/Cues are visible.
  • GPX Export Constraint: Control points MUST NOT be exported as <wpt> tags in GPX files to prevent polluting the bike computer screen. Only the final track <trk> is exported.
  • Edit Restoration: When a saved track is opened in Edit mode, dragging a point only recalculates the adjacent segments, preserving the rest of the complex geometry.

Block 2: POI and Cue Point Physics

  • Class A: Free POIs (Absolute Coordinates)
    • Definition: Springs, cafes, campsites.
    • Behavior: Bound strictly to the Earth (GPS Lat/Lng).
    • Interaction: When the track is dragged away, the POI remains in its original position.
  • Class B: Track Cues (Relative Context)
    • Definition: Dangers, turns, fords (meaningful only when riding the track).
    • Micro-Shift (Auto-Snap): If the track is dragged and the new line is < 30 meters from the Cue, the Cue automatically snaps to the nearest point on the new line.
    • Macro-Shift (Orphan State): If the new line moves significantly (> 30 meters), the Cue is "Orphaned".
      • Visual State: Turns semi-transparent (Ghost state), gets a red dashed border, and displays a broken chain icon (🔗).
      • UX Alert: Triggers a micro-alert near the UI: "⚠️ You have 1 cue off route! [Delete] / [Keep]".
      • Resolution: User can click to delete it, or manually drag it back onto the new track line.

3. Tech Stack Preferences & Constraints

  • State Management: plannerStore (Zustand) must be updated to manage controlPoints alongside geojson. zundo history must track control points.
  • Backend Schema: Update the database and api-libs models to support a controlPoints array inside the Route entity.
  • Spatial Calculations: Use turf.pointToLineDistance and turf.nearestPointOnLine to handle the threshold logic (< 30m) for Cue snapping.

4. Known Boundaries

  • NEVER auto-delete an Orphaned Cue. The system must ask the user or wait for manual deletion.
  • NEVER export control points as <wpt> in GPX.
  • ALWAYS decouple visual geometry (thousands of coordinates) from logical anchors (5-10 control points) during the save/load cycle.
  • ALWAYS check for orphaned cues silently in the background right after buildRoute succeeds.

5. Implementation Roadmap / Commands

  1. Database / Schema: Extend Route model in API/DB to include waypoints JSON. Update /routes POST/PUT endpoints.
  2. Frontend Store: Update trackService.ts to push/pull waypoints. Refactor handleOpenTrack to restore waypoints into plannerStore.
  3. Visibility Logic: Hook useWaypointMarkers into isReadonly map state to toggle visibility.
  4. GPX Exporter: Verify exportToGpx strictly filters out routing control points.
  5. Orphan Logic: In useBuildRoute (or a dedicated observer), calculate the distance of all Cues to the new line. Dispatch snapCue or markCueOrphaned actions.
  6. Orphan UI: Create the Ghost styling and the broken chain icon in MapLibre HTML markers. Add the floating alert toast.