Skip to content

fix(coverage): Site Planner estimate placement, floor, offline terrain, threads and credit - #7758

Merged
jamesarich merged 11 commits into
mainfrom
fix/coverage-garth-stack
Oct 24, 2026
Merged

jamesarich merged 11 commits into
mainfrom
fix/coverage-garth-stack

Conversation

@jamesarich

@jamesarich jamesarich commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator

The on-device Site Planner estimate drew its bands half a cell off, cut the outer edge at a fixed -130 dBm, failed offline over downloaded terrain, tied up every Dispatchers.Default worker and never credited Mapterhorn. Supersedes #7757, whose commit is cherry-picked here.

🐛 Bug Fixes

Testing Performed

  • CoverageContoursTest: half-cell ring bounds, 1-wide grid rejected.
  • SitePlannerEstimateTest: -139 dBm sensitivity gives a -139..-89 ramp.
  • PolarCoverageTest: radials dispatch on the dispatcher passed in.
  • OfflineTerrainTest: region tile read with no request, network still asked when no region holds the tile, a region's coarse tile refused for ground outside it, parent-tile crop.
  • No screenshot of the form credit yet.

fixes #7752
fixes #7753
fixes #7754
fixes #7755
fixes #7756

Summary by CodeRabbit

  • New Features
    • Site Planner coverage estimates can use terrain data from downloaded offline regions.
    • Terrain credit appears in Site Planner, and attribution is shown when terrain or a coverage layer is visible.
  • Bug Fixes
    • Coverage outlines now extend to the edges of their sampled area, and undersized grids are rejected.
    • Terrain estimates can fall back to available offline terrain when a requested tile fails to load or decode.
    • Coverage color ranges start at receiver sensitivity and extend 50 dB above it.
  • Performance
    • Coverage calculations use available processor capacity for parallel work.

mvanhorn and others added 10 commits October 24, 2026 07:24
Band rectangles now extend half a cell beyond their outermost sample
centers, and iteration and merging include every row and column. A
regression test checks that a uniform 2×2 grid merges into one closed
rectangle with the expected half-cell bounds.

Fixes #7752
`lonAt` and `latAt` divide by `width - 1` and `height - 1`, and the bands now
reach every row and column, so a one-wide grid would emit non-finite
coordinates.
The style used fixed -130..-80 dBm that the form never changed, so LongFast lost
its -139..-130 edge and a -120 dBm receiver was drawn hearing where it cannot.
The dead `minDbm`/`maxDbm` params go with it.

fixes #7756
The sweep queued every radial on `Dispatchers.Default` with no limit, and the
mesh service shares it, so incoming radio data waited behind the estimate.
Radials and tile decodes now run on a view of Default one worker short.

fixes #7754
The estimate read only its own cache and the network, so a site inside a
downloaded region failed offline. Region terrain stores are now read after the
cache and before the network, and a region downloaded below the estimate zoom
stands in with its coarser parent tile. Stand-ins are dropped from the shared
cache when the estimate ends, so a later online run downloads the real tile.

fixes #7753
The overlay is built from Mapterhorn terrain, but the credit only showed while
hillshade or contours drew. The Google map now credits it whenever a coverage
layer is visible, and the Site Planner form carries the credit on every flavor.
MapLibre has no map-side credit for it: `GeoJsonOptions` takes no attribution.

fixes #7755
Every region holds the planet tile, so an unbounded walk gave any site anywhere
a stand-in a pixel wide instead of failing. The tile scope is now canceled
before stand-ins are forgotten, so a decode finishing in between cannot leave
one in the shared cache.
The corrupt-tile retry now sits inside the same fallback, and decode and the
ancestor walk are reshaped for detekt.
@coderabbitai

coderabbitai Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Walkthrough

Coverage estimation now uses bounded computation and can read terrain from downloaded offline regions. Contour geometry and signal-band bounds have changed. The map and Site Planner form display Mapterhorn terrain attribution.

Changes: Site Planner coverage

