Further classes movement and cleanup

This commit is contained in:
Arty Bishop committed 2025-12-27 15:01:33 +00:00
1 parent 542cfcfd50
commit 622d4e3632
27 files changed
+684 -578

No files matched your search

@@ -26,56 +26,52 @@ import org.json.JSONObject
import java.io.InputStream
import kotlin.math.pow
class DataParser(private val defaultDispatcher: CoroutineDispatcher) {
class DataParser(private val dispatcher: CoroutineDispatcher) {
suspend fun parseCSVStream(csvStream: InputStream): List<OrbitalData> =
withContext(defaultDispatcher) {
val parsedItems = mutableListOf<OrbitalData>()
csvStream.bufferedReader().useLines { lines ->
lines.forEachIndexed { index, line ->
if (index != 0) {
val values = line.split(",")
parseCSV(values)?.let { tle -> parsedItems.add(tle) }
}
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
}
suspend fun parseTLEStream(tleStream: InputStream): List<OrbitalData> =
withContext(defaultDispatcher) {
val tleStrings = mutableListOf(String(), String(), String())
val parsedItems = mutableListOf<OrbitalData>()
var lineIndex = 0
tleStream.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
}
}
} catch (exception: Exception) {
return@withContext parsedItems
}
suspend fun parseJSONStream(jsonStream: InputStream): List<SatRadio> {
return withContext(defaultDispatcher) {
val parsedItems = mutableListOf<SatRadio>()
try {
val jsonArray = JSONArray(jsonStream.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? {
@@ -120,8 +116,9 @@ class DataParser(private val defaultDispatcher: CoroutineDispatcher) {
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())
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
@@ -154,7 +151,7 @@ class DataParser(private val defaultDispatcher: CoroutineDispatcher) {
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] }
for (i in 0 until month - 1) dayOfYear += daysArray[i]
return dayOfYear
}
}
@@ -18,9 +18,10 @@
package com.rtbishop.look4sat.data
import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.ISettingsSource
import com.rtbishop.look4sat.domain.ISettingsRepository
import com.rtbishop.look4sat.model.DataState
import com.rtbishop.look4sat.model.SatEntry
import com.rtbishop.look4sat.model.SatItem
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -34,7 +35,7 @@ class DataRepository(
private val radioSource: IRadiosStorage,
private val remoteSource: INetworkSource,
private val repositoryScope: CoroutineScope,
private val settingsManager: ISettingsSource
private val settingsRepository: ISettingsRepository
) : IDataRepository {
private val exceptionHandler = CoroutineExceptionHandler { _, exception ->
@@ -48,10 +49,17 @@ class DataRepository(
override fun getRadiosTotal() = radioSource.getRadiosTotal()
override fun getSatelliteTypes() = remoteSource.satelliteSourcesMap.keys.sorted()
override suspend fun getEntriesWithModes() = entrySource.getEntriesWithModes()
override suspend fun getEntriesWithIds(ids: List<Int>) = entrySource.getEntriesWithIds(ids)
override suspend fun getEntriesWithSelection(): List<SatItem> {
val selectedIds = settingsRepository.loadEntriesSelection()
return getEntriesWithModes().onEach { it.isSelected = it.catnum in selectedIds }
}
override suspend fun getRadiosWithId(id: Int) = radioSource.getRadiosWithId(id)
override fun updateFromFile(uri: String) {
@@ -69,7 +77,7 @@ class DataRepository(
_updateState.value = DataState.Loading
repositoryScope.launch(exceptionHandler) {
val importedEntries = mutableListOf<SatEntry>()
val sourcesMap = settingsManager.sourcesMap
val sourcesMap = remoteSource.satelliteSourcesMap
val jobsMap = sourcesMap.mapValues { async { remoteSource.getDataStream(it.value) } }
jobsMap.mapValues { job -> job.value.await() }.forEach { entry ->
entry.value?.let { stream ->
@@ -78,7 +86,7 @@ class DataRepository(
// parse tle stream
val satellites = importSatellites(stream)
val catnums = satellites.map { it.data.catnum }
settingsManager.saveSatType(type, catnums)
settingsRepository.saveSatType(type, catnums)
importedEntries.addAll(satellites)
}
"McCants", "Classified" -> {
@@ -86,7 +94,7 @@ class DataRepository(
val unzipped = ZipInputStream(stream).apply { nextEntry }
val satellites = importSatellites(unzipped)
val catnums = satellites.map { it.data.catnum }
settingsManager.saveSatType(type, catnums)
settingsRepository.saveSatType(type, catnums)
importedEntries.addAll(satellites)
}
else -> {
@@ -94,7 +102,7 @@ class DataRepository(
val parsed = dataParser.parseCSVStream(stream)
val satellites = parsed.map { data -> SatEntry(data) }
val catnums = satellites.map { it.data.catnum }
settingsManager.saveSatType(type, catnums)
settingsRepository.saveSatType(type, catnums)
importedEntries.addAll(satellites)
}
}
@@ -104,7 +112,7 @@ class DataRepository(
setUpdateSuccessful()
}
repositoryScope.launch(exceptionHandler) {
remoteSource.getDataStream(remoteSource.radioApi)?.let { stream ->
remoteSource.getDataStream(remoteSource.radioSourceUrl)?.let { stream ->
radioSource.insertRadios(dataParser.parseJSONStream(stream))
}
}
@@ -125,7 +133,7 @@ class DataRepository(
}
private fun setUpdateSuccessful(updateTime: Long = System.currentTimeMillis()) {
settingsManager.setLastUpdateTime(updateTime)
settingsRepository.setLastUpdateTime(updateTime)
_updateState.value = DataState.Success(updateTime)
}
@@ -21,7 +21,36 @@ import java.io.InputStream
interface INetworkSource {
val radioApi: String get() = "https://db.satnogs.org/api/transmitters/?format=json&status=active"
val radioSourceUrl: String get() = "https://db.satnogs.org/api/transmitters/?format=json"
val satelliteSourcesMap: Map<String, String>
get() = mapOf(
"All" to "https://celestrak.com/NORAD/elements/gp.php?GROUP=active&FORMAT=csv",
"Amsat" to "https://amsat.org/tle/current/nasabare.txt",
"Amateur" to "https://celestrak.com/NORAD/elements/gp.php?GROUP=amateur&FORMAT=csv",
"Classified" to "https://www.prismnet.com/~mmccants/tles/classfd.zip",
"Cubesat" to "https://celestrak.com/NORAD/elements/gp.php?GROUP=cubesat&FORMAT=csv",
"Education" to "https://celestrak.com/NORAD/elements/gp.php?GROUP=education&FORMAT=csv",
"Engineer" to "https://celestrak.com/NORAD/elements/gp.php?GROUP=engineering&FORMAT=csv",
"Geostationary" to "https://celestrak.com/NORAD/elements/gp.php?GROUP=geo&FORMAT=csv",
"Globalstar" to "https://celestrak.com/NORAD/elements/gp.php?GROUP=globalstar&FORMAT=csv",
"GNSS" to "https://celestrak.com/NORAD/elements/gp.php?GROUP=gnss&FORMAT=csv",
"Intelsat" to "https://celestrak.com/NORAD/elements/gp.php?GROUP=intelsat&FORMAT=csv",
"Iridium" to "https://celestrak.com/NORAD/elements/gp.php?GROUP=iridium-NEXT&FORMAT=csv",
"McCants" to "https://www.prismnet.com/~mmccants/tles/inttles.zip",
"Military" to "https://celestrak.com/NORAD/elements/gp.php?GROUP=military&FORMAT=csv",
"New" to "https://celestrak.com/NORAD/elements/gp.php?GROUP=last-30-days&FORMAT=csv",
"OneWeb" to "https://celestrak.com/NORAD/elements/gp.php?GROUP=oneweb&FORMAT=csv",
"Orbcomm" to "https://celestrak.com/NORAD/elements/gp.php?GROUP=orbcomm&FORMAT=csv",
"R4UAB" to "https://r4uab.ru/satonline.txt",
"Resource" to "https://celestrak.com/NORAD/elements/gp.php?GROUP=resource&FORMAT=csv",
"SatNOGS" to "https://celestrak.com/NORAD/elements/gp.php?GROUP=satnogs&FORMAT=csv",
"Science" to "https://celestrak.com/NORAD/elements/gp.php?GROUP=science&FORMAT=csv",
"Spire" to "https://celestrak.com/NORAD/elements/gp.php?GROUP=spire&FORMAT=csv",
"Starlink" to "https://celestrak.com/NORAD/elements/gp.php?GROUP=starlink&FORMAT=csv",
"Swarm" to "https://celestrak.com/NORAD/elements/gp.php?GROUP=swarm&FORMAT=csv",
"Weather" to "https://celestrak.com/NORAD/elements/gp.php?GROUP=weather&FORMAT=csv",
"X-Comm" to "https://celestrak.com/NORAD/elements/gp.php?GROUP=x-comm&FORMAT=csv"
)
suspend fun getDataStream(url: String): InputStream?
}
@@ -15,8 +15,10 @@
* 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
package com.rtbishop.look4sat.data
import com.rtbishop.look4sat.domain.ISatelliteRepository
import com.rtbishop.look4sat.domain.Satellite
import com.rtbishop.look4sat.model.GeoPos
import com.rtbishop.look4sat.model.SatPass
import com.rtbishop.look4sat.model.SatPos
@@ -28,7 +30,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.withContext
class SatelliteManager(private val defaultDispatcher: CoroutineDispatcher) : ISatelliteManager {
class SatelliteRepository(private val dispatcher: CoroutineDispatcher) : ISatelliteRepository {
private var passes = listOf<SatPass>()
private val _calculatedPasses = MutableSharedFlow<List<SatPass>>(replay = 1)
@@ -37,13 +39,13 @@ class SatelliteManager(private val defaultDispatcher: CoroutineDispatcher) : ISa
override fun getPasses(): List<SatPass> = passes
override suspend fun getPosition(sat: Satellite, pos: GeoPos, time: Long): SatPos {
return withContext(defaultDispatcher) { sat.getPosition(pos, time) }
return withContext(dispatcher) { sat.getPosition(pos, time) }
}
override suspend fun getTrack(
sat: Satellite, pos: GeoPos, start: Long, end: Long
): List<SatPos> {
return withContext(defaultDispatcher) {
return withContext(dispatcher) {
val positions = mutableListOf<SatPos>()
var currentTime = start
while (currentTime < end) {
@@ -57,7 +59,7 @@ class SatelliteManager(private val defaultDispatcher: CoroutineDispatcher) : ISa
override suspend fun processRadios(
sat: Satellite, pos: GeoPos, radios: List<SatRadio>, time: Long
): List<SatRadio> {
return withContext(defaultDispatcher) {
return withContext(dispatcher) {
val satPos = sat.getPosition(pos, time)
val copiedList = radios.map { it.copy() }
copiedList.forEach { transmitter ->
@@ -73,7 +75,7 @@ class SatelliteManager(private val defaultDispatcher: CoroutineDispatcher) : ISa
}
override suspend fun processPasses(passList: List<SatPass>, time: Long): List<SatPass> {
return withContext(defaultDispatcher) {
return withContext(dispatcher) {
passList.forEach { pass ->
if (!pass.isDeepSpace) {
val timeStart = pass.aosTime
@@ -96,7 +98,7 @@ class SatelliteManager(private val defaultDispatcher: CoroutineDispatcher) : ISa
passes = newPasses
_calculatedPasses.emit(newPasses)
} else {
withContext(defaultDispatcher) {
withContext(dispatcher) {
val allPasses = mutableListOf<SatPass>()
satList.forEach { satellite ->
allPasses.addAll(satellite.getPasses(pos, time, hoursAhead))
@@ -20,7 +20,7 @@ package com.rtbishop.look4sat.domain
import com.rtbishop.look4sat.model.OrbitalData
import kotlin.math.*
class DeepSpaceSat(data: OrbitalData) : Satellite(data) {
class DeepSpaceSatellite(data: OrbitalData) : Satellite(data) {
private val c1: Double
private val c4: Double
@@ -31,10 +31,14 @@ interface IDataRepository {
fun getRadiosTotal(): Flow<Int>
fun getSatelliteTypes(): List<String>
suspend fun getEntriesWithModes(): List<SatItem>
suspend fun getEntriesWithIds(ids: List<Int>): List<Satellite>
suspend fun getEntriesWithSelection(): List<SatItem>
suspend fun getRadiosWithId(id: Int): List<SatRadio>
fun updateFromFile(uri: String)
@@ -1,32 +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.model.GeoPos
import kotlinx.coroutines.flow.StateFlow
interface ILocationSource {
val stationPosition: StateFlow<GeoPos>
fun setGpsPosition(): Boolean
fun setGeoPosition(latitude: Double, longitude: Double, altitude: Double = 0.0): Boolean
fun setQthPosition(locator: String): Boolean
}
@@ -6,7 +6,7 @@ import com.rtbishop.look4sat.model.SatPos
import com.rtbishop.look4sat.model.SatRadio
import kotlinx.coroutines.flow.SharedFlow
interface ISatelliteManager {
interface ISatelliteRepository {
val calculatedPasses: SharedFlow<List<SatPass>>
@@ -0,0 +1,12 @@
package com.rtbishop.look4sat.domain
import kotlinx.coroutines.flow.StateFlow
interface ISensorRepository {
val orientation: StateFlow<Pair<Float, Float>>
fun enableSensor()
fun disableSensor()
}
@@ -0,0 +1,100 @@
/*
* 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.model.GeoPos
import kotlinx.coroutines.flow.StateFlow
interface ISettingsRepository {
val stationPosition: StateFlow<GeoPos>
fun setGpsPosition(): Boolean
fun setGeoPosition(latitude: Double, longitude: Double, altitude: Double = 0.0): Boolean
fun setQthPosition(locator: String): Boolean
fun getHoursAhead(): Int
fun setHoursAhead(hoursAhead: Int)
fun getMinElevation(): Double
fun setMinElevation(minElevation: Double)
fun isUtcEnabled(): Boolean
fun setUtcState(value: Boolean)
fun getLastUpdateTime(): Long
fun setLastUpdateTime(updateTime: Long)
fun isUpdateEnabled(): Boolean
fun setUpdateState(value: Boolean)
fun isSensorEnabled(): Boolean
fun setSensorState(value: Boolean)
fun isSweepEnabled(): Boolean
fun setSweepState(value: Boolean)
fun saveModesSelection(modes: List<String>)
fun loadModesSelection(): List<String>
fun saveEntriesSelection(catnums: List<Int>)
fun loadEntriesSelection(): List<Int>
fun saveSatType(type: String, catnums: List<Int>)
fun loadSatType(type: String): List<Int>
fun getRotatorEnabled(): Boolean
fun setRotatorEnabled(value: Boolean)
fun getRotatorServer(): String
fun setRotatorServer(value: String)
fun getRotatorPort(): String
fun setRotatorPort(value: String)
fun getBTEnabled(): Boolean
fun setBTEnabled(value: Boolean)
fun getBTDeviceAddr(): String
fun setBTDeviceAddr(value: String)
fun getBTDeviceName(): String
fun setBTDeviceName(value: String)
fun getBTFormat(): String
fun setBTFormat(value: String)
}
@@ -20,7 +20,7 @@ package com.rtbishop.look4sat.domain
import com.rtbishop.look4sat.model.OrbitalData
import kotlin.math.*
class NearEarthSat(data: OrbitalData) : Satellite(data) {
class NearEarthSatellite(data: OrbitalData) : Satellite(data) {
private val aodp: Double
private val aycof: Double
@@ -110,8 +110,8 @@ abstract class Satellite(val data: OrbitalData) {
}
private fun calculateSDP4orSGP4(tsince: Double) {
if (data.isDeepSpace) (this as DeepSpaceSat).calculateSDP4(tsince)
else (this as NearEarthSat).calculateSGP4(tsince)
if (data.isDeepSpace) (this as DeepSpaceSatellite).calculateSDP4(tsince)
else (this as NearEarthSatellite).calculateSGP4(tsince)
}
// Converts the sat position and velocity vectors to km and km/sec
@@ -39,5 +39,6 @@ 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) DeepSpaceSat(this) else NearEarthSat(this)
fun getSatellite(): Satellite =
if (isDeepSpace) DeepSpaceSatellite(this) else NearEarthSatellite(this)
}