diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/GridQso.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/GridQso.kt index cdd83275..4f5bc310 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/GridQso.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/GridQso.kt @@ -131,3 +131,13 @@ fun GridQso.stationGridSetKey(): String? = */ fun List.scopedToStation(stationId: String?): List = if (stationId == null) this else filter { it.stationGridSetKey() == stationId } + +/** + * For every grid in the store, the callsign of the earliest QSO — scoped to the + * operated-grid selector's choice first, so a specific 台址 labels each worked + * cell with ITS first contact; null ("All") keeps the global first call. + */ +fun Map>.firstCallsByGrid(stationId: String?): Map = + mapNotNull { (grid, qsos) -> + qsos.scopedToStation(stationId).minByOrNull { it.epochMs }?.let { grid to it.call } + }.toMap() diff --git a/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/model/GridQsoStationScopeTest.kt b/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/model/GridQsoStationScopeTest.kt index fe372938..433e1ec2 100644 --- a/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/model/GridQsoStationScopeTest.kt +++ b/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/model/GridQsoStationScopeTest.kt @@ -94,4 +94,35 @@ class GridQsoStationScopeTest { val list = listOf(qso("A", myGrid = "OL62")) assertEquals(emptyList(), list.scopedToStation("PM01")) } + + // --- firstCallsByGrid --- + + @Test + fun `first call per grid picks the earliest of the scoped records`() { + val store = mapOf( + "OL62" to listOf( + qso("OTHER", myGrid = "PM01").copy(epochMs = 10), + qso("MINE", myGrid = "OL62").copy(epochMs = 20) + ), + "PM95" to listOf(qso("MINE2", myGrid = "OL62").copy(epochMs = 30)) + ) + assertEquals(mapOf("OL62" to "MINE", "PM95" to "MINE2"), store.firstCallsByGrid("OL62")) + } + + @Test + fun `first call per grid with All uses every record`() { + val store = mapOf( + "OL62" to listOf( + qso("OTHER", myGrid = "PM01").copy(epochMs = 10), + qso("MINE", myGrid = "OL62").copy(epochMs = 20) + ) + ) + assertEquals(mapOf("OL62" to "OTHER"), store.firstCallsByGrid(null)) + } + + @Test + fun `grids without scoped records get no first call`() { + val store = mapOf("OL62" to listOf(qso("A", myGrid = "PM01"))) + assertEquals(emptyMap(), store.firstCallsByGrid("OL62")) + } } diff --git a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapScreen.kt b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapScreen.kt index 8ec681c7..5eac2468 100644 --- a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapScreen.kt +++ b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapScreen.kt @@ -85,6 +85,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.rtbishop.look4sat.core.domain.model.AwardCalculator import com.rtbishop.look4sat.core.domain.model.AwardProgress import com.rtbishop.look4sat.core.domain.model.AwardType +import com.rtbishop.look4sat.core.domain.model.firstCallsByGrid import com.rtbishop.look4sat.core.domain.model.MapSource import com.rtbishop.look4sat.core.domain.model.myStationGrids import com.rtbishop.look4sat.core.domain.model.scopedToStation @@ -306,13 +307,6 @@ private fun MapScreen( // Selected award filter. Lives in an Activity-scoped ViewModel so it // survives page switches; defaults to VUCC only once per process (cold start). var selectedAward by mapFilterViewModel.selectedAward - // First callsign worked in each grid (earliest QSO by time), used by the - // "首通呼号" label mode — derived from the same QSO store as the worked fills. - val firstCallsByGrid: Map = remember(uiState.workedGridQsos) { - uiState.workedGridQsos.mapNotNull { (grid, qsos) -> - qsos.minByOrNull { it.epochMs }?.let { grid to it.call } - }.toMap() - } // 台址名单: QSOs grouped by the 台址's GRID SET. A station location (台址) // can span several grids (MY_GRIDSQUARE + MY_VUCC_GRIDS), and QSOs whose // location covers the same grid set belong to the same 台址; groups with an @@ -353,6 +347,13 @@ private fun MapScreen( if (selectedStationId == null) uiState.workedGrids else stationGroups.firstOrNull { it.id == selectedStationId }?.worked ?: emptySet() } + // First callsign worked in each grid (earliest QSO by time), used by the + // "首通呼号" label mode. Scoped to the selected 台址 like the fills: with a + // specific operated grid chosen, the label must show ITS first contact, not + // another 台址's (user decision 2026-10-05); "All" = global first call. + val firstCallsByGrid: Map = remember(uiState.workedGridQsos, selectedStationId) { + uiState.workedGridQsos.firstCallsByGrid(selectedStationId) + } val isGridMode = uiState.isGridMode // True when this composition restored a saved viewport. Only grid mode // restores the viewport the user left (satellite mode keeps its original