From 855d96bbfe0e9062c4ca4d1b6241961e09234848 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sat, 24 Oct 2026 02:11:07 -0700 Subject: [PATCH] fix: center coverage bands on computed grid samples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../feature/coverage/CoverageContours.kt | 19 +++++++++--------- .../feature/coverage/CoverageContoursTest.kt | 20 +++++++++++++++++++ 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/feature/coverage/src/commonMain/kotlin/org/meshtastic/feature/coverage/CoverageContours.kt b/feature/coverage/src/commonMain/kotlin/org/meshtastic/feature/coverage/CoverageContours.kt index e8dffe8b5b2..0a43c2bb5b3 100644 --- a/feature/coverage/src/commonMain/kotlin/org/meshtastic/feature/coverage/CoverageContours.kt +++ b/feature/coverage/src/commonMain/kotlin/org/meshtastic/feature/coverage/CoverageContours.kt @@ -38,21 +38,21 @@ internal fun CoverageGrid.bands(ranges: List>): // Greedily merge horizontal runs of in-band cells into rectangles: far fewer polygons than // one per cell, and the map renders a handful of rings rather than thousands. val taken = BooleanArray(width * height) - for (y in 0 until height - 1) { + for (y in 0 until height) { var x = 0 - while (x < width - 1) { + while (x < width) { 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)) { + while (runEnd + 1 < width && !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 && + runBottom + 1 < height && (x..runEnd).all { inBand(it, runBottom + 1, range) } && (x..runEnd).none { taken[(runBottom + 1) * width + it] } ) { @@ -63,11 +63,11 @@ internal fun CoverageGrid.bands(ranges: List>): } 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()), + lonAt(x - HALF_CELL) to latAt(y - HALF_CELL), + lonAt(runEnd + HALF_CELL) to latAt(y - HALF_CELL), + lonAt(runEnd + HALF_CELL) to latAt(runBottom + HALF_CELL), + lonAt(x - HALF_CELL) to latAt(runBottom + HALF_CELL), + lonAt(x - HALF_CELL) to latAt(y - HALF_CELL), ), ) x = runEnd + 1 @@ -142,6 +142,7 @@ $features """ } +private const val HALF_CELL = 0.5 private const val DEFAULT_BANDS = 6 private const val EMPTY_FEATURE_COLLECTION = """{"type":"FeatureCollection","properties":{"generator":"meshtastic-kp1812"},"features":[]}""" diff --git a/feature/coverage/src/commonTest/kotlin/org/meshtastic/feature/coverage/CoverageContoursTest.kt b/feature/coverage/src/commonTest/kotlin/org/meshtastic/feature/coverage/CoverageContoursTest.kt index b93bbb5e6a8..da2862ac890 100644 --- a/feature/coverage/src/commonTest/kotlin/org/meshtastic/feature/coverage/CoverageContoursTest.kt +++ b/feature/coverage/src/commonTest/kotlin/org/meshtastic/feature/coverage/CoverageContoursTest.kt @@ -17,6 +17,7 @@ package org.meshtastic.feature.coverage import kotlin.test.Test +import kotlin.test.assertEquals import kotlin.test.assertTrue class CoverageContoursTest { @@ -48,4 +49,23 @@ class CoverageContoursTest { assertTrue("\"name\": \"Tom's \\\"Hill\\\" \\\\ relay\"" in geoJson, geoJson.take(300)) } + + @Test + fun mergedBandExtendsHalfACellBeyondEverySample() { + val grid = + CoverageGrid( + site = Site("Test", 10.0, 26.0, 915.0, 30.0, -130.0), + width = 2, + height = 2, + north = 12.0, + south = 8.0, + east = 32.0, + west = 20.0, + dbm = DoubleArray(4) { -90.0 }, + ) + + val ring = grid.bands(listOf(-130.0..-80.0)).single().rings.single() + + assertEquals(listOf(14.0 to 14.0, 38.0 to 14.0, 38.0 to 6.0, 14.0 to 6.0, 14.0 to 14.0), ring) + } }