mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-02 19:17:21 +00:00
Compare commits
3
Commits
v4.4.8-ba7opf.4
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c86893f6b1 | ||
|
|
cad085c25b | ||
|
|
cba5eb7efe |
No files matched your search
@@ -19,6 +19,7 @@ package com.rtbishop.look4sat.core.data.database
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.MapColumn
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
@@ -39,6 +40,18 @@ interface Look4SatDao {
|
||||
@Query("SELECT * FROM entries WHERE catnum IN (:selectedIds)")
|
||||
suspend fun getEntriesWithIds(selectedIds: List<Int>): List<SatEntry>
|
||||
|
||||
@Query("SELECT catnum, epoch FROM entries")
|
||||
suspend fun getEntriesEpochs(): Map<@MapColumn("catnum") Int, @MapColumn("epoch") Double>
|
||||
|
||||
@Query("SELECT catnum, name FROM entries")
|
||||
suspend fun getEntriesNames(): Map<@MapColumn("catnum") Int, @MapColumn("name") String>
|
||||
|
||||
@Query("UPDATE entries SET name = :name WHERE catnum = :catnum")
|
||||
suspend fun renameEntry(catnum: Int, name: String)
|
||||
|
||||
@Query("DELETE FROM entries WHERE catnum IN (:ids)")
|
||||
suspend fun deleteEntriesWithIds(ids: List<Int>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertEntries(entries: List<SatEntry>)
|
||||
|
||||
|
||||
@@ -46,8 +46,8 @@ class DatabaseRepo(
|
||||
override suspend fun updateTLEFromFile(uri: String): Int = withContext(dispatcher) {
|
||||
var importedCount = 0
|
||||
remoteSource.getFileStream(uri)?.let { stream ->
|
||||
val entries = parseSatelliteStream(uri, unwrapIfZipped(uri, stream))
|
||||
localSource.insertEntries(entries)
|
||||
val entries = mergeEntries(listOf(parseSatelliteStream(uri, unwrapIfZipped(uri, stream))))
|
||||
insertFresherEntries(entries)
|
||||
settingsRepo.setSatelliteTypeIds(customSourceType, entries.map { it.catnum })
|
||||
importedCount = entries.size
|
||||
}
|
||||
@@ -93,14 +93,15 @@ class DatabaseRepo(
|
||||
settingsRepo.updateDataSourcesStatus(
|
||||
(tleResults + radioResults).associate { (url, result) -> url to result.code }
|
||||
)
|
||||
// parse fetched data concurrently and associate known built-in URLs with existing type filters.
|
||||
val importedEntries = tleResults.flatMap { (rawUrl, result) ->
|
||||
// parse each source on its own: the type filters need every source's catnums, while the
|
||||
// stored elements are merged across sources below by freshness.
|
||||
val parsedPerSource = tleResults.map { (rawUrl, result) ->
|
||||
val normUrl = normalizeUrl(rawUrl)
|
||||
val entries = result.stream?.let { parseSatelliteStream(normUrl, unwrapIfZipped(normUrl, it)) }.orEmpty()
|
||||
val type = builtinTypesByUrl[normUrl] ?: 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 = radioResults.flatMap { (rawUrl, result) ->
|
||||
val normUrl = normalizeUrl(rawUrl)
|
||||
@@ -115,8 +116,14 @@ class DatabaseRepo(
|
||||
localSource.deleteManagedRadios()
|
||||
localSource.insertRadios(importedRadios)
|
||||
}
|
||||
if (importedEntries.isNotEmpty()) localSource.insertEntries(importedEntries)
|
||||
setUpdateSuccessful(System.currentTimeMillis())
|
||||
// Elements always come from the freshest source for every satellite; only
|
||||
// elements newer than the stored ones are written back.
|
||||
insertFresherEntries(mergeEntries(parsedPerSource))
|
||||
// keep the previous timestamp when every source failed, so the next launch retries
|
||||
val hasFetchedData = parsedPerSource.any { entries -> entries.isNotEmpty() } || importedRadios.isNotEmpty()
|
||||
val previousTimestamp = settingsRepo.databaseState.value.updateTimestamp
|
||||
if (hasFetchedData) pruneStaleEntries()
|
||||
setUpdateSuccessful(if (hasFetchedData) System.currentTimeMillis() else previousTimestamp)
|
||||
}
|
||||
|
||||
override suspend fun clearAllData() = withContext(dispatcher) {
|
||||
@@ -155,6 +162,62 @@ class DatabaseRepo(
|
||||
line.count { it == ',' } >= 4
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the data of every source: orbital elements always come from the set with the newest
|
||||
* epoch, while the name comes from the first source that provides it. Source order is thus a
|
||||
* naming preference only, which also keeps names stable when sources leapfrog each other.
|
||||
*/
|
||||
private fun mergeEntries(sourceEntries: List<List<OrbitalData>>): List<OrbitalData> {
|
||||
val preferredNames = mutableMapOf<Int, String>()
|
||||
val freshestEntries = mutableMapOf<Int, OrbitalData>()
|
||||
sourceEntries.forEach { entries ->
|
||||
entries.forEach { entry ->
|
||||
val name = entry.name.trim()
|
||||
if (name.isNotBlank()) preferredNames.getOrPut(entry.catnum) { name }
|
||||
val current = freshestEntries[entry.catnum]
|
||||
if (current == null || entry.epochDaynum > current.epochDaynum) {
|
||||
freshestEntries[entry.catnum] = entry
|
||||
}
|
||||
}
|
||||
}
|
||||
return freshestEntries.values.map { entry ->
|
||||
val name = preferredNames[entry.catnum]
|
||||
if (name == null || name == entry.name) entry else entry.copy(name = name)
|
||||
}
|
||||
}
|
||||
|
||||
/** Stores the entries that are newer than the ones already saved, renaming the rest in place. */
|
||||
private suspend fun insertFresherEntries(entries: List<OrbitalData>) {
|
||||
val storedEpochs = localSource.getEntriesEpochs()
|
||||
val (fresherEntries, staleEntries) = entries.partition { entry ->
|
||||
val storedEpoch = storedEpochs[entry.catnum]
|
||||
storedEpoch == null || entry.epochDaynum > OrbitalData.epochToDaynum(storedEpoch)
|
||||
}
|
||||
localSource.insertEntries(fresherEntries)
|
||||
// a reordered source list has to rename satellites right away, even the ones holding
|
||||
// elements that are newer than the ones just parsed
|
||||
if (staleEntries.isNotEmpty()) {
|
||||
val storedNames = localSource.getEntriesNames()
|
||||
val renamedEntries = staleEntries.filter { entry -> entry.name != storedNames[entry.catnum] }
|
||||
if (renamedEntries.isNotEmpty()) {
|
||||
localSource.renameEntries(renamedEntries.associate { it.catnum to it.name })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops satellites that no enabled source has refreshed for a month: they either decayed or
|
||||
* disappeared from every catalog. Manually imported data ages out the same way, and every
|
||||
* source republishes active satellites well within that window.
|
||||
*/
|
||||
private suspend fun pruneStaleEntries() {
|
||||
val currentDaynum = OrbitalData.timeToDaynum(System.currentTimeMillis())
|
||||
val staleIds = localSource.getEntriesEpochs()
|
||||
.filterValues { epoch -> currentDaynum - OrbitalData.epochToDaynum(epoch) > 30.0 }
|
||||
.keys.toList()
|
||||
if (staleIds.isNotEmpty()) localSource.deleteEntriesWithIds(staleIds)
|
||||
}
|
||||
|
||||
private suspend fun setUpdateSuccessful(timestamp: Long) {
|
||||
settingsRepo.updateDatabaseState(
|
||||
DatabaseState(localSource.getRadiosTotal(), localSource.getEntriesTotal(), timestamp)
|
||||
|
||||
@@ -43,6 +43,16 @@ class LocalSource(private val look4SatDao: Look4SatDao) : ILocalSource {
|
||||
return selectedOrbitalObjects
|
||||
}
|
||||
|
||||
override suspend fun getEntriesEpochs() = look4SatDao.getEntriesEpochs()
|
||||
|
||||
override suspend fun getEntriesNames() = look4SatDao.getEntriesNames()
|
||||
|
||||
override suspend fun renameEntries(names: Map<Int, String>) =
|
||||
names.forEach { (catnum, name) -> look4SatDao.renameEntry(catnum, name) }
|
||||
|
||||
override suspend fun deleteEntriesWithIds(ids: List<Int>) =
|
||||
ids.chunked(999).forEach { idsPart -> look4SatDao.deleteEntriesWithIds(idsPart) }
|
||||
|
||||
override suspend fun insertEntries(entries: List<OrbitalData>) = look4SatDao.insertEntries(entries.toEntity())
|
||||
|
||||
override suspend fun deleteEntries() = look4SatDao.deleteEntries()
|
||||
|
||||
+147
-3
@@ -44,6 +44,7 @@ import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import java.io.InputStream
|
||||
import java.time.LocalDate
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class DatabaseRepoTest {
|
||||
@@ -51,6 +52,10 @@ class DatabaseRepoTest {
|
||||
private val dispatcher = StandardTestDispatcher()
|
||||
private val dataParser = DataParser(dispatcher)
|
||||
|
||||
// fixtures carry a current epoch: stale entries are pruned on every remote update
|
||||
private val todayEpoch = LocalDate.now().let { "%02d%03d".format(it.year % 100, it.dayOfYear) }
|
||||
private val fresherEpoch = "$todayEpoch.71955234".toDouble()
|
||||
|
||||
@Test
|
||||
fun `manual satellite import parses csv stream from content uri`() = runTest(dispatcher) {
|
||||
val uri = "content://look4sat/import/satellites"
|
||||
@@ -150,20 +155,144 @@ class DatabaseRepoTest {
|
||||
assertEquals(listOf(25544), settingsRepo.satelliteTypeIdsByType["All"])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `remote update takes the freshest elements and the preferred name`() = runTest(dispatcher) {
|
||||
val primaryUrl = "https://example.com/primary.txt"
|
||||
val secondaryUrl = "https://example.com/secondary.txt"
|
||||
val localSource = FakeLocalSource()
|
||||
val remoteSource = FakeRemoteSource().apply {
|
||||
networkStreams[primaryUrl] = { validTleStream() }
|
||||
networkStreams[secondaryUrl] = { fresherTleStream() }
|
||||
}
|
||||
val settingsRepo = FakeSettingsRepo(
|
||||
dataSources = DataSourcesSettings(
|
||||
satelliteUrls = listOf(primaryUrl, secondaryUrl),
|
||||
transceiversUrls = emptyList()
|
||||
)
|
||||
)
|
||||
val repository = DatabaseRepo(dispatcher, dataParser, localSource, remoteSource, settingsRepo)
|
||||
|
||||
repository.updateFromRemote()
|
||||
|
||||
val entry = localSource.insertedEntries.single { it.catnum == 25544 }
|
||||
assertEquals("ISS (ZARYA)", entry.name)
|
||||
assertEquals(fresherEpoch, entry.epoch, 1e-8)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `manual satellite import does not overwrite fresher data`() = runTest(dispatcher) {
|
||||
val freshUri = "content://look4sat/import/fresh"
|
||||
val staleUri = "content://look4sat/import/stale"
|
||||
val localSource = FakeLocalSource()
|
||||
val remoteSource = FakeRemoteSource().apply {
|
||||
fileStreams[freshUri] = { fresherTleStream() }
|
||||
fileStreams[staleUri] = { validTleStream() }
|
||||
}
|
||||
val settingsRepo = FakeSettingsRepo()
|
||||
val repository = DatabaseRepo(dispatcher, dataParser, localSource, remoteSource, settingsRepo)
|
||||
|
||||
assertEquals(1, repository.updateTLEFromFile(freshUri))
|
||||
assertEquals(1, repository.updateTLEFromFile(staleUri))
|
||||
assertEquals(fresherEpoch, localSource.insertedEntries.single().epoch, 1e-8)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `remote update renames satellites even when their elements are not newer`() = runTest(dispatcher) {
|
||||
val preferredUrl = "https://example.com/preferred.txt"
|
||||
val freshUrl = "https://example.com/fresh.txt"
|
||||
val localSource = FakeLocalSource()
|
||||
val remoteSource = FakeRemoteSource().apply {
|
||||
networkStreams[freshUrl] = { fresherTleStream() }
|
||||
}
|
||||
val settingsRepo = FakeSettingsRepo(
|
||||
dataSources = DataSourcesSettings(
|
||||
satelliteUrls = listOf(freshUrl),
|
||||
transceiversUrls = emptyList()
|
||||
)
|
||||
)
|
||||
val repository = DatabaseRepo(dispatcher, dataParser, localSource, remoteSource, settingsRepo)
|
||||
repository.updateFromRemote()
|
||||
assertEquals("ISS", localSource.insertedEntries.single().name)
|
||||
|
||||
// the user moves a source with older elements, but a better name, to the top of the list
|
||||
remoteSource.networkStreams[preferredUrl] = { validTleStream() }
|
||||
settingsRepo.dataSourcesSettings.value = DataSourcesSettings(
|
||||
satelliteUrls = listOf(preferredUrl, freshUrl),
|
||||
transceiversUrls = emptyList()
|
||||
)
|
||||
repository.updateFromRemote()
|
||||
|
||||
val entry = localSource.insertedEntries.single()
|
||||
assertEquals("ISS (ZARYA)", entry.name)
|
||||
assertEquals(fresherEpoch, entry.epoch, 1e-8)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failed remote update records the attempt without claiming success`() = runTest(dispatcher) {
|
||||
val failingUrl = "https://example.com/offline.txt"
|
||||
val localSource = FakeLocalSource()
|
||||
val remoteSource = FakeRemoteSource()
|
||||
val settingsRepo = FakeSettingsRepo(
|
||||
dataSources = DataSourcesSettings(
|
||||
satelliteUrls = listOf(failingUrl),
|
||||
transceiversUrls = emptyList()
|
||||
)
|
||||
)
|
||||
settingsRepo.databaseState.value = DatabaseState(0, 0, 1_000L)
|
||||
val repository = DatabaseRepo(dispatcher, dataParser, localSource, remoteSource, settingsRepo)
|
||||
repository.updateFromRemote()
|
||||
|
||||
// the update timestamp stays put so the data is not presented as fresh, while the recorded
|
||||
// attempt keeps the next app launch from hammering a source that is already failing
|
||||
assertEquals(1_000L, settingsRepo.databaseState.value.updateTimestamp)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `remote update prunes satellites no source refreshed for a month`() = runTest(dispatcher) {
|
||||
val url = "https://example.com/fresh.txt"
|
||||
val localSource = FakeLocalSource()
|
||||
val remoteSource = FakeRemoteSource().apply {
|
||||
networkStreams[url] = { fresherTleStream() }
|
||||
}
|
||||
val settingsRepo = FakeSettingsRepo(
|
||||
dataSources = DataSourcesSettings(
|
||||
satelliteUrls = listOf(url),
|
||||
transceiversUrls = emptyList()
|
||||
)
|
||||
)
|
||||
val repository = DatabaseRepo(dispatcher, dataParser, localSource, remoteSource, settingsRepo)
|
||||
// a satellite that disappeared from every catalog: elements stored a month and a half ago
|
||||
val staleDate = LocalDate.now().minusDays(45)
|
||||
val staleEpoch = "%02d%03d".format(staleDate.year % 100, staleDate.dayOfYear).toDouble() + 0.5
|
||||
localSource.insertEntries(
|
||||
listOf(OrbitalData("OLD SAT", staleEpoch, 15.5, 0.001, 51.6, 309.4, 203.6, 299.8, 99999, 0.0003, 0.0000128))
|
||||
)
|
||||
|
||||
repository.updateFromRemote()
|
||||
|
||||
assertEquals(listOf(25544), localSource.insertedEntries.map { it.catnum })
|
||||
}
|
||||
|
||||
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
|
||||
ISS (ZARYA),1998-067A,${LocalDate.now()}T12:28:09.322176,15.48582035,.0004694,51.6447,309.4881,203.6966,299.8876,0,U,25544,999,31220,.31985E-4,.1288E-4,0
|
||||
""".trimIndent().byteInputStream()
|
||||
|
||||
private fun validTleStream(): InputStream = """
|
||||
ISS (ZARYA)
|
||||
1 25544U 98067A 21320.51955234 .00001288 00000+0 31985-4 0 9990
|
||||
1 25544U 98067A $todayEpoch.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 fresherTleStream(): InputStream = """
|
||||
ISS
|
||||
1 25544U 98067A $todayEpoch.71955234 .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
|
||||
1 98248U $todayEpoch.16675926 .00015724 00000-0 97477-3 0 00013
|
||||
2 98248 097.5373 310.9694 0011309 278.1232 340.7230 15.09766181000012
|
||||
""".trimIndent().byteInputStream()
|
||||
}
|
||||
@@ -195,6 +324,21 @@ private class FakeLocalSource : ILocalSource {
|
||||
|
||||
override suspend fun getEntriesWithIds(ids: List<Int>): List<OrbitalObject> = emptyList()
|
||||
|
||||
override suspend fun getEntriesEpochs(): Map<Int, Double> =
|
||||
insertedEntries.associate { entry -> entry.catnum to entry.epoch }
|
||||
|
||||
override suspend fun getEntriesNames(): Map<Int, String> =
|
||||
insertedEntries.associate { entry -> entry.catnum to entry.name }
|
||||
|
||||
override suspend fun renameEntries(names: Map<Int, String>) = names.forEach { (catnum, name) ->
|
||||
val index = insertedEntries.indexOfFirst { entry -> entry.catnum == catnum }
|
||||
if (index >= 0) insertedEntries[index] = insertedEntries[index].copy(name = name)
|
||||
}
|
||||
|
||||
override suspend fun deleteEntriesWithIds(ids: List<Int>) {
|
||||
insertedEntries.removeAll { entry -> entry.catnum in ids }
|
||||
}
|
||||
|
||||
override suspend fun insertEntries(entries: List<OrbitalData>) {
|
||||
insertedEntries += entries
|
||||
}
|
||||
|
||||
+4
@@ -282,6 +282,10 @@ private class FakeLocalSourceForSearch(
|
||||
override suspend fun getEntriesTotal(): Int = items.size
|
||||
override suspend fun getEntriesList(): List<SatItem> = items
|
||||
override suspend fun getEntriesWithIds(ids: List<Int>): List<OrbitalObject> = emptyList()
|
||||
override suspend fun getEntriesEpochs(): Map<Int, Double> = emptyMap()
|
||||
override suspend fun getEntriesNames(): Map<Int, String> = emptyMap()
|
||||
override suspend fun renameEntries(names: Map<Int, String>) = Unit
|
||||
override suspend fun deleteEntriesWithIds(ids: List<Int>) = Unit
|
||||
override suspend fun insertEntries(entries: List<OrbitalData>) = Unit
|
||||
override suspend fun deleteEntries() = Unit
|
||||
override suspend fun getIdsWithModes(modes: List<String>): List<Int> = sstvIds
|
||||
|
||||
+29
-17
@@ -37,30 +37,42 @@ data class OrbitalData(
|
||||
val xno: Double = meanmo * TWO_PI / MIN_PER_DAY
|
||||
val orbitalPeriod: Double = MIN_PER_DAY / meanmo
|
||||
val isDeepSpace: Boolean = orbitalPeriod >= 225.0 // NearEarth (period < 225 min) or DeepSpace (period >= 225 min)
|
||||
|
||||
/** Absolute age of this element set, used to pick the freshest data among several sources. */
|
||||
val epochDaynum: Double = epochToDaynum(epoch)
|
||||
|
||||
fun getObject(): OrbitalObject = if (isDeepSpace) DeepSpaceObject(this) else NearEarthObject(this)
|
||||
|
||||
/** Check if satellite has likely decayed by the given time. */
|
||||
fun hasDecayed(currentTimeMillis: Long): Boolean {
|
||||
if (ndot == 0.0) return false
|
||||
val currentDaynum = (currentTimeMillis - 315446400000L) / 86400000.0
|
||||
val epochDaynum = epochToDaynum(epoch)
|
||||
return CelestialComputer.hasDecayed(meanmo, ndot, epochDaynum, currentDaynum)
|
||||
return CelestialComputer.hasDecayed(meanmo, ndot, epochDaynum, timeToDaynum(currentTimeMillis))
|
||||
}
|
||||
|
||||
private fun epochToDaynum(epoch: Double): Double {
|
||||
var year = kotlin.math.floor(epoch * 1E-3)
|
||||
val day = (epoch * 1E-3 - year) * 1000.0
|
||||
year = if (year < 57) year + 2000 else year + 1900
|
||||
// daynum = days since 31 Dec 1979, Julian date of 31Dec79 = 2444238.5
|
||||
val jan1Jd = julianDateOfYear(year)
|
||||
return jan1Jd + day - 2444238.5
|
||||
}
|
||||
companion object {
|
||||
/** Days since 31 Dec 1979, the reference point every daynum in the app is based on. */
|
||||
fun timeToDaynum(timeMillis: Long): Double = (timeMillis - 315446400000L) / 86400000.0
|
||||
|
||||
private fun julianDateOfYear(theYear: Double): Double {
|
||||
val aYear = theYear - 1
|
||||
val a = kotlin.math.floor(aYear / 100).toLong()
|
||||
val b = 2 - a + a / 4
|
||||
val i = kotlin.math.floor(365.25 * aYear).toLong()
|
||||
return i + (30.6001 * 14).toLong() + 1720994.5 + b
|
||||
/**
|
||||
* Days since 31 Dec 1979 for a TLE style YYDDD.ffffffff epoch. Raw epochs are not
|
||||
* comparable across decades (a 1999 epoch reads as 99xxx, a 2026 one as 26xxx), so
|
||||
* freshness checks must always go through this conversion.
|
||||
*/
|
||||
fun epochToDaynum(epoch: Double): Double {
|
||||
var year = kotlin.math.floor(epoch * 1E-3)
|
||||
val day = (epoch * 1E-3 - year) * 1000.0
|
||||
year = if (year < 57) year + 2000 else year + 1900
|
||||
// daynum = days since 31 Dec 1979, Julian date of 31Dec79 = 2444238.5
|
||||
val jan1Jd = julianDateOfYear(year)
|
||||
return jan1Jd + day - 2444238.5
|
||||
}
|
||||
|
||||
private fun julianDateOfYear(theYear: Double): Double {
|
||||
val aYear = theYear - 1
|
||||
val a = kotlin.math.floor(aYear / 100).toLong()
|
||||
val b = 2 - a + a / 4
|
||||
val i = kotlin.math.floor(365.25 * aYear).toLong()
|
||||
return i + (30.6001 * 14).toLong() + 1720994.5 + b
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,10 @@ interface ILocalSource {
|
||||
suspend fun getEntriesTotal(): Int
|
||||
suspend fun getEntriesList(): List<SatItem>
|
||||
suspend fun getEntriesWithIds(ids: List<Int>): List<OrbitalObject>
|
||||
suspend fun getEntriesEpochs(): Map<Int, Double>
|
||||
suspend fun getEntriesNames(): Map<Int, String>
|
||||
suspend fun renameEntries(names: Map<Int, String>)
|
||||
suspend fun deleteEntriesWithIds(ids: List<Int>)
|
||||
suspend fun insertEntries(entries: List<OrbitalData>)
|
||||
suspend fun deleteEntries()
|
||||
suspend fun getIdsWithModes(modes: List<String>): List<Int>
|
||||
|
||||
@@ -112,6 +112,8 @@
|
||||
<string name="prefs_data_update_success">Actualización completada con éxito</string>
|
||||
|
||||
<string name="prefs_data_sources_title">Custom data sources</string>
|
||||
<string name="prefs_data_sources_sat_hint">Prioridad arriba: nombres de satélites</string>
|
||||
<string name="prefs_data_sources_transceivers_hint">Prioridad arriba: datos de transceptores</string>
|
||||
<string name="prefs_data_import_satellites_error">No satellites imported. Select a valid TLE/3LE (.txt) or OMM (.csv) file.</string>
|
||||
<string name="prefs_data_import_transceivers_error">No transceivers imported. Select a valid SatNOGS (.json) file.</string>
|
||||
<string name="prefs_data_sources_url_title" translatable="false">URL</string>
|
||||
|
||||
@@ -111,6 +111,8 @@
|
||||
<string name="prefs_data_update_success">Обновление прошло успешно</string>
|
||||
|
||||
<string name="prefs_data_sources_title">Свои источники данных</string>
|
||||
<string name="prefs_data_sources_sat_hint">Приоритет сверху: названия спутников</string>
|
||||
<string name="prefs_data_sources_transceivers_hint">Приоритет сверху: данные трансиверов</string>
|
||||
<string name="prefs_data_import_satellites_error">No satellites imported. Select a valid TLE/3LE (.txt) or OMM (.csv) file.</string>
|
||||
<string name="prefs_data_import_transceivers_error">No transceivers imported. Select a valid SatNOGS (.json) file.</string>
|
||||
<string name="prefs_data_sources_url_title" translatable="false">URL</string>
|
||||
|
||||
@@ -112,6 +112,8 @@
|
||||
<string name="prefs_data_update_success">යාවත්කාලීනය සම්පූර්ණ කිරීම සාර්ථකයි</string>
|
||||
|
||||
<string name="prefs_data_sources_title">Custom data sources</string>
|
||||
<string name="prefs_data_sources_sat_hint">ඉහළ ප්රමුඛතාව: චන්ද්රිකා නම්</string>
|
||||
<string name="prefs_data_sources_transceivers_hint">ඉහළ ප්රමුඛතාව: සම්ප්රේෂක දත්ත</string>
|
||||
<string name="prefs_data_import_satellites_error">No satellites imported. Select a valid TLE/3LE (.txt) or OMM (.csv) file.</string>
|
||||
<string name="prefs_data_import_transceivers_error">No transceivers imported. Select a valid SatNOGS (.json) file.</string>
|
||||
<string name="prefs_data_sources_url_title" translatable="false">URL</string>
|
||||
|
||||
@@ -142,6 +142,8 @@
|
||||
<string name="prefs_data_update_success">Güncelleme başarıyla tamamlandı</string>
|
||||
|
||||
<string name="prefs_data_sources_title">Özel veri kaynakları</string>
|
||||
<string name="prefs_data_sources_sat_hint">Öncelik üstte: uydu adları</string>
|
||||
<string name="prefs_data_sources_transceivers_hint">Öncelik üstte: transceiver verileri</string>
|
||||
<string name="prefs_data_import_satellites_error">No satellites imported. Select a valid TLE/3LE (.txt) or OMM (.csv) file.</string>
|
||||
<string name="prefs_data_import_transceivers_error">No transceivers imported. Select a valid SatNOGS (.json) file.</string>
|
||||
<string name="prefs_data_sources_satellites_label" translatable="false">Satellites data</string>
|
||||
|
||||
@@ -112,6 +112,8 @@
|
||||
<string name="prefs_data_update_success">Оновлення успішне</string>
|
||||
|
||||
<string name="prefs_data_sources_title">Custom data sources</string>
|
||||
<string name="prefs_data_sources_sat_hint">Пріоритет зверху: назви супутників</string>
|
||||
<string name="prefs_data_sources_transceivers_hint">Пріоритет зверху: дані трансиверів</string>
|
||||
<string name="prefs_data_import_satellites_error">No satellites imported. Select a valid TLE/3LE (.txt) or OMM (.csv) file.</string>
|
||||
<string name="prefs_data_import_transceivers_error">No transceivers imported. Select a valid SatNOGS (.json) file.</string>
|
||||
<string name="prefs_data_sources_url_title" translatable="false">URL</string>
|
||||
|
||||
@@ -241,6 +241,8 @@
|
||||
<string name="prefs_data_import_transceivers_error">未导入收发器。请选择有效的 SatNOGS (.json) 文件。</string>
|
||||
|
||||
<string name="prefs_data_sources_title">自定义数据源</string>
|
||||
<string name="prefs_data_sources_sat_hint">轨道数据优先取列表最新星历,此顺序仅影响卫星名称</string>
|
||||
<string name="prefs_data_sources_transceivers_hint">靠前优先:收发器数据</string>
|
||||
<string name="prefs_data_sources_restore">恢复默认数据源</string>
|
||||
<string name="prefs_data_sources_tle_switch">自定义TLE URL</string>
|
||||
<string name="prefs_data_sources_transceivers_switch">自定义收发器URL</string>
|
||||
|
||||
@@ -269,6 +269,8 @@
|
||||
<string name="prefs_data_import_transceivers_error">No transceivers imported. Select a valid SatNOGS (.json) file.</string>
|
||||
|
||||
<string name="prefs_data_sources_title">Custom data sources</string>
|
||||
<string name="prefs_data_sources_sat_hint">Orbital data takes the freshest elements; this order only affects satellite names</string>
|
||||
<string name="prefs_data_sources_transceivers_hint">Top priority affects transceivers data</string>
|
||||
<string name="prefs_data_sources_restore">Restore default sources</string>
|
||||
<string name="prefs_data_sources_tle_switch">Custom TLE URL</string>
|
||||
<string name="prefs_data_sources_transceivers_switch">Custom transceivers URL</string>
|
||||
|
||||
+12
@@ -469,6 +469,7 @@ fun DataSourcesDialog(
|
||||
sourceSection(
|
||||
sectionKey = "sat",
|
||||
labelResId = R.string.prefs_data_sources_satellites_label,
|
||||
hintResId = R.string.prefs_data_sources_sat_hint,
|
||||
urls = satUrls,
|
||||
listState = listState,
|
||||
draggedId = satDraggedId,
|
||||
@@ -483,6 +484,7 @@ fun DataSourcesDialog(
|
||||
sourceSection(
|
||||
sectionKey = "tx",
|
||||
labelResId = R.string.prefs_data_sources_transceivers_label,
|
||||
hintResId = R.string.prefs_data_sources_transceivers_hint,
|
||||
urls = txUrls,
|
||||
listState = listState,
|
||||
draggedId = txDraggedId,
|
||||
@@ -501,6 +503,7 @@ fun DataSourcesDialog(
|
||||
private fun LazyListScope.sourceSection(
|
||||
sectionKey: String,
|
||||
labelResId: Int,
|
||||
hintResId: Int,
|
||||
urls: List<Pair<Long, String>>,
|
||||
listState: LazyListState,
|
||||
draggedId: MutableState<Long>,
|
||||
@@ -523,6 +526,15 @@ private fun LazyListScope.sourceSection(
|
||||
)
|
||||
IconCard(action = onAdd, resId = R.drawable.ic_add)
|
||||
}
|
||||
// States what the order below actually decides: for satellites the elements come from
|
||||
// whichever enabled source has the freshest epoch, so the order only picks the name.
|
||||
Text(
|
||||
text = stringResource(hintResId),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 2.dp, bottom = 8.dp)
|
||||
)
|
||||
}
|
||||
itemsIndexed(urls, key = { _, entry -> "$sectionKey-${entry.first}" }) { index, (id, url) ->
|
||||
val enabledTint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[versions]
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
appVersionCode = "557"
|
||||
appVersionCode = "560"
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
appVersionName = "4.4.8-ba7opf.4"
|
||||
appVersionName = "4.4.8-ba7opf.6"
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
compileSdk = "37"
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
|
||||
Reference in new issue
Block a user