Repository navigation
[Bug]: Coverage estimate bands are drawn half a grid cell southeast of where they were computed #7752
Description
Steps to reproduce the bug
- Open the map and start a Site Planner coverage estimate for a site near a sharp ridge, with a large max range (for example 150 km).
- Let the estimate finish and look at the band edges and terrain shadows against the basemap.
Actual behavior
The whole overlay is shifted half a grid cell east and half a cell south of the terrain that shaped it. At a 150 km range a cell is a little over 1 km, so shadows sit roughly 0.6 km east and south of the ridge that casts them. On the north and west edges the disc stops short of the radius, on the south and east edges it runs a cell past it, and the last row and column of samples are never drawn.
Expected behavior
Each band cell is centered on the point whose signal it shows.
Additional information
PolarCoverage.toGrid samples the grid at cell corners (nodes):
Lines 137 to 140 in 48cd688
| for (y in 0 until resolution) { | |
| val lat = north - (north - south) * y / (resolution - 1) | |
| for (x in 0 until resolution) { | |
| val lon = west + (east - west) * x / (resolution - 1) |
CoverageGrid.bands then draws the value at node (x, y) as the rectangle from node x to x+1 and row y to y+1, and loops only to width - 1 / height - 1:
Lines 41 to 71 in 48cd688
| for (y in 0 until height - 1) { | |
| var x = 0 | |
| while (x < width - 1) { | |
| if (taken[y * width + x] || !inBand(x, y, range)) { | |
| x++ | |
| continue | |
| } | |
| var runEnd = x | |
| while (runEnd + 1 < width - 1 && !taken[y * width + runEnd + 1] && inBand(runEnd + 1, y, range)) { | |
| runEnd++ | |
| } | |
| // Extend downwards while the whole run stays in band. | |
| var runBottom = y | |
| while ( | |
| runBottom + 1 < height - 1 && | |
| (x..runEnd).all { inBand(it, runBottom + 1, range) } && | |
| (x..runEnd).none { taken[(runBottom + 1) * width + it] } | |
| ) { | |
| runBottom++ | |
| } | |
| for (yy in y..runBottom) { | |
| for (xx in x..runEnd) taken[yy * width + xx] = true | |
| } | |
| rings.add( | |
| listOf( | |
| lonAt(x.toDouble()) to latAt(y.toDouble()), | |
| lonAt(runEnd + 1.0) to latAt(y.toDouble()), | |
| lonAt(runEnd + 1.0) to latAt(runBottom + 1.0), | |
| lonAt(x.toDouble()) to latAt(runBottom + 1.0), | |
| lonAt(x.toDouble()) to latAt(y.toDouble()), | |
| ), |
Suggested fix: draw each run from x - 0.5 to runEnd + 0.5 and from y - 0.5 to runBottom + 0.5 (and loop over all width / height), or sample the grid at cell centers instead of nodes. maxRangeKm in CoverageGrid.kt should use the same positions.
The iOS app fixes the same thing in meshtastic/Meshtastic-Apple#2694.
Activity
mvanhorn commentedon Oct 24, 2026
CoverageGrid.bands treated each sample as the northwest corner of its rectangle instead of its center. Its iteration and merging bounds also excluded the final row and column.
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.
#7757