Extracted implementation classes into data module

This commit is contained in:
Arty Bishop committed 2025-12-27 15:01:45 +00:00
1 parent 5a449d4133
commit 28e926eb86
54 files changed
+247 -153

No files matched your search

@@ -1,157 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.data
import com.rtbishop.look4sat.domain.model.OrbitalData
import com.rtbishop.look4sat.domain.model.SatRadio
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.json.JSONObject
import java.io.InputStream
import kotlin.math.pow
class DataParser(private val dispatcher: CoroutineDispatcher) {
suspend fun parseCSVStream(stream: InputStream): List<OrbitalData> = withContext(dispatcher) {
val parsedItems = mutableListOf<OrbitalData>()
stream.bufferedReader().useLines { lines ->
lines.forEachIndexed { index, line ->
if (index != 0) {
val values = line.split(",")
parseCSV(values)?.let { tle -> parsedItems.add(tle) }
}
}
}
return@withContext parsedItems
}
suspend fun parseTLEStream(stream: InputStream): List<OrbitalData> = withContext(dispatcher) {
val tleStrings = mutableListOf(String(), String(), String())
val parsedItems = mutableListOf<OrbitalData>()
var lineIndex = 0
stream.bufferedReader().forEachLine { line ->
tleStrings[lineIndex] = line
if (lineIndex < 2) {
lineIndex++
} else {
val isLineOneValid = tleStrings[1].substring(0, 1) == "1"
val isLineTwoValid = tleStrings[2].substring(0, 1) == "2"
if (!isLineOneValid && !isLineTwoValid) return@forEachLine
parseTLE(tleStrings)?.let { tle -> parsedItems.add(tle) }
lineIndex = 0
}
}
return@withContext parsedItems
}
suspend fun parseJSONStream(stream: InputStream): List<SatRadio> = withContext(dispatcher) {
val parsedItems = mutableListOf<SatRadio>()
try {
val jsonArray = JSONArray(stream.bufferedReader().readText())
for (index in 0 until jsonArray.length()) {
val jsonObject = jsonArray.getJSONObject(index)
parseJSON(jsonObject)?.let { parsedItems.add(it) }
}
return@withContext parsedItems
} catch (exception: Exception) {
return@withContext parsedItems
}
}
private fun parseCSV(values: List<String>): OrbitalData? {
try {
val name = values[0]
val year = values[2].substring(0, 4)
val month = values[2].substring(5, 7)
val dayOfMonth = values[2].substring(8, 10)
val dayInt = getDayOfYear(year.toInt(), month.toInt(), dayOfMonth.toInt())
val day = if (dayInt < 10) "00$dayInt" else if (dayInt < 100) "0$dayInt" else "$dayInt"
val hour = values[2].substring(11, 13).toInt() * 3600000 // ms in one hour
val min = values[2].substring(14, 16).toInt() * 60000 // ms in one minute
val sec = values[2].substring(17, 19).toInt() * 1000 // ms in one second
val ms = values[2].substring(20, 26).toInt() / 1000.0 // microseconds to ms
val frac = ((hour + min + sec + ms) / 86400000.0).toString()
val epoch = "${year.substring(2)}$day${frac.substring(1)}".toDouble()
val meanmo = values[3].toDouble()
val eccn = values[4].toDouble()
val incl = values[5].toDouble()
val raan = values[6].toDouble()
val argper = values[7].toDouble()
val meanan = values[8].toDouble()
val catnum = values[11].toInt()
val bstar = values[14].toDouble()
return OrbitalData(name, epoch, meanmo, eccn, incl, raan, argper, meanan, catnum, bstar)
} catch (exception: Exception) {
return null
}
}
private fun parseTLE(tle: List<String>): OrbitalData? {
if (tle[1].substring(0, 1) != "1" && tle[2].substring(0, 1) != "2") {
return null
}
try {
val name: String = tle[0].trim()
val epoch: Double = tle[1].substring(18, 32).toDouble()
val meanmo: Double = tle[2].substring(52, 63).toDouble()
val eccn: Double = tle[2].substring(26, 33).toDouble() / 10000000.0
val incl: Double = tle[2].substring(8, 16).toDouble()
val raan: Double = tle[2].substring(17, 25).toDouble()
val argper: Double = tle[2].substring(34, 42).toDouble()
val meanan: Double = tle[2].substring(43, 51).toDouble()
val catnum: Int = tle[1].substring(2, 7).trim().toInt()
val bstar: Double = 1.0e-5 * tle[1].substring(53, 59).toDouble() / 10.0.pow(
tle[1].substring(60, 61).toDouble()
)
return OrbitalData(name, epoch, meanmo, eccn, incl, raan, argper, meanan, catnum, bstar)
} catch (exception: Exception) {
return null
}
}
private fun parseJSON(json: JSONObject): SatRadio? {
try {
val uuid = json.getString("uuid")
val info = json.getString("description")
val isAlive = json.getBoolean("alive")
val downlink = if (json.isNull("downlink_low")) null
else json.getLong("downlink_low")
val uplink = if (json.isNull("uplink_low")) null
else json.getLong("uplink_low")
val mode = if (json.isNull("mode")) null
else json.getString("mode")
val isInverted = json.getBoolean("invert")
val catnum = if (json.isNull("norad_cat_id")) null
else json.getInt("norad_cat_id")
return SatRadio(uuid, info, isAlive, downlink, uplink, mode, isInverted, catnum)
} catch (exception: Exception) {
return null
}
}
private fun getDayOfYear(year: Int, month: Int, dayOfMonth: Int): Int {
if (month == 1) return dayOfMonth
val daysArray = arrayOf(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
var dayOfYear = dayOfMonth
// If leap year increment Feb days
if (((year / 4 == 0) && (year / 100 != 0)) || (year / 400 == 0)) daysArray[1]++
for (i in 0 until month - 1) dayOfYear += daysArray[i]
return dayOfYear
}
}
@@ -1,106 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.data
import com.rtbishop.look4sat.domain.model.DatabaseState
import com.rtbishop.look4sat.domain.model.SatEntry
import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.domain.predict.IDatabaseRepo
import com.rtbishop.look4sat.domain.predict.ISettingsRepo
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
import java.io.InputStream
import java.util.zip.ZipInputStream
class DatabaseRepo(
private val dispatcher: CoroutineDispatcher,
private val dataParser: DataParser,
private val dataSource: IDataSource,
private val localStorage: ILocalStorage,
private val settingsRepository: ISettingsRepo
) : IDatabaseRepo {
override suspend fun updateFromFile(uri: String) = withContext(dispatcher) {
val importedSatellites = dataSource.getFileStream(uri)?.let { importSatellites(it) }
importedSatellites?.let { localStorage.insertEntries(it) }
setUpdateSuccessful(System.currentTimeMillis())
}
override suspend fun updateFromWeb() = withContext(dispatcher) {
val sourcesMap = settingsRepository.satelliteSourcesMap
val importedEntries = mutableListOf<SatEntry>()
val importedRadios = mutableListOf<SatRadio>()
// fetch
val jobsMap = sourcesMap.mapValues { async { dataSource.getNetworkStream(it.value) } }
val jobRadios = async { dataSource.getNetworkStream(settingsRepository.radioSourceUrl) }
// parse
jobsMap.mapValues { job -> job.value.await() }.forEach { entry ->
entry.value?.let { stream ->
when (val type = entry.key) {
"AMSAT", "R4UAB" -> {
// parse tle stream
val satellites = importSatellites(stream)
val catnums = satellites.map { it.data.catnum }
settingsRepository.saveSatType(type, catnums)
importedEntries.addAll(satellites)
}
"McCants", "Classified" -> {
// unzip and parse tle stream
val unzipped = ZipInputStream(stream).apply { nextEntry }
val satellites = importSatellites(unzipped)
val catnums = satellites.map { it.data.catnum }
settingsRepository.saveSatType(type, catnums)
importedEntries.addAll(satellites)
}
else -> {
// parse csv stream
val parsed = dataParser.parseCSVStream(stream)
val satellites = parsed.map { data -> SatEntry(data) }
val catnums = satellites.map { it.data.catnum }
settingsRepository.saveSatType(type, catnums)
importedEntries.addAll(satellites)
}
}
}
}
jobRadios.await()?.let { importedRadios.addAll(dataParser.parseJSONStream(it)) }
// insert
localStorage.insertEntries(importedEntries)
localStorage.insertRadios(importedRadios)
setUpdateSuccessful(System.currentTimeMillis())
}
override suspend fun clearAllData() = withContext(dispatcher) {
localStorage.deleteEntries()
localStorage.deleteRadios()
setUpdateSuccessful(0L)
}
private suspend fun setUpdateSuccessful(timestamp: Long) = withContext(dispatcher) {
val satellitesTotal = localStorage.getEntriesTotal()
val radiosTotal = localStorage.getRadiosTotal()
settingsRepository.saveDatabaseState(DatabaseState(satellitesTotal, radiosTotal, timestamp))
}
private suspend fun importSatellites(stream: InputStream): List<SatEntry> {
return dataParser.parseTLEStream(stream).map { data -> SatEntry(data) }
}
}
@@ -1,235 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.data
import com.rtbishop.look4sat.domain.model.GeoPos
import com.rtbishop.look4sat.domain.model.SatPass
import com.rtbishop.look4sat.domain.model.SatPos
import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.domain.predict.ISatelliteRepo
import com.rtbishop.look4sat.domain.predict.ISettingsRepo
import com.rtbishop.look4sat.domain.predict.Satellite
import com.rtbishop.look4sat.domain.utility.round
import com.rtbishop.look4sat.domain.utility.toDegrees
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext
class SatelliteRepo(
private val dispatcher: CoroutineDispatcher,
private val localStorage: ILocalStorage,
private val settingsRepo: ISettingsRepo
) : ISatelliteRepo {
private val _passes = MutableStateFlow<List<SatPass>>(emptyList())
override val passes: StateFlow<List<SatPass>> = _passes
private val _satellites = MutableStateFlow<List<Satellite>>(emptyList())
override val satellites: StateFlow<List<Satellite>> = _satellites
override suspend fun getRadiosWithId(id: Int) = localStorage.getRadiosWithId(id)
override suspend fun initRepository() = withContext(dispatcher) {
settingsRepo.satelliteSelection.collect { selectedIds ->
_satellites.update { localStorage.getEntriesWithIds(selectedIds) }
val (hoursAhead, minElevation) = settingsRepo.passesSettings.value
calculatePasses(System.currentTimeMillis(), hoursAhead, minElevation)
}
}
override suspend fun getPosition(sat: Satellite, pos: GeoPos, time: Long): SatPos {
return withContext(dispatcher) { sat.getPosition(pos, time) }
}
override suspend fun getTrack(
sat: Satellite, pos: GeoPos, start: Long, end: Long
): List<SatPos> {
return withContext(dispatcher) {
val positions = mutableListOf<SatPos>()
var currentTime = start
while (currentTime < end) {
positions.add(sat.getPosition(pos, currentTime))
currentTime += 15000
}
positions
}
}
override suspend fun processRadios(
sat: Satellite, pos: GeoPos, radios: List<SatRadio>, time: Long
): List<SatRadio> {
return withContext(dispatcher) {
val satPos = sat.getPosition(pos, time)
val copiedList = radios.map { it.copy() }
copiedList.forEach { transmitter ->
transmitter.downlink?.let { transmitter.downlink = satPos.getDownlinkFreq(it) }
transmitter.uplink?.let { transmitter.uplink = satPos.getUplinkFreq(it) }
}
copiedList.map { it.copy() }
}
}
override suspend fun processPasses(passList: List<SatPass>, time: Long): List<SatPass> {
return withContext(dispatcher) {
passList.forEach { pass ->
if (!pass.isDeepSpace) {
val timeStart = pass.aosTime
if (time > timeStart) {
val deltaNow = time.minus(timeStart).toFloat()
val deltaTotal = pass.losTime.minus(timeStart).toFloat()
pass.progress = (deltaNow / deltaTotal).round(2)
}
}
}
passList.filter { pass -> pass.progress < 1.0 }.map { it.copy() }
}
}
override suspend fun calculatePasses(time: Long, hoursAhead: Int, minElevation: Double) {
if (_satellites.value.isNotEmpty()) {
withContext(dispatcher) {
val allPasses = mutableListOf<SatPass>()
val stationPos = settingsRepo.stationPosition.value
_satellites.value.forEach { satellite ->
allPasses.addAll(satellite.getPasses(stationPos, time, hoursAhead))
}
_passes.update { allPasses.filter(time, hoursAhead, minElevation) }
}
} else {
_passes.update { emptyList() }
}
}
private fun Satellite.getPasses(pos: GeoPos, time: Long, hours: Int): List<SatPass> {
val passes = mutableListOf<SatPass>()
val endDate = time + hours * 60L * 60L * 1000L
val quarterOrbitMin = (this.data.orbitalPeriod / 4.0).toInt()
var startDate = time
var shouldRewind = true
var lastAosDate: Long
var count = 0
if (this.willBeSeen(pos)) {
if (this.data.isDeepSpace) {
passes.add(getGeoPass(this, pos, time))
} else {
do {
if (count > 0) shouldRewind = false
val pass = getLeoPass(this, pos, startDate, shouldRewind)
lastAosDate = pass.aosTime
passes.add(pass)
startDate = pass.losTime + (quarterOrbitMin * 3) * 60L * 1000L
count++
} while (lastAosDate < endDate)
}
}
return passes
}
private fun List<SatPass>.filter(time: Long, hoursAhead: Int, minElev: Double): List<SatPass> {
val timeFuture = time + (hoursAhead * 60L * 60L * 1000L)
return this.filter { it.losTime > time }.filter { it.aosTime < timeFuture }
.filter { it.maxElevation > minElev }.sortedBy { it.aosTime }
}
private fun getGeoPass(sat: Satellite, pos: GeoPos, time: Long): SatPass {
val satPos = sat.getPosition(pos, time)
val aos = time - 24 * 60L * 60L * 1000L
val los = time + 24 * 60L * 60L * 1000L // val tca = (aos + los) / 2
val az = satPos.azimuth.toDegrees().round(1)
val elev = satPos.elevation.toDegrees().round(1)
val alt = satPos.altitude
return SatPass(aos, az, los, az, alt.toInt(), elev, sat)
}
private fun getLeoPass(sat: Satellite, pos: GeoPos, time: Long, rewind: Boolean): SatPass {
val quarterOrbitMin = (sat.data.orbitalPeriod / 4.0).toInt()
var calendarTimeMillis = time
var elevation: Double
var maxElevation = 0.0
var alt = 0.0 // var tcaAz = 0.0
// rewind 1/4 of an orbit
if (rewind) calendarTimeMillis += -quarterOrbitMin * 60L * 1000L
var satPos = sat.getPosition(pos, calendarTimeMillis)
if (satPos.elevation > 0.0) {
// move forward in 30 second intervals until the sat goes below the horizon
do {
calendarTimeMillis += 30 * 1000L
satPos = sat.getPosition(pos, calendarTimeMillis)
} while (satPos.elevation > 0.0)
// move forward 3/4 of an orbit
calendarTimeMillis += quarterOrbitMin * 3 * 60L * 1000L
}
// find the next time sat comes above the horizon
do {
calendarTimeMillis += 60L * 1000L
satPos = sat.getPosition(pos, calendarTimeMillis)
elevation = satPos.elevation
if (elevation > maxElevation) {
maxElevation = elevation
alt = satPos.altitude // tcaAz = satPos.azimuth.toDegrees()
}
} while (satPos.elevation < 0.0)
// refine to 3 seconds
calendarTimeMillis += -60L * 1000L
do {
calendarTimeMillis += 3L * 1000L
satPos = sat.getPosition(pos, calendarTimeMillis)
elevation = satPos.elevation
if (elevation > maxElevation) {
maxElevation = elevation
alt = satPos.altitude // tcaAz = satPos.azimuth.toDegrees()
}
} while (satPos.elevation < 0.0)
val aos = satPos.time
val aosAz = satPos.azimuth.toDegrees().round(1)
// find when sat goes below
do {
calendarTimeMillis += 30L * 1000L
satPos = sat.getPosition(pos, calendarTimeMillis)
elevation = satPos.elevation
if (elevation > maxElevation) {
maxElevation = elevation
alt = satPos.altitude // tcaAz = satPos.azimuth.toDegrees()
}
} while (satPos.elevation > 0.0)
// refine to 3 seconds
calendarTimeMillis += -30L * 1000L
do {
calendarTimeMillis += 3L * 1000L
satPos = sat.getPosition(pos, calendarTimeMillis)
elevation = satPos.elevation
if (elevation > maxElevation) {
maxElevation = elevation
alt = satPos.altitude // tcaAz = satPos.azimuth.toDegrees()
}
} while (satPos.elevation > 0.0)
val los = satPos.time // val tca = (aos + los) / 2
val losAz = satPos.azimuth.toDegrees().round(1)
val elev = maxElevation.toDegrees().round(1)
return SatPass(aos, aosAz, los, losAz, alt.toInt(), elev, sat)
}
}
@@ -1,80 +0,0 @@
package com.rtbishop.look4sat.domain.data
import com.rtbishop.look4sat.domain.model.SatItem
import com.rtbishop.look4sat.domain.predict.ISelectionRepo
import com.rtbishop.look4sat.domain.predict.ISettingsRepo
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
@OptIn(ExperimentalCoroutinesApi::class)
class SelectionRepo(
private val dispatcher: CoroutineDispatcher,
private val localStorage: ILocalStorage,
private val settingsRepo: ISettingsRepo
) : ISelectionRepo {
private val currentItems = MutableStateFlow<List<SatItem>>(emptyList())
private val currentType = MutableStateFlow("All")
private val currentQuery = MutableStateFlow("")
private val itemsWithType = currentType.flatMapLatest { type ->
currentItems.map { items -> items.filterByType(type) }
}
private val itemsWithQuery = currentQuery.flatMapLatest { query ->
itemsWithType.map { items -> items.filterByQuery(query) }
}
override fun getCurrentType() = currentType.value
override fun getTypesList() = settingsRepo.satelliteSourcesMap.keys.sorted()
override suspend fun getEntriesFlow() = withContext(dispatcher) {
val selectedIds = settingsRepo.satelliteSelection.value
currentItems.value = localStorage.getEntriesList().map { item ->
item.copy(isSelected = item.catnum in selectedIds)
}
return@withContext itemsWithQuery
}
override suspend fun setType(type: String) = withContext(dispatcher) {
currentType.value = type
}
override suspend fun setQuery(query: String) = withContext(dispatcher) {
currentQuery.value = query
}
override suspend fun setSelection(selectAll: Boolean) = withContext(dispatcher) {
setSelection(itemsWithQuery.first().map { item -> item.catnum }, selectAll)
}
override suspend fun setSelection(ids: List<Int>, isTicked: Boolean) = withContext(dispatcher) {
currentItems.value = currentItems.value.map { item ->
if (item.catnum in ids) item.copy(isSelected = isTicked) else item
}
}
override suspend fun saveSelection() = withContext(dispatcher) {
val currentSelection = currentItems.value.filter { it.isSelected }.map { it.catnum }
settingsRepo.saveEntriesSelection(currentSelection)
}
private suspend fun List<SatItem>.filterByType(type: String) = withContext(dispatcher) {
if (type == "All") return@withContext this@filterByType
val catnums = settingsRepo.loadSatType(type)
if (catnums.isEmpty()) return@withContext this@filterByType
return@withContext this@filterByType.filter { item -> item.catnum in catnums }
}
private suspend fun List<SatItem>.filterByQuery(query: String) = withContext(dispatcher) {
if (query.isBlank()) return@withContext this@filterByQuery
return@withContext try {
this@filterByQuery.filter { it.catnum == query.toInt() }
} catch (e: Exception) {
this@filterByQuery.filter { item -> item.name.lowercase().contains(query.lowercase()) }
}
}
}
@@ -17,4 +17,6 @@
*/
package com.rtbishop.look4sat.domain.model
import com.rtbishop.look4sat.domain.predict.OrbitalData
data class SatEntry(val data: OrbitalData, var comment: String? = null)
@@ -17,7 +17,6 @@
*/
package com.rtbishop.look4sat.domain.predict
import com.rtbishop.look4sat.domain.model.OrbitalData
import kotlin.math.abs
import kotlin.math.atan2
import kotlin.math.cos
@@ -67,16 +66,16 @@ class DeepSpaceSatellite(data: OrbitalData) : Satellite(data) {
val coef = qoms24 * tsi.pow(4.0)
val coef1 = coef / psisq.pow(3.5)
val c2 = coef1 * dsv.xnodp * (dsv.aodp * (1.0 + 1.5 * etasq + eeta * (4.0 + etasq))
+ 0.75 * CK2 * tsi / psisq * x3thm1 * (8.0 + 3.0 * etasq * (8.0 + etasq)))
+ 0.75 * CK2 * tsi / psisq * x3thm1 * (8.0 + 3.0 * etasq * (8.0 + etasq)))
c1 = data.bstar * c2
dsv.sinio = sin(data.xincl)
val a3ovk2 = -J3_HARMONIC / CK2
x1mth2 = 1.0 - dsv.theta2
c4 =
2 * dsv.xnodp * coef1 * dsv.aodp * dsv.betao2 * (eta * (2.0 + 0.5 * etasq) + data.eccn
* (0.5 + 2 * etasq) - 2 * CK2 * tsi / (dsv.aodp * psisq)
* (-3 * x3thm1 * (1.0 - 2 * eeta + etasq * (1.5 - 0.5 * eeta)) + (0.75 * x1mth2
* (2.0 * etasq - eeta * (1.0 + etasq)) * cos(2.0 * data.omegao))))
* (0.5 + 2 * etasq) - 2 * CK2 * tsi / (dsv.aodp * psisq)
* (-3 * x3thm1 * (1.0 - 2 * eeta + etasq * (1.5 - 0.5 * eeta)) + (0.75 * x1mth2
* (2.0 * etasq - eeta * (1.0 + etasq)) * cos(2.0 * data.omegao))))
val theta4 = dsv.theta2 * dsv.theta2
val temp1 = 3.0 * CK2 * pinvsq * dsv.xnodp
val temp2 = temp1 * CK2 * pinvsq
@@ -180,6 +179,7 @@ class DeepSpaceSatellite(data: OrbitalData) : Satellite(data) {
var omgdot = 0.0
var xnodot = 0.0
var xnodp = 0.0
// Used by dpsec and dpper parts of Deep()
var xll = 0.0
var omgadf = 0.0
@@ -188,6 +188,7 @@ class DeepSpaceSatellite(data: OrbitalData) : Satellite(data) {
var xinc = 0.0
var xn = 0.0
var t = 0.0
// Used by thetg and Deep()
var ds50 = 0.0
}
@@ -222,6 +223,7 @@ class DeepSpaceSatellite(data: OrbitalData) : Satellite(data) {
private val omegaq: Double
private var zmol = 0.0
private var zmos = 0.0
// Many fields below cannot be final because they are iteratively refined
private var savtsn = 0.0
private var ee2 = 0.0
@@ -796,16 +798,16 @@ class DeepSpaceSatellite(data: OrbitalData) : Satellite(data) {
x2li = xli + xli
xndot =
d2201 * sin(x2omi + xli - g22) + d2211 * sin(xli - g22) + (d3210
* sin(xomi + xli - g32)) + d3222 * sin(-xomi + xli - g32) + (d4410
* sin(x2omi + x2li - g44)) + d4422 * sin(x2li - g44) + (d5220
* sin(xomi + xli - g52)) + d5232 * sin(-xomi + xli - g52) + (d5421
* sin(xomi + x2li - g54)) + d5433 * sin(-xomi + x2li - g54)
* sin(xomi + xli - g32)) + d3222 * sin(-xomi + xli - g32) + (d4410
* sin(x2omi + x2li - g44)) + d4422 * sin(x2li - g44) + (d5220
* sin(xomi + xli - g52)) + d5232 * sin(-xomi + xli - g52) + (d5421
* sin(xomi + x2li - g54)) + d5433 * sin(-xomi + x2li - g54)
xnddt =
d2201 * cos(x2omi + xli - g22) + d2211 * cos(xli - g22) + (d3210
* cos(xomi + xli - g32)) + d3222 * cos(-xomi + xli - g32) + (d5220
* cos(xomi + xli - g52)) + d5232 * cos(-xomi + xli - g52) + (2
* (d4410 * cos(x2omi + x2li - g44) + d4422 * cos(x2li - g44) + (d5421
* cos(xomi + x2li - g54)) + d5433 * cos(-xomi + x2li - g54)))
* cos(xomi + xli - g32)) + d3222 * cos(-xomi + xli - g32) + (d5220
* cos(xomi + xli - g52)) + d5232 * cos(-xomi + xli - g52) + (2
* (d4410 * cos(x2omi + x2li - g44) + d4422 * cos(x2li - g44) + (d5421
* cos(xomi + x2li - g54)) + d5433 * cos(-xomi + x2li - g54)))
}
xldot = xni + xfact
xnddt *= xldot
@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.model
package com.rtbishop.look4sat.domain.predict
data class GeoPos(
val latitude: Double,
@@ -17,7 +17,6 @@
*/
package com.rtbishop.look4sat.domain.predict
import com.rtbishop.look4sat.domain.model.OrbitalData
import kotlin.math.abs
import kotlin.math.atan2
import kotlin.math.cos
@@ -88,7 +87,7 @@ class NearEarthSatellite(data: OrbitalData) : Satellite(data) {
val coef1 = coef / psisq.pow(3.5)
val bstar = data.bstar
val c2 = coef1 * xnodp * (aodp * (1.0 + 1.5 * etasq + eeta * (4.0 + etasq)) + 0.75
* CK2 * tsi / psisq * x3thm1 * (8.0 + 3.0 * etasq * (8.0 + etasq)))
* CK2 * tsi / psisq * x3thm1 * (8.0 + 3.0 * etasq * (8.0 + etasq)))
c1 = bstar * c2
sinio = sin(data.xincl)
val a3ovk2 = -J3_HARMONIC / CK2
@@ -96,9 +95,9 @@ class NearEarthSatellite(data: OrbitalData) : Satellite(data) {
x1mth2 = 1.0 - theta2
val omegao = data.omegao
c4 = 2 * xnodp * coef1 * aodp * betao2 * (eta * (2.0 + 0.5 * etasq) + eo * (0.5 + 2 * etasq)
- 2 * CK2 * tsi / (aodp * psisq) * (-3 * x3thm1 * (1.0 - 2 * eeta + etasq
* (1.5 - 0.5 * eeta)) + 0.75 * x1mth2 * (2.0 * etasq - eeta * (1.0 + etasq))
* cos(2.0 * omegao)))
- 2 * CK2 * tsi / (aodp * psisq) * (-3 * x3thm1 * (1.0 - 2 * eeta + etasq
* (1.5 - 0.5 * eeta)) + 0.75 * x1mth2 * (2.0 * etasq - eeta * (1.0 + etasq))
* cos(2.0 * omegao)))
c5 = 2.0 * coef1 * aodp * betao2 * (1.0 + 2.75 * (etasq + eeta) + eeta * etasq)
val theta4 = sqr(theta2)
val temp1 = 3.0 * CK2 * pinvsq * xnodp
@@ -15,14 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.model
import com.rtbishop.look4sat.domain.predict.DEG2RAD
import com.rtbishop.look4sat.domain.predict.DeepSpaceSatellite
import com.rtbishop.look4sat.domain.predict.MIN_PER_DAY
import com.rtbishop.look4sat.domain.predict.NearEarthSatellite
import com.rtbishop.look4sat.domain.predict.Satellite
import com.rtbishop.look4sat.domain.predict.TWO_PI
package com.rtbishop.look4sat.domain.predict
data class OrbitalData(
val name: String,
@@ -44,6 +37,5 @@ data class OrbitalData(
// Space objects are classified as NearEarth (period < 225 min) or DeepSpace (period >= 225 min)
val isDeepSpace: Boolean = orbitalPeriod >= 225.0
) {
fun getSatellite(): Satellite =
if (isDeepSpace) DeepSpaceSatellite(this) else NearEarthSatellite(this)
fun getSatellite(): Satellite = if (isDeepSpace) DeepSpaceSatellite(this) else NearEarthSatellite(this)
}
@@ -15,9 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.model
import com.rtbishop.look4sat.domain.predict.Satellite
package com.rtbishop.look4sat.domain.predict
data class SatPass(
val aosTime: Long = 0L,
@@ -15,12 +15,8 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.model
package com.rtbishop.look4sat.domain.predict
import com.rtbishop.look4sat.domain.predict.DEG2RAD
import com.rtbishop.look4sat.domain.predict.EARTH_RADIUS
import com.rtbishop.look4sat.domain.predict.RAD2DEG
import com.rtbishop.look4sat.domain.predict.SPEED_OF_LIGHT
import kotlin.math.acos
import kotlin.math.asin
import kotlin.math.atan2
@@ -67,8 +63,7 @@ data class SatPos(
val rads = azimuth * DEG2RAD
val lat = asin(sin(latitude) * cos(beta) + (cos(latitude) * sin(beta) * cos(rads)))
val lon = (longitude + atan2(
sin(rads) * sin(beta) * cos(latitude),
cos(beta) - sin(latitude) * sin(lat)
sin(rads) * sin(beta) * cos(latitude), cos(beta) - sin(latitude) * sin(lat)
))
rangeCirclePoints.add(GeoPos(lat * RAD2DEG, lon * RAD2DEG))
}
@@ -17,9 +17,6 @@
*/
package com.rtbishop.look4sat.domain.predict
import com.rtbishop.look4sat.domain.model.GeoPos
import com.rtbishop.look4sat.domain.model.OrbitalData
import com.rtbishop.look4sat.domain.model.SatPos
import kotlin.math.abs
import kotlin.math.acos
import kotlin.math.asin
@@ -64,7 +61,7 @@ abstract class Satellite(val data: OrbitalData) {
var qoms24 = 0.0
var s4 = 0.0
internal fun willBeSeen(pos: GeoPos): Boolean {
fun willBeSeen(pos: GeoPos): Boolean {
return if (data.meanmo < 1e-8) false
else {
val sma = 331.25 * exp(ln(MIN_PER_DAY / data.meanmo) * (2.0 / 3.0))
@@ -75,7 +72,7 @@ abstract class Satellite(val data: OrbitalData) {
}
}
internal fun getPosition(pos: GeoPos, time: Long): SatPos {
fun getPosition(pos: GeoPos, time: Long): SatPos {
satPos = SatPos()
// Date/time at which the position and velocity were calculated
julUTC = calcCurrentDaynum(time) + 2444238.5
@@ -352,7 +349,7 @@ abstract class Satellite(val data: OrbitalData) {
val e = 0.01675104 - (0.0000418 + 0.000000126 * solTime) * solTime
val c = radians(
((1.919460 - (0.004789 + 0.000014 * solTime) * solTime) * sin(m))
+ ((0.020094 - 0.000100 * solTime) * sin(2 * m)) + 0.000293 * sin(3 * m)
+ ((0.020094 - 0.000100 * solTime) * sin(2 * m)) + 0.000293 * sin(3 * m)
)
val o = radians(modulus(259.18 - 1934.142 * solTime, 360.0))
val lsa = modulus(l + c - radians(0.00569 - 0.00479 * sin(o)), TWO_PI)
@@ -360,7 +357,7 @@ abstract class Satellite(val data: OrbitalData) {
var r = (1.0000002 * (1.0 - sqr(e)) / (1.0 + e * cos(nu)))
val eps = radians(
23.452294 - (0.0130125 + (0.00000164 - 0.000000503 * solTime) * solTime)
* solTime + 0.00256 * cos(o)
* solTime + 0.00256 * cos(o)
)
r *= ASTRONOMICAL_UNIT
return Vector4(r, r * cos(lsa), r * sin(lsa) * cos(eps), r * sin(lsa) * sin(eps))
@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.predict
package com.rtbishop.look4sat.domain.repository
interface IDatabaseRepo {
@@ -1,9 +1,10 @@
package com.rtbishop.look4sat.domain.predict
package com.rtbishop.look4sat.domain.repository
import com.rtbishop.look4sat.domain.model.GeoPos
import com.rtbishop.look4sat.domain.model.SatPass
import com.rtbishop.look4sat.domain.model.SatPos
import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.domain.predict.SatPass
import com.rtbishop.look4sat.domain.predict.SatPos
import com.rtbishop.look4sat.domain.predict.Satellite
import kotlinx.coroutines.flow.StateFlow
interface ISatelliteRepo {
@@ -1,4 +1,4 @@
package com.rtbishop.look4sat.domain.predict
package com.rtbishop.look4sat.domain.repository
import com.rtbishop.look4sat.domain.model.SatItem
import kotlinx.coroutines.flow.Flow
@@ -15,12 +15,12 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.predict
package com.rtbishop.look4sat.domain.repository
import com.rtbishop.look4sat.domain.model.DatabaseState
import com.rtbishop.look4sat.domain.model.GeoPos
import com.rtbishop.look4sat.domain.model.OtherSettings
import com.rtbishop.look4sat.domain.model.PassesSettings
import com.rtbishop.look4sat.domain.predict.GeoPos
import kotlinx.coroutines.flow.StateFlow
interface ISettingsRepo {
@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.data
package com.rtbishop.look4sat.domain.source
import java.io.InputStream
@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.data
package com.rtbishop.look4sat.domain.source
import com.rtbishop.look4sat.domain.model.SatEntry
import com.rtbishop.look4sat.domain.model.SatItem
@@ -1,4 +1,4 @@
package com.rtbishop.look4sat.domain.data
package com.rtbishop.look4sat.domain.source
import kotlinx.coroutines.flow.StateFlow
@@ -17,7 +17,7 @@
*/
package com.rtbishop.look4sat.domain.utility
import com.rtbishop.look4sat.domain.model.GeoPos
import com.rtbishop.look4sat.domain.predict.GeoPos
object QthConverter {
@@ -1,115 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain
import com.rtbishop.look4sat.domain.data.DataParser
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ExperimentalCoroutinesApi
class DataParserTest {
private val testDispatcher = StandardTestDispatcher()
private val dataParser = DataParser(testDispatcher)
private val validCSVStream = """
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,2024-03-09T05:45:04.737024,15.49756209,.0005741,51.6418,90.7424,343.9724,92.8274,0,U,25544,999,44305,.25016E-3,.1373E-3,0
FLTSATCOM 8 (USA 46),1989-077A,2022-01-07T11:37:38.074080,1.00273350,.0001114,12.9044,1.3272,91.5769,260.4200,0,U,20253,999,24434,0,-.85E-6,0
""".trimIndent().byteInputStream()
private val invalidCSVStream = """
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
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
""".trimIndent().byteInputStream()
private val validTLEStream = """
ISS (ZARYA)
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
ISS (ZARYA)
1 25544U 98067A 24069.23963816 .00013730 00000+0 25016-3 0 9999
2 25544 51.6418 90.7424 0005741 343.9724 92.8274 15.49756209443058
FLTSATCOM 8 (USA 46)
1 20253U 89077A 22007.48446845 -.00000085 00000+0 00000+0 0 9999
2 20253 12.9044 1.3272 0001114 91.5769 260.4200 1.00273350244345
""".trimIndent().byteInputStream()
private val invalidTLEStream = """
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 val validJSONStream = """
[{"uuid":"UzPz4gcsNBPKPKAFPmer7g","description":"Upper side band (drifting)","alive":true,"type":"Transmitter","uplink_low":null,"uplink_high":null,"uplink_drift":null,"downlink_low":136658500,"downlink_high":null,"downlink_drift":null,"mode":"USB","mode_id":9,"uplink_mode":null,"invert":false,"baud":null,"sat_id":"SCHX-0895-2361-9925-0309","norad_cat_id":965,"status":"active","updated":"2019-04-18T05:39:53.343316Z","citation":"CITATION NEEDED - https://xkcd.com/285/","service":"Unknown","coordination":"","coordination_url":""}]
""".trimIndent().byteInputStream()
private val invalidJSONStream = """
[{"description":"Upper side band (drifting)","alive":true,"type":"Transmitter","uplink_low":null,"uplink_high":null,"uplink_drift":null,"downlink_low":136658500,"downlink_high":null,"downlink_drift":null,"mode":"USB","mode_id":9,"uplink_mode":null,"invert":false,"baud":null,"sat_id":"SCHX-0895-2361-9925-0309","norad_cat_id":965,"status":"active","updated":"2019-04-18T05:39:53.343316Z","citation":"CITATION NEEDED - https://xkcd.com/285/","service":"Unknown","coordination":"","coordination_url":""}]
""".trimIndent().byteInputStream()
@Test
fun `Given valid CSV stream returns valid data`() = runTest(testDispatcher) {
val parsedList = dataParser.parseCSVStream(validCSVStream)
assert(parsedList[0].epoch == 21320.51955234)
assert(parsedList[1].epoch == 24069.23963816)
}
@Test
fun `Given invalid CSV stream returns empty list`() = runTest(testDispatcher) {
val parsedList = dataParser.parseCSVStream(invalidCSVStream)
assert(parsedList.isEmpty())
}
@Test
fun `Given valid TLE stream returns valid data`() = runTest(testDispatcher) {
val parsedList = dataParser.parseTLEStream(validTLEStream)
assert(parsedList[0].epoch == 21320.51955234)
assert(parsedList[1].epoch == 24069.23963816)
}
@Test
fun `Given invalid TLE stream returns empty list`() = runTest(testDispatcher) {
val parsedList = dataParser.parseTLEStream(invalidTLEStream)
assert(parsedList.isEmpty())
}
@Test
fun `Given valid data streams parsed results match`() = runTest(testDispatcher) {
val csvResult = dataParser.parseCSVStream(validCSVStream)
val tleResult = dataParser.parseTLEStream(validTLEStream)
assert(csvResult == tleResult)
}
@Test
fun `Given valid JSON stream returns valid data`() = runTest(testDispatcher) {
val parsedList = dataParser.parseJSONStream(validJSONStream)
assert(parsedList[0].downlink == 136658500L)
}
@Test
fun `Given invalid JSON stream returns empty list`() = runTest(testDispatcher) {
val parsedList = dataParser.parseJSONStream(invalidJSONStream)
assert(parsedList.isEmpty())
}
@Test
fun `Function isLeapYear returns correct data`() = runTest(testDispatcher) {
val years = listOf(1900, 1984, 1994, 2016, 2022, 2024, 2042, 2048)
val answers = listOf(false, true, false, true, false, true, false, true)
val results = years.map { dataParser.isLeapYear(it) }
assert(results == answers)
}
}