Layer / File(s) Summary
Bound coverage computation
feature/coverage/src/commonMain/.../CoverageCompute.kt, feature/coverage/src/{androidMain,iosMain,jvmMain}/.../CoverageCompute.*.kt, feature/coverage/src/commonMain/.../PolarCoverage.kt, feature/coverage/src/commonTest/.../PolarCoverageTest.kt
A shared dispatcher limits parallelism based on available processor count. Polar sweeps use the supplied dispatcher. Tests check dispatcher use.
Offline terrain lookup and estimation
feature/coverage/build.gradle.kts, feature/coverage/src/commonMain/.../{RegionTerrain,MapterhornTiles,MapterhornElevation,SitePlannerEstimate,TerrainCache}.kt, androidApp/src/{fdroid,google}/..., desktopApp/src/main/..., feature/coverage/src/commonTest/.../OfflineTerrainTest.kt
Estimates check offline terrain stores before downloading tiles. If a requested tile cannot be decoded, the elevation source searches for a usable offline ancestor. Android and desktop entry points pass available terrain to coverage estimation. Tests cover store lookup and descendant-tile extraction.
Coverage grid and signal bands
feature/coverage/src/commonMain/.../{CoverageContours,CoverageGrid,SitePlannerEstimate}.kt, feature/map/src/commonMain/.../{SitePlannerParams,SitePlannerSheet}.kt, feature/coverage/src/commonTest/.../{CoverageContoursTest,SitePlannerEstimateTest}.kt
Contour generation includes the final grid row and column and extends cell boundaries by half a cell. Grid dimensions below two are rejected. Signal bands start at receiver sensitivity and span 50 dB. The form parameters no longer include minimum and maximum dBm values.
Terrain attribution
androidApp/src/google/.../MapView.kt, feature/map/src/commonMain/.../SitePlannerSheet.kt, core/resources/src/commonMain/composeResources/values/strings.xml, .skills/compose-ui/strings-index.txt
Map attribution is enabled when a coverage layer is visible. The Site Planner form displays the Mapterhorn terrain credit.

Priority: Normal

Severity of issue fixed: Medium

Merge Risk: Moderate · up to 4d14a

Offline estimates can miss downloaded terrain near the antimeridian or use region terrain outside its bounds, and MapLibre coverage can remain visible without Mapterhorn credit. Resolve the region-boundary defects and attribution gap before merging; the effect of a 404 on available coarser terrain also needs clarification.

Pre-merge checks: Passed 6 · Failed 2

Failed Failed checks: 2 warnings

