mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
fix: transponder filters - mutual exclusion and beacon-free fallback
Two correctness fixes for the FM/Linear/SSTV filters: 1. Satellite type picker: virtual transponder types (AMSAT Live FM, AMSAT Live Linear, Live SSTV) are now mutually exclusive with the regular TLE-source types - ticking a virtual type clears Amateur/ CubeSat/etc and vice versa, so selecting 'AMSAT Live FM' no longer unions in hundreds of non-transponder satellites. Virtual types still combine with each other. Also: an empty virtual-type resolve now yields an empty list instead of falling back to 'show all'. 2. Mutual-match fallback: when the AMSAT lists are empty (first run / sync failure) the DB mode filter used downlinkMode alone, so any downlink-only beacon sharing a mode label was misclassified (ISS/IO-86 CW beacons into Linear, FO-29's FM Digitalker into FM). New getIdsWithModesAndUplink() requires uplinkLow IS NOT NULL, matching real transponders only. SSTV filter keeps the old query (SSTV sats are often downlink-only). Shared virtual-type names moved to Sources.virtualTypeNames.
This commit is contained in:
1 parent
b295500d14
commit
cf1e2b99b4
12 files changed
+66
-7
No files matched your search
@@ -48,6 +48,12 @@ interface Look4SatDao {
|
|||||||
@Query("SELECT catnum FROM radios WHERE downlinkMode IN (:modes) AND isAlive = 1")
|
@Query("SELECT catnum FROM radios WHERE downlinkMode IN (:modes) AND isAlive = 1")
|
||||||
suspend fun getIdsWithModes(modes: List<String>): List<Int>
|
suspend fun getIdsWithModes(modes: List<String>): List<Int>
|
||||||
|
|
||||||
|
/** Like [getIdsWithModes] but only matches transponder records with an uplink,
|
||||||
|
* excluding downlink-only beacons/telemetry that share the same mode label
|
||||||
|
* (e.g. CW beacons, FM voice-synthesis beacons) from FM/Linear filter fallback. */
|
||||||
|
@Query("SELECT catnum FROM radios WHERE downlinkMode IN (:modes) AND uplinkLow IS NOT NULL AND isAlive = 1")
|
||||||
|
suspend fun getIdsWithModesAndUplink(modes: List<String>): List<Int>
|
||||||
|
|
||||||
@Query("SELECT COUNT(*) FROM radios")
|
@Query("SELECT COUNT(*) FROM radios")
|
||||||
suspend fun getRadiosTotal(): Int
|
suspend fun getRadiosTotal(): Int
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ class SatelliteRepo(
|
|||||||
|
|
||||||
override suspend fun getSatelliteIdsWithModes(modes: List<String>) = localStorage.getIdsWithModes(modes)
|
override suspend fun getSatelliteIdsWithModes(modes: List<String>) = localStorage.getIdsWithModes(modes)
|
||||||
|
|
||||||
|
override suspend fun getSatelliteIdsWithModesAndUplink(modes: List<String>) = localStorage.getIdsWithModesAndUplink(modes)
|
||||||
|
|
||||||
override suspend fun initRepository() = withContext(dispatcher) {
|
override suspend fun initRepository() = withContext(dispatcher) {
|
||||||
combine(
|
combine(
|
||||||
settingsRepo.selectedIds,
|
settingsRepo.selectedIds,
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class SelectionRepo(
|
|||||||
null // null = no filtering
|
null // null = no filtering
|
||||||
} else {
|
} else {
|
||||||
val ids = resolveTypeIds(types)
|
val ids = resolveTypeIds(types)
|
||||||
if (ids.isEmpty()) null else ids.toHashSet()
|
if (ids.isEmpty()) emptySet() else ids.toHashSet()
|
||||||
}
|
}
|
||||||
currentItems.map { items ->
|
currentItems.map { items ->
|
||||||
if (catnumSet == null) items else items.filter { it.catnum in catnumSet }
|
if (catnumSet == null) items else items.filter { it.catnum in catnumSet }
|
||||||
@@ -72,9 +72,9 @@ class SelectionRepo(
|
|||||||
val idsSet = mutableSetOf<Int>()
|
val idsSet = mutableSetOf<Int>()
|
||||||
types.forEach { type ->
|
types.forEach { type ->
|
||||||
when (type) {
|
when (type) {
|
||||||
"AMSAT Live FM" -> idsSet.addAll(settingsRepo.getAmSatFmCatnums())
|
Sources.virtualTypeNames[0] -> idsSet.addAll(settingsRepo.getAmSatFmCatnums())
|
||||||
"AMSAT Live Linear" -> idsSet.addAll(settingsRepo.getAmSatLinearCatnums())
|
Sources.virtualTypeNames[1] -> idsSet.addAll(settingsRepo.getAmSatLinearCatnums())
|
||||||
"Live SSTV" -> idsSet.addAll(localSource.getIdsWithModes(listOf("SSTV")))
|
Sources.virtualTypeNames[2] -> idsSet.addAll(localSource.getIdsWithModes(listOf("SSTV")))
|
||||||
else -> idsSet.addAll(settingsRepo.getSatelliteTypesIds(listOf(type)))
|
else -> idsSet.addAll(settingsRepo.getSatelliteTypesIds(listOf(type)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,7 +83,7 @@ class SelectionRepo(
|
|||||||
|
|
||||||
override fun getTypesList() = buildList {
|
override fun getTypesList() = buildList {
|
||||||
// 三个转发器/活动虚拟类型排在最前.
|
// 三个转发器/活动虚拟类型排在最前.
|
||||||
addAll(listOf("AMSAT Live FM", "AMSAT Live Linear", "Live SSTV"))
|
addAll(Sources.virtualTypeNames)
|
||||||
addAll(Sources.satelliteDataUrls.keys.sorted().toMutableList().apply { removeAt(0) })
|
addAll(Sources.satelliteDataUrls.keys.sorted().toMutableList().apply { removeAt(0) })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class LocalSource(private val look4SatDao: Look4SatDao) : ILocalSource {
|
|||||||
override suspend fun deleteEntries() = look4SatDao.deleteEntries()
|
override suspend fun deleteEntries() = look4SatDao.deleteEntries()
|
||||||
|
|
||||||
override suspend fun getIdsWithModes(modes: List<String>) = look4SatDao.getIdsWithModes(modes)
|
override suspend fun getIdsWithModes(modes: List<String>) = look4SatDao.getIdsWithModes(modes)
|
||||||
|
override suspend fun getIdsWithModesAndUplink(modes: List<String>) = look4SatDao.getIdsWithModesAndUplink(modes)
|
||||||
|
|
||||||
private fun FrameworkEntry.toDomain() = OrbitalData(
|
private fun FrameworkEntry.toDomain() = OrbitalData(
|
||||||
this.name, this.epoch, this.meanmo, this.eccn, this.incl,
|
this.name, this.epoch, this.meanmo, this.eccn, this.incl,
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ private class FakeLocalSource : ILocalSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getIdsWithModes(modes: List<String>): List<Int> = emptyList()
|
override suspend fun getIdsWithModes(modes: List<String>): List<Int> = emptyList()
|
||||||
|
override suspend fun getIdsWithModesAndUplink(modes: List<String>): List<Int> = emptyList()
|
||||||
|
|
||||||
override suspend fun getRadiosTotal(): Int = insertedRadios.size
|
override suspend fun getRadiosTotal(): Int = insertedRadios.size
|
||||||
|
|
||||||
|
|||||||
+21
@@ -156,6 +156,26 @@ class SelectionRepoSearchTest {
|
|||||||
assertEquals(sampleItems.size, repo.getEntriesFlow().first().size)
|
assertEquals(sampleItems.size, repo.getEntriesFlow().first().size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `virtual type with empty AMSAT list shows empty not everything`() = runTest {
|
||||||
|
// 根因2回归: AMSAT 清单未同步(空)时, 选虚拟类型应显示空列表, 而非全部卫星.
|
||||||
|
val repo = createRepo(items = sampleItems, amSatFm = emptySet())
|
||||||
|
repo.setTypes(listOf("AMSAT Live FM"))
|
||||||
|
assertTrue(repo.getEntriesFlow().first().isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `virtual type combined with regular type unions both lists`() = runTest {
|
||||||
|
val repo = createRepo(
|
||||||
|
items = sampleItems,
|
||||||
|
amSatFm = setOf(25544),
|
||||||
|
amSatLinear = setOf(7530)
|
||||||
|
)
|
||||||
|
repo.setTypes(listOf("AMSAT Live FM", "AMSAT Live Linear"))
|
||||||
|
val results = repo.getEntriesFlow().first()
|
||||||
|
assertEquals(setOf(25544, 7530), results.map { it.catnum }.toSet())
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `types list has virtual transponder types first and keeps All`() {
|
fun `types list has virtual transponder types first and keeps All`() {
|
||||||
val repo = createRepo(sampleItems)
|
val repo = createRepo(sampleItems)
|
||||||
@@ -192,6 +212,7 @@ private class FakeLocalSourceForSearch(
|
|||||||
override suspend fun insertEntries(entries: List<OrbitalData>) = Unit
|
override suspend fun insertEntries(entries: List<OrbitalData>) = Unit
|
||||||
override suspend fun deleteEntries() = Unit
|
override suspend fun deleteEntries() = Unit
|
||||||
override suspend fun getIdsWithModes(modes: List<String>): List<Int> = sstvIds
|
override suspend fun getIdsWithModes(modes: List<String>): List<Int> = sstvIds
|
||||||
|
override suspend fun getIdsWithModesAndUplink(modes: List<String>): List<Int> = sstvIds
|
||||||
override suspend fun getRadiosTotal(): Int = 0
|
override suspend fun getRadiosTotal(): Int = 0
|
||||||
override suspend fun getRadiosWithId(id: Int): List<SatRadio> = emptyList()
|
override suspend fun getRadiosWithId(id: Int): List<SatRadio> = emptyList()
|
||||||
override suspend fun insertRadios(radios: List<SatRadio>, isCustom: Boolean) = Unit
|
override suspend fun insertRadios(radios: List<SatRadio>, isCustom: Boolean) = Unit
|
||||||
|
|||||||
+5
@@ -70,4 +70,9 @@ interface ISatelliteRepo {
|
|||||||
* Empty modes -> empty result; used to filter the mutual-match satellite
|
* Empty modes -> empty result; used to filter the mutual-match satellite
|
||||||
* set to transponder satellites (FM voice / linear). */
|
* set to transponder satellites (FM voice / linear). */
|
||||||
suspend fun getSatelliteIdsWithModes(modes: List<String>): List<Int>
|
suspend fun getSatelliteIdsWithModes(modes: List<String>): List<Int>
|
||||||
|
|
||||||
|
/** Like [getSatelliteIdsWithModes] but only matches records with an uplink
|
||||||
|
* (real transponders), excluding downlink-only beacons/telemetry that share
|
||||||
|
* the same mode label. Used by the FM/Linear fallback filter. */
|
||||||
|
suspend fun getSatelliteIdsWithModesAndUplink(modes: List<String>): List<Int>
|
||||||
}
|
}
|
||||||
@@ -29,6 +29,7 @@ interface ILocalSource {
|
|||||||
suspend fun insertEntries(entries: List<OrbitalData>)
|
suspend fun insertEntries(entries: List<OrbitalData>)
|
||||||
suspend fun deleteEntries()
|
suspend fun deleteEntries()
|
||||||
suspend fun getIdsWithModes(modes: List<String>): List<Int>
|
suspend fun getIdsWithModes(modes: List<String>): List<Int>
|
||||||
|
suspend fun getIdsWithModesAndUplink(modes: List<String>): List<Int>
|
||||||
suspend fun getRadiosTotal(): Int
|
suspend fun getRadiosTotal(): Int
|
||||||
suspend fun getRadiosWithId(id: Int): List<SatRadio>
|
suspend fun getRadiosWithId(id: Int): List<SatRadio>
|
||||||
suspend fun insertRadios(radios: List<SatRadio>, isCustom: Boolean = false)
|
suspend fun insertRadios(radios: List<SatRadio>, isCustom: Boolean = false)
|
||||||
|
|||||||
@@ -62,4 +62,10 @@ object Sources {
|
|||||||
"FM" to "https://www.amsat.org/live-fm-satellites/",
|
"FM" to "https://www.amsat.org/live-fm-satellites/",
|
||||||
"Linear" to "https://www.amsat.org/live-linear-satellites/"
|
"Linear" to "https://www.amsat.org/live-linear-satellites/"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** Virtual satellite-selection types: transponder/activity filters shown
|
||||||
|
* at the top of the type picker. They resolve to live lists (AMSAT pages
|
||||||
|
* or mode=SSTV radios) instead of persisted per-type IDs, and are
|
||||||
|
* mutually exclusive with the regular TLE-source types in the picker. */
|
||||||
|
val virtualTypeNames = listOf("AMSAT Live FM", "AMSAT Live Linear", "Live SSTV")
|
||||||
}
|
}
|
||||||
+4
-1
@@ -307,11 +307,14 @@ class MutualViewModel(
|
|||||||
val amSatFm = settingsRepo.getAmSatFmCatnums()
|
val amSatFm = settingsRepo.getAmSatFmCatnums()
|
||||||
val amSatLinear = settingsRepo.getAmSatLinearCatnums()
|
val amSatLinear = settingsRepo.getAmSatLinearCatnums()
|
||||||
val filteredSatellites = if (amSatFm.isEmpty() && amSatLinear.isEmpty()) {
|
val filteredSatellites = if (amSatFm.isEmpty() && amSatLinear.isEmpty()) {
|
||||||
|
// 回退到数据库模式过滤: 只用带上行频率的转发器记录, 排除纯下行
|
||||||
|
// 信标/遥测(如 CW 信标、FM 语音合成信标), 避免 ISS/IO-86/FO-29 等
|
||||||
|
// 因共享模式标签(CW/FM)被误判进错误类别.
|
||||||
val filterModes = buildList {
|
val filterModes = buildList {
|
||||||
if (state.filterFM) add("FM")
|
if (state.filterFM) add("FM")
|
||||||
if (state.filterLinear) addAll(listOf("USB", "LSB", "CW", "SSB"))
|
if (state.filterLinear) addAll(listOf("USB", "LSB", "CW", "SSB"))
|
||||||
}
|
}
|
||||||
val idsWithModes = satelliteRepo.getSatelliteIdsWithModes(filterModes)
|
val idsWithModes = satelliteRepo.getSatelliteIdsWithModesAndUplink(filterModes)
|
||||||
if (idsWithModes.isEmpty()) satellites
|
if (idsWithModes.isEmpty()) satellites
|
||||||
else satellites.filter { it.data.catnum in idsWithModes }
|
else satellites.filter { it.data.catnum in idsWithModes }
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class FakeSatelliteRepo(
|
|||||||
override suspend fun getRadios(sat: OrbitalObject, pos: GeoPos, radios: List<SatRadio>, time: Long): List<SatRadio> = TODO()
|
override suspend fun getRadios(sat: OrbitalObject, pos: GeoPos, radios: List<SatRadio>, time: Long): List<SatRadio> = TODO()
|
||||||
override suspend fun getRadiosWithId(id: Int): List<SatRadio> = TODO()
|
override suspend fun getRadiosWithId(id: Int): List<SatRadio> = TODO()
|
||||||
override suspend fun getSatelliteIdsWithModes(modes: List<String>): List<Int> = emptyList()
|
override suspend fun getSatelliteIdsWithModes(modes: List<String>): List<Int> = emptyList()
|
||||||
|
override suspend fun getSatelliteIdsWithModesAndUplink(modes: List<String>): List<Int> = emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+13
-1
@@ -42,6 +42,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import com.rtbishop.look4sat.core.presentation.MainTheme
|
import com.rtbishop.look4sat.core.presentation.MainTheme
|
||||||
import com.rtbishop.look4sat.core.presentation.R
|
import com.rtbishop.look4sat.core.presentation.R
|
||||||
import com.rtbishop.look4sat.core.presentation.SharedDialog
|
import com.rtbishop.look4sat.core.presentation.SharedDialog
|
||||||
|
import com.rtbishop.look4sat.core.domain.source.Sources
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -54,9 +55,20 @@ private fun MultiTypesDialogPreview() {
|
|||||||
internal fun MultiTypesDialog(
|
internal fun MultiTypesDialog(
|
||||||
allTypes: List<String>, types: List<String>, cancel: () -> Unit, accept: (List<String>) -> Unit
|
allTypes: List<String>, types: List<String>, cancel: () -> Unit, accept: (List<String>) -> Unit
|
||||||
) {
|
) {
|
||||||
|
// 虚拟类型(转发器/活动筛选)与普通 TLE 源类型互斥: 勾虚拟类型时取消普通类型,
|
||||||
|
// 勾普通类型时取消虚拟类型. 虚拟类型之间可多选组合.
|
||||||
|
val virtualTypes = Sources.virtualTypeNames
|
||||||
val selected = remember { mutableStateOf(types.toSet()) }
|
val selected = remember { mutableStateOf(types.toSet()) }
|
||||||
val toggle = { type: String ->
|
val toggle = { type: String ->
|
||||||
selected.value = if (type in selected.value) selected.value - type else selected.value + type
|
selected.value = if (type in selected.value) {
|
||||||
|
selected.value - type
|
||||||
|
} else if (type in virtualTypes) {
|
||||||
|
// 勾选虚拟类型: 移除所有普通类型, 保留已勾的虚拟类型.
|
||||||
|
(selected.value.filterTo(mutableSetOf()) { it in virtualTypes }) + type
|
||||||
|
} else {
|
||||||
|
// 勾选普通类型: 移除所有虚拟类型, 保留已勾的普通类型.
|
||||||
|
(selected.value.filterTo(mutableSetOf()) { it !in virtualTypes }) + type
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val onAccept = { accept(selected.value.toList()) }
|
val onAccept = { accept(selected.value.toList()) }
|
||||||
SharedDialog(title = stringResource(R.string.sat_type_title), onCancel = cancel, onAccept = onAccept) {
|
SharedDialog(title = stringResource(R.string.sat_type_title), onCancel = cancel, onAccept = onAccept) {
|
||||||
|
|||||||
Reference in new issue
Block a user