diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepo.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepo.kt index 40dd4843..d5ed5aa1 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepo.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepo.kt @@ -84,11 +84,11 @@ class DatabaseRepo( val type = builtinTypesByUrl[url] ?: customSourceType importedTypeIds.getOrPut(type) { mutableListOf() }.addAll(entries.map { it.catnum }) entries - } + }.distinctBy { it.catnum } importedTypeIds.forEach { (type, ids) -> settingsRepo.setSatelliteTypeIds(type, ids.distinct()) } val importedRadios = radioJobs.awaitAll().flatMap { (url, stream) -> stream?.let { dataParser.parseJSONStream(unwrapIfZipped(url, it)) }.orEmpty() - } + }.filter { it.uuid.isNotBlank() }.distinctBy { it.uuid } // insert parsed data into the database localSource.insertEntries(importedEntries) localSource.insertRadios(importedRadios) diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt index 787a841b..7811de91 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt @@ -98,8 +98,11 @@ class SettingsRepo( private val keyTransceiversUrl = "transceiversUrl" private val keySatelliteUrls = "satelliteUrls" private val keyTransceiversUrls = "transceiversUrls" + private val keySatnogsTleSourceMigration = "satnogsTleSourceMigration" private val separatorComma = "," private val separatorUrl = "\n" + private val legacyCelestrakSatnogsUrl = + "https://celestrak.org/NORAD/elements/gp.php?GROUP=satnogs&FORMAT=csv" //region # Satellites selection settings private val _satelliteSelection = MutableStateFlow(getSelectedIds()) @@ -399,6 +402,7 @@ class SettingsRepo( preferences.edit { putString(keySatelliteUrls, settings.satelliteUrls.joinToString(separatorUrl)) putString(keyTransceiversUrls, settings.transceiversUrls.joinToString(separatorUrl)) + putBoolean(keySatnogsTleSourceMigration, true) } _dataSourcesSettings.value = settings } @@ -409,7 +413,7 @@ class SettingsRepo( defaultUrls = Sources.satelliteDataUrls.values.filter { it.isNotBlank() }, legacyEnabledKey = keyUseCustomTle, legacyUrlKey = keyTleUrl - ), + ).migrateSatnogsTleSource(), transceiversUrls = getDataSourceUrls( key = keyTransceiversUrls, defaultUrls = Sources.transceiversDataUrls.values.filter { it.isNotBlank() }, @@ -436,6 +440,31 @@ class SettingsRepo( defaultUrls } } + + private fun List.migrateSatnogsTleSource(): List { + if (preferences.getBoolean(keySatnogsTleSourceMigration, false)) return this + val satnogsTleUrl = Sources.satelliteDataUrls["SatNOGS"].orEmpty() + if (satnogsTleUrl.isBlank() || containsSourceUrl(satnogsTleUrl)) return this + val celestrakSatnogsIndex = indexOfFirst { isSameSourceUrl(it, legacyCelestrakSatnogsUrl) } + if (celestrakSatnogsIndex < 0) return this + + val migrated = toMutableList().apply { add(celestrakSatnogsIndex + 1, satnogsTleUrl) } + preferences.edit { + putString(keySatelliteUrls, migrated.joinToString(separatorUrl)) + putBoolean(keySatnogsTleSourceMigration, true) + } + return migrated + } + + private fun List.containsSourceUrl(url: String): Boolean = any { isSameSourceUrl(it, url) } + + private fun isSameSourceUrl(first: String, second: String): Boolean = + normalizeSourceUrl(first).equals(normalizeSourceUrl(second), ignoreCase = true) + + private fun normalizeSourceUrl(url: String): String { + val trimmed = url.trim() + return if (trimmed.startsWith("http", ignoreCase = true)) trimmed else "https://$trimmed" + } //endregion //region # Radio control settings 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 c5837e4f..e0c952c4 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 @@ -31,6 +31,7 @@ import com.rtbishop.look4sat.core.domain.predict.OrbitalObject import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo import com.rtbishop.look4sat.core.domain.source.ILocalSource import com.rtbishop.look4sat.core.domain.source.IRemoteSource +import com.rtbishop.look4sat.core.domain.source.Sources import com.rtbishop.look4sat.core.domain.utility.DataParser import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow @@ -103,6 +104,27 @@ class DatabaseRepoTest { assertEquals(listOf(25544), settingsRepo.satelliteTypeIdsByType["Other"]) } + @Test + fun `remote update imports satellites from real SatNOGS TLE source`() = runTest(dispatcher) { + val satnogsUrl = Sources.satelliteDataUrls.getValue("SatNOGS") + val localSource = FakeLocalSource() + val remoteSource = FakeRemoteSource().apply { + networkStreams[satnogsUrl] = { jamxTleStream() } + } + val settingsRepo = FakeSettingsRepo( + dataSources = DataSourcesSettings( + satelliteUrls = listOf(satnogsUrl), + transceiversUrls = emptyList() + ) + ) + val repository = DatabaseRepo(dispatcher, dataParser, localSource, remoteSource, settingsRepo) + + repository.updateFromRemote() + + assertTrue(localSource.insertedEntries.any { it.name == "JAMX-0825b" && it.catnum == 98248 }) + assertEquals(listOf(98248), settingsRepo.satelliteTypeIdsByType["SatNOGS"]) + } + private fun validCsvStream(): InputStream = """ OBJECT_NAME,OBJECT_ID,EPOCH,MEAN_MOTION,ECCENTRICITY,INCLINATION,RA_OF_ASC_NODE,ARG_OF_PERICENTER,MEAN_ANOMALY,EPHEMERIS_TYPE,CLASSIFICATION_TYPE,NORAD_CAT_ID,ELEMENT_SET_NO,REV_AT_EPOCH,BSTAR,MEAN_MOTION_DOT,MEAN_MOTION_DDOT ISS (ZARYA),1998-067A,2021-11-16T12:28:09.322176,15.48582035,.0004694,51.6447,309.4881,203.6966,299.8876,0,U,25544,999,31220,.31985E-4,.1288E-4,0 @@ -113,6 +135,12 @@ class DatabaseRepoTest { 1 25544U 98067A 21320.51955234 .00001288 00000+0 31985-4 0 9990 2 25544 51.6447 309.4881 0004694 203.6966 299.8876 15.48582035312205 """.trimIndent().byteInputStream() + + private fun jamxTleStream(): InputStream = """ + JAMX-0825b + 1 98248U 26237.16675926 .00015724 00000-0 97477-3 0 00013 + 2 98248 097.5373 310.9694 0011309 278.1232 340.7230 15.09766181000012 + """.trimIndent().byteInputStream() } private class FakeRemoteSource : IRemoteSource { diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/source/Sources.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/source/Sources.kt index 122252fc..ca911155 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/source/Sources.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/source/Sources.kt @@ -20,6 +20,7 @@ package com.rtbishop.look4sat.core.domain.source object Sources { val satelliteDataUrls = mapOf( "All" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=active&FORMAT=csv", + "SatNOGS" to "https://db.satnogs.org/api/tle/?format=3le", "Amateur" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=amateur&FORMAT=csv", "Brightest" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=visual&FORMAT=csv", "Cubesat" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=cubesat&FORMAT=csv", @@ -35,7 +36,7 @@ object Sources { "OneWeb" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=oneweb&FORMAT=csv", "Orbcomm" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=orbcomm&FORMAT=csv", "Resource" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=resource&FORMAT=csv", - "SatNOGS" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=satnogs&FORMAT=csv", + "CelesTrak SatNOGS" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=satnogs&FORMAT=csv", "Science" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=science&FORMAT=csv", "Spire" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=spire&FORMAT=csv", "Starlink" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=starlink&FORMAT=csv",