Slightly changed classes structure for readability

This commit is contained in:
Arty Bishop committed 2025-12-27 15:01:45 +00:00
1 parent 19d0c1b108
commit 549a915b24
40 files changed
+309 -381

No files matched your search

@@ -35,7 +35,7 @@ class MainApplication : Application() {
enableStrictMode()
super.onCreate()
container = MainContainer(this)
container.mainScope.launch { checkAutoUpdate() }
container.appScope.launch { checkAutoUpdate() }
}
private suspend fun checkAutoUpdate() {
@@ -6,25 +6,23 @@ import android.hardware.Sensor
import android.hardware.SensorManager
import android.location.LocationManager
import androidx.room.Room
import com.rtbishop.look4sat.data.database.MainDatabase
import com.rtbishop.look4sat.data.database.Look4SatDb
import com.rtbishop.look4sat.data.framework.BluetoothReporter
import com.rtbishop.look4sat.data.framework.NetworkReporter
import com.rtbishop.look4sat.data.repository.DatabaseRepo
import com.rtbishop.look4sat.data.repository.SatelliteRepo
import com.rtbishop.look4sat.data.repository.SelectionRepo
import com.rtbishop.look4sat.data.repository.SensorsRepo
import com.rtbishop.look4sat.data.repository.SettingsRepo
import com.rtbishop.look4sat.data.source.FileSource
import com.rtbishop.look4sat.data.source.LocalSource
import com.rtbishop.look4sat.data.source.RemoteSource
import com.rtbishop.look4sat.data.source.SensorSource
import com.rtbishop.look4sat.domain.repository.IDatabaseRepo
import com.rtbishop.look4sat.domain.repository.ISatelliteRepo
import com.rtbishop.look4sat.domain.repository.ISelectionRepo
import com.rtbishop.look4sat.domain.repository.ISensorsRepo
import com.rtbishop.look4sat.domain.repository.ISettingsRepo
import com.rtbishop.look4sat.domain.source.IFileSource
import com.rtbishop.look4sat.domain.source.ILocalSource
import com.rtbishop.look4sat.domain.source.IRemoteSource
import com.rtbishop.look4sat.domain.source.ISensorSource
import com.rtbishop.look4sat.domain.utility.DataParser
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
@@ -37,7 +35,8 @@ import org.osmdroid.config.Configuration
class MainContainer(private val context: Context) {
private val localSource = provideLocalSource()
val mainScope = provideMainScope()
private val mainHandler = CoroutineExceptionHandler { _, error -> println("MainHandler: $error") }
val appScope = CoroutineScope(SupervisorJob() + Dispatchers.Default + mainHandler)
val settingsRepo = provideSettingsRepo()
val selectionRepo = provideSelectionRepo()
val satelliteRepo = provideSatelliteRepo()
@@ -52,40 +51,30 @@ class MainContainer(private val context: Context) {
return NetworkReporter(CoroutineScope(Dispatchers.IO))
}
fun provideSensorSource(): ISensorSource {
fun provideSensorsRepo(): ISensorsRepo {
val manager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
val sensor = manager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
return SensorSource(manager, sensor)
return SensorsRepo(manager, sensor)
}
private fun provideDatabaseRepo(): IDatabaseRepo {
val dataParser = DataParser(Dispatchers.Default)
val fileSource = provideFileSource()
val remoteSource = provideRemoteSource()
return DatabaseRepo(Dispatchers.Default, dataParser, fileSource, localSource, remoteSource, settingsRepo)
}
private fun provideFileSource(): IFileSource {
return FileSource(context.contentResolver, Dispatchers.IO)
return DatabaseRepo(Dispatchers.Default, dataParser, localSource, remoteSource, settingsRepo)
}
private fun provideLocalSource(): ILocalSource {
val database = Room.databaseBuilder(context, MainDatabase::class.java, "Look4SatDBv313").apply {
val database = Room.databaseBuilder(context, Look4SatDb::class.java, "Look4SatDBv313").apply {
// addMigrations(MIGRATION_1_2)
fallbackToDestructiveMigration()
}.build()
return LocalSource(database.storageDao())
}
private fun provideMainScope(): CoroutineScope {
val mainHandler = CoroutineExceptionHandler { _, error -> println("Look4Sat: $error") }
return CoroutineScope(SupervisorJob() + Dispatchers.Default + mainHandler)
return LocalSource(database.look4SatDao())
}
private fun provideRemoteSource(): IRemoteSource {
val cache = Cache(context.cacheDir, MainApplication.MAX_OKHTTP_CACHE_SIZE)
val httpClient = OkHttpClient.Builder().cache(cache).build()
return RemoteSource(httpClient, Dispatchers.IO)
return RemoteSource(context.contentResolver, httpClient, Dispatchers.IO)
}
private fun provideSatelliteRepo(): ISatelliteRepo {
@@ -29,8 +29,8 @@ import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.viewmodel.compose.viewModel
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.domain.predict.SatPos
import com.rtbishop.look4sat.domain.predict.Satellite
import com.rtbishop.look4sat.domain.predict.OrbitalObject
import com.rtbishop.look4sat.domain.predict.OrbitalPos
import org.osmdroid.tileprovider.tilesource.XYTileSource
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.CustomZoomButtonsController
@@ -69,7 +69,7 @@ fun MapScreen() {
val positions = viewModel.positions.collectAsState(initial = null)
val satTrack = viewModel.track.collectAsState(initial = null)
val footprint = viewModel.footprint.collectAsState(initial = null)
val positionClick = { satellite: Satellite -> viewModel.selectSatellite(satellite) }
val positionClick = { orbitalObject: OrbitalObject -> viewModel.selectSatellite(orbitalObject) }
Column(modifier = Modifier.padding(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
ElevatedCard(modifier = Modifier.fillMaxSize()) {
MapView(modifier = Modifier.fillMaxSize()) { mapView ->
@@ -97,7 +97,7 @@ private fun setStationPosition(stationPos: GeoPos, mapView: MapView) {
}
}
private fun setPositions(posMap: Map<Satellite, GeoPos>, mapView: MapView, action: (Satellite) -> Unit) {
private fun setPositions(posMap: Map<OrbitalObject, GeoPos>, mapView: MapView, action: (OrbitalObject) -> Unit) {
val markers = FolderOverlay()
try {
posMap.entries.forEach {
@@ -156,8 +156,8 @@ private fun setSatelliteTrack(satTrack: List<List<GeoPos>>, mapView: MapView) {
// mapView.controller.animateTo(center)
}
private fun setFootprint(satPos: SatPos, mapView: MapView) {
val footprintPoints = satPos.getRangeCircle().map { GeoPoint(it.latitude, it.longitude) }
private fun setFootprint(orbitalPos: OrbitalPos, mapView: MapView) {
val footprintPoints = orbitalPos.getRangeCircle().map { GeoPoint(it.latitude, it.longitude) }
try {
val footprintOverlay = Polyline().apply {
outlinePaint.set(footprintPaint)
@@ -24,8 +24,8 @@ import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.rtbishop.look4sat.MainApplication
import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.domain.predict.SatPos
import com.rtbishop.look4sat.domain.predict.Satellite
import com.rtbishop.look4sat.domain.predict.OrbitalObject
import com.rtbishop.look4sat.domain.predict.OrbitalPos
import com.rtbishop.look4sat.domain.repository.ISatelliteRepo
import com.rtbishop.look4sat.domain.repository.ISettingsRepo
import com.rtbishop.look4sat.domain.utility.clipLat
@@ -51,7 +51,7 @@ class MapViewModel(private val satelliteRepo: ISatelliteRepo, settingsRepo: ISet
private var allSatellites = satelliteRepo.satellites.value
private var dataUpdateJob: Job? = null
private var dataUpdateRate = 1000L
private var selectedSatellite: Satellite? = null
private var selectedOrbitalObject: OrbitalObject? = null
val stationPosition = flow {
val osmLat = clipLat(stationPos.latitude)
@@ -62,14 +62,14 @@ class MapViewModel(private val satelliteRepo: ISatelliteRepo, settingsRepo: ISet
private val _track = MutableSharedFlow<List<List<GeoPos>>>()
val track: SharedFlow<List<List<GeoPos>>> = _track
private val _footprint = MutableSharedFlow<SatPos>()
val footprint: SharedFlow<SatPos> = _footprint
private val _footprint = MutableSharedFlow<OrbitalPos>()
val footprint: SharedFlow<OrbitalPos> = _footprint
private val _mapData = MutableSharedFlow<MapData>()
// val mapData: SharedFlow<MapData> = _mapData
private val _positions = MutableSharedFlow<Map<Satellite, GeoPos>>()
val positions: SharedFlow<Map<Satellite, GeoPos>> = _positions
private val _positions = MutableSharedFlow<Map<OrbitalObject, GeoPos>>()
val positions: SharedFlow<Map<OrbitalObject, GeoPos>> = _positions
// fun scrollSelection(decrement: Boolean) {
// if (allSatellites.isNotEmpty()) {
@@ -88,37 +88,37 @@ class MapViewModel(private val satelliteRepo: ISatelliteRepo, settingsRepo: ISet
if (allSatellites.isNotEmpty()) {
if (catnum == -1) {
allPasses.find { pass -> pass.progress < 100 && !pass.isDeepSpace }
?.let { pass -> selectSatellite(pass.satellite) }
?.let { pass -> selectSatellite(pass.orbitalObject) }
} else {
allSatellites.find { it.data.catnum == catnum }?.let { selectSatellite(it) }
}
}
}
fun selectSatellite(satellite: Satellite, updateFreq: Long = dataUpdateRate) {
selectedSatellite = satellite
fun selectSatellite(orbitalObject: OrbitalObject, updateFreq: Long = dataUpdateRate) {
selectedOrbitalObject = orbitalObject
viewModelScope.launch {
dataUpdateJob?.cancelAndJoin()
dataUpdateJob = launch {
val dateNow = Date()
getSatTrack(satellite, stationPos, dateNow)
getSatTrack(orbitalObject, stationPos, dateNow)
while (isActive) {
dateNow.time = System.currentTimeMillis()
getPositions(allSatellites, stationPos, dateNow)
getSatFootprint(satellite, stationPos, dateNow)
getSatData(satellite, stationPos, dateNow)
getSatFootprint(orbitalObject, stationPos, dateNow)
getSatData(orbitalObject, stationPos, dateNow)
delay(updateFreq)
}
}
}
}
private suspend fun getSatTrack(satellite: Satellite, pos: GeoPos, date: Date) {
private suspend fun getSatTrack(orbitalObject: OrbitalObject, pos: GeoPos, date: Date) {
val satTracks = mutableListOf<List<GeoPos>>()
val currentTrack = mutableListOf<GeoPos>()
val endDate = Date(date.time + (satellite.data.orbitalPeriod * 2.4 * 60000L).toLong())
val endDate = Date(date.time + (orbitalObject.data.orbitalPeriod * 2.4 * 60000L).toLong())
var oldLongitude = 0.0
satelliteRepo.getTrack(satellite, pos, date.time, endDate.time).forEach { satPos ->
satelliteRepo.getTrack(orbitalObject, pos, date.time, endDate.time).forEach { satPos ->
val osmLat = clipLat(satPos.latitude.toDegrees())
val osmLon = clipLon(satPos.longitude.toDegrees())
val currentPosition = GeoPos(osmLat, osmLon)
@@ -142,9 +142,9 @@ class MapViewModel(private val satelliteRepo: ISatelliteRepo, settingsRepo: ISet
_track.emit(satTracks)
}
private suspend fun getPositions(satellites: List<Satellite>, pos: GeoPos, date: Date) {
val positions = mutableMapOf<Satellite, GeoPos>()
satellites.forEach { satellite ->
private suspend fun getPositions(orbitalObjects: List<OrbitalObject>, pos: GeoPos, date: Date) {
val positions = mutableMapOf<OrbitalObject, GeoPos>()
orbitalObjects.forEach { satellite ->
val satPos = satelliteRepo.getPosition(satellite, pos, date.time)
val osmLat = clipLat(satPos.latitude.toDegrees())
val osmLon = clipLon(satPos.longitude.toDegrees())
@@ -153,12 +153,12 @@ class MapViewModel(private val satelliteRepo: ISatelliteRepo, settingsRepo: ISet
_positions.emit(positions)
}
private suspend fun getSatFootprint(satellite: Satellite, pos: GeoPos, date: Date) {
val satPos = satelliteRepo.getPosition(satellite, pos, date.time)
private suspend fun getSatFootprint(orbitalObject: OrbitalObject, pos: GeoPos, date: Date) {
val satPos = satelliteRepo.getPosition(orbitalObject, pos, date.time)
_footprint.emit(satPos)
}
private suspend fun getSatData(sat: Satellite, pos: GeoPos, date: Date) {
private suspend fun getSatData(sat: OrbitalObject, pos: GeoPos, date: Date) {
var aosTime = 0L.toTimerString()
allPasses.find { pass -> pass.catNum == sat.data.catnum && pass.progress < 100 }
?.let { satPass ->
@@ -36,9 +36,9 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.predict.NearEarthSatellite
import com.rtbishop.look4sat.domain.predict.NearEarthObject
import com.rtbishop.look4sat.domain.predict.OrbitalData
import com.rtbishop.look4sat.domain.predict.SatPass
import com.rtbishop.look4sat.domain.predict.OrbitalPass
import com.rtbishop.look4sat.presentation.MainTheme
import com.rtbishop.look4sat.presentation.components.PullRefreshIndicator
import com.rtbishop.look4sat.presentation.components.PullRefreshState
@@ -114,13 +114,13 @@ private fun PassPreview() {
val data = OrbitalData(
"Satellite", 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 45000, 0.0
)
val satellite = NearEarthSatellite(data)
val pass = SatPass(1L, 0.0, 10L, 180.0, 850, 45.0, satellite, 0.5f)
val satellite = NearEarthObject(data)
val pass = OrbitalPass(1L, 0.0, 10L, 180.0, 850, 45.0, satellite, 0.5f)
MainTheme { Pass(pass = pass, { _, _ -> }) }
}
@Composable
private fun Pass(pass: SatPass, navToRadar: (Int, Long) -> Unit, modifier: Modifier = Modifier) {
private fun Pass(pass: OrbitalPass, navToRadar: (Int, Long) -> Unit, modifier: Modifier = Modifier) {
Surface(color = MaterialTheme.colorScheme.background, modifier = modifier) {
Surface(modifier = Modifier
.padding(bottom = 2.dp)
@@ -207,7 +207,7 @@ private fun Pass(pass: SatPass, navToRadar: (Int, Long) -> Unit, modifier: Modif
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun PassesCard(
refreshState: PullRefreshState, isRefreshing: Boolean, passes: List<SatPass>, navToRadar: (Int, Long) -> Unit
refreshState: PullRefreshState, isRefreshing: Boolean, passes: List<OrbitalPass>, navToRadar: (Int, Long) -> Unit
) {
ElevatedCard(modifier = Modifier.fillMaxSize()) {
Box(Modifier.pullRefresh(refreshState)) {
@@ -1,6 +1,6 @@
package com.rtbishop.look4sat.presentation.passes
import com.rtbishop.look4sat.domain.predict.SatPass
import com.rtbishop.look4sat.domain.predict.OrbitalPass
data class PassesState(
val isDialogShown: Boolean,
@@ -12,7 +12,7 @@ data class PassesState(
val hours: Int,
val elevation: Double,
val modes: List<String>,
val itemsList: List<SatPass>,
val itemsList: List<OrbitalPass>,
val takeAction: (PassesAction) -> Unit
)
@@ -26,7 +26,7 @@ import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.rtbishop.look4sat.MainApplication
import com.rtbishop.look4sat.domain.model.PassesSettings
import com.rtbishop.look4sat.domain.predict.SatPass
import com.rtbishop.look4sat.domain.predict.OrbitalPass
import com.rtbishop.look4sat.domain.repository.ISatelliteRepo
import com.rtbishop.look4sat.domain.repository.ISettingsRepo
import com.rtbishop.look4sat.domain.utility.toTimerString
@@ -99,7 +99,7 @@ class PassesViewModel(
satelliteRepo.calculatePasses(System.currentTimeMillis(), hoursAhead, minElevation, modes)
}
private fun setPassInfo(passes: List<SatPass>, timeNow: Long) {
private fun setPassInfo(passes: List<OrbitalPass>, timeNow: Long) {
if (passes.isEmpty()) return
try {
val nextPass = passes.first { it.aosTime.minus(timeNow) > 0 }
@@ -17,6 +17,6 @@
*/
package com.rtbishop.look4sat.presentation.radar
import com.rtbishop.look4sat.domain.predict.SatPos
import com.rtbishop.look4sat.domain.predict.OrbitalPos
data class RadarData(val satPos: SatPos, val satTrack: List<SatPos> = emptyList())
data class RadarData(val orbitalPos: OrbitalPos, val satTrack: List<OrbitalPos> = emptyList())
@@ -69,7 +69,7 @@ fun RadarScreen() {
) {
viewModel.radarData.value?.let { data ->
RadarViewCompose(
item = data.satPos,
item = data.orbitalPos,
items = data.satTrack,
azimElev = viewModel.orientation.value
)
@@ -29,14 +29,14 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.sp
import com.rtbishop.look4sat.domain.predict.OrbitalPos
import com.rtbishop.look4sat.domain.predict.PI_2
import com.rtbishop.look4sat.domain.predict.SatPos
import com.rtbishop.look4sat.domain.utility.toRadians
import kotlin.math.cos
import kotlin.math.sin
@Composable
fun RadarViewCompose(item: SatPos, items: List<SatPos>, azimElev: Pair<Float, Float>) {
fun RadarViewCompose(item: OrbitalPos, items: List<OrbitalPos>, azimElev: Pair<Float, Float>) {
val radarColor = Color(0xFFDCDCDC)
val primaryColor = Color(0xFFFFE082)
val secondaryColor = Color(0xFFDC0000)
@@ -92,7 +92,7 @@ private fun DrawScope.drawTrack(path: Path, effect: PathEffect, color: Color, ef
drawPath(path, effectColor, style = Stroke(pathEffect = effect))
}
private fun DrawScope.drawPosition(item: SatPos, radius: Float, posRadius: Float, color: Color) {
private fun DrawScope.drawPosition(item: OrbitalPos, radius: Float, posRadius: Float, color: Color) {
val satX = sph2CartX(item.azimuth, item.elevation, radius.toDouble())
val satY = sph2CartY(item.azimuth, item.elevation, radius.toDouble())
drawCircle(color, 16f, center.copy(satX, -satY))
@@ -122,7 +122,7 @@ private fun DrawScope.drawSweep(center: Offset, degrees: Float, radius: Float, c
rotate(-90 + degrees, center) { drawCircle(brush, radius, style = Fill) }
}
private fun createTrackPath(positions: List<SatPos>, radius: Float): Path {
private fun createTrackPath(positions: List<OrbitalPos>, radius: Float): Path {
val trackPath = Path()
positions.forEachIndexed { index, satPos ->
val passX = sph2CartX(satPos.azimuth, satPos.elevation, radius.toDouble())
@@ -32,12 +32,12 @@ import com.rtbishop.look4sat.data.framework.BluetoothReporter
import com.rtbishop.look4sat.data.framework.NetworkReporter
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 com.rtbishop.look4sat.domain.predict.OrbitalObject
import com.rtbishop.look4sat.domain.predict.OrbitalPass
import com.rtbishop.look4sat.domain.predict.OrbitalPos
import com.rtbishop.look4sat.domain.repository.ISatelliteRepo
import com.rtbishop.look4sat.domain.repository.ISensorsRepo
import com.rtbishop.look4sat.domain.repository.ISettingsRepo
import com.rtbishop.look4sat.domain.source.ISensorSource
import com.rtbishop.look4sat.domain.utility.round
import com.rtbishop.look4sat.domain.utility.toDegrees
import kotlinx.coroutines.delay
@@ -51,20 +51,20 @@ class RadarViewModel(
private val networkReporter: NetworkReporter,
private val satelliteRepo: ISatelliteRepo,
private val settingsRepo: ISettingsRepo,
private val sensorSource: ISensorSource
private val sensorsRepo: ISensorsRepo
) : ViewModel() {
private val stationPos = settingsRepo.stationPosition.value
private val magDeclination = getMagDeclination(stationPos)
val transmitters = mutableStateOf<List<SatRadio>>(emptyList())
val radarData = mutableStateOf<RadarData?>(null)
val orientation = mutableStateOf(sensorSource.orientation.value)
val orientation = mutableStateOf(sensorsRepo.orientation.value)
init {
if (settingsRepo.otherSettings.value.stateOfSensors) {
viewModelScope.launch {
sensorSource.enableSensor()
sensorSource.orientation.collect { data ->
sensorsRepo.enableSensor()
sensorsRepo.orientation.collect { data ->
orientation.value = Pair(data.first + magDeclination, data.second)
}
}
@@ -72,7 +72,7 @@ class RadarViewModel(
}
override fun onCleared() {
sensorSource.disableSensor()
sensorsRepo.disableSensor()
super.onCleared()
}
@@ -89,10 +89,10 @@ class RadarViewModel(
while (isActive) {
val timeNow = System.currentTimeMillis()
val satPos =
satelliteRepo.getPosition(satPass.satellite, stationPos, timeNow)
sendPassData(satPass, satPos, satPass.satellite)
satelliteRepo.getPosition(satPass.orbitalObject, stationPos, timeNow)
sendPassData(satPass, satPos, satPass.orbitalObject)
sendPassDataBT(satPos)
processRadios(transmitters, satPass.satellite, timeNow)
processRadios(transmitters, satPass.orbitalObject, timeNow)
delay(1000)
}
}
@@ -109,33 +109,33 @@ class RadarViewModel(
return GeomagneticField(latitude, longitude, geoPos.altitude.toFloat(), time).declination
}
private fun sendPassData(satPass: SatPass, satPos: SatPos, satellite: Satellite) {
private fun sendPassData(orbitalPass: OrbitalPass, orbitalPos: OrbitalPos, orbitalObject: OrbitalObject) {
viewModelScope.launch {
var track: List<SatPos> = emptyList()
if (!satPass.isDeepSpace) {
var track: List<OrbitalPos> = emptyList()
if (!orbitalPass.isDeepSpace) {
track = satelliteRepo.getTrack(
satellite, stationPos, satPass.aosTime, satPass.losTime
orbitalObject, stationPos, orbitalPass.aosTime, orbitalPass.losTime
)
}
if (settingsRepo.getRotatorState()) {
val server = settingsRepo.getRotatorAddress()
val port = settingsRepo.getRotatorPort().toInt()
val azimuth = satPos.azimuth.toDegrees().round(1)
val elevation = satPos.elevation.toDegrees().round(1)
val azimuth = orbitalPos.azimuth.toDegrees().round(1)
val elevation = orbitalPos.elevation.toDegrees().round(1)
networkReporter.reportRotation(server, port, azimuth, elevation)
}
radarData.value = RadarData(satPos, track)
radarData.value = RadarData(orbitalPos, track)
}
}
private fun sendPassDataBT(satPos: SatPos) {
private fun sendPassDataBT(orbitalPos: OrbitalPos) {
viewModelScope.launch {
if (settingsRepo.getBluetoothState()) {
val btDevice = settingsRepo.getBluetoothAddress()
if (bluetoothReporter.isConnected()) {
val format = settingsRepo.getBluetoothFormat()
val azimuth = satPos.azimuth.toDegrees().round(0).toInt()
val elevation = satPos.elevation.toDegrees().round(0).toInt()
val azimuth = orbitalPos.azimuth.toDegrees().round(0).toInt()
val elevation = orbitalPos.elevation.toDegrees().round(0).toInt()
bluetoothReporter.reportRotation(format, azimuth, elevation)
} else if (!bluetoothReporter.isConnecting()) {
Log.i("BTReporter", "BTReporter: Attempting to connect...")
@@ -145,10 +145,10 @@ class RadarViewModel(
}
}
private fun processRadios(radios: List<SatRadio>, satellite: Satellite, time: Long) {
private fun processRadios(radios: List<SatRadio>, orbitalObject: OrbitalObject, time: Long) {
viewModelScope.launch {
delay(125)
val list = satelliteRepo.getRadios(satellite, stationPos, radios, time)
val list = satelliteRepo.getRadios(orbitalObject, stationPos, radios, time)
transmitters.value = list
}
}
@@ -164,7 +164,7 @@ class RadarViewModel(
container.provideNetworkReporter(),
container.satelliteRepo,
container.settingsRepo,
container.provideSensorSource()
container.provideSensorsRepo()
)
}
}
@@ -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.data.database.dao
package com.rtbishop.look4sat.data.database
import androidx.room.Dao
import androidx.room.Insert
@@ -23,17 +23,16 @@ import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import com.rtbishop.look4sat.data.database.entity.SatEntry
import com.rtbishop.look4sat.data.database.entity.SatItem
import com.rtbishop.look4sat.data.database.entity.SatRadio
import com.rtbishop.look4sat.domain.model.SatItem
@Dao
interface StorageDao {
interface Look4SatDao {
@Query("SELECT COUNT(*) FROM entries")
suspend fun getEntriesTotal(): Int
//@Transaction
@Query("SELECT catnum, name FROM entries ORDER BY name ASC")
@Query("SELECT catnum, name, 0 as isSelected FROM entries ORDER BY name ASC")
suspend fun getEntriesList(): List<SatItem>
@Transaction
@@ -19,13 +19,12 @@ package com.rtbishop.look4sat.data.database
import androidx.room.Database
import androidx.room.RoomDatabase
import com.rtbishop.look4sat.data.database.dao.StorageDao
import com.rtbishop.look4sat.data.database.entity.SatEntry
import com.rtbishop.look4sat.data.database.entity.SatRadio
@Database(entities = [SatEntry::class, SatRadio::class], version = 1, exportSchema = false)
abstract class MainDatabase : RoomDatabase() {
abstract fun storageDao(): StorageDao
abstract class Look4SatDb : RoomDatabase() {
abstract fun look4SatDao(): Look4SatDao
}
//val MIGRATION_1_2 = object : Migration(1, 2) {
@@ -17,9 +17,18 @@
*/
package com.rtbishop.look4sat.data.database.entity
import androidx.room.Embedded
import androidx.room.Entity
import com.rtbishop.look4sat.domain.predict.OrbitalData
@Entity(tableName = "entries", primaryKeys = ["catnum"])
data class SatEntry(@Embedded val data: OrbitalData, var comment: String? = null)
data class SatEntry(
val name: String,
val epoch: Double,
val meanmo: Double,
val eccn: Double,
val incl: Double,
val raan: Double,
val argper: Double,
val meanan: Double,
val catnum: Int,
val bstar: Double
)
@@ -1,20 +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.data.database.entity
data class SatItem(val catnum: Int, val name: String)
@@ -32,6 +32,5 @@ data class SatRadio(
var uplinkHigh: Long?,
val uplinkMode: String?,
val isInverted: Boolean,
val catnum: Int?,
var comment: String? = null
val catnum: Int?
)
@@ -18,15 +18,14 @@
package com.rtbishop.look4sat.data.repository
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.OrbitalData
import com.rtbishop.look4sat.domain.repository.IDatabaseRepo
import com.rtbishop.look4sat.domain.repository.ISettingsRepo
import com.rtbishop.look4sat.domain.source.IFileSource
import com.rtbishop.look4sat.domain.source.ILocalSource
import com.rtbishop.look4sat.domain.source.IRemoteSource
import com.rtbishop.look4sat.domain.source.Sources
import com.rtbishop.look4sat.domain.utility.DataParser
import java.io.InputStream
import java.util.zip.ZipInputStream
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.async
@@ -35,32 +34,31 @@ import kotlinx.coroutines.withContext
class DatabaseRepo(
private val dispatcher: CoroutineDispatcher,
private val dataParser: DataParser,
private val fileSource: IFileSource,
private val localSource: ILocalSource,
private val remoteSource: IRemoteSource,
private val settingsRepo: ISettingsRepo
) : IDatabaseRepo {
override suspend fun updateFromFile(uri: String) = withContext(dispatcher) {
val importedSatellites = fileSource.getFileStream(uri)?.let { importSatellites(it) }
val importedSatellites = remoteSource.getFileStream(uri)?.let { dataParser.parseTLEStream(it) }
importedSatellites?.let { localSource.insertEntries(it) }
setUpdateSuccessful(System.currentTimeMillis())
}
override suspend fun updateFromRemote() = withContext(dispatcher) {
val importedEntries = mutableListOf<SatEntry>()
val importedEntries = mutableListOf<OrbitalData>()
val importedRadios = mutableListOf<SatRadio>()
// fetch
val jobsMap = settingsRepo.satelliteSourcesMap.mapValues { async { remoteSource.getRemoteStream(it.value) } }
val jobRadios = async { remoteSource.getRemoteStream(settingsRepo.radioSourceUrl) }
val jobsMap = Sources.satelliteDataUrls.mapValues { async { remoteSource.getNetworkStream(it.value) } }
val jobRadios = async { remoteSource.getNetworkStream(Sources.radioDataUrl) }
// 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 }
val satellites = dataParser.parseTLEStream(stream)
val catnums = satellites.map { it.catnum }
settingsRepo.setSatelliteTypeIds(type, catnums)
importedEntries.addAll(satellites)
}
@@ -68,17 +66,16 @@ class DatabaseRepo(
"McCants", "Classified" -> {
// unzip and parse tle stream
val unzipped = ZipInputStream(stream).apply { nextEntry }
val satellites = importSatellites(unzipped)
val catnums = satellites.map { it.data.catnum }
val satellites = dataParser.parseTLEStream(unzipped)
val catnums = satellites.map { it.catnum }
settingsRepo.setSatelliteTypeIds(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 }
val satellites = dataParser.parseCSVStream(stream)
val catnums = satellites.map { it.catnum }
settingsRepo.setSatelliteTypeIds(type, catnums)
importedEntries.addAll(satellites)
}
@@ -103,8 +100,4 @@ class DatabaseRepo(
val numberOfSatellites = localSource.getEntriesTotal()
settingsRepo.updateDatabaseState(DatabaseState(numberOfRadios, numberOfSatellites, timestamp))
}
private suspend fun importSatellites(stream: InputStream): List<SatEntry> {
return dataParser.parseTLEStream(stream).map { data -> SatEntry(data) }
}
}
@@ -19,9 +19,9 @@ package com.rtbishop.look4sat.data.repository
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 com.rtbishop.look4sat.domain.predict.OrbitalObject
import com.rtbishop.look4sat.domain.predict.OrbitalPass
import com.rtbishop.look4sat.domain.predict.OrbitalPos
import com.rtbishop.look4sat.domain.repository.ISatelliteRepo
import com.rtbishop.look4sat.domain.repository.ISettingsRepo
import com.rtbishop.look4sat.domain.source.ILocalSource
@@ -39,11 +39,11 @@ class SatelliteRepo(
private val settingsRepo: ISettingsRepo
) : ISatelliteRepo {
private val _passes = MutableStateFlow<List<SatPass>>(emptyList())
override val passes: StateFlow<List<SatPass>> = _passes
private val _passes = MutableStateFlow<List<OrbitalPass>>(emptyList())
override val passes: StateFlow<List<OrbitalPass>> = _passes
private val _satellites = MutableStateFlow<List<Satellite>>(emptyList())
override val satellites: StateFlow<List<Satellite>> = _satellites
private val _satellites = MutableStateFlow<List<OrbitalObject>>(emptyList())
override val satellites: StateFlow<List<OrbitalObject>> = _satellites
override suspend fun getRadiosWithId(id: Int) = localStorage.getRadiosWithId(id)
@@ -55,13 +55,13 @@ class SatelliteRepo(
}
}
override suspend fun getPosition(sat: Satellite, pos: GeoPos, time: Long): SatPos {
override suspend fun getPosition(sat: OrbitalObject, pos: GeoPos, time: Long): OrbitalPos {
return withContext(dispatcher) { sat.getPosition(pos, time) }
}
override suspend fun getTrack(sat: Satellite, pos: GeoPos, start: Long, end: Long): List<SatPos> {
override suspend fun getTrack(sat: OrbitalObject, pos: GeoPos, start: Long, end: Long): List<OrbitalPos> {
return withContext(dispatcher) {
val positions = mutableListOf<SatPos>()
val positions = mutableListOf<OrbitalPos>()
var currentTime = start
while (currentTime < end) {
positions.add(sat.getPosition(pos, currentTime))
@@ -71,7 +71,12 @@ class SatelliteRepo(
}
}
override suspend fun getRadios(sat: Satellite, pos: GeoPos, radios: List<SatRadio>, time: Long): List<SatRadio> {
override suspend fun getRadios(
sat: OrbitalObject,
pos: GeoPos,
radios: List<SatRadio>,
time: Long
): List<SatRadio> {
return withContext(dispatcher) {
val satPos = sat.getPosition(pos, time)
val copiedList = radios.map { it.copy() }
@@ -83,7 +88,7 @@ class SatelliteRepo(
}
}
override suspend fun processPasses(passList: List<SatPass>, time: Long): List<SatPass> {
override suspend fun processPasses(passList: List<OrbitalPass>, time: Long): List<OrbitalPass> {
return withContext(dispatcher) {
passList.forEach { pass ->
if (!pass.isDeepSpace) {
@@ -102,7 +107,7 @@ class SatelliteRepo(
override suspend fun calculatePasses(time: Long, hoursAhead: Int, minElevation: Double, modes: List<String>) {
if (_satellites.value.isNotEmpty()) {
withContext(dispatcher) {
val newPasses = mutableListOf<SatPass>()
val newPasses = mutableListOf<OrbitalPass>()
val idsWithModes = localStorage.getIdsWithModes(modes)
_satellites.value.forEach { satellite ->
if (idsWithModes.isEmpty() || satellite.data.catnum in idsWithModes) {
@@ -116,8 +121,8 @@ class SatelliteRepo(
}
}
private fun Satellite.getPasses(pos: GeoPos, time: Long, hours: Int): List<SatPass> {
val passes = mutableListOf<SatPass>()
private fun OrbitalObject.getPasses(pos: GeoPos, time: Long, hours: Int): List<OrbitalPass> {
val passes = mutableListOf<OrbitalPass>()
val endDate = time + hours * 60L * 60L * 1000L
val quarterOrbitMin = (this.data.orbitalPeriod / 4.0).toInt()
var startDate = time
@@ -141,23 +146,23 @@ class SatelliteRepo(
return passes
}
private fun List<SatPass>.filter(time: Long, hoursAhead: Int, minElev: Double): List<SatPass> {
private fun List<OrbitalPass>.filter(time: Long, hoursAhead: Int, minElev: Double): List<OrbitalPass> {
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 {
private fun getGeoPass(sat: OrbitalObject, pos: GeoPos, time: Long): OrbitalPass {
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)
return OrbitalPass(aos, az, los, az, alt.toInt(), elev, sat)
}
private fun getLeoPass(sat: Satellite, pos: GeoPos, time: Long, rewind: Boolean): SatPass {
private fun getLeoPass(sat: OrbitalObject, pos: GeoPos, time: Long, rewind: Boolean): OrbitalPass {
val quarterOrbitMin = (sat.data.orbitalPeriod / 4.0).toInt()
var calendarTimeMillis = time
var elevation: Double
@@ -229,6 +234,6 @@ class SatelliteRepo(
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)
return OrbitalPass(aos, aosAz, los, losAz, alt.toInt(), elev, sat)
}
}
@@ -4,6 +4,7 @@ import com.rtbishop.look4sat.domain.model.SatItem
import com.rtbishop.look4sat.domain.repository.ISelectionRepo
import com.rtbishop.look4sat.domain.repository.ISettingsRepo
import com.rtbishop.look4sat.domain.source.ILocalSource
import com.rtbishop.look4sat.domain.source.Sources
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
@@ -30,7 +31,7 @@ class SelectionRepo(
override fun getCurrentType() = currentType.value
override fun getTypesList() = settingsRepo.satelliteSourcesMap.keys.sorted()
override fun getTypesList() = Sources.satelliteDataUrls.keys.sorted()
override suspend fun getEntriesFlow() = withContext(dispatcher) {
val selectedIds = settingsRepo.selectedIds.value
@@ -15,20 +15,20 @@
* 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.data.source
package com.rtbishop.look4sat.data.repository
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import com.rtbishop.look4sat.domain.predict.RAD2DEG
import com.rtbishop.look4sat.domain.source.ISensorSource
import com.rtbishop.look4sat.domain.repository.ISensorsRepo
import kotlin.math.round
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlin.math.round
class SensorSource(private val sensorManager: SensorManager, private val sensor: Sensor?) :
SensorEventListener, ISensorSource {
class SensorsRepo(private val sensorManager: SensorManager, private val sensor: Sensor?) :
SensorEventListener, ISensorsRepo {
private val _orientation = MutableStateFlow(Pair(0f, 0f))
private val rotationMatrix = FloatArray(9)
@@ -1,16 +0,0 @@
package com.rtbishop.look4sat.data.source
import android.content.ContentResolver
import android.net.Uri
import com.rtbishop.look4sat.domain.source.IFileSource
import java.io.InputStream
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
class FileSource(
private val contentResolver: ContentResolver, private val dispatcher: CoroutineDispatcher
) : IFileSource {
override suspend fun getFileStream(uri: String): InputStream? {
return withContext(dispatcher) { contentResolver.openInputStream(Uri.parse(uri)) }
}
}
@@ -18,75 +18,75 @@
package com.rtbishop.look4sat.data.source
import com.rtbishop.look4sat.data.database.entity.SatEntry as FrameworkEntry
import com.rtbishop.look4sat.data.database.entity.SatItem as FrameworkItem
import com.rtbishop.look4sat.data.database.entity.SatRadio as FrameworkRadio
import com.rtbishop.look4sat.domain.model.SatEntry as DomainEntry
import com.rtbishop.look4sat.domain.model.SatItem as DomainItem
import com.rtbishop.look4sat.domain.model.SatRadio as DomainRadio
import com.rtbishop.look4sat.data.database.dao.StorageDao
import com.rtbishop.look4sat.data.database.Look4SatDao
import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.domain.predict.Satellite
import com.rtbishop.look4sat.domain.predict.OrbitalData
import com.rtbishop.look4sat.domain.predict.OrbitalObject
import com.rtbishop.look4sat.domain.source.ILocalSource
class LocalSource(private val storageDao: StorageDao) : ILocalSource {
class LocalSource(private val look4SatDao: Look4SatDao) : ILocalSource {
//region # Entries region
override suspend fun getEntriesTotal() = storageDao.getEntriesTotal()
override suspend fun getEntriesTotal() = look4SatDao.getEntriesTotal()
override suspend fun getEntriesList(): List<DomainItem> {
return storageDao.getEntriesList().toDomainItems()
}
override suspend fun getEntriesList() = look4SatDao.getEntriesList()
override suspend fun getEntriesWithIds(ids: List<Int>): List<Satellite> {
val selectedSatellites = mutableListOf<Satellite>()
override suspend fun getEntriesWithIds(ids: List<Int>): List<OrbitalObject> {
val selectedOrbitalObjects = mutableListOf<OrbitalObject>()
ids.chunked(999).forEach { idsPart ->
val entries = storageDao.getEntriesWithIds(idsPart)
selectedSatellites.addAll(entries.map { entry -> entry.data.getSatellite() })
val entries = look4SatDao.getEntriesWithIds(idsPart)
selectedOrbitalObjects.addAll(entries.toDomain().map { entry -> entry.getObject() })
}
return selectedSatellites
return selectedOrbitalObjects
}
override suspend fun insertEntries(entries: List<DomainEntry>) {
storageDao.insertEntries(entries.toFrameworkEntries())
}
override suspend fun insertEntries(entries: List<OrbitalData>) = look4SatDao.insertEntries(entries.toEntity())
override suspend fun deleteEntries() = storageDao.deleteEntries()
override suspend fun deleteEntries() = look4SatDao.deleteEntries()
override suspend fun getIdsWithModes(modes: List<String>) = storageDao.getIdsWithModes(modes)
override suspend fun getIdsWithModes(modes: List<String>) = look4SatDao.getIdsWithModes(modes)
private fun DomainEntry.toFramework() = FrameworkEntry(this.data, this.comment)
private fun FrameworkEntry.toDomain() = OrbitalData(
this.name, this.epoch, this.meanmo, this.eccn, this.incl,
this.raan, this.argper, this.meanan, this.catnum, this.bstar
)
private fun FrameworkItem.toDomain() = DomainItem(this.catnum, this.name, false)
private fun OrbitalData.toEntity() = FrameworkEntry(
this.name, this.epoch, this.meanmo, this.eccn, this.incl,
this.raan, this.argper, this.meanan, this.catnum, this.bstar
)
private fun List<DomainEntry>.toFrameworkEntries() = this.map { entry -> entry.toFramework() }
private fun List<OrbitalData>.toEntity() = this.map { item -> item.toEntity() }
private fun List<FrameworkItem>.toDomainItems() = this.map { item -> item.toDomain() }
private fun List<FrameworkEntry>.toDomain() = this.map { item -> item.toDomain() }
//endregion
//region # Radios region
override suspend fun getRadiosTotal() = storageDao.getRadiosTotal()
override suspend fun getRadiosTotal() = look4SatDao.getRadiosTotal()
override suspend fun getRadiosWithId(id: Int): List<SatRadio> {
return storageDao.getRadiosWithId(id).toDomainRadios()
return look4SatDao.getRadiosWithId(id).toDomainRadios()
}
override suspend fun insertRadios(radios: List<SatRadio>) {
storageDao.insertRadios(radios.toFrameworkRadios())
look4SatDao.insertRadios(radios.toFrameworkRadios())
}
override suspend fun deleteRadios() = storageDao.deleteRadios()
override suspend fun deleteRadios() = look4SatDao.deleteRadios()
private fun DomainRadio.toFramework() = FrameworkRadio(
this.uuid, this.info, this.isAlive, this.downlinkLow, this.downlinkHigh, this.downlinkMode,
this.uplinkLow, this.uplinkHigh, this.uplinkMode, this.isInverted, this.catnum, this.comment
this.uplinkLow, this.uplinkHigh, this.uplinkMode, this.isInverted, this.catnum
)
private fun FrameworkRadio.toDomain() = DomainRadio(
this.uuid, this.info, this.isAlive, this.downlinkLow, this.downlinkHigh, this.downlinkMode,
this.uplinkLow, this.uplinkHigh, this.uplinkMode, this.isInverted, this.catnum, this.comment
this.uplinkLow, this.uplinkHigh, this.uplinkMode, this.isInverted, this.catnum
)
private fun List<DomainRadio>.toFrameworkRadios() = this.map { radio -> radio.toFramework() }
@@ -17,6 +17,8 @@
*/
package com.rtbishop.look4sat.data.source
import android.content.ContentResolver
import android.net.Uri
import com.rtbishop.look4sat.domain.source.IRemoteSource
import java.io.InputStream
import kotlinx.coroutines.CoroutineDispatcher
@@ -25,15 +27,27 @@ import okhttp3.OkHttpClient
import okhttp3.Request
class RemoteSource(
private val httpClient: OkHttpClient, private val dispatcher: CoroutineDispatcher
private val contentResolver: ContentResolver,
private val httpClient: OkHttpClient,
private val dispatcher: CoroutineDispatcher
) : IRemoteSource {
override suspend fun getRemoteStream(url: String): InputStream? = withContext(dispatcher) {
override suspend fun getFileStream(uri: String): InputStream? = withContext(dispatcher) {
try {
val request = Request.Builder().url(url).build()
httpClient.newCall(request).execute().body?.byteStream()
val fileUri = Uri.parse(uri)
contentResolver.openInputStream(fileUri)
} catch (exception: Exception) {
println("RemoteSource fetching stream exception: $exception")
println("RemoteSource file stream exception: $exception")
null
}
}
override suspend fun getNetworkStream(url: String): InputStream? = withContext(dispatcher) {
try {
val networkRequest = Request.Builder().url(url).build()
httpClient.newCall(networkRequest).execute().body?.byteStream()
} catch (exception: Exception) {
println("RemoteSource network stream exception: $exception")
null
}
}
@@ -1,22 +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.model
import com.rtbishop.look4sat.domain.predict.OrbitalData
data class SatEntry(val data: OrbitalData, var comment: String? = null)
@@ -17,4 +17,4 @@
*/
package com.rtbishop.look4sat.domain.model
data class SatItem(val catnum: Int, val name: String, val isSelected: Boolean)
data class SatItem(val catnum: Int, val name: String, val isSelected: Boolean = false)
@@ -28,6 +28,5 @@ data class SatRadio(
var uplinkHigh: Long?,
val uplinkMode: String?,
val isInverted: Boolean,
val catnum: Int?,
var comment: String? = null
val catnum: Int?
)
@@ -0,0 +1,20 @@
package com.rtbishop.look4sat.domain.predict
const val ASTRONOMICAL_UNIT = 1.49597870691E8
const val DEG2RAD = 0.017453292519943295
const val RAD2DEG = 57.29577951308232
const val EARTH_RADIUS = 6378.137
const val EPSILON = 1.0E-12
const val FLAT_FACT = 3.35281066474748E-3
const val J3_HARMONIC = -2.53881E-6
const val MIN_PER_DAY = 1.44E3
const val SEC_PER_DAY = 8.6400E4
const val SOLAR_RADIUS = 6.96000E5
const val SPEED_OF_LIGHT = 2.99792458E8
const val PI = 3.141592653589793
const val PI_2 = PI / 2.0
const val TWO_PI = PI * 2.0
const val TWO_THIRDS = 2.0 / 3.0
const val CK2 = 5.413079E-4
const val CK4 = 6.209887E-7
const val XKE = 7.43669161E-2
@@ -25,7 +25,7 @@ import kotlin.math.pow
import kotlin.math.sin
import kotlin.math.sqrt
class DeepSpaceSatellite(data: OrbitalData) : Satellite(data) {
class DeepSpaceObject(data: OrbitalData) : OrbitalObject(data) {
private val c1: Double
private val c4: Double
@@ -24,7 +24,7 @@ import kotlin.math.pow
import kotlin.math.sin
import kotlin.math.sqrt
class NearEarthSatellite(data: OrbitalData) : Satellite(data) {
class NearEarthObject(data: OrbitalData) : OrbitalObject(data) {
private val aodp: Double
private val aycof: Double
@@ -27,15 +27,14 @@ data class OrbitalData(
val argper: Double,
val meanan: Double,
val catnum: Int,
val bstar: Double,
val xincl: Double = incl * DEG2RAD,
val xnodeo: Double = raan * DEG2RAD,
val omegao: Double = argper * DEG2RAD,
val xmo: Double = meanan * DEG2RAD,
val xno: Double = meanmo * TWO_PI / MIN_PER_DAY,
val orbitalPeriod: Double = MIN_PER_DAY / meanmo,
// Space objects are classified as NearEarth (period < 225 min) or DeepSpace (period >= 225 min)
val isDeepSpace: Boolean = orbitalPeriod >= 225.0
val bstar: Double
) {
fun getSatellite(): Satellite = if (isDeepSpace) DeepSpaceSatellite(this) else NearEarthSatellite(this)
val xincl: Double = incl * DEG2RAD
val xnodeo: Double = raan * DEG2RAD
val omegao: Double = argper * DEG2RAD
val xmo: Double = meanan * DEG2RAD
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)
fun getObject(): OrbitalObject = if (isDeepSpace) DeepSpaceObject(this) else NearEarthObject(this)
}
@@ -30,30 +30,11 @@ import kotlin.math.pow
import kotlin.math.sin
import kotlin.math.sqrt
const val ASTRONOMICAL_UNIT = 1.49597870691E8
const val DEG2RAD = 0.017453292519943295
const val RAD2DEG = 57.29577951308232
const val EARTH_RADIUS = 6378.137
const val EPSILON = 1.0E-12
const val FLAT_FACT = 3.35281066474748E-3
const val J3_HARMONIC = -2.53881E-6
const val MIN_PER_DAY = 1.44E3
const val SEC_PER_DAY = 8.6400E4
const val SOLAR_RADIUS = 6.96000E5
const val SPEED_OF_LIGHT = 2.99792458E8
const val PI = 3.141592653589793
const val PI_2 = PI / 2.0
const val TWO_PI = PI * 2.0
const val TWO_THIRDS = 2.0 / 3.0
const val CK2 = 5.413079E-4
const val CK4 = 6.209887E-7
const val XKE = 7.43669161E-2
abstract class Satellite(val data: OrbitalData) {
abstract class OrbitalObject(val data: OrbitalData) {
private val position = Vector4()
private val velocity = Vector4()
private var satPos = SatPos()
private var orbitalPos = OrbitalPos()
private var eclipseDepth = 0.0
private var gsPosTheta = 0.0
private var julUTC = 0.0
@@ -72,8 +53,8 @@ abstract class Satellite(val data: OrbitalData) {
}
}
fun getPosition(pos: GeoPos, time: Long): SatPos {
satPos = SatPos()
fun getPosition(pos: GeoPos, time: Long): OrbitalPos {
orbitalPos = OrbitalPos()
// Date/time at which the position and velocity were calculated
julUTC = calcCurrentDaynum(time) + 2444238.5
// Convert satellite's epoch time to Julian and calculate time since epoch in minutes
@@ -88,10 +69,10 @@ abstract class Satellite(val data: OrbitalData) {
// Angles in rads, dist in km, vel in km/S. Calculate sat Az, El, Range and Range-rate.
calculateObs(julUTC, position, velocity, pos, squintVector)
calculateLatLonAlt(julUTC)
satPos.time = time
satPos.eclipsed = isEclipsed()
satPos.eclipseDepth = eclipseDepth
return satPos
orbitalPos.time = time
orbitalPos.eclipsed = isEclipsed()
orbitalPos.eclipseDepth = eclipseDepth
return orbitalPos
}
private fun calcCurrentDaynum(now: Long): Double {
@@ -118,8 +99,8 @@ abstract class Satellite(val data: OrbitalData) {
}
private fun calculateSDP4orSGP4(tsince: Double) {
if (data.isDeepSpace) (this as DeepSpaceSatellite).calculateSDP4(tsince)
else (this as NearEarthSatellite).calculateSGP4(tsince)
if (data.isDeepSpace) (this as DeepSpaceObject).calculateSDP4(tsince)
else (this as NearEarthObject).calculateSGP4(tsince)
}
// Converts the sat position and velocity vectors to km and km/sec
@@ -164,13 +145,13 @@ abstract class Satellite(val data: OrbitalData) {
var azim = atan(-topE / topS)
if (topS > 0.0) azim += PI
if (azim < 0.0) azim += TWO_PI
satPos.azimuth = azim
satPos.elevation = asin(topZ / range.w)
satPos.distance = range.w
satPos.distanceRate = dot(range, rgvel) / range.w
var elevation = satPos.elevation / TWO_PI * 360.0
orbitalPos.azimuth = azim
orbitalPos.elevation = asin(topZ / range.w)
orbitalPos.distance = range.w
orbitalPos.distanceRate = dot(range, rgvel) / range.w
var elevation = orbitalPos.elevation / TWO_PI * 360.0
if (elevation > 90) elevation = 180 - elevation
satPos.aboveHorizon = elevation - 0 > EPSILON
orbitalPos.aboveHorizon = elevation - 0 > EPSILON
}
// Returns the ECI position and velocity of the observer
@@ -196,26 +177,26 @@ abstract class Satellite(val data: OrbitalData) {
// Calculate the geodetic position of an object given its ECI pos and time
private fun calculateLatLonAlt(time: Double) {
satPos.theta = atan2(position.y, position.x)
satPos.longitude = mod2PI(satPos.theta - thetaGJD(time))
orbitalPos.theta = atan2(position.y, position.x)
orbitalPos.longitude = mod2PI(orbitalPos.theta - thetaGJD(time))
val r = sqrt(sqr(position.x) + sqr(position.y))
val e2 = FLAT_FACT * (2.0 - FLAT_FACT)
satPos.latitude = atan2(position.z, r)
orbitalPos.latitude = atan2(position.z, r)
var phi: Double
var c: Double
var i = 0
var converged: Boolean
do {
phi = satPos.latitude
phi = orbitalPos.latitude
c = invert(sqrt(1.0 - e2 * sqr(sin(phi))))
satPos.latitude = atan2(position.z + EARTH_RADIUS * c * e2 * sin(phi), r)
converged = abs(satPos.latitude - phi) < EPSILON
orbitalPos.latitude = atan2(position.z + EARTH_RADIUS * c * e2 * sin(phi), r)
converged = abs(orbitalPos.latitude - phi) < EPSILON
} while (i++ < 10 && !converged)
satPos.altitude = r / cos(satPos.latitude) - EARTH_RADIUS * c
var temp = satPos.latitude
orbitalPos.altitude = r / cos(orbitalPos.latitude) - EARTH_RADIUS * c
var temp = orbitalPos.latitude
if (temp > PI_2) {
temp -= TWO_PI
satPos.latitude = temp
orbitalPos.latitude = temp
}
}
@@ -302,7 +283,7 @@ abstract class Satellite(val data: OrbitalData) {
internal fun calculatePhase(xlt: Double, xnode: Double, omgadf: Double) {
var phaseValue = xlt - xnode - omgadf + TWO_PI
if (phaseValue < 0.0) phaseValue += TWO_PI
satPos.phase = mod2PI(phaseValue)
orbitalPos.phase = mod2PI(phaseValue)
}
// Sets perigee and checks and adjusts the calculation if the perigee is less tan 156KM
@@ -17,17 +17,17 @@
*/
package com.rtbishop.look4sat.domain.predict
data class SatPass(
data class OrbitalPass(
val aosTime: Long = 0L,
val aosAzimuth: Double = 90.0,
val losTime: Long = 0L,
val losAzimuth: Double = 270.0,
val altitude: Int = 1000,
val maxElevation: Double = 75.0,
val satellite: Satellite,
val orbitalObject: OrbitalObject,
var progress: Float = 0.0f
) {
val catNum: Int = satellite.data.catnum
val name: String = satellite.data.name
val isDeepSpace: Boolean = satellite.data.isDeepSpace
val catNum: Int = orbitalObject.data.catnum
val name: String = orbitalObject.data.name
val isDeepSpace: Boolean = orbitalObject.data.isDeepSpace
}
@@ -25,7 +25,7 @@ import kotlin.math.pow
import kotlin.math.sin
import kotlin.math.sqrt
data class SatPos(
data class OrbitalPos(
var azimuth: Double = 0.0,
var elevation: Double = 0.0,
var latitude: Double = 0.0,
@@ -2,19 +2,19 @@ package com.rtbishop.look4sat.domain.repository
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 com.rtbishop.look4sat.domain.predict.OrbitalObject
import com.rtbishop.look4sat.domain.predict.OrbitalPass
import com.rtbishop.look4sat.domain.predict.OrbitalPos
import kotlinx.coroutines.flow.StateFlow
interface ISatelliteRepo {
val passes: StateFlow<List<SatPass>>
val satellites: StateFlow<List<Satellite>>
val passes: StateFlow<List<OrbitalPass>>
val satellites: StateFlow<List<OrbitalObject>>
suspend fun getRadiosWithId(id: Int): List<SatRadio>
suspend fun initRepository()
suspend fun getPosition(sat: Satellite, pos: GeoPos, time: Long): SatPos
suspend fun getTrack(sat: Satellite, pos: GeoPos, start: Long, end: Long): List<SatPos>
suspend fun getRadios(sat: Satellite, pos: GeoPos, radios: List<SatRadio>, time: Long): List<SatRadio>
suspend fun processPasses(passList: List<SatPass>, time: Long): List<SatPass>
suspend fun getPosition(sat: OrbitalObject, pos: GeoPos, time: Long): OrbitalPos
suspend fun getTrack(sat: OrbitalObject, pos: GeoPos, start: Long, end: Long): List<OrbitalPos>
suspend fun getRadios(sat: OrbitalObject, pos: GeoPos, radios: List<SatRadio>, time: Long): List<SatRadio>
suspend fun processPasses(passList: List<OrbitalPass>, time: Long): List<OrbitalPass>
suspend fun calculatePasses(time: Long, hoursAhead: Int, minElevation: Double, modes: List<String>)
}
@@ -1,8 +1,8 @@
package com.rtbishop.look4sat.domain.source
package com.rtbishop.look4sat.domain.repository
import kotlinx.coroutines.flow.StateFlow
interface ISensorSource {
interface ISensorsRepo {
val orientation: StateFlow<Pair<Float, Float>>
fun enableSensor()
fun disableSensor()
@@ -25,37 +25,6 @@ import kotlinx.coroutines.flow.StateFlow
interface ISettingsRepo {
val radioSourceUrl: String get() = "https://db.satnogs.org/api/transmitters/?format=json"
val satelliteSourcesMap: Map<String, String>
get() = mapOf(
"All" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=active&FORMAT=csv",
"Amsat" to "https://amsat.org/tle/current/nasabare.txt",
"Amateur" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=amateur&FORMAT=csv",
"Classified" to "https://www.mmccants.org/tles/classfd.zip",
"Cubesat" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=cubesat&FORMAT=csv",
"Education" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=education&FORMAT=csv",
"Engineer" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=engineering&FORMAT=csv",
"Geostationary" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=geo&FORMAT=csv",
"Globalstar" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=globalstar&FORMAT=csv",
"GNSS" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=gnss&FORMAT=csv",
"Intelsat" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=intelsat&FORMAT=csv",
"Iridium" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=iridium-NEXT&FORMAT=csv",
"McCants" to "https://www.mmccants.org/tles/inttles.zip",
"Military" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=military&FORMAT=csv",
"New" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=last-30-days&FORMAT=csv",
"OneWeb" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=oneweb&FORMAT=csv",
"Orbcomm" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=orbcomm&FORMAT=csv",
"R4UAB" to "https://r4uab.ru/satonline.txt",
"Resource" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=resource&FORMAT=csv",
"SatNOGS" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=satnogs&FORMAT=csv",
"Science" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=science&FORMAT=csv",
"Spire" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=spire&FORMAT=csv",
"Starlink" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=starlink&FORMAT=csv",
"Swarm" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=swarm&FORMAT=csv",
"Weather" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=weather&FORMAT=csv",
"X-Comm" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=x-comm&FORMAT=csv"
)
//region # Satellites selection settings
val selectedIds: StateFlow<List<Int>>
fun setSelectedIds(ids: List<Int>)
@@ -1,24 +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.source
import java.io.InputStream
interface IFileSource {
suspend fun getFileStream(uri: String): InputStream?
}
@@ -17,16 +17,16 @@
*/
package com.rtbishop.look4sat.domain.source
import com.rtbishop.look4sat.domain.model.SatEntry
import com.rtbishop.look4sat.domain.model.SatItem
import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.domain.predict.Satellite
import com.rtbishop.look4sat.domain.predict.OrbitalData
import com.rtbishop.look4sat.domain.predict.OrbitalObject
interface ILocalSource {
suspend fun getEntriesTotal(): Int
suspend fun getEntriesList(): List<SatItem>
suspend fun getEntriesWithIds(ids: List<Int>): List<Satellite>
suspend fun insertEntries(entries: List<SatEntry>)
suspend fun getEntriesWithIds(ids: List<Int>): List<OrbitalObject>
suspend fun insertEntries(entries: List<OrbitalData>)
suspend fun deleteEntries()
suspend fun getIdsWithModes(modes: List<String>): List<Int>
suspend fun getRadiosTotal(): Int
@@ -20,5 +20,6 @@ package com.rtbishop.look4sat.domain.source
import java.io.InputStream
interface IRemoteSource {
suspend fun getRemoteStream(url: String): InputStream?
suspend fun getFileStream(uri: String): InputStream?
suspend fun getNetworkStream(url: String): InputStream?
}
@@ -0,0 +1,33 @@
package com.rtbishop.look4sat.domain.source
object Sources {
const val radioDataUrl = "https://db.satnogs.org/api/transmitters/?format=json"
val satelliteDataUrls = mapOf(
"All" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=active&FORMAT=csv",
"Amsat" to "https://amsat.org/tle/current/nasabare.txt",
"Amateur" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=amateur&FORMAT=csv",
"Classified" to "https://www.mmccants.org/tles/classfd.zip",
"Cubesat" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=cubesat&FORMAT=csv",
"Education" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=education&FORMAT=csv",
"Engineer" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=engineering&FORMAT=csv",
"Geostationary" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=geo&FORMAT=csv",
"Globalstar" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=globalstar&FORMAT=csv",
"GNSS" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=gnss&FORMAT=csv",
"Intelsat" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=intelsat&FORMAT=csv",
"Iridium" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=iridium-NEXT&FORMAT=csv",
"McCants" to "https://www.mmccants.org/tles/inttles.zip",
"Military" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=military&FORMAT=csv",
"New" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=last-30-days&FORMAT=csv",
"OneWeb" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=oneweb&FORMAT=csv",
"Orbcomm" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=orbcomm&FORMAT=csv",
"R4UAB" to "https://r4uab.ru/satonline.txt",
"Resource" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=resource&FORMAT=csv",
"SatNOGS" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=satnogs&FORMAT=csv",
"Science" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=science&FORMAT=csv",
"Spire" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=spire&FORMAT=csv",
"Starlink" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=starlink&FORMAT=csv",
"Swarm" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=swarm&FORMAT=csv",
"Weather" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=weather&FORMAT=csv",
"X-Comm" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=x-comm&FORMAT=csv"
)
}