Skip to content

Mission planning: Add mission library#2654

Open
ArturoManzoli wants to merge 14 commits into
bluerobotics:masterfrom
ArturoManzoli:add-mission-library
Open

Mission planning: Add mission library#2654
ArturoManzoli wants to merge 14 commits into
bluerobotics:masterfrom
ArturoManzoli:add-mission-library

Conversation

@ArturoManzoli
Copy link
Copy Markdown
Contributor

@ArturoManzoli ArturoManzoli commented Apr 29, 2026

  • Added a 'Mission Library' to store, organize and reuse missions and mission segments;
  • Save and restore missions from the library directly to the planning map;
  • Resize, rotate, drag and join multiple missions on the map before saving or uploading them to the vehicle;
  • Import and export missions as .cmp files to share them between users or installations;
  • Each saved mission keeps its own thumbnail, vehicle type, cruise speed and mission estimates (length, ETA, energy, coverage);
  • Insert a saved mission anywhere in the existing planning: at either endpoint or between any two waypoints via the segment radial menu;
  • Plan, save and reload missions fully offline (no vehicle connection required);
  • Select the type of vehicle that the mission is being designed for;
  • Highlighted first and last waypoints of the current mission for clearer endpoint identification;
  • Context-menu actions (add waypoint, simple path, survey, mission from library) now extend the mission from whichever egde waypoint is closer to the cursor;

Mission Library:

Screenshare.-.2026-04-29.11_56_26.AM.mp4

Bidirectional element addition:

Screenshare.-.2026-04-29.1_22_22.PM.mp4

@github-actions
Copy link
Copy Markdown

Automated PR Review (Claude)

0. Summary

Verdict: MINOR SUGGESTIONS

Minor items to address: 1.1, 1.2, 1.3, 2.1, 6.1, 6.2, 6.3

This PR adds a mission library feature to the mission planning view, allowing users to save, organize, reload, import/export, and reposition missions. It introduces a MissionLibraryModal component, a library.ts helper module, new types (SavedMission, MissionEstimatesSnapshot), store additions for persistence, context-menu and segment-radial-menu integration for inserting library missions, a free-placement UI (drag/scale/rotate), endpoint-aware insertion, and highlighted first/last waypoint markers. The Leaflet zoom behaviour is also tweaked across both the planning map and the main Map widget.


1. Correctness & Implementation Bugs

1.1 (minor) — MissionLibraryModal.vue template line ~108: The condition mission.estimates?.length && mission.estimates.length !== '—' is checking .length on the MissionEstimatesSnapshot object, which has a property called length (a string like "1.26 km"). This works at runtime, but it is confusing and fragile because it looks like an array .length check. The truthiness check (&& mission.estimates.length) would be false for the empty string "" but not for the dash "—", so the logic is technically correct, but the naming collision with Array.prototype.length makes the template misleading. Consider renaming the length property on MissionEstimatesSnapshot to something like totalLength or pathLength to avoid this ambiguity.

1.2 (minor) — MissionPlanningView.vue (diff line ~1569): When pendingSimplePathInsertIndex.value !== null, the anchor for the live-measure line is wps[0]. However, if the user clicks multiple times in simple-path mode with insertIndex = 0, each new waypoint is spliced at index 0. The anchor (wps[0]) then becomes the waypoint the user just placed (not the original first waypoint). This is likely the intended chained-extension behaviour, but if the array somehow becomes empty in an edge case (e.g. undo during simple-path mode), accessing wps[0] would be undefined, leading to a runtime error. A guard like if (!anchor) return null would be safer in currentMeasureAnchor.

1.3 (minor) — insertMissionIntoSegment: The bounds check segmentIndex >= planning.length - 1 prevents inserting after the last waypoint. This means the function cannot append after the final waypoint via segmentIndex = planning.length - 1. This seems intentional (append uses a different code path), but the combination of -1 as "insert at start" and the separate bounds logic may surprise future callers. A brief inline comment clarifying this constraint would help.


2. AGENTS.md Adherence

2.1 (minor) — Comment policy ("why" not "what"): Several added comments describe what the code does rather than why:

  • // Waypoint connecting line (diff ~2599)
  • // Survey polygons (diff ~2572)
  • // Waypoint markers (start = green, end = red, middle = white) (diff ~2611)
  • // Bounding polygon (rotates and scales with the mission)… (diff ~2627)

Per AGENTS.md: "No new comments unless explaining 'why', never 'what'."

2.2 (nit) — JSDoc: The new types and functions are thoroughly documented. No issues with empty @param/@returns entries. Good adherence here.

2.3 (nit) — Local-storage keys: New keys cockpit-planned-vehicle-type, cockpit-mission-library, and cockpit-mission-library-thumbnails all start with cockpit-. ✓

2.4 (nit) — Dependency usage: uuid and date-fns are already in package.json. No new dependencies added. ✓


3. Security

3.1 No obfuscated or intentionally unreadable code found.

3.2 The btoa() calls in generateMissionThumbnail produce base64-encoded SVG from locally constructed strings. The SVG content is derived entirely from numeric coordinate data — no user-supplied text is injected into SVG elements without sanitisation. However, the SVG is rendered via <img :src="..."> which naturally sandboxes script execution. Acceptable.

3.3 No hidden Unicode, zero-width characters, RTL overrides, or homoglyph attacks detected.

3.4 No unexpected network calls. The only external URL constructed is the Google Earth link (earth.google.com) which is rendered as a plain <a> tag with target="_blank" rel="noopener". Acceptable.

3.5 No changes to build scripts, CI workflows, Dockerfiles, or Electron main-process code.

3.6 No new use of eval, Function(), v-html, or equivalent. JSON.parse is used on user-imported .cmp files but the parsed result is validated through isSavedMission / instanceOfCockpitMission type guards before use. Acceptable.

3.7 No new dependencies added.

3.8 No other suspicious patterns detected.


4. Performance

4.1 (nit) — currentMissionSnapshot (computed) uses JSON.parse(JSON.stringify(...)) to deep-clone waypoints and surveys on every reactive access. This is passed as a prop to MissionLibraryModal which is conditionally rendered (v-if), so it only evaluates while the modal is open. Acceptable, but could become expensive if the planning has many waypoints and surveys. Worth noting for future optimisation if performance becomes an issue.

4.2 The placement preview rebuild is properly coalesced via requestAnimationFrame. The cancelAnimationFrame cleanup in cancelFreePlacement prevents orphaned frames. Good.

4.3 Map event listeners (mousemove, mouseup) for placement, scale, and rotation handles are correctly added on mousedown and removed on mouseup. No leak concerns.


5. UI / UX

5.1 (nit) — The mission library modal uses a fixed width: 860px; height: 650px with max-width: 90% but no max-height percentage. On shorter viewports (<700px height), the modal may overflow. Consider adding max-height: 90vh or similar.

5.2 (nit) — The placement toolbar inputs (<input type="number">) lack aria-label attributes. Screen readers would benefit from labels like "Width scale percent" and "Rotation degrees".

5.3 The endpoint markers use !important on background-color (.endpoint-marker). This is needed to override the base marker classes and is scoped, so it's acceptable.


6. Code Quality & Style

6.1 (minor) — MissionLibraryModal.vue line ~393: if (friendly[type]) return friendly[type] as string — this could use optional chaining + nullish coalescing to avoid the double lookup: return friendly[type] ?? .... Per AGENTS.md: "Use optional chaining (?.) when possible in typescript."

6.2 (minor) — MissionPlanningView.vue (diff lines ~2109-2125): There are many module-level const declarations for placement constants (PLACEMENT_SCALE_MIN_PERCENT, PLACEMENT_TOOLBAR_ANCHOR_LEFT, etc.) — 11 in total. While individually fine, grouping them into a single PlacementConfig object would reduce visual noise and improve discoverability. This is a suggestion, not a requirement.

6.3 (minor) — library.ts: isSavedMission type guard checks typeof value.id === 'string' and a few top-level keys, but doesn't validate waypoints array entries (e.g., each waypoint having id, coordinates, altitude). An adversarial .cmp file could pass the guard but crash later when the app tries to read wp.coordinates[0]. Consider at minimum checking value.waypoints.length === 0 || (typeof value.waypoints[0]?.id === 'string').

6.4 (nit) — Several let variables are used at module scope with null initialization for Leaflet event handler state (placementDragStartLatLng, scaleDragInitial, rotationDragInitial, placementRebuildRafHandle). These are correct but follow the existing codebase pattern (ignoreNextClick, etc.), so no action needed.


7. Tests

