diff --git a/feature/roaming/src/main/java/com/rtbishop/look4sat/feature/roaming/RoamingScreen.kt b/feature/roaming/src/main/java/com/rtbishop/look4sat/feature/roaming/RoamingScreen.kt index b3afbb2b..1380f32f 100644 --- a/feature/roaming/src/main/java/com/rtbishop/look4sat/feature/roaming/RoamingScreen.kt +++ b/feature/roaming/src/main/java/com/rtbishop/look4sat/feature/roaming/RoamingScreen.kt @@ -447,7 +447,14 @@ private val latMargin = mapOf( // Ported lines 311-611: longitude -> (20 deg zone letter, 2 deg digit, 2' letter, 30" digit) private fun encodeLon(inputLon: Double): List { - var longitude = inputLon + // The decompiled range table uses closed bounds on both adjacent cells + // (e.g. -20..0 followed by 0..20), and `when` picks the first match. Exact + // field/square/subsquare boundaries therefore fell into the previous cell: + // lon=0 produced I... instead of J..., lon=108 lost one square/subsquare. + // A tiny positive nudge implements the standard half-open [low, high) + // convention without rewriting the faithful lookup table; keep +180 inside + // the final R cell. + var longitude = if (inputLon < 180.0) inputLon + 1e-10 else 180.0 - 1e-10 var d = 0.0 val str = when { (longitude >= -180.0) && (longitude <= -160.0) -> { longitude += 180.0; "A" } @@ -528,7 +535,9 @@ private fun encodeLon(inputLon: Double): List { // Ported lines 612-914: latitude -> (10 deg zone letter, 1 deg digit, 1' letter, 15" digit) private fun encodeLat(inputLat: Double): List { - var latitude = inputLat + // Same closed-bound issue as encodeLon: nudge into the half-open cell so an + // exact boundary latitude does not fall back into the previous field. + var latitude = if (inputLat < 90.0) inputLat + 1e-10 else 90.0 - 1e-10 var d2 = 0.0 var d3 = 0.0 val str5 = when { diff --git a/feature/roaming/src/test/java/com/rtbishop/look4sat/feature/roaming/RoamingStateTest.kt b/feature/roaming/src/test/java/com/rtbishop/look4sat/feature/roaming/RoamingStateTest.kt index 99444302..46e4439f 100644 --- a/feature/roaming/src/test/java/com/rtbishop/look4sat/feature/roaming/RoamingStateTest.kt +++ b/feature/roaming/src/test/java/com/rtbishop/look4sat/feature/roaming/RoamingStateTest.kt @@ -111,6 +111,36 @@ class RoamingStateTest { assertEquals("RR99xx99", state.loc) } + @Test + fun `exact cell boundaries belong to the upper cell`() { + // The decompiled range table closes both adjacent cells (-20..0 then + // 0..20) and `when` takes the first match, so an exact boundary used to + // fall back into the previous cell: (0,0) encoded as II99xx99. + assertEquals("JJ00aa00", stateOf(0.0, 0.0).loc) + assertEquals("JJ01ma00", stateOf(1.0, 1.0).loc) + assertEquals("OL42aa00", stateOf(22.0, 108.0).loc) + assertEquals("OL42gm00", stateOf(22.5, 108.5).loc) + assertEquals("OL42dg00", stateOf(22.25, 108.25).loc) + } + + @Test + fun `matches the independent Maidenhead converter across the grid`() { + // Two independent implementations of the same standard must agree; + // divergence previously affected every exact-boundary coordinate. + var lat = -90.0 + while (lat <= 90.0) { + var lon = -180.0 + while (lon <= 180.0) { + val ported = stateOf(lat, lon).loc + val reference = + com.rtbishop.look4sat.core.domain.utility.positionToQth(lat, lon, 8) + assertEquals("mismatch at ($lat, $lon)", reference?.lowercase(), ported.lowercase()) + lon += 5.0 + } + lat += 5.0 + } + } + @Test fun `gps state after fix matches showLocation`() { val state = stateOf(22.314066, 108.706575)