Skip to content

[Bug]: Coverage estimate bands are drawn half a grid cell southeast of where they were computed #7752

Closed

Description

@garthvh

Steps to reproduce the bug

  1. 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).
  2. 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):

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:

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

  1. mvanhorn commented on Oct 24, 2026

    @mvanhorn
    Contributor

    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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions