fix(map): scope first-call labels to the selected operated grid (台址)

This commit is contained in:
atsunatsu committed 2026-10-05 02:19:06 +08:00
1 parent 7589f3f37d
commit a459cfb046
3 files changed
+49 -7

No files matched your search

@@ -131,3 +131,13 @@ fun GridQso.stationGridSetKey(): String? =
*/ */
fun List<GridQso>.scopedToStation(stationId: String?): List<GridQso> = fun List<GridQso>.scopedToStation(stationId: String?): List<GridQso> =
if (stationId == null) this else filter { it.stationGridSetKey() == stationId } 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<String, List<GridQso>>.firstCallsByGrid(stationId: String?): Map<String, String> =
mapNotNull { (grid, qsos) ->
qsos.scopedToStation(stationId).minByOrNull { it.epochMs }?.let { grid to it.call }
}.toMap()
@@ -94,4 +94,35 @@ class GridQsoStationScopeTest {
val list = listOf(qso("A", myGrid = "OL62")) val list = listOf(qso("A", myGrid = "OL62"))
assertEquals(emptyList<GridQso>(), list.scopedToStation("PM01")) assertEquals(emptyList<GridQso>(), 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<String, String>(), store.firstCallsByGrid("OL62"))
}
} }
@@ -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.AwardCalculator
import com.rtbishop.look4sat.core.domain.model.AwardProgress import com.rtbishop.look4sat.core.domain.model.AwardProgress
import com.rtbishop.look4sat.core.domain.model.AwardType 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.MapSource
import com.rtbishop.look4sat.core.domain.model.myStationGrids import com.rtbishop.look4sat.core.domain.model.myStationGrids
import com.rtbishop.look4sat.core.domain.model.scopedToStation 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 // Selected award filter. Lives in an Activity-scoped ViewModel so it
// survives page switches; defaults to VUCC only once per process (cold start). // survives page switches; defaults to VUCC only once per process (cold start).
var selectedAward by mapFilterViewModel.selectedAward 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<String, String> = 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 (台址) // 台址名单: QSOs grouped by the 台址's GRID SET. A station location (台址)
// can span several grids (MY_GRIDSQUARE + MY_VUCC_GRIDS), and QSOs whose // 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 // 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 if (selectedStationId == null) uiState.workedGrids
else stationGroups.firstOrNull { it.id == selectedStationId }?.worked ?: emptySet() 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<String, String> = remember(uiState.workedGridQsos, selectedStationId) {
uiState.workedGridQsos.firstCallsByGrid(selectedStationId)
}
val isGridMode = uiState.isGridMode val isGridMode = uiState.isGridMode
// True when this composition restored a saved viewport. Only grid mode // True when this composition restored a saved viewport. Only grid mode
// restores the viewport the user left (satellite mode keeps its original // restores the viewport the user left (satellite mode keeps its original