diff --git a/s57_layer/src/s57_layer.cpp b/s57_layer/src/s57_layer.cpp index 5a51f89..fe08693 100644 --- a/s57_layer/src/s57_layer.cpp +++ b/s57_layer/src/s57_layer.cpp @@ -139,11 +139,14 @@ void S57Layer::updateBounds(double, double, double, double* min_x, double* min_y { tide_offset_ = new_offset; RCLCPP_INFO_STREAM(logger_, "Tide offset updated: " << tide_offset_ << " m (water above chart datum)"); + // Mark tiles for regeneration but keep the cached costmap as a + // fallback so the controller has a usable costmap during regen. + // generateTile rebuilds the costmap from grids when chart_count=0, + // and the empty-charts early-return clears the pointer for safety. for(auto& t: tiles_) { t.second.complete = false; t.second.chart_count = 0; - t.second.costmap = nullptr; t.second.needs_update = true; } current_ = false; @@ -303,9 +306,10 @@ void S57Layer::generateTile(TileID id) if(current_charts_.empty()) { tiles_[id].complete = true; - // No tile costmap is created. In updateCosts, a nullptr tile - // with complete=true is recognized as uncharted territory and - // handled according to allow_uncharted_. + // Clear any stale costmap from a prior era when this tile had + // chart coverage. updateCosts treats nullptr+complete as uncharted + // and handles it according to allow_uncharted_. + tiles_[id].costmap = nullptr; return; } @@ -336,6 +340,19 @@ void S57Layer::generateTile(TileID id) else all_charts_available = false; } + + // No chart from current_charts_ overlaps this tile. Treat as uncharted — + // same handling as the empty-current_charts_ early return — and clear + // any costmap left over from a prior era when this tile had coverage. + if(grids.empty() && all_charts_available) + { + tiles_[id].complete = true; + tiles_[id].costmap = nullptr; + tiles_[id].chart_count = 0; + tiles_[id].needs_update = true; + return; + } + tiles_[id].complete = all_charts_available; if(grids.size() > tiles_[id].chart_count) { @@ -523,6 +540,17 @@ void S57Layer::getDatasetsCallback(GetDatasetsClient::SharedFuture future) RCLCPP_DEBUG_STREAM(logger_, "Received " << result->datasets.size() << " datasets from service"); } + // Compare new dataset labels against current_charts_ before overwriting. + // bounds_need_update fires every ~5% of window movement, so this callback + // runs frequently while the boat moves; in steady-state survey areas the + // label set rarely changes. If unchanged, skip the per-tile invalidation + // at the end of this function so the cache survives. + std::unordered_set new_labels; + for(const auto& d: result->datasets) new_labels.insert(d.label); + std::unordered_set old_labels; + for(const auto& c: current_charts_) old_labels.insert(c.label); + bool chart_list_unchanged = (new_labels == old_labels); + current_charts_ = result->datasets; chart_bounds_.clear(); @@ -593,9 +621,22 @@ void S57Layer::getDatasetsCallback(GetDatasetsClient::SharedFuture future) grids_.erase(grid); } - // mark all tiles as needing regeneration - for(auto& t: tiles_) - t.second.complete = false; + // Mark tiles as needing regeneration only when the chart set actually + // changed — unchanged callbacks are common while the boat moves and + // would otherwise force a full re-evaluation of every cached tile. + // Also reset chart_count so the rebuild branch in generateTile fires + // unconditionally; otherwise a same-count chart swap (one chart removed, + // one added) would leave stale tile costmaps because grids.size() would + // equal the previous chart_count. + if(!chart_list_unchanged) + { + for(auto& t: tiles_) + { + t.second.complete = false; + t.second.chart_count = 0; + t.second.needs_update = true; + } + } }