Check name Status Explanation Resolution
Tests Prove The Path, Not The End State Warning The prior failure remains unresolved. OfflineTerrainTest.aRegionTileIsReadWithoutTheNetwork() writes bytes with store.writeTile(...), then asserts that MapterhornTiles.bytes(...) returns those b… Replace the populated-region round-trip with an instrumented filesystem or tile-read wrapper. Assert that the offline store read path was invoked and that the HTTP client received zero requests. Keep the missing-region request-count test as…
Regression Coverage For Changed Behavior Warning Regression coverage is incomplete for changed behavior. - MapterhornTiles.bytes and offlineBytes now combine the coverage cache, offline regions, and network. OfflineTerrainTest uses `store = nu… Add focused common tests for tile-source precedence, fresh lookup, failing and multiple offline regions, edge-only bounds, ancestor fallback, the z6/no-ancestor cases, stand-in and TerrainCache cleanup, and estimate cancellation. Add a te…
Passed Passed checks: 6 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title accurately summarizes the main changes: Site Planner estimate placement, signal floor, offline terrain, dispatcher usage, and attribution credit. It is specific and concise enough for projec…
Linked Issues check Passed The PR meets the coding requirements for all active direct issues. [#7752] CoverageGrid.bands includes the final row and column and places boundaries half a cell around each sample; grids below 2×2 …
Out of Scope Changes check Passed The changes stay within the linked issue scope. Dispatcher abstractions, offline-region wiring, cache cleanup, string resources, dependency updates, and tests directly support the concurrency, offline…
Sibling Call Sites And Presence Semantics Passed The diff does not introduce a nullable physical measurement or a new zero-valued default. It removes the unused SitePlannerParams minDbm/maxDbm fields and adds optional offline terrain represented by …
Moved Code Diffed Against Its Original Passed PASS. The authoritative diff shows no type or function moved to another file. New files add coverageComputeDispatcher, platform processorCount(), and RegionTerrain; they do not replace deleted d…
Full details: Title check

Explanation

The title accurately summarizes the main changes: Site Planner estimate placement, signal floor, offline terrain, dispatcher usage, and attribution credit. It is specific and concise enough for project history.

Full details: Linked Issues check

Explanation

The PR meets the coding requirements for all active direct issues. [#7752] CoverageGrid.bands includes the final row and column and places boundaries half a cell around each sample; grids below 2×2 are rejected. [#7753] MapterhornTiles checks the coverage cache, region terrain, and then the network. Coarser region tiles are cropped, limited by region bounds, and removed after estimation. [#7754] Radials and tile decoding use a Dispatchers.Default view with one worker reserved. [#7755] Google coverage attribution and the Site Planner form include Mapterhorn credit. [#7756] Coverage bands start at receiver sensitivity and span 50 dB. The added tests cover these behaviors.

Full details: Out of Scope Changes check

Explanation

The changes stay within the linked issue scope. Dispatcher abstractions, offline-region wiring, cache cleanup, string resources, dependency updates, and tests directly support the concurrency, offline terrain, attribution, and coverage objectives. No unrelated product behavior is established by the change summary.

Full details: Sibling Call Sites And Presence Semantics

Explanation

The diff does not introduce a nullable physical measurement or a new zero-valued default. It removes the unused SitePlannerParams minDbm/maxDbm fields and adds optional offline terrain represented by an empty list. All three rememberCoverageEstimate call sites pass the updated terrain list, and all RegionTerrain constructions are present in the F-Droid, Google, and desktop paths. No unfixed sibling call site or in-scope 0 default is shown.

Full details: Moved Code Diffed Against Its Original

Explanation

PASS. The authoritative diff shows no type or function moved to another file. New files add coverageComputeDispatcher, platform processorCount(), and RegionTerrain; they do not replace deleted declarations. The MapterhornElevation.decode split preserves the original fetch/decode body, while the added fallback and limited dispatcher are explicit changes documented in code and the PR. ElevationSource, AutoCloseable, and both overrides remain. Existing callers use the new signatures correctly, and no caller still references removed SitePlannerParams.minDbm or maxDbm fields.

Full details: Tests Prove The Path, Not The End State

Explanation

The prior failure remains unresolved. OfflineTerrainTest.aRegionTileIsReadWithoutTheNetwork() writes bytes with store.writeTile(...), then asserts that MapterhornTiles.bytes(...) returns those bytes and that requests equal zero. This is a fake backing-store round trip, not an independent proof that the offline fallback read path ran. aRegionsCoarseTileIsOnlyUsedForGroundTheRegionCovers() uses the same seeded-store pattern through offlineBytes(...). The missing-region request test is only a fallback guard because the base implementation also issued that request. The geometry, grid validation, dispatcher, descendant, and sensitivity tests assert changed behavior that would fail when their covered production changes are reverted.

Resolution

Replace the populated-region round-trip with an instrumented filesystem or tile-read wrapper. Assert that the offline store read path was invoked and that the HTTP client received zero requests. Keep the missing-region request-count test as a fallback regression guard, and make the populated-region assertion fail when the offline fallback is reverted.

Full details: Regression Coverage For Changed Behavior

Explanation

Regression coverage is incomplete for changed behavior. - MapterhornTiles.bytes and offlineBytes now combine the coverage cache, offline regions, and network. OfflineTerrainTest uses store = null and one successful region, so it does not prove cache precedence, fresh = true bypassing both stores, failed regional reads being skipped, multiple-region selection, or the positive-area boundary rule. Missing test: use separate stores with conflicting bytes, a failing first region and valid second region, fresh reads, and edge-only bounds. - MapterhornElevation.decode/fromCoarserRegionTile, forgetStandIns, TerrainCache.forget, and the estimateCoverageGeoJson cleanup path have no tests. The existing tests only cover raw lookup and ElevationTile.descendant. Missing test: make the requested and fresh tile decodes fail, provide a valid ancestor, assert fallback down to MIN_ZOOM and failure with no ancestor, then verify stand-ins are removed after success and cancellation. Verify cancellation still runs cleanup and that TerrainCache.forget removes resolved and in-flight entries. - coverageComputeDispatcher is the production default, but PolarCoverageTest only supplies a custom dispatcher. The new Android, JVM, and iOS processorCount() actuals are also untested. Missing test: add a controllable processor-count seam, assert the default limit is maxOf(1, count - 1), and cover each platform actual. - CoverageGrid now rejects both dimensions below two, but CoverageContoursTest checks only width = 1. Missing test: assert a 2 x 1 grid is rejected. - Google attribution and terrain wiring have no UI or extracted-state tests. MapView changed attribution when a visible coverage layer exists, SitePlannerSheet added the form credit, and Google/F-Droid/Desktop now pass offline terrain stores. Missing tests: assert attribution appears and disappears with coverage visibility, assert the credit renders, assert Google filters hasTerrain regions and passes their stores and bounds, and assert F-Droid and Desktop pass their region terrain.

Resolution

Add focused common tests for tile-source precedence, fresh lookup, failing and multiple offline regions, edge-only bounds, ancestor fallback, the z6/no-ancestor cases, stand-in and TerrainCache cleanup, and estimate cancellation. Add a test seam for processorCount() with default-dispatcher assertions plus Android/JVM/iOS actual coverage. Add the height-one grid test. Extract or test the Google attribution and terrain-selection state, add a Compose test for the form credit, and add flavor-specific propagation tests for Google, F-Droid, and Desktop.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added bugfix PR tag desktop Desktop target labels Oct 24, 2026
@jamesarich
jamesarich marked this pull request as ready for review October 24, 2026 13:05
@jamesarich

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1


Review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between e21ddba and c524499.

Files selected for processing: 23
  • .skills/compose-ui/strings-index.txt
  • androidApp/src/fdroid/kotlin/org/meshtastic/app/map/component/SitePlannerSlot.kt
  • androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt
  • core/resources/src/commonMain/composeResources/values/strings.xml
  • desktopApp/src/main/kotlin/org/meshtastic/desktop/map/DesktopSitePlannerSlot.kt
  • feature/coverage/build.gradle.kts
  • feature/coverage/src/androidMain/kotlin/org/meshtastic/feature/coverage/CoverageCompute.android.kt
  • feature/coverage/src/commonMain/kotlin/org/meshtastic/feature/coverage/CoverageCompute.kt
  • feature/coverage/src/commonMain/kotlin/org/meshtastic/feature/coverage/CoverageContours.kt
  • feature/coverage/src/commonMain/kotlin/org/meshtastic/feature/coverage/CoverageGrid.kt
  • feature/coverage/src/commonMain/kotlin/org/meshtastic/feature/coverage/MapterhornElevation.kt
  • feature/coverage/src/commonMain/kotlin/org/meshtastic/feature/coverage/MapterhornTiles.kt
  • feature/coverage/src/commonMain/kotlin/org/meshtastic/feature/coverage/PolarCoverage.kt
  • feature/coverage/src/commonMain/kotlin/org/meshtastic/feature/coverage/SitePlannerEstimate.kt
  • feature/coverage/src/commonMain/kotlin/org/meshtastic/feature/coverage/TerrainCache.kt
  • feature/coverage/src/commonTest/kotlin/org/meshtastic/feature/coverage/CoverageContoursTest.kt
  • feature/coverage/src/commonTest/kotlin/org/meshtastic/feature/coverage/OfflineTerrainTest.kt
  • feature/coverage/src/commonTest/kotlin/org/meshtastic/feature/coverage/PolarCoverageTest.kt
  • feature/coverage/src/commonTest/kotlin/org/meshtastic/feature/coverage/SitePlannerEstimateTest.kt
  • feature/coverage/src/iosMain/kotlin/org/meshtastic/feature/coverage/CoverageCompute.ios.kt
  • feature/coverage/src/jvmMain/kotlin/org/meshtastic/feature/coverage/CoverageCompute.jvm.kt
  • feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/SitePlannerParams.kt
  • feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/SitePlannerSheet.kt
Files with no reviewable changes: 1
  • feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/SitePlannerParams.kt

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review.

@coderabbitai

coderabbitai Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3


Review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between e21ddba and c524499.

Files selected for processing: 23
  • .skills/compose-ui/strings-index.txt
  • androidApp/src/fdroid/kotlin/org/meshtastic/app/map/component/SitePlannerSlot.kt
  • androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt
  • core/resources/src/commonMain/composeResources/values/strings.xml
  • desktopApp/src/main/kotlin/org/meshtastic/desktop/map/DesktopSitePlannerSlot.kt
  • feature/coverage/build.gradle.kts
  • feature/coverage/src/androidMain/kotlin/org/meshtastic/feature/coverage/CoverageCompute.android.kt
  • feature/coverage/src/commonMain/kotlin/org/meshtastic/feature/coverage/CoverageCompute.kt
  • feature/coverage/src/commonMain/kotlin/org/meshtastic/feature/coverage/CoverageContours.kt
  • feature/coverage/src/commonMain/kotlin/org/meshtastic/feature/coverage/CoverageGrid.kt
  • feature/coverage/src/commonMain/kotlin/org/meshtastic/feature/coverage/MapterhornElevation.kt
  • feature/coverage/src/commonMain/kotlin/org/meshtastic/feature/coverage/MapterhornTiles.kt
  • feature/coverage/src/commonMain/kotlin/org/meshtastic/feature/coverage/PolarCoverage.kt
  • feature/coverage/src/commonMain/kotlin/org/meshtastic/feature/coverage/SitePlannerEstimate.kt
  • feature/coverage/src/commonMain/kotlin/org/meshtastic/feature/coverage/TerrainCache.kt
  • feature/coverage/src/commonTest/kotlin/org/meshtastic/feature/coverage/CoverageContoursTest.kt
  • feature/coverage/src/commonTest/kotlin/org/meshtastic/feature/coverage/OfflineTerrainTest.kt
  • feature/coverage/src/commonTest/kotlin/org/meshtastic/feature/coverage/PolarCoverageTest.kt
  • feature/coverage/src/commonTest/kotlin/org/meshtastic/feature/coverage/SitePlannerEstimateTest.kt
  • feature/coverage/src/iosMain/kotlin/org/meshtastic/feature/coverage/CoverageCompute.ios.kt
  • feature/coverage/src/jvmMain/kotlin/org/meshtastic/feature/coverage/CoverageCompute.jvm.kt
  • feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/SitePlannerParams.kt
  • feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/SitePlannerSheet.kt
Files with no reviewable changes: 1
  • feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/SitePlannerParams.kt

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 2 remain after this review.

A region keeps every z6-z12 tile it touches, and a z6 tile spans hundreds of
km, so a site far outside the region still got stand-in terrain offline. Each
region now comes with its bounds, and a tile is read from it only for ground
inside them.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2


Review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between c524499 and 4d14ad6.

Files selected for processing: 8
  • androidApp/src/fdroid/kotlin/org/meshtastic/app/map/component/SitePlannerSlot.kt
  • androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt
  • desktopApp/src/main/kotlin/org/meshtastic/desktop/map/DesktopSitePlannerSlot.kt
  • feature/coverage/src/commonMain/kotlin/org/meshtastic/feature/coverage/MapterhornElevation.kt
  • feature/coverage/src/commonMain/kotlin/org/meshtastic/feature/coverage/MapterhornTiles.kt
  • feature/coverage/src/commonMain/kotlin/org/meshtastic/feature/coverage/RegionTerrain.kt
  • feature/coverage/src/commonMain/kotlin/org/meshtastic/feature/coverage/SitePlannerEstimate.kt
  • feature/coverage/src/commonTest/kotlin/org/meshtastic/feature/coverage/OfflineTerrainTest.kt
Files skipped from review as they are similar to previous changes: 6
  • androidApp/src/fdroid/kotlin/org/meshtastic/app/map/component/SitePlannerSlot.kt
  • androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt
  • desktopApp/src/main/kotlin/org/meshtastic/desktop/map/DesktopSitePlannerSlot.kt
  • feature/coverage/src/commonMain/kotlin/org/meshtastic/feature/coverage/MapterhornTiles.kt
  • feature/coverage/src/commonTest/kotlin/org/meshtastic/feature/coverage/OfflineTerrainTest.kt
  • feature/coverage/src/commonMain/kotlin/org/meshtastic/feature/coverage/MapterhornElevation.kt

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 1 remain after this review.

@jamesarich
jamesarich added this pull request to the merge queue Oct 24, 2026
Merged via the queue into main with commit bb78b38 Oct 24, 2026
17 checks passed
@jamesarich
jamesarich deleted the fix/coverage-garth-stack branch October 24, 2026 14:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment