Skip to content

Port camp2 multi-layer + OSM/WMTS map system into deployed CAMP; retire obsoleted + dead ROS 1 code#60

Open
rolker wants to merge 49 commits into
jazzyfrom
feature/issue-59
Open

Port camp2 multi-layer + OSM/WMTS map system into deployed CAMP; retire obsoleted + dead ROS 1 code#60
rolker wants to merge 49 commits into
jazzyfrom
feature/issue-59

Conversation

@rolker
Copy link
Copy Markdown
Owner

@rolker rolker commented Jun 2, 2026

Closes #59

Plan: Port camp2 multi-layer + OSM/WMTS map system into deployed CAMP

Issue

#59

Context

Deployed CAMP (CCOMAutonomousMissionPlanner, built from src/camp/) is
single-background and chart-centric: one GDAL raster is the QGraphicsScene
(chart-native WKT projection), every overlay is parented to that BackgroundRaster and
positioned via geoToPixel(), and there is no layer model. src/camp2/ (a second
executable
in the same CMake project, CMakeLists.txt:237+) contains a self-contained
map subsystem CAMP lacks — a Map (QAbstractItemModel) + Layer/LayerList tree,
OSM/WMTS tiles with disk cache, and GDAL rasters reprojected into a global
Web-Mercator scene as ordinary layers.

The crux is the scene coordinate system: adopting the camp2 map system means adopting
Web Mercator as CAMP's scene projection, which touches every overlay's positioning
(~25 files call geoToPixel). It is tractable because overlays already store canonical
position as QGeoCoordinate and derive scene pixels on demand — the core change is
swapping geoToPixel(geo, bgr) for web_mercator::geoToMap(geo) and dropping the
parent-to-background rule.

Framing: adopt the framework, preserve the functionality

This is not "replace camp with camp2." camp2's ROS layer set is only geometry,
grids, markers — it has no AIS, collision monitor, platform, nav_source, mission
planning, vector datasets, measuring tool, or orbit. So most of camp is re-homed onto
the new framework, not replaced. Three buckets:

Bucket Members Handling
True replacements (camp2 has a counterpart) background-raster, grids, markers Parity-audit each pair; port any camp-only feature into the camp2/shared version before deleting camp's
camp-only carryover (no camp2 counterpart) AIS, collision_monitor, platform, nav_source, mission items (waypoint/trackline/survey/search/avoid), vector datasets, measuring tool, orbit Re-home onto camp::map::Layer — preserve all logic, swap only the coordinate substrate
camp2-only additions (pure gains) OSM tiles, WMTS, multi-layer tree, drag/drop reorder, ros/geometry New capability

Parity rule (load-bearing): never delete a camp original until its replacement is
confirmed ≥ parity — verified by test or manual check, not merely "it compiles." The
audit is bidirectional: keep camp2's improvements too (e.g. marker CUBE).

Resolved Decisions (from discussion, 2026-06-01)

  • End-state: shared map library. Extract camp2's map substrate into libcamp_map
    (namespaced camp::map::) that both CCOMAutonomousMissionPlanner and the camp2
    executable link. camp2 is NOT retired — it stays a working sandbox. No code
    duplication. Lib boundary for the ROS-dependent layers (single lib vs sibling
    libcamp_map_ros) is a PR2 detail.
  • Parity audit is committed as a living doc (docs/parity/ or .agents/) — a
    reviewable, durable matrix per replacement pair, not just inline analysis.
  • Testing where practicable. Add unit tests for pure logic / data paths (camp has
    gtest in test/); use manual verification for Qt rendering / UI seams that aren't
    unit-testable. Don't force tests onto framework glue.
  • Depth preserved as a first-class layer type. Multiple depth layers may coexist,
    toggled in the tree; getDepthRaster() (single) → getDepth(geo) query over the
    enabled depth layers in tree order, first valid wins. Keeps depth-aware A*,
    survey/trackline generation, and the cursor depth readout. (Raster pair's parity gate.)
  • Settings: one-time reset — no QSettings key migration.
  • Record decisions via a new ADR system in camp (docs/decisions/, ADR-0001
    "adopt ADRs" + architecture ADR). Lands with PR3a.
  • No radar on this boat — radar-via-grids stays general-purpose; ROS 1 RadarSector
    still deleted (PR1). Not a deployment gate.
  • Scale-dependent rendering: preserve behaviorbgr->mapScale()MapView
    viewport scale; distances stay geodesic. Implementation detail.

Approach (stacked PRs on feature/issue-59)

  1. PR1 — dead-code sweep (no behavior change): delete radar/ (ROS 1, disabled),
    geoviz/ (ROS 1, 0 CMake refs), sonar_manager/ (ROS 1, .cpp not built),
    sound_play/ (commented out), scaledview.{h,cpp} (unused); clean CMakeLists.txt.
  2. Phase 0 — parity audit (doc-only, gates later replacements): for each
    true-replacement pair (raster, grids, markers) commit a matrix
    camp feature | camp2 feature | delta | resolution; confirm the carryover inventory.
    Known deltas to capture: raster→depth band (camp-only) + color tables (parity ✓);
    markers→DELETE/DELETEALL actions (camp) vs CUBE + expiry timer (camp2); grids→
    one-class-both-types (camp) vs split classes (camp2), colormap/range parity.
  3. PR2 — extract libcamp_map: move camp2's map substrate (+ shared ROS layer
    framework) into a shared library; repoint the camp2 executable to link it and
    confirm camp2 still builds and runs unchanged (self-verifying). No camp changes yet.
  4. PR3a — camp adopts the lib: web-mercator scene; Map owns the scene;
    AutonomousVehicleProject shrinks toward a mission model contributing a mission
    layer; charts shown via reprojected RasterLayer; add OSM tile layer; add a
    geoToPixelweb_mercator::geoToMap shim in GeoGraphicsItem so not-yet-migrated
    overlays still compile/run. Bootstrap docs/decisions/ + write the architecture ADR.
    Nothing deleted yet.
  5. PR3b — tabbed Layers/Mission UI: split into a tabbed dock (Layers = MapTreeView;
    Mission = existing treeView); background stops being a mission-tree node.
  6. PR3c — depth-layer type: depth-retaining raster layer + order-resolved
    getDepth(geo) query → satisfies the raster pair's parity gate.
  7. PR4 — mission-item migration: port editable overlays (Waypoint, TrackLine,
    SurveyPattern, SurveyArea, AvoidArea, SearchPattern, vector
    Point/LineString/Polygon) off the shim to web_mercator::geoToMap; repoint
    depth-aware survey/trackline + A* at the getDepth(geo) query. Note: A* iterates a
    pixel grid today — it needs a defined planning grid/resolution under the multi-depth
    model (more than a query repoint; may be its own sub-PR).
  8. PR5 — overlay migration + parity-gated replacement + manager retirement: re-home
    camp-only overlays (NavSource, Markers-carryover bits, Platform/ShipTrack,
    CollisionMonitor, AISContact) onto the framework. For the replacement pairs
    (grids, markers): confirm ≥ parity first (port DELETE/DELETEALL into shared markers
    if expiry doesn't cover it; verify grid colormap/range), then switch camp to the shared
    layers and retire camp's grids/grid.* + markers. Delete the four overlay manager
    windows (GridManager, AISManager, MarkersManager, CollisionMonitorManager) +
    menu actions — visibility moves to inline layer-tree checkboxes.
  9. PR6 — parity-confirmed retirement + cleanup: with depth preserved, delete
    backgroundraster.*, backgrounddetails.*, georeferenced.*; remove the
    geoToPixel shim + updateBackground/updateProjectedPoints plumbing; add the
    missing .agents/README.md (architecture section).

Each PR builds and runs on its own; PR1, Phase 0, and PR2 are low-risk and land first.

Testing (where practicable)

Test Where PR
web_mercator geo↔map round-trip + metersPerUnit at latitude gtest (pure fn) PR2
depth-layer getDepth(geo) order resolution (overlap → top enabled wins) gtest (logic) PR3c
marker action coverage incl. DELETE/DELETEALL parity gtest if converter is separable; else manual PR5
grid colormap/value-range parity manual vs deployed bag replay PR5
scene/UI wiring, tile fetch, depth cursor readout manual (Qt rendering, not unit-testable) PR3a/PR3c

Files to Change (representative; full tables in the issue)

File Change
CMakeLists.txt Remove retired dirs (PR1); add libcamp_map + relink camp2 (PR2); link camp (PR3a); drop manager UIs (PR5)
src/camp/{radar,geoviz,sonar_manager,sound_play}/, scaledview.* Delete (PR1)
docs/parity/*.md Committed parity matrices (Phase 0)
src/camp2/{map,map_view,map_tiles,wmts,map_tree_view,background,raster,util}/ (+ shared ros/) Extract into libcamp_map (PR2)
docs/decisions/0001-*.md, docs/decisions/000X-web-mercator-scene-and-layer-model.md New ADR system + architecture ADR (PR3a)
new depth-raster layer + depth query Depth-as-layer, getDepth(geo) resolution (PR3c)
autonomousvehicleproject.{h,cpp} Drop scene/background ownership; mission-model only; getDepthRaster() → depth query (PR3a/PR3c)
mainwindow.{ui,cpp} Tabbed Layers/Mission dock; wire Map; remove manager show() actions (PR3b/PR5)
geographicsitem.{h,cpp}, projectview.{h,cpp} geoToPixel shim → geoToMap; cursor depth via query (PR3a/PR4)
astar.cpp, surveyarea.cpp, trackline.cpp Depth-aware planning/survey → getDepth(geo); A* planning grid (PR4)
overlay classes (waypoint, survey*, vector/*, ais/*, markers/*, platform_manager/*, collision_monitor/*, nav_source, grids/grid) Migrate to geoToMap; re-home onto Layer framework; parity-gated retirement of replaced ones (PR4/PR5)
backgroundraster.*, backgrounddetails.*, georeferenced.*, grids/grid.* Delete once replacements confirmed ≥ parity (PR5/PR6)
.agents/README.md, test/ Create README (PR6); add unit tests per the Testing table

Principles Self-Check

Principle Consideration
Improve incrementally Foundational change staged into small, individually-buildable, parity-gated PRs; PR2 is self-verifying via the unchanged camp2 binary
A change includes its consequences Parity rule prevents silent feature loss; depth/markers/grids deltas captured before deletion; tests added where practicable
Only what's needed Shared lib avoids duplication; reuse over rewrite; one-time settings reset
Capture decisions, not just implementations New camp ADR + committed parity doc record the why durably
Test what breaks Unit tests on pure logic (web_mercator, depth resolution, marker actions); manual verification reserved for Qt/UI glue

ADR Compliance

ADR Triggered How addressed
0002 worktree isolation Yes Work on feature/issue-59 worktree; PRs to jazzy
0008 ROS 2 conventions Yes Removes ROS 1 holdouts; migrated overlays stay rclcpp/tf2
0013 progress.md vocabulary Yes ## Plan Authored / ## Plan Review entries recorded
new camp ADR Yes (PR3a) Bootstrap docs/decisions/ + architecture ADR (Web-Mercator scene, two-model split, depth-as-layer, shared lib)

Consequences

If we change... Also update... Included?
Scene projection → Web Mercator every overlay's geo→scene conversion (via shim, then migrated) Yes (PR3a–PR5)
Extract libcamp_map camp2 executable relinks it; camp links it Yes (PR2/PR3a)
Replace camp grids/markers parity port (DELETEALL, colormap) before deletion Yes (Phase 0 + PR5)
getDepthRaster()getDepth(geo) astar.cpp, surveyarea.cpp, trackline.cpp, projectview.cpp, menu gating Yes (PR3c/PR4)
Remove manager windows mainwindow menu + action wiring Yes (PR5)
Mission layer in shared scene Hypack/mission export reads mission model, not scene Verify in PR4

Open Questions

  • Lib boundary: single libcamp_map vs splitting ROS-dependent layers into libcamp_map_ros — settle in PR2.

Estimated Scope

Multiple PRs (PR1, Phase 0, PR2, PR3a/b/c, PR4, PR5, PR6 — ~9 units), stacked on
feature/issue-59. PR1/Phase 0/PR2 are low-risk; PR3a is the foundational scene swap;
PR3b–PR6 are incremental, parity-gated migration and cleanup.

Claude Code Agent added 5 commits June 1, 2026 20:23
Stage the camp2 map-system port (Web-Mercator scene, multi-layer + OSM/WMTS,
tabbed Layers/Mission UI, manager-window retirement) into 6 self-contained PRs;
PR1 dead-code sweep and PR2 substrate import carry no behavior change.
Depth preserved as a first-class layer type (multiple layers, tree-order
overlap resolution); one-time settings reset; start an ADR system in camp;
no radar on this boat; preserve scale-dependent rendering behavior.
Reframe as adopt-framework-preserve-functionality (three buckets); extract
camp2 map into shared libcamp_map (camp2 not retired); commit parity matrices
(Phase 0) and gate every replacement on verified >= parity; add tests where
practicable; split PR3 (3a/3b/3c) + geoToPixel shim. Resolves review findings.
Claude Code Agent and others added 3 commits June 2, 2026 19:21
Delete the dead ROS 1 and disabled code the camp2 map port obsoletes,
ahead of the substrate import. No behavior change.

- radar/ (ROS 1 RadarSector/OpenGL; superseded by the grids path) plus
  its orphaned menu actions, slots, and showRadar/selectRadarColor
  project signals (the connections that consumed them were already
  commented out, so the menu items were invisible no-ops)
- geoviz/ (ROS 1, 0 CMakeLists refs)
- sonar_manager/ (.cpp not compiled; only a stale .ui was referenced)
- sound_play/ (commented out pending ROS 2 port) plus dead remnants,
  including the inert "Say something" menu item whose slot was already
  commented out
- scaledview.{h,cpp} (unused legacy zoom/pan helper)
- RadarDisplayType/GeovizDisplayType GeoGraphicsItem type() enum entries
- CMakeLists.txt references to all of the above

Part of #59.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rolker rolker changed the title [PLAN] Port camp2 multi-layer + OSM/WMTS map system into deployed CAMP; retire obsoleted + dead ROS 1 code Port camp2 multi-layer + OSM/WMTS map system into deployed CAMP; retire obsoleted + dead ROS 1 code Jun 3, 2026
Claude Code Agent and others added 6 commits June 2, 2026 21:08
Per-pair parity matrices built from actual source (file:line cited),
gating later replacements. Confirms the three buckets and records the
retirement blockers each true-replacement pair must clear before its
camp original is deleted:

- raster: depth band + getDepth(geo) query is camp-only (-> PR3c);
  color tables / mipmap / band compositing are parity (verbatim copy)
- markers: DELETEALL all-namespace fan-out is a camp2 regression;
  DELETE leaks empty objects; CUBE is a camp2 gain to keep
- grids: OccupancyGrid colormap is byte-for-byte parity; GridMap
  speed colormap + fixed range is camp-only (bag-replay verify)

Open UX decisions (GridMap speed coloring, marker fill-alpha) recorded
per pair for resolution in PR5.

Part of #59.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- GridMap: drop camp's situational speed colormap, adopt camp2 grayscale
  auto-range. Reusable selectable-colormap facility (camp::map::ColorMap,
  also a depth-shading consumer) split out to #63, out of #59 scope.
- Markers: fill alpha becomes a config option rather than hardcoding
  camp's half or camp2's full; default chosen at PR5.

Part of #59.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…p_map_ros

Core is verified ROS-free and ros/ depends on it one-directionally, so the
split enforces the layering at compile time and lets pure-logic gtests link
Qt/GDAL only. PR2 will create two targets (libcamp_map_ros PUBLIC-links
libcamp_map).

Part of #59.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Split camp2's map substrate into two shared libraries that the camp2
executable (and, from PR3a, CCOMAutonomousMissionPlanner) link:

- libcamp_map     — pure Qt/GDAL map core (map model, Web-Mercator scene,
                    OSM/WMTS tiles, reprojected rasters, tree view, tools).
                    Verified ROS-free: ldd shows zero ROS libraries.
- libcamp_map_ros — ROS-dependent layers (geometry, grids, markers) + node
                    plumbing; links libcamp_map + rclcpp/tf2/messages.

To break the core<->ros circular dependency, invert Map's node creation:
Map no longer constructs a camp::ros::Node (which lives in the ros layer).
It now stores its ToolsManager and exposes Map::toolsManager(); the app
layer (MainWindow) attaches the ROS node there — same parent, same
discovery via the item tree, same construction timing. This is what makes
a genuinely ROS-free core possible rather than just a file move.

camp2 builds unchanged and links the new libs (self-verifying); the
CCOMAutonomousMissionPlanner build is unaffected. Drops the unused
Qt5::Test from the libraries (kept on the camp2 exe, which uses
QAbstractItemModelTester) and an unused <QApplication> include in map.cpp.

Part of #59.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rolker rolker marked this pull request as ready for review June 3, 2026 02:57
Copilot AI review requested due to automatic review settings June 3, 2026 02:57
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR advances the #59 migration plan by (1) deleting dead/disabled ROS 1-era CAMP UI/code, (2) committing the Phase 0 parity audit docs that gate later replacements, and (3) extracting the camp2 map subsystem into shared libraries with an explicit ROS-free core boundary.

Changes:

  • Decouple camp2::map::Map from ROS by exposing Map::toolsManager() and moving ROS Node construction to the app layer (MainWindow).
  • Introduce shared libraries camp_map (Qt/GDAL, ROS-free) and camp_map_ros (ROS-dependent layers) and relink the camp2 executable against them.
  • Remove unused/disabled ROS 1 remnants from src/camp/ (radar/geoviz/sonar_manager/sound_play/scaledview) and clean up associated UI actions/slots/signals; add docs/parity/ matrices.

Reviewed changes

Copilot reviewed 34 out of 34 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/camp2/map/map.h Adds toolsManager() accessor to support ROS-free map core.
src/camp2/map/map.cpp Stops constructing the ROS node inside Map; stores/returns ToolsManager.
src/camp2/main/main_window.cpp Constructs and wires the ROS Node in the app layer using map_->toolsManager().
CMakeLists.txt Extracts camp_map/camp_map_ros libs and relinks camp2; removes retired CAMP sources/UIS.
docs/parity/* Adds committed parity audit docs for raster/markers/grids.
src/camp/mainwindow.{ui,h,cpp} Removes dead radar/sound_play UI actions and commented-out wiring.
src/camp/* deleted files Deletes dead/unused ROS 1 or unbuilt components (radar/geoviz/sonar_manager/sound_play/scaledview).
.agent/work-plans/issue-59/{plan.md,progress.md} Captures the work plan/progress for #59 and the PR sequencing rationale.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Claude Code Agent and others added 10 commits June 4, 2026 21:25
Bootstrap the PR3a design before scene code: record the decisions that let
deployed camp adopt camp2's Web-Mercator map framework while preserving camp's
functionality.

- Scene projection -> Web Mercator (EPSG:3857), owned by camp::map::Map; charts
  become reprojected raster layers, not the scene foundation.
- Two-model split: Layer tree (Map/MapTreeView) vs Mission tree (slimmed
  AutonomousVehicleProject), tabbed in one dock.
- Depth as a first-class layer type; getDepthRaster() -> getDepth(geo) order
  resolution; parity-gates retiring BackgroundRaster (PR3c).
- One shared library split at the ROS boundary (libcamp_map / libcamp_map_ros);
  camp2 kept as sandbox.
- Migration via a geoToPixel->web_mercator::geoToMap shim: the parent-offset
  subtraction is coordinate-agnostic, so the real PR3a work is reparenting
  overlay items into Map's scene, not editing the 24 call sites. Nothing
  deleted in PR3a.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rLayer

Deployed camp's QGraphicsScene becomes the camp::map::Map Web-Mercator scene
(ADR-0002). Substrate swap only — no overlays migrated yet, nothing deleted.

- CMake: link CCOMAutonomousMissionPlanner against camp_map_ros (transitively
  camp_map + the src/camp2 include path).
- AutonomousVehicleProject owns a camp::map::Map; scene() returns Map::scene().
  openBackground adds a reprojected raster::RasterLayer (exact GDAL warp to
  EPSG:3857) for chart DISPLAY.
- BackgroundRaster is demoted to a non-painting scene anchor at the origin
  (ItemHasNoContents + raised Z): it keeps its georeferencing + depth band as the
  depth oracle (getDepth) and remains the parent every overlay attaches to, so the
  existing overlay/findParentBackgroundRaster machinery is untouched. This keeps
  mission items AND the live ROS overlays (AIS, platform, grid, collision monitor,
  nav_source, markers, measuring tool) working without per-overlay edits.
- geoToPixel shim: GeoGraphicsItem::geoToPixel now returns web_mercator::geoToMap;
  the parent-offset subtraction is coordinate-agnostic and the anchor sits at the
  origin, so it reduces to absolute Web-Mercator. The bg argument is ignored.
- ProjectView mouse<->geo conversions and Waypoint drag readout switch from the
  background raster's pixel space to web_mercator::mapToGeo / geoToMap.

Depth stays in BackgroundRaster until it becomes a first-class layer in PR3c
(parity gate to retire the raster). Chart display via RasterLayer (no depth)
is why both coexist for now.

Build: camp package builds green (both binaries + libs). Runtime/visual
verification (chart + overlay alignment on a display) still pending.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Web Mercator is Y-increasing-north; a QGraphicsView is Y-increasing-down, so the
reprojected chart and overlays rendered upside down. camp::MapView compensates
with a negative-Y view scale; ProjectView was missing it.

- Constructor applies scale(1, -1). Uniform wheel zooms preserve the sign;
  fitInView multiplies the current transform so the flip survives.
- updateBackground now fitInView()s the chart's Web-Mercator extent on load
  (preserving the flip) instead of centering at 1:1, so the chart fits the view.

Overlay labels use ItemIgnoresTransformations and stay upright. Mouse<->geo is
unaffected (mapToScene already inverts the view transform). Visually verified:
chart 13283 renders north-up and fits the view on load.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
camp's markers (local-frame LINE_STRIP with baked Y reflection + pixel_size_
units) and grids (drawImage costmap) render flipped/mis-scaled on the PR3a-i
Web-Mercator flipped-Y scene. Deferred to PR5 (Bucket-A retire-and-replace by
camp2 ros/markers + ros/grids, which are scene-correct) rather than patched.
Noted in progress + parity audit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…w fixes)

Addresses pre-push review findings (Claude + Copilot adversarial passes,
cross-confirmed):

- RasterLayer lifecycle (cross-confirmed High): openBackground created a new
  display RasterLayer every call and nothing removed the old one, so opening a
  second chart stacked both and deleting a background orphaned its layer. Track
  m_currentRasterLayer; replace it on open and tear it down on delete (its dtor
  aborts/joins the async load). Restores the pre-PR3a single-displayed-chart
  behaviour; multi-chart management remains PR3b (layer tree).
- topLevelLayers() null guard: skip creating the chart layer if the layer list
  is unexpectedly absent, instead of handing RasterLayer a null parent.
- fitInView robustness (Copilot): build the chart's Web-Mercator extent from all
  four corners, not just topLeft/bottomRight, so a rotated/sheared georeferenced
  chart isn't under-fit on load.

Build green. Lower-priority review notes (camp_map vs camp_map_ros breadth;
dropped setSceneRect) left as-is with rationale recorded in progress.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…odel)

Two-model split (ADR-0002). The left panel's single mission treeView becomes a
QTabWidget with two tabs:
- Mission: the existing treeView (AutonomousVehicleProject) — edits the plan.
- Layers: a camp::map_tree_view::MapTreeView bound to project->map()
  (camp::map::Map) — backgrounds, OSM/OpenSeaMap/NOAA-WMTS tiles, and chart
  RasterLayers, with inline visibility checkboxes + the opacity delegate. This
  is also where the stacked-chart layers from PR3a-i become user-manageable
  (toggle visibility), addressing the multi-chart review note.

detailsView follows the active tab: mission selection on the Mission tab,
cleared on the Layers tab (layer detail widgets are future work). Built in code
(reparent treeView into the tab widget, insert at its splitter slot) to avoid
reworking the .ui splitter layout / Designer custom-widget promotion.

Deferred (documented): background still appears as a mission-tree node — moving
it out is entangled with project save/load persistence and lands with the PR3c
depth-as-layer / persistence migration.

Build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ire managers

First PR5 step: replace camp's flipped, BackgroundRaster-parented grids/markers
with camp2's scene-correct ros overlays, hosted on camp's existing ROS node
(unified, not a second node).

- camp::ros::Node gains an adopt constructor (tools_manager, node, buffer): uses
  an externally-owned rclcpp node + tf buffer instead of spawning its own thread,
  defers manager creation to the event loop, and skips rclcpp::shutdown() / the
  thread teardown in the destructor (the node belongs to the host). It also skips
  the geometry (PolygonStamped) manager, since camp keeps its own collision-zone
  renderer for those topics (avoids double-drawing). camp2's standalone path is
  unchanged (default spawning ctor + geometry).
- MainWindow attaches a camp::ros::Node to project->map()->toolsManager() on the
  first ROSLink rosConnected; it auto-discovers OccupancyGrid/GridMap/Marker
  topics and creates layers in the Layers tab (rendered correctly, via ColorMap).
- Retired camp's GridManager + MarkersManager: creation, members, the two
  manager-window menu actions/slots. camp's AIS, collision-monitor, platform,
  nav-source overlays are unchanged for now (no camp2 equivalent; re-homed later).

Build green (both binaries). Runtime verification pending (needs a running ROS
with costmap/marker topics).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 5, 2026 09:12
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 52 out of 52 changed files in this pull request and generated 15 comments.

Comment on lines +72 to +76
// Last received message, cached so a colormap change can re-render without
// waiting for the next publish (live costmaps would also pick it up).
grid_map_msgs::msg::GridMap last_msg_;
bool has_last_msg_ = false;

Comment thread src/camp2/ros/grids/grid_map.cpp Outdated
Comment on lines 36 to 40
void GridMap::gridMapCallback(const grid_map_msgs::msg::GridMap &data)
{
last_msg_ = data; // [camp#63] keep the latest for colormap re-render
has_last_msg_ = true;
if(!process_future_.isRunning())
Comment on lines 204 to 208
void RasterLayer::imageReady()
{
auto result = future_watcher_.result();
prepareGeometryChange();

Comment thread src/camp2/ros/grids/grid_map.cpp Outdated
Comment on lines +112 to +116
// [camp#63] Normalised value -> colour via the selectable ramp
// (colorNormalized clamps to [0,1]); default grayscale reproduces the
// prior output.
value = (value - min_value) / (max_value - min_value);
uint8_t ival = std::min(1.0,std::max(0.0, value))*255;
grid_layer_data.grid_image.setPixelColor(QPoint(size.x()-1-iterator.getUnwrappedIndex().x(), iterator.getUnwrappedIndex().y()), QColor(ival, ival, ival, 255));
grid_layer_data.grid_image.setPixelColor(QPoint(size.x()-1-iterator.getUnwrappedIndex().x(), iterator.getUnwrappedIndex().y()), cm.colorNormalized(value));
Comment thread src/camp/projectview.cpp Outdated
Comment on lines 68 to 72
case MouseMode::addWaypoint:
if(bg)
{
m_project->addWaypoint(bg->pixelToGeo(mapToScene(event->pos())));
m_project->addWaypoint(web_mercator::mapToGeo(mapToScene(event->pos())));
}
Comment thread src/camp/projectview.cpp
Comment on lines 217 to 220
QPointF transformedMouse = mapToScene(event->pos());
BackgroundRaster *bg = m_project->getBackgroundRaster();
BackgroundRaster *dr = m_project->getDepthRaster();
if(bg)
{
Comment thread CMakeLists.txt
Comment on lines +291 to +295
install(TARGETS camp_map
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
RUNTIME DESTINATION bin)

Comment thread CMakeLists.txt
Comment on lines +343 to +347
install(TARGETS camp_map_ros
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
RUNTIME DESTINATION bin)

Comment thread src/camp/surveyarea.cpp
Comment on lines 328 to +332
std::vector<QGeoCoordinate> ret;
for(int i = 0; i < guidePath.size(); i++)
{
double depth = depthRaster.getDepth(guidePath[i]);
double depth = project->getDepth(guidePath[i]);
// [#59 PR3c] Skip guide points with no depth coverage rather than
Comment on lines +122 to +130
double min_value = std::numeric_limits<double>::max();
double max_value = std::numeric_limits<double>::lowest();
for(float v : values)
{
if(std::isnan(v) || (has_nodata && v == nodata))
continue;
min_value = std::min(min_value, double(v));
max_value = std::max(max_value, double(v));
}
Claude Code Agent and others added 2 commits June 5, 2026 05:21
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s overlays)

PR5 step 1 retired camp's GridManager/MarkersManager; the underlying classes are
now unreferenced (confirmed: only stale comments mention them). Remove them and
their now-orphaned converter test:
- src/camp/grids/ (grid, grid_manager + .ui)
- src/camp/markers/ (markers, markers_manager, markers_converter + .ui)
- test/test_markers_converter.cpp (tested camp's retired markers converter)
- CMakeLists SOURCES/UIS entries + the test_markers_converter gtest.

Grids/markers are now exclusively camp2's scene-correct ros overlays. Build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 5, 2026 09:34
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 66 out of 66 changed files in this pull request and generated 4 comments.

Comment on lines +219 to +227
void RasterLayer::setColormap(map::ColorMap::Type type)
{
if(type == colormap_.type())
return;
colormap_.setType(type);
writeSettings();
if(!filename_.isEmpty()) // re-warp + re-shade with the new ramp
loadFile(filename_);
}
Comment thread src/camp/astar.h
Comment on lines +83 to +88
float depthAt(int x, int y) const
{
if(x < 0 || y < 0 || x >= gridSize || y >= gridSize)
return unknownDepth;
return depthGrid[y*gridSize + x];
}
Comment thread docs/parity/README.md Outdated
Comment on lines +70 to +74
**Open decisions — RESOLVED 2026-06-02:**
- **GridMap colormap:** drop camp's situational "speed" ramp → adopt camp2
grayscale auto-range. A reusable selectable-colormap facility
(`camp::map::ColorMap`, also a depth-shading consumer) is tracked out of #59
scope in [#63](https://github.com/rolker/camp/issues/63).
Comment on lines +3 to +6
#include <array>
#include <cmath>
#include <vector>

Claude Code Agent added 2 commits June 5, 2026 09:15
Brings in the field-import callback-group concurrency fix (#67) merged to
jazzy. The only conflict is a modify/delete on src/camp/grids/grid.cpp:
#67 patched the legacy Grid class (dedicated callback group + TF wait),
while this branch's PR5 retires the entire legacy grids/markers families
in favour of the camp2 ROS overlays. Resolution: keep the deletion.

The starvation #67 fixed cannot recur in the camp2 path: the overlay's
heavy QImage build + TF lookup run on a QtConcurrent worker off the ROS
executor (isRunning() frame-drop guard), so the grid can never starve
overlays or realtime telemetry regardless of callback group.

# Conflicts:
#	src/camp/grids/grid.cpp
…orm cache)

The camp2 ROS layers resolved earth<-frame with a zero-timeout
lookupTransform(TimePointZero) and dropped the whole frame on any miss,
so a momentary TF stream hiccup blanked grids/markers (white flicker /
dropped maps). The legacy Grid carried a dedicated-callback-group + 0.5s
TF wait for this (#67); that file is retired by PR5, and the camp2 path
already runs heavy work off the executor on a QtConcurrent worker, so a
blocking wait is unnecessary here.

Instead of waiting, cache the last good earth<-frame transform per
frame_id and reuse it while it is younger than a 1s staleness budget.
The earth<-map/odom transform is quasi-static, so holding the last
placement across a brief gap is imperceptible and far better than
blanking; cold start (frame never resolved) or an over-budget gap still
throws so callers drop as before. Bounded so a sustained outage or a
genuinely moving frame ages out rather than showing stale data.

The decision is extracted as a pure function
(lookupEarthTransformCached) so the fresh / reuse / expire / cold-start
branches are unit-tested without a node or the system clock;
Layer::lookupEarthTransform is the thin mutex-guarded wrapper (the
lookup runs on QtConcurrent workers for grids and ROS callback threads
for markers). All four call sites (occupancy_grid, grid_map, markers,
polygon) inherit the behaviour via transformToWebMercator.

- new: src/camp2/ros/transform_cache.{h,cpp}
- test: test/test_transform_cache.cpp (7 cases, all passing)
- docs: markers parity row updated (transform-cache, not message-buffer)
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 71 out of 71 changed files in this pull request and generated 6 comments.

Comment on lines +156 to +164
void GridMap::setColormap(map::ColorMap::Type type)
{
if(type == colormap_.type())
return;
colormap_.setType(type);
writeSettings();
if(has_last_msg_) // re-render the cached grid with the new ramp
gridMapCallback(last_msg_);
}
Comment on lines +122 to +130
double min_value = std::numeric_limits<double>::max();
double max_value = std::numeric_limits<double>::lowest();
for(float v : values)
{
if(std::isnan(v) || (has_nodata && v == nodata))
continue;
min_value = std::min(min_value, double(v));
max_value = std::max(max_value, double(v));
}
Comment on lines 77 to 79
for(const auto & layer: grid_map.getLayers())
{
GridMapLayerData grid_layer_data;
Comment thread src/camp2/ros/grids/grid_map.cpp Outdated
Comment on lines 36 to 42
void GridMap::gridMapCallback(const grid_map_msgs::msg::GridMap &data)
{
last_msg_ = data; // [camp#63] keep the latest for colormap re-render
has_last_msg_ = true;
if(!process_future_.isRunning())
{
process_future_ = QtConcurrent::run(this, &GridMap::processGridMap, data);
Comment on lines +219 to +227
void RasterLayer::setColormap(map::ColorMap::Type type)
{
if(type == colormap_.type())
return;
colormap_.setType(type);
writeSettings();
if(!filename_.isEmpty()) // re-warp + re-shade with the new ramp
loadFile(filename_);
}
Comment on lines +256 to +261
if(type != colormap_.type())
{
colormap_.setType(type);
if(!filename_.isEmpty())
loadFile(filename_);
}
Claude Code Agent added 5 commits June 5, 2026 09:54
The camp#63 colormap feature left GridMap's shared state (colormap_,
last_msg_, has_last_msg_, process_future_) accessed from three threads —
the ROS subscription callback, the UI thread (setColormap / contextMenu /
readSettings), and the QtConcurrent render worker — with no
synchronization. That is a data race / UB (sporadic crashes). Separately,
gridMapCallback() only launched a render when process_future_ was idle and
nothing re-checked when the worker finished, so a colormap change made
mid-render was silently dropped (the UI kept the old ramp until the next
publish; for a static grid, forever).

Fix:
- Add a QMutex guarding all shared state. setColormap/readSettings take it
  for their colormap_ writes; gridMapCallback takes it for last_msg_.
- The worker is now fully isolated: processGridMap takes the message and
  colormap by value, snapshotted under the lock at launch, so it never
  touches guarded state. The body moves to renderToData() returning bool;
  processGridMap emits on success then calls onProcessFinished().
- Replace the QFuture::isRunning() check with an explicit
  rendering_/render_pending_ coalescing model: a request arriving
  mid-render sets render_pending_, and onProcessFinished() (worker thread,
  under the lock) relaunches exactly once with the latest message+colormap.
  No lost renders, no unbounded queueing. A QFutureWatcher was unusable
  here — it is GUI-thread-affined but gridMapCallback runs on the ROS
  executor thread.
- Add a destructor that sets shutdown_ (stops the worker relaunching) and
  joins the in-flight future, closing a latent teardown use-after-free.

No unit test: GridMap is GUI/ROS-coupled with no seam. Verified by build +
the existing 35-test suite; the coalescing logic is small and commented.
…nt raster)

Three robustness fixes in the camp2 RasterLayer (camp#63 depth shading):

- Use-after-free: setColormap() re-invokes loadFile(), which called
  setFuture() on a new QtConcurrent job without stopping the previous one.
  The watcher only tracks the latest future, so a replaced job became
  untracked — it kept running, raced the new job on shared state, and
  could outlive the layer (the dtor only joins the watched future).
  loadFile() now aborts and joins any in-flight load, then re-arms
  abort_flag_, before launching the new one.

- Failed-load guard: imageReady() applied result.scale/pos from a
  default-constructed (zero-filled) LoadResult when a load failed
  (null/unreprojectable dataset), collapsing the item to zero scale at
  the origin and clearing the status. It now bails on an empty mipmap set
  and reports "(load failed)".

- Constant raster vanishing: a scalar raster whose valid samples are all
  equal gave min_value == max_value, hitting ColorMap::color()'s
  degenerate max<=min guard (transparent for every pixel) — the raster
  disappeared. Widen the range when min==max (as grid_map already does)
  so the value maps to the top of the ramp; an all-nodata raster keeps
  min/max crossed and correctly stays transparent.
…ct guard

- astar extendedPathAverageDepth(): the obstacle checks passed a double
  coordinate to Context::depthAt(int,int), which truncated toward zero
  instead of flooring. At the grid boundary a point at e.g. y=-0.3 read
  row 0 (a real cell) instead of being treated as out-of-grid (obstacle),
  so an off-grid point could read as free water. Make every conversion
  explicit: floor() for the cell containing a fractional coordinate
  (matching the existing floor_x/ceil_x straddle), round() for the
  nearest-cell cost sample. Removes the implicit narrowing and the
  major/minor-axis asymmetry between the two branches.

- trackline planPath(): guard autonomousVehicleProject() before
  dereferencing it (avp->hasDepth()). It can return null for an item not
  yet attached to a project (construction/load), which would crash.
Low-risk fixes from the PR #60 triage:

- color_map.cpp: include <algorithm> explicitly (std::min/max in
  colorNormalized were only transitively available).
- grid_map.h: include <QImage>/<QPointF>/<string>/<utility>/<vector> so the
  GridMapLayerData/GridMapData structs don't depend on transitive includes.
- surveyarea generateNextLine(): capture guidePath.size() once as int and
  iterate/clamp against it, removing the signed/unsigned loop comparison.
- parity README: ColorMap is no longer "tracked out of #59 scope" — it is
  implemented in this PR and consumed by grid_map and the raster layer;
  update the decision note to match.
Copilot AI review requested due to automatic review settings June 5, 2026 14:26
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 71 out of 71 changed files in this pull request and generated 4 comments.

Comment thread src/camp/surveyarea.cpp
Comment on lines +252 to 255
AutonomousVehicleProject *project = autonomousVehicleProject();

if(wps.size() > 2 && project->hasDepth())
{
Comment thread test/test_color_map.cpp
Comment on lines +65 to +76
TEST(ColorMap, NameRoundTrip)
{
for(auto type : ColorMap::allTypes())
{
bool ok = false;
EXPECT_EQ(ColorMap::typeFromName(ColorMap::name(type), &ok), type);
EXPECT_TRUE(ok);
}
bool ok = true;
EXPECT_EQ(ColorMap::typeFromName("nope", &ok), ColorMap::Grayscale);
EXPECT_FALSE(ok);
}
Comment thread src/camp/trackline.cpp
Comment on lines +291 to +301
// Pre-sample depth at each cell centre (geo query, scene-independent).
astar::Context c;
c.start.x = start.x();
c.start.y = start.y();
c.finish.x = finish.x();
c.finish.y = finish.y();
c.map = depthRaster;
c.gridSize = N;
c.depthGrid.resize(static_cast<size_t>(N)*N);
for(int gy = 0; gy < N; gy++)
for(int gx = 0; gx < N; gx++)
{
const QPointF centre(originX + (gx+0.5)*cellSize, originY + (gy+0.5)*cellSize);
const float d = avp->getDepth(web_mercator::mapToGeo(centre));
c.depthGrid[static_cast<size_t>(gy)*N + gx] = std::isnan(d) ? astar::Context::unknownDepth : d;
}
Comment thread src/camp/surveyarea.h
typedef boost::geometry::model::multi_linestring<BLineString> BMultiLineString;

std::vector<QGeoCoordinate> generateNextLine(std::vector<QGeoCoordinate> const &guidePath, BackgroundRaster const &depthRaster, double tanHalfSwath, int side, BPolygon const &area_poly, double stepSize, BMultiLineString const & previousLines);
std::vector<QGeoCoordinate> generateNextLine(std::vector<QGeoCoordinate> const &guidePath, AutonomousVehicleProject *project, double tanHalfSwath, int side, BPolygon const &area_poly, double stepSize, BMultiLineString const & previousLines);
Claude Code Agent added 2 commits June 5, 2026 10:49
…TS-only ops)

ProjectView gated waypoint / trackline / survey-pattern / survey-area /
avoid-area / search-pattern creation, the mouse-move cursor readout +
pending-item updates, and the right-click boat-command menu (Hover/Goto/
Idle/Look Here) on a BackgroundRaster being loaded. That guard dates to
when the raster defined the coordinate system; since PR3a the scene is
Web Mercator and every conversion uses web_mercator::mapToGeo(), which is
chart-independent. The guard now needlessly blocked all mission planning
and boat commands over OSM/WMTS-only backgrounds.

Remove those guards. Depth readout still queries by geo and is simply
omitted (existing isnan check) when no source covers the cursor.

Kept the two genuinely chart-dependent uses: saving/restoring the view
center and fitting the view to the loaded chart's extent
(bg->boundingRect()/pixelToGeo()). bg is also still read in
mousePressEvent for the middle-button MeasuringTool, which is parented to
it — that coupling belongs with the eventual BackgroundRaster retirement.

Resolves PR #60 triage findings #9-#15. GUI input path, not covered by the
unit suite (still 35/35) — needs a runtime check (create items / read
cursor over an OSM-only background) before merge.
Copilot AI review requested due to automatic review settings June 5, 2026 14:49
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 71 out of 71 changed files in this pull request and generated 6 comments.

Comment on lines 4 to 6
#include "../map/layer.h"
#include "../map/color_map.h"
#include <QFutureWatcher>
Comment on lines +248 to +256
void RasterLayer::setColormap(map::ColorMap::Type type)
{
if(type == colormap_.type())
return;
colormap_.setType(type);
writeSettings();
if(!filename_.isEmpty()) // re-warp + re-shade with the new ramp
loadFile(filename_);
}
Comment on lines 99 to +107
if(!grid_map::GridMapRosConverter::fromMessage(data, grid_map))
{
RCLCPP_WARN_STREAM_THROTTLE(node->get_logger(), clock, 2, "Unable to convert GridMap message");
return;
return false;
}
if(grid_map.getLayers().empty())
{
RCLCPP_WARN_STREAM_THROTTLE(node->get_logger(), clock, 2.0, "Got GridMap message with no layers");
return;
return false;
Comment on lines 1 to 12
#include "grid_map.h"
#include <grid_map_ros/grid_map_ros.hpp>
#include "../../map_view/web_mercator.h"
#include <geometry_msgs/msg/pose_stamped.hpp>
#include "../node.h"
#include "marine_autonomy/gz4d_geo.h"
#include <tf2/utils.h>
#include "grid_layer.h"
#include <QMenu>
#include <QAction>
#include <QSettings>

Comment thread src/camp2/ros/node.cpp
Comment on lines 57 to +67
Node::~Node()
{
emit shuttingDownRos();
rclcpp::shutdown();
node_thread_.quit();
node_thread_.wait();
if(owns_thread_)
{
// Only the node we spawned do we shut down; an adopted node belongs to its
// owner and may still be in use elsewhere.
rclcpp::shutdown();
node_thread_.quit();
node_thread_.wait();
}
Comment thread src/camp/surveyarea.cpp
Comment on lines +252 to 255
AutonomousVehicleProject *project = autonomousVehicleProject();

if(wps.size() > 2 && project->hasDepth())
{
Claude Code Agent added 2 commits June 5, 2026 11:18
First increment of the BackgroundRaster retirement. The deployed planner's
depth oracle rode on the BackgroundRaster (a QGraphicsItem/MissionItem):
AutonomousVehicleProject::getDepth queried m_currentDepthRaster, a
BackgroundRaster*. That welded depth data to the chart's graphics identity
and was one of the three structural blockers to deleting BackgroundRaster
(the others: overlay parent-anchoring, and the mapScale/scaledPixelSize
helpers).

Extract depth into a standalone, non-graphics DepthRaster (inherits
Georeferenced; loads only the Float32 depth band). AutonomousVehicleProject
now owns a DepthRaster, created from the chart file in openBackground() and
torn down alongside the chart's RasterLayer — mirroring the existing
m_currentRasterLayer ownership. getDepth/hasDepth query it; the public API
is unchanged.

The load mirrors BackgroundRaster's exactly (same first-Float32-band
detection, same GF_Read, same Georeferenced geo->pixel), so depth values
are unchanged. getDepth degrades safely (NaN) on a missing/invalid chart so
the planner reads "unknown" (unsafe) rather than a garbage depth.

- new: src/camp/depth_raster.{h,cpp}
- test: test/test_depth_raster.cpp (safe-degradation: invalid file -> NaN,
  no crash). Real-chart depth correctness is verified in sim.
- AVP: m_currentDepthRaster (BackgroundRaster*) -> m_depthRaster
  (DepthRaster*, owned); removed the now-unused getDepthRaster().

BackgroundRaster still loads its depth band (now redundant) and the full
chart image; both are removed in a later increment once this is sim-verified.

SAFETY: depth feeds shoal avoidance. Verify in sim (cursor depth readout +
planner avoiding shallow water over a real depth chart) before field use.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Port camp2 multi-layer + OSM/WMTS map system into deployed CAMP; retire obsoleted + dead ROS 1 code

2 participants