mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-05 12:07:25 +00:00
fix(map): 多格台址覆盖单格台址时通联计入单格统计(方案A单向覆盖)
- scopedToStation 由 key 精确匹配改为网格集合包含(记录 MY 网格集 ⊇ 选中台址网格集): 选中 OL62 时 OL62/63(四格点同理)的通联一并计入;反向不泄漏(选中宽台址不见单格记录) - stationGroups 每组 worked 改经 scopedToStation 推导——地图填绿/VUCC 计数/选择器计数/ 首通标签/点格子弹窗详情五出口单源;分组条目仍只列实际拥有的台址 - GridQsoStationScopeTest 改写精确匹配断言 + 新增覆盖/不泄漏/集合包含 3 例(共 14), 全量 test 通过
This commit is contained in:
1 parent
09539ff13a
commit
f35d78186c
3 files changed
+50
-12
No files matched your search
@@ -124,13 +124,23 @@ fun GridQso.stationGridSetKey(): String? =
|
||||
myStationGrids().takeIf { it.isNotEmpty() }?.sorted()?.joinToString(",")
|
||||
|
||||
/**
|
||||
* Scopes a per-grid QSO list to the operated-grid selector's choice:
|
||||
* [stationId] == null ("All") keeps the list as-is, otherwise only records
|
||||
* whose [stationGridSetKey] matches stay. Records without a MY-side grid
|
||||
* belong to no 台址 and are dropped under any specific scope.
|
||||
* Scopes a per-grid QSO list to the operated-grid selector's choice with
|
||||
* COVERING semantics (2026-10-05): [stationId] is a grid-set key such as
|
||||
* "OL62" or "OL62,OL63", and a record stays when its own [myStationGrids]
|
||||
* COVER that key (record grid set ⊇ key set). So a multi-grid 台址 that
|
||||
* covers a single-grid one (OL62/63 covers OL62) contributes its records to
|
||||
* the single-grid scope's statistics — map fills, dialog lists and first-call
|
||||
* labels all derive from this one rule. The reverse does NOT hold: records
|
||||
* under a narrower 台址 never leak into a wider scope (single OL62 records
|
||||
* stay out of the OL62/63 scope). Records without a MY-side grid belong to
|
||||
* no 台址 and are dropped under any specific scope.
|
||||
*/
|
||||
fun List<GridQso>.scopedToStation(stationId: String?): List<GridQso> =
|
||||
if (stationId == null) this else filter { it.stationGridSetKey() == stationId }
|
||||
if (stationId == null) this
|
||||
else {
|
||||
val scope = stationId.split(',').toSet()
|
||||
filter { it.myStationGrids().containsAll(scope) }
|
||||
}
|
||||
|
||||
/**
|
||||
* For every grid in the store, the callsign of the earliest QSO — scoped to the
|
||||
|
||||
+22
-2
@@ -74,12 +74,32 @@ class GridQsoStationScopeTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `specific scope keeps only the matching station's records`() {
|
||||
fun `specific scope keeps covering station's records too`() {
|
||||
// 方案 A 覆盖语义: 选中单格台址 OL62 时,覆盖它的多格台址 (OL62,PM01)
|
||||
// 的通联也计入;反向不泄漏。
|
||||
val multi = qso("A", myGrids = setOf("OL62", "PM01"))
|
||||
val single = qso("B", myGrid = "OL62")
|
||||
val list = listOf(multi, single)
|
||||
assertEquals(listOf(multi), list.scopedToStation("OL62,PM01"))
|
||||
assertEquals(listOf(single), list.scopedToStation("OL62"))
|
||||
assertEquals(listOf(multi, single), list.scopedToStation("OL62"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `narrower station never leaks into a wider scope`() {
|
||||
val single = qso("B", myGrid = "OL62")
|
||||
val pair = qso("A", myGrids = setOf("OL62", "PM01"))
|
||||
// 宽台址 (OL62,PM01) 选中时,单格 OL62 的记录不出现
|
||||
assertEquals(emptyList<GridQso>(), listOf(single, pair).scopedToStation("OL62,PM01").filter { it === single })
|
||||
assertEquals(listOf(pair), listOf(single, pair).scopedToStation("OL62,PM01"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `covering is per-cell set containment not string equality`() {
|
||||
// 多格台址的 key 是排序拼接,覆盖判定必须按网格集合而非字符串
|
||||
val quad = qso("A", myGrids = setOf("OL62", "OL63", "OL64", "OL65"))
|
||||
assertEquals(listOf(quad), listOf(quad).scopedToStation("OL63"))
|
||||
assertEquals(listOf(quad), listOf(quad).scopedToStation("OL62,OL65"))
|
||||
assertEquals(emptyList<GridQso>(), listOf(quad).scopedToStation("OL62,PM01"))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -314,16 +314,24 @@ private fun MapScreen(
|
||||
// can differ between records of the same physical 台址 when LoTW omits
|
||||
// optional fields). Data synced before multi-grid support falls back to
|
||||
// one group per grid, preserving the old selector exactly.
|
||||
// Each group's `worked` cells are derived through scopedToStation (the
|
||||
// covering rule): a single-grid 台址 also counts the cells worked by any
|
||||
// multi-grid 台址 covering it (OL62 ← OL62/63, 四格点同理) — the reverse
|
||||
// never leaks (2026-10-05 用户定稿, 方案 A).
|
||||
val stationGroups: List<StationGroup> = remember(uiState.workedGridQsos) {
|
||||
val byGridSet = LinkedHashMap<String, Pair<Set<String>, MutableSet<String>>>()
|
||||
for ((grid, qsos) in uiState.workedGridQsos) {
|
||||
val gridSets = LinkedHashMap<String, Set<String>>()
|
||||
for ((_, qsos) in uiState.workedGridQsos) {
|
||||
for (q in qsos) {
|
||||
val key = q.stationGridSetKey() ?: continue
|
||||
byGridSet.getOrPut(key) { q.myStationGrids() to mutableSetOf() }.second.add(grid)
|
||||
gridSets.getOrPut(key) { q.myStationGrids() }
|
||||
}
|
||||
}
|
||||
byGridSet.map { (key, v) -> StationGroup(id = key, grids = v.first, worked = v.second) }
|
||||
.sortedByDescending { it.workedCount }
|
||||
gridSets.map { (key, grids) ->
|
||||
val worked = uiState.workedGridQsos.filterValues { qsos ->
|
||||
qsos.scopedToStation(key).isNotEmpty()
|
||||
}.keys
|
||||
StationGroup(id = key, grids = grids, worked = worked)
|
||||
}.sortedByDescending { it.workedCount }
|
||||
}
|
||||
// Selected 台址 for VUCC counting; defaults to the group with the most
|
||||
// worked grids. Null = "All" (every 台址 combined). Empty when the store
|
||||
|
||||
Reference in new issue
Block a user