No tests are added or modified in this PR. The mission library logic (thumbnail generation, isSavedMission type guard, computeMissionLocation, cloneMissionForPlanning) would benefit from unit tests, especially the coordinate transformation functions. Not blocking since the existing codebase has minimal test coverage for mission planning, but worth flagging.


8. Documentation

8.1 (nit) — The Leaflet zoom behaviour change (wheelPxPerZoomLevel, zoomSnap, etc.) is applied to both the Map widget and the planning map. This changes existing user-facing zoom behaviour and could be considered changelog-worthy. No README update is needed since this works identically in Lite and Standalone.

8.2 In-code JSDoc is thorough across all new public functions, types, and interfaces. Good.


9. Nitpicks / Optional

9.1 (nit) — The MissionLibraryModal.vue file is placed in src/components/ rather than src/components/mission-planning/ where all other mission-planning components live. Consider moving it for consistency.

9.2 (nit) — vehicleTypeLabel in MissionLibraryModal.vue duplicates the vehicle-type-to-label mapping that also exists in plannedVehicleTypeItems in MissionPlanningView.vue. Consider extracting a shared constant or utility.

9.3 (nit) — The .hidden CSS class in MissionLibraryModal.vue (display: none) duplicates Tailwind's built-in hidden utility which is already used elsewhere in the template. Since the <input> already has class="hidden", the scoped CSS rule is redundant and could be removed (Tailwind's hidden class applies display: none).

9.4 (nit) — The loadDraftMission function in the base code is async (returns Promise<void>), but it's called from finalizeMissionPlacement without await. The promise rejection would be unhandled if tryFetchHome throws. Consider adding .catch(...) or await.


Generated by Claude. This is advisory; a human reviewer must still approve.

@ArturoManzoli ArturoManzoli force-pushed the add-mission-library branch 2 times, most recently from 98fa8d9 to 69bf1be Compare April 29, 2026 19:12
@ES-Alexander ES-Alexander added the docs-needed Change needs to be documented label May 5, 2026
Tame the default Leaflet wheel/pinch zoom so a single mouse-wheel notch
(or pinch step) advances exactly one zoom level instead of jumping two,
which felt jumpy and easy to overshoot on the planning map and on the
general map widget.

Made-with: Cursor
Add SavedMission (CockpitMission plus library metadata: id, name,
description, vehicle type, timestamps, thumbnail, estimates snapshot)
and MissionEstimatesSnapshot (pre-formatted strings captured at save
time so the library can render summaries without recomputing against
the user's currently planned mission).

Made-with: Cursor
Add helpers used by the mission library:
- generateMissionThumbnail: builds a base64 SVG preview of a mission
  (waypoints path, survey polygons, fallback "No path" label) with the
  first/last waypoints rendered in orange and 25% larger so the
  endpoints stand out at a glance on the library cards.
- computeMissionLocation: returns the centroid of a mission's
  waypoints/surveys, falling back to the saved map center when empty.
- isSavedMission: type guard used when ingesting library entries from
  imported files or persisted storage.

Made-with: Cursor
Add `missionLibraryVisibility` state and matching `isMissionLibraryVisible`
getter so the mission planning view can open and close the library
modal through the same shared interface store used for the rest of the
app's modals (config, video library, etc.).

Made-with: Cursor
Add `plannedVehicleType` (persisted via useBlueOsStorage as
`cockpit-planned-vehicle-type`) so users can pick the vehicle type to
plan for when no vehicle is connected, and `effectiveVehicleType` that
prefers the connected vehicle's reported type and falls back to the
planned one. This unblocks vehicle-type-specific planning features
(mission estimates, vehicle-specific UI cues) while planning offline.

Made-with: Cursor
…tures

Switch the mission estimates composable and MissionEstimates panel to
read from missionStore.effectiveVehicleType so they keep working when
the planner is used offline. Add a "Planning for" selector to the
mission planning sidebar (visible only while no vehicle is connected)
that exposes Surface Boat, Submarine, UAV, and Ground Rover as the
supported categories the user can plan against.

Made-with: Cursor
Add the persisted `savedMissions` collection (under
`cockpit-mission-library`, synced to BlueOS when available) plus the
helpers used by the library UI:
- saveMissionToLibrary: creates a new entry or updates an existing one
  in place by id, baking a thumbnail and the metadata (name,
  description, vehicle type, estimates snapshot, timestamps) at save
  time so the library can render summaries cheaply.
- deleteSavedMission: removes an entry by id.
- getSavedMissionLocation: thin wrapper over the libs helper for
  consumers that already hold the store.

Made-with: Cursor
Add MissionLibraryModal: the full library UI mounted by the mission
planning view. Renders saved missions as a grid of preview cards
(thumbnail, name, date, vehicle-type and stats pills with a frosted-
glass background for readability over the mission preview), supports
saving the current mission via a dedicated dialog (native inputs with
explicit labels, Enter-to-save, name and description fields), and
opens a detail dialog with full estimates, vehicle-type tag, and an
"Open in Google Earth" link. Also exposes an `openSaveOnMount` prop so
callers can land users directly on the save form.

Made-with: Cursor
Replace the separate "Save mission to file" / "Load mission from file"
toolbar buttons with a single bookshelf entry that opens the new
MissionLibraryModal, and drop the now-unused saveMissionToFile /
loadMissionFromFile helpers (and their `format` / `saveAs` imports).
Wire the modal to the current planner state (snapshot of
waypoints/surveys, mission estimates, effective vehicle type) and
handle library loads via a placement-choice dialog so the user is
asked where the mission should land before it touches the current
plan. The "Keep original location" path replays the saved settings
via loadDraftMission; the repositioning path arrives in a follow-up
commit.

Made-with: Cursor
Extend the library load flow with a "Reposition on map" option that
drops the saved mission onto the planning map at 1:1 scale and lets
the user drag, scale (independent X/Y, hold Shift for uniform), and
rotate it via on-map handles plus numeric overlays before committing.
A green check confirms the placement, a red trash cancels, and a
restore button resets scale and rotation to defaults.

Also add the segment-insert third option to the radial menu shown when
hovering between two existing waypoints ("Insert mission from library
here") and the routing in finalizeMissionPlacement /
appendMissionToPlanning / insertMissionIntoSegment so loading a library
mission either:
- splices into the requested segment (when the load came from the
  radial menu), preserving waypoint ids consistently between the
  top-level and survey-internal lists so editing the survey later
  doesn't leave an orphan polygon on the map;
- appends to the current planning when the planner already has work
  in progress (so a second library load doesn't wipe the first); or
- replays the saved settings when the planner is empty.

Made-with: Cursor
Replace the single "Mission library" entry plan with a hover submenu
in the map context menu that exposes "Save mission to library" (only
enabled when there's something to save) and "Add mission from
library". The save entry opens the library modal directly on the save
form via a new `openSaveOnMount` prop / `missionLibraryOpenSaveOnMount`
flag, while the add entry reuses the existing toolbar entry point.

Both segment-insert intent and save-on-mount intent are now cleared
on every fresh open of the library so a plain toolbar open never
inherits stale flags from a previous interaction.

Made-with: Cursor
Render the first and last waypoints of the current mission with the
new `.endpoint-marker` style (orange fill via `#ff9800`, scaled up to
1.25x via transform around the center) so it's visually obvious which
waypoints are the mission's start and end. The `endpoint-marker` class
is added through `createWaypointMarkerHtml`'s new `isEndpoint` flag,
which is computed via the small `isEndpointWaypoint(id)` helper and
threaded through every place that builds or refreshes a waypoint
icon (updateWaypointMarkers, both marker-creation paths,
applySelectedWaypointMarkerVisual, and the click-clear handler) so the
highlight stays consistent across selection, drag, zoom, and survey
entry/exit re-renders.

Made-with: Cursor
Make "Add waypoint here" from the map context menu act relative to the
closer endpoint when the cursor isn't on the existing path: if the
click is closer to the first waypoint, the new waypoint is prepended
instead of appended; otherwise it falls back to the previous
append-to-end behavior. Segment-proximity insertion (splitting the
path between two waypoints) still wins when the cursor is on or near
a segment.

Done by adding an optional `insertIndex` to addWaypoint (splice when
provided, push otherwise) and a small `getEndpointInsertIndexForLatLng`
helper that picks 0 vs. undefined based on which endpoint is nearer.

Made-with: Cursor
…oser endpoint

Extend the existing endpoint-bias from "add waypoint" to the other context-menu add
operations so right-clicking near the start of an existing mission adds the new survey,
simple path, or library mission at the start of the planning instead of always appending.

Made-with: Cursor
@ArturoManzoli ArturoManzoli force-pushed the add-mission-library branch from 69bf1be to 0174b54 Compare May 19, 2026 14:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs-needed Change needs to be documented

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants