From cba5eb7efe206e79a3d30b476d24bb9031b4937e Mon Sep 17 00:00:00 2001 From: atsunatsu Date: Sat, 3 Oct 2026 03:02:00 +0800 Subject: [PATCH] =?UTF-8?q?feat(data):=20=E5=A4=9A=E6=BA=90=E6=98=9F?= =?UTF-8?q?=E5=8E=86=E6=8C=89=E5=8E=86=E5=85=83=E5=8F=96=E6=9C=80=E6=96=B0?= =?UTF-8?q?=EF=BC=88=E5=AF=B9=E9=BD=90=E4=B8=8A=E6=B8=B8=EF=BC=89=EF=BC=8C?= =?UTF-8?q?=E6=BA=90=E9=A1=BA=E5=BA=8F=E5=8F=AA=E5=BD=B1=E5=93=8D=E5=8D=AB?= =?UTF-8?q?=E6=98=9F=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 采用上游 6f3e3351 的合并口径:同一 NORAD 号的轨道元素取历元最新者,名字取 列表中第一个提供该星的源,源顺序因此退化为命名偏好。fork 此前是 flatMap + distinctBy{catnum}(列表靠前者全胜)——实测 R4UAB 的元素平均比 CelesTrak 旧 8.4h(78 颗共有业余星中 67 颗更旧),却因排在列表前面而抢占。 - OrbitalData:暴露 epochDaynum / timeToDaynum / epochToDaynum(跨源比较必须 走 daynum,原始 YYDDD 跨十年不可比) - DatabaseRepo:新增 mergeEntries(历元最新元素 + 首个提供者的名字)、 insertFresherEntries(只写入更新过的元素,并对未更新者原地改名,使重排源 顺序立即生效)、pruneStaleEntries(30 天无源刷新的星清理) - updateFromRemote 改为按源分别解析;保留 fork 的 importedTypeIds 类型映射 (按每源结果统计,供筛选 chips 与自定义源使用);全部源失败时保留上次成功 时间戳,下次启动重试 - updateTLEFromFile 走同一合并链路(手工导入同样只升不降) - 新增 5 个单测(含 prune 与失败保时间戳),fixture 历元改为当前(旧历元 会被新 prune 规则清掉);两个 fake 补齐 ILocalSource 新接口 测试:core:domain 171 / core:data 121 全绿。 --- .../core/data/database/Look4SatDao.kt | 13 ++ .../core/data/repository/DatabaseRepo.kt | 77 ++++++++- .../look4sat/core/data/source/LocalSource.kt | 10 ++ .../core/data/repository/DatabaseRepoTest.kt | 150 +++++++++++++++++- .../repository/SelectionRepoSearchTest.kt | 4 + .../core/domain/predict/OrbitalData.kt | 46 ++++-- .../core/domain/source/ILocalSource.kt | 4 + 7 files changed, 277 insertions(+), 27 deletions(-) diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/database/Look4SatDao.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/database/Look4SatDao.kt index 2e7a0219..5f004a6a 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/database/Look4SatDao.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/database/Look4SatDao.kt @@ -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): List + @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) + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertEntries(entries: List) 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 b61b04e6..c45c08b7 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 @@ -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 { + val preferredNames = mutableMapOf() + val freshestEntries = mutableMapOf() + 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) { + 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) diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/source/LocalSource.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/source/LocalSource.kt index baba33e1..4e20ef0b 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/source/LocalSource.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/source/LocalSource.kt @@ -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) = + names.forEach { (catnum, name) -> look4SatDao.renameEntry(catnum, name) } + + override suspend fun deleteEntriesWithIds(ids: List) = + ids.chunked(999).forEach { idsPart -> look4SatDao.deleteEntriesWithIds(idsPart) } + override suspend fun insertEntries(entries: List) = look4SatDao.insertEntries(entries.toEntity()) override suspend fun deleteEntries() = look4SatDao.deleteEntries() 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 e6b2833f..a5ed56cc 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 @@ -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): List = emptyList() + override suspend fun getEntriesEpochs(): Map = + insertedEntries.associate { entry -> entry.catnum to entry.epoch } + + override suspend fun getEntriesNames(): Map = + insertedEntries.associate { entry -> entry.catnum to entry.name } + + override suspend fun renameEntries(names: Map) = 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) { + insertedEntries.removeAll { entry -> entry.catnum in ids } + } + override suspend fun insertEntries(entries: List) { insertedEntries += entries } diff --git a/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/SelectionRepoSearchTest.kt b/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/SelectionRepoSearchTest.kt index 65668211..8a961265 100644 --- a/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/SelectionRepoSearchTest.kt +++ b/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/SelectionRepoSearchTest.kt @@ -282,6 +282,10 @@ private class FakeLocalSourceForSearch( override suspend fun getEntriesTotal(): Int = items.size override suspend fun getEntriesList(): List = items override suspend fun getEntriesWithIds(ids: List): List = emptyList() + override suspend fun getEntriesEpochs(): Map = emptyMap() + override suspend fun getEntriesNames(): Map = emptyMap() + override suspend fun renameEntries(names: Map) = Unit + override suspend fun deleteEntriesWithIds(ids: List) = Unit override suspend fun insertEntries(entries: List) = Unit override suspend fun deleteEntries() = Unit override suspend fun getIdsWithModes(modes: List): List = sstvIds diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/predict/OrbitalData.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/predict/OrbitalData.kt index c9336cee..90c26530 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/predict/OrbitalData.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/predict/OrbitalData.kt @@ -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 + } } } diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/source/ILocalSource.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/source/ILocalSource.kt index b23ae86e..2b975bf3 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/source/ILocalSource.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/source/ILocalSource.kt @@ -26,6 +26,10 @@ interface ILocalSource { suspend fun getEntriesTotal(): Int suspend fun getEntriesList(): List suspend fun getEntriesWithIds(ids: List): List + suspend fun getEntriesEpochs(): Map + suspend fun getEntriesNames(): Map + suspend fun renameEntries(names: Map) + suspend fun deleteEntriesWithIds(ids: List) suspend fun insertEntries(entries: List) suspend fun deleteEntries() suspend fun getIdsWithModes(modes: List): List