diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/LoTWRepository.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/LoTWRepository.kt index 449cc5c2..5ef60f57 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/LoTWRepository.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/LoTWRepository.kt @@ -94,14 +94,33 @@ class LoTWRepository : ILoTWRepository { // (LoTW writes it lowercase). Match case-insensitively to be safe. if (!body.contains("", ignoreCase = true)) return null val grids = mutableSetOf() + // ADIF fields of one QSO record span multiple lines and are terminated by + // . Satellite QSOs carry SAT (plus ); ground + // QSOs omit it. Grid fields (GRIDSQUARE / VUCC_GRIDS) must only be + // collected for records whose PROP_MODE is SAT, otherwise the map mixes + // in terrestrial contacts. + var propMode: String? = null for (raw in body.lineSequence()) { val line = raw.trim() - val value = when { - line.startsWith(" line.substringAfter('>') - line.startsWith(" line.substringAfter('>') - else -> continue - }.substringBefore("E<").trim().uppercase() - if (value.length >= 4) grids.add(value.take(4)) + when { + line.equals("", ignoreCase = true) -> propMode = null + line.startsWith(" { + propMode = line.substringAfter('>').substringBefore("E<").trim().uppercase() + } + line.startsWith(" { + if (propMode == "SAT") { + // VUCC_GRIDS holds a comma-separated PAIR of grids + // ("EN52en,EN53fa") for contacts spanning two squares — + // split on ',' and take the 4-char field of each, or the + // second grid is silently dropped. + val value = line.substringAfter('>').substringBefore("E<") + value.split(',').forEach { grid -> + val field = grid.trim().uppercase() + if (field.length >= 4) grids.add(field.take(4)) + } + } + } + } } return grids } diff --git a/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepoTest.kt b/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepoTest.kt index 774b4e86..5cce9e71 100644 --- a/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepoTest.kt +++ b/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepoTest.kt @@ -214,7 +214,10 @@ private class FakeSettingsRepo(dataSources: DataSourcesSettings = defaultDataSou ) override val otherSettings: StateFlow = MutableStateFlow( - OtherSettings(false, false, false, false, false, false, false, false) + OtherSettings( + false, false, false, false, false, false, false, + shouldSeeWarning = false, shouldSeeWhatsNew = false + ) ) override val dataSourcesSettings: MutableStateFlow = MutableStateFlow(dataSources) @@ -270,6 +273,24 @@ private class FakeSettingsRepo(dataSources: DataSourcesSettings = defaultDataSou override fun getAmSatCallsign(): String = "" override fun setAmSatCallsign(callsign: String) = Unit + + override val wavelogSettings: StateFlow = + MutableStateFlow(com.rtbishop.look4sat.core.domain.model.WavelogSettings()) + + override fun updateWavelogSettings(settings: com.rtbishop.look4sat.core.domain.model.WavelogSettings) = Unit + + override fun getWorkedGrids(): Set = emptySet() + + private val workedGrids = MutableStateFlow(emptySet()) + + override fun setWorkedGrids(grids: Set) { + workedGrids.value = grids + } + + override val lotwSettings: StateFlow = + MutableStateFlow(com.rtbishop.look4sat.core.domain.model.LoTWSettings()) + + override fun updateLoTWSettings(settings: com.rtbishop.look4sat.core.domain.model.LoTWSettings) = Unit } private fun defaultDataSourcesSettings(): DataSourcesSettings { diff --git a/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/LoTWRepositoryTest.kt b/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/LoTWRepositoryTest.kt new file mode 100644 index 00000000..9f6cba64 --- /dev/null +++ b/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/LoTWRepositoryTest.kt @@ -0,0 +1,68 @@ +package com.rtbishop.look4sat.core.data.repository + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class LoTWRepositoryTest { + + private val repo = LoTWRepository() + + private fun report(vararg records: String): String = + buildString { + append("3.1.4\n") + append("look4sat\n") + append("\n") + records.forEach { append(it).append("\n") } + } + + @Test + fun parseRejectsBodyWithoutEoh() { + assertNull(repo.parseConfirmedGrids("Username/password incorrect")) + } + + @Test + fun parseCollectsOnlySatelliteGrids() { + // Satellite QSO: PROP_MODE SAT, confirmed, opponent grid. + val satQso = "BA7OPF\n" + + "20260820\n" + + "SAT\n" + + "FO-29\n" + + "NL47\n" + + "\n" + // Ground QSO: same grid but no PROP_MODE -> must be excluded. + val groundQso = "BA7OPF\n" + + "20260821\n" + + "NL47\n" + + "\n" + val result = repo.parseConfirmedGrids(report(satQso, groundQso)) + assertEquals(setOf("NL47"), result) + } + + @Test + fun parseSupportsVuccGridPairsAndSixCharGrids() { + val satQso = "SAT\n" + + "SO-50\n" + + "OM60IL\n" + + "EN52en,EN53fa\n" + + "\n" + val result = repo.parseConfirmedGrids(report(satQso)) + assertEquals(setOf("OM60", "EN52", "EN53"), result) + } + + @Test + fun parsePropModeDoesNotLeakAcrossRecords() { + // PROP_MODE SAT record first, then a ground QSO with a grid — the second + // record must not inherit the satellite flag after its own . + val satQso = "SAT\nOL62\n\n" + val groundQso = "PM00\n\n" + val result = repo.parseConfirmedGrids(report(satQso, groundQso)) + assertEquals(setOf("OL62"), result) + } + + @Test + fun parseReturnsEmptySetForReportWithoutGrids() { + val satQso = "SAT\nAO-07\n\n" + assertEquals(emptySet(), repo.parseConfirmedGrids(report(satQso))) + } +}