Skip to content

Short-circuit elevation>0 to LETHAL in s57_layer get_cost_from_grid#22

Merged
rolker merged 2 commits into
jazzyfrom
feature/issue-21
Apr 25, 2026
Merged

Short-circuit elevation>0 to LETHAL in s57_layer get_cost_from_grid#22
rolker merged 2 commits into
jazzyfrom
feature/issue-21

Conversation

@rolker

@rolker rolker commented Apr 25, 2026

Copy link
Copy Markdown
Owner

Summary

  • Short-circuit elevation > 0.0 to LETHAL_OBSTACLE in get_cost_from_grid, before the tide-offset depth math runs.
  • Add two unit tests in test_tide_offset.cpp: LandFeatureLethalAcrossAllTides (would fail prior to this fix) and SubmergedCellUsesTideAdjustedDepth (guards existing depth-from-tide behavior).
  • Move get_cost_from_grid from private to protected so the test fixture can subclass. No external API change.

Background and analysis in #21 (field bag 2026-04-24T17.39.30, BizzyBoat). The "land = elevation 1.0" trick in marine_charts was incompatible with non-zero tide_offset_ — every coastline became navigable at any tide above ~1 m above MLLW.

Test plan

  • colcon test --packages-select s57_layer — 25 tests, 0 failures
  • New tests LandFeatureLethalAcrossAllTides and SubmergedCellUsesTideAdjustedDepth ran and passed
  • Spot-check on next BizzyBoat run: shoreline must remain LETHAL across the full tidal cycle in the local + global costmaps
  • Cross-check that planned routes do not skim across land at high tide (was the failure mode before the fix)

Closes #21


Authored-By: `Claude Code Agent`
Model: `Claude Opus 4.7 (1M context)`

Land / lethal S57 features (LNDARE, COALNE, CAUSWY, HULKES, PONTON,
SLCONS, FSHFAC, GATCON, OBSTRN, PILPNT, PYLONS, DAMCON, DYKCON) are
rasterized into the elevation channel with a constant 1.0 in
marine_charts/src/s57_dataset.cpp, with the explicit comment "no min
depth values, so lethal".  That encoding worked while tide_offset_
was always 0 (depth = -1 + 0 = -1 < minimum_depth_ = 0 → LETHAL),
but breaks the moment the chart_datum_frame / sea_surface_frame
parameters are set: as soon as tide_offset_ exceeds ~minimum_depth_
+ 1.0, the depth math (-elevation + tide_offset_) makes every
coastline cell report a positive depth and become FREE / CAUTION.

In Portsmouth field bag 2026-04-24T17.39.30 we observed tide_offset_
ramping smoothly from 1.67 m to 2.0 m during normal in-water
operation — i.e., for the entire run, every land polygon in the S57
chart was navigable in the costmap.  This is what was meant by
"costmap no longer looks correct now that we are accounting for
tides" (#21).

Fix: short-circuit elevation > 0 to LETHAL_OBSTACLE before applying
the tide-offset depth math.  Anything above chart datum is
shoreline / intertidal / built feature and should be lethal
regardless of tide.  The depth math then runs only for genuinely
submerged cells, where it makes sense.

Tests:
- LandFeatureLethalAcrossAllTides — sweeps tide_offset across
  [-1, 0, 0.5, 1, 2, 3, 5, 7] m for an elevation = 1.0 cell and
  asserts LETHAL at every level.  Would fail prior to this fix at
  any tide >= 1.0 m.
- SubmergedCellUsesTideAdjustedDepth — guards the existing
  depth = -elevation + tide_offset_ behavior for negative-elevation
  cells: 5 m-deep at -1 m tide is not lethal, at +2 m tide it is
  FREE_SPACE; a 1 m-deep cell drained to -0.5 m at low tide is
  lethal.

Visibility change: get_cost_from_grid moved from private to
protected so the test fixture can subclass S57Layer.  No external
API change.

Closes #21

---
**Authored-By**: \`Claude Code Agent\`
**Model**: \`Claude Opus 4.7 (1M context)\`
Copilot AI review requested due to automatic review settings April 25, 2026 11:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Fixes an incorrect interaction between tide offset and how S57 land features are rasterized, ensuring land/shoreline remains non-navigable regardless of tide height.

Changes:

  • Short-circuit elevation > 0.0 to LETHAL_OBSTACLE in S57Layer::get_cost_from_grid() before applying tide-adjusted depth math.
  • Add unit tests to validate land stays lethal across tides and submerged cells still use tide-adjusted depth behavior.
  • Change get_cost_from_grid access to enable direct testing via subclassing.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
s57_layer/src/s57_layer.cpp Adds elevation > 0 early return to keep land lethal regardless of tide_offset_.
s57_layer/src/s57_layer.h Changes member visibility to protected to support test subclass access.
s57_layer/test/test_tide_offset.cpp Adds a test subclass + 1x1 grid helper and two unit tests covering land vs submerged behavior with tides.
Comments suppressed due to low confidence (1)

s57_layer/src/s57_layer.h:47

  • Changing the access specifier from private to protected makes all following helpers and internal state (e.g., worldToLatLon, subscriptions/maps, tile cache, thresholds, etc.) part of the subclass API. That’s a broader encapsulation break than what the tests need (calling get_cost_from_grid + setting tide_offset_). Consider keeping most members private and exposing only a minimal protected surface (e.g., keep get_cost_from_grid protected and add a protected setTideOffset()), or otherwise re-introduce private: after the specific items that must be testable.
protected:
  using GetDatasetsClient =
    rclcpp::Client<s57_msgs::srv::GetDatasets>;

  unsigned char get_cost_from_grid(grid_map::GridMap &grid, const grid_map::Index &index);


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

Comment on lines +269 to +279
// Helper: build a 1x1 grid_map with all S57 channels initialized to NaN.
// Caller fills in only the channel(s) they want to test.
static grid_map::GridMap makeEmptyS57Grid()
{
grid_map::GridMap grid({"elevation", "overhead", "restricted",
"unsurveyed", "caution"});
grid.setGeometry(grid_map::Length(1.0, 1.0), 1.0,
grid_map::Position(0.0, 0.0));
for (const auto & name : grid.getLayers()) {
grid[name].setConstant(std::numeric_limits<float>::quiet_NaN());
}

Copilot AI Apr 25, 2026

Copy link

Choose a reason for hiding this comment

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

makeEmptyS57Grid() uses std::numeric_limits<float>::quiet_NaN() but this test file doesn’t include <limits>. Please add the missing standard header to avoid relying on transitive includes (which can break with different compilers / include order).

Copilot uses AI. Check for mistakes.
Two findings from Copilot review of e8e4ec0:

- test_tide_offset.cpp uses std::numeric_limits<float>::quiet_NaN()
  but did not include <limits>; it worked via transitive includes.
  Add the explicit include.

- s57_layer.h previously moved the entire private: block to protected:,
  exposing all internal helpers (worldToLatLon, llToWorld, callbacks)
  and member state (subscription / tile cache / thresholds, etc.) to
  any subclass.  The tests only need get_cost_from_grid and
  tide_offset_.  Tighten the protected surface to just those two; the
  rest goes back to private.  No external API change.

---
**Authored-By**: \`Claude Code Agent\`
**Model**: \`Claude Opus 4.7 (1M context)\`
@rolker
rolker merged commit b91ce29 into jazzy Apr 25, 2026
1 check passed
@rolker
rolker deleted the feature/issue-21 branch April 25, 2026 12:30
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.

S57 land features lose LETHAL marking when tide_offset > minimum_depth

2 participants