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

@@ -19,7 +19,7 @@ package com.rtbishop.look4sat
import android.app.Application import android.app.Application
import com.rtbishop.look4sat.domain.IDataRepository import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.ISettingsSource import com.rtbishop.look4sat.domain.ISettingsRepository
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber import timber.log.Timber
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@@ -33,7 +33,7 @@ class MainApplication : Application() {
lateinit var dataRepository: IDataRepository lateinit var dataRepository: IDataRepository
@Inject @Inject
lateinit var settingsSource: ISettingsSource lateinit var settingsRepository: ISettingsRepository
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@@ -44,8 +44,8 @@ class MainApplication : Application() {
} }
private fun checkAutoUpdate() { private fun checkAutoUpdate() {
if (settingsSource.isUpdateEnabled()) { if (settingsRepository.isUpdateEnabled()) {
val timeDelta = System.currentTimeMillis() - settingsSource.getLastUpdateTime() val timeDelta = System.currentTimeMillis() - settingsRepository.getLastUpdateTime()
if (timeDelta > 172800000) { // 48 hours in ms if (timeDelta > 172800000) { // 48 hours in ms
val sdf = SimpleDateFormat("d MMM yyyy - HH:mm:ss", Locale.getDefault()) val sdf = SimpleDateFormat("d MMM yyyy - HH:mm:ss", Locale.getDefault())
Timber.d("Started periodic data update on ${sdf.format(Date())}") Timber.d("Started periodic data update on ${sdf.format(Date())}")
@@ -25,6 +25,7 @@ import android.location.LocationManager
import androidx.room.Room import androidx.room.Room
import com.rtbishop.look4sat.data.DataParser import com.rtbishop.look4sat.data.DataParser
import com.rtbishop.look4sat.data.DataRepository import com.rtbishop.look4sat.data.DataRepository
import com.rtbishop.look4sat.data.SatelliteRepository
import com.rtbishop.look4sat.domain.* import com.rtbishop.look4sat.domain.*
import com.rtbishop.look4sat.framework.* import com.rtbishop.look4sat.framework.*
import com.rtbishop.look4sat.framework.data.* import com.rtbishop.look4sat.framework.data.*
@@ -55,12 +56,14 @@ object MainModule {
@Provides @Provides
@Singleton @Singleton
fun provideNetworkReporter(): NetworkReporter = NetworkReporter(CoroutineScope(Dispatchers.IO)) fun provideNetworkReporter(): NetworkReporter {
return NetworkReporter(CoroutineScope(Dispatchers.IO))
}
@Provides @Provides
@Singleton @Singleton
fun provideDataRepository( fun provideDataRepository(
@ApplicationContext context: Context, settings: ISettingsSource @ApplicationContext context: Context, settings: ISettingsRepository
): IDataRepository { ): IDataRepository {
val db = Room.databaseBuilder(context, MainDatabase::class.java, "Look4SatDb") val db = Room.databaseBuilder(context, MainDatabase::class.java, "Look4SatDb")
.addMigrations(MIGRATION_1_2).fallbackToDestructiveMigration().build() .addMigrations(MIGRATION_1_2).fallbackToDestructiveMigration().build()
@@ -75,28 +78,23 @@ object MainModule {
@Provides @Provides
@Singleton @Singleton
fun provideLocationSource( fun provideSensorRepository(@ApplicationContext context: Context): ISensorRepository {
@ApplicationContext context: Context, settings: ISettingsSource
): ILocationSource {
val manager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
return LocationSource(manager, settings)
}
@Provides
@Singleton
fun provideOrientationSource(@ApplicationContext context: Context): OrientationSource {
val manager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager val manager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
val sensor = manager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR) val sensor = manager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
return OrientationSource(manager, sensor) return SensorRepository(manager, sensor)
} }
@Provides @Provides
@Singleton @Singleton
fun provideSatelliteManager(): ISatelliteManager = SatelliteManager(Dispatchers.Default) fun provideSatelliteRepository(): ISatelliteRepository {
return SatelliteRepository(Dispatchers.Default)
}
@Provides @Provides
@Singleton @Singleton
fun provideSettingsSource(@ApplicationContext context: Context): ISettingsSource { fun provideSettingsRepository(@ApplicationContext context: Context): ISettingsRepository {
return SettingsSource(context.getSharedPreferences("default", Context.MODE_PRIVATE)) val manager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val preferences = context.getSharedPreferences("default", Context.MODE_PRIVATE)
return SettingsRepository(manager, preferences)
} }
} }
@@ -1,84 +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.framework
import android.location.Criteria
import android.location.LocationManager
import androidx.core.location.LocationManagerCompat
import androidx.core.os.CancellationSignal
import com.rtbishop.look4sat.domain.ILocationSource
import com.rtbishop.look4sat.domain.ISettingsSource
import com.rtbishop.look4sat.model.GeoPos
import com.rtbishop.look4sat.utility.QthConverter
import com.rtbishop.look4sat.utility.round
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import timber.log.Timber
import java.util.concurrent.Executors
class LocationSource(
private val manager: LocationManager,
private val settings: ISettingsSource,
) : ILocationSource {
private val _stationPosition = MutableStateFlow(settings.loadStationPosition())
private val defaultProvider = LocationManager.PASSIVE_PROVIDER
private val executor = Executors.newSingleThreadExecutor()
private val timeoutSignal = CancellationSignal().apply {
setOnCancelListener { _stationPosition.value = _stationPosition.value }
}
override val stationPosition: StateFlow<GeoPos> = _stationPosition
override fun setGpsPosition(): Boolean {
if (!LocationManagerCompat.isLocationEnabled(manager)) return false
try {
val criteria = Criteria().apply { isCostAllowed = true }
val provider = manager.getBestProvider(criteria, true) ?: defaultProvider
Timber.d("Requesting location for $provider provider")
LocationManagerCompat.getCurrentLocation(manager, provider, timeoutSignal, executor) {
it?.let { setGeoPosition(it.latitude, it.longitude, it.altitude) }
}
} catch (exception: SecurityException) {
Timber.d("No permissions were given")
}
return true
}
override fun setGeoPosition(latitude: Double, longitude: Double, altitude: Double): Boolean {
val newLongitude = if (longitude > 180.0) longitude - 180 else longitude
val locator = QthConverter.positionToQth(latitude, newLongitude) ?: return false
saveGeoPos(latitude, newLongitude, altitude, locator)
return true
}
override fun setQthPosition(locator: String): Boolean {
val position = QthConverter.qthToPosition(locator) ?: return false
saveGeoPos(position.latitude, position.longitude, 0.0, locator)
return true
}
private fun saveGeoPos(latitude: Double, longitude: Double, altitude: Double, locator: String) {
val newLat = latitude.round(4)
val newLon = longitude.round(4)
val newAlt = altitude.round(1)
val timestamp = System.currentTimeMillis()
Timber.d("Received new Position($newLat, $newLon, $newAlt) & Locator $locator")
_stationPosition.value = GeoPos(newLat, newLon, newAlt, locator, timestamp)
settings.saveStationPosition(stationPosition.value)
}
}
@@ -21,27 +21,27 @@ import android.hardware.Sensor
import android.hardware.SensorEvent import android.hardware.SensorEvent
import android.hardware.SensorEventListener import android.hardware.SensorEventListener
import android.hardware.SensorManager import android.hardware.SensorManager
import com.rtbishop.look4sat.domain.ISensorRepository
import com.rtbishop.look4sat.domain.RAD2DEG import com.rtbishop.look4sat.domain.RAD2DEG
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlin.math.round import kotlin.math.round
class OrientationSource( class SensorRepository(private val manager: SensorManager, private val sensor: Sensor?) :
private val manager: SensorManager, private val sensor: Sensor? SensorEventListener, ISensorRepository {
) : SensorEventListener {
private val rotationMatrix = FloatArray(9) private val rotationMatrix = FloatArray(9)
private val orientationValues = FloatArray(3) private val orientationValues = FloatArray(3)
private var sensorAccuracy: Int = SensorManager.SENSOR_STATUS_UNRELIABLE private var sensorAccuracy: Int = SensorManager.SENSOR_STATUS_UNRELIABLE
private val _orientation = MutableStateFlow(Pair(0f, 0f)) private val _orientation = MutableStateFlow(Pair(0f, 0f))
val orientation: StateFlow<Pair<Float, Float>> = _orientation override val orientation: StateFlow<Pair<Float, Float>> = _orientation
fun startListening() = sensor?.let { override fun enableSensor() {
manager.registerListener(this, it, 16000) sensor?.let { manager.registerListener(this, it, 16000) }
} }
fun stopListening() = manager.unregisterListener(this) override fun disableSensor() = manager.unregisterListener(this)
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) { override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
sensorAccuracy = accuracy sensorAccuracy = accuracy
@@ -0,0 +1,277 @@
/*
* 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.framework
import android.content.SharedPreferences
import android.location.Criteria
import android.location.LocationManager
import androidx.core.content.edit
import androidx.core.location.LocationManagerCompat
import androidx.core.os.CancellationSignal
import com.rtbishop.look4sat.domain.ISettingsRepository
import com.rtbishop.look4sat.model.GeoPos
import com.rtbishop.look4sat.utility.QthConverter
import com.rtbishop.look4sat.utility.round
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import timber.log.Timber
import java.util.concurrent.Executors
class SettingsRepository(
private val manager: LocationManager, private val preferences: SharedPreferences
) : ISettingsRepository {
private val keyModes = "satModes"
private val keyCompass = "compass"
private val keyRadarSweep = "radarSweep"
private val keyTimeUTC = "timeUTC"
private val keyLastUpdateTime = "lastUpdateTime"
private val keyAutoUpdateEnabled = "autoUpdateEnabled"
private val keyHoursAhead = "hoursAhead"
private val keyMinElevation = "minElevation"
private val keyRotator = "isRotatorEnabled"
private val keyRotatorAddress = "rotatorAddress"
private val keyRotatorPort = "rotatorPort"
private val keyBTEnabled = "isBTEnabled"
private val keyBTDeviceName = "BTDeviceName"
private val keyBTDeviceAddr = "BTDeviceAddr"
private val keyBTFormat = "BTFormat"
private val keyLatitude = "stationLat"
private val keyLongitude = "stationLon"
private val keyAltitude = "stationAlt"
private val keyLocator = "stationQTH"
private val keyLocTimestamp = "locTimestamp"
private val keySelection = "selection"
//region # Station position settings
private val _stationPosition = MutableStateFlow(loadStationPosition())
private val defaultProvider = LocationManager.PASSIVE_PROVIDER
private val executor = Executors.newSingleThreadExecutor()
private val timeoutSignal = CancellationSignal().apply {
setOnCancelListener { _stationPosition.value = loadStationPosition() }
}
override val stationPosition: StateFlow<GeoPos> = _stationPosition
override fun setGpsPosition(): Boolean {
if (!LocationManagerCompat.isLocationEnabled(manager)) return false
try {
val criteria = Criteria().apply { isCostAllowed = true }
val provider = manager.getBestProvider(criteria, true) ?: defaultProvider
Timber.d("Requesting location for $provider provider")
LocationManagerCompat.getCurrentLocation(manager, provider, timeoutSignal, executor) {
it?.let { setGeoPosition(it.latitude, it.longitude, it.altitude) }
}
} catch (exception: SecurityException) {
Timber.d("No permissions were given")
}
return true
}
override fun setGeoPosition(latitude: Double, longitude: Double, altitude: Double): Boolean {
val newLongitude = if (longitude > 180.0) longitude - 180 else longitude
val locator = QthConverter.positionToQth(latitude, newLongitude) ?: return false
saveGeoPos(latitude, newLongitude, altitude, locator)
return true
}
override fun setQthPosition(locator: String): Boolean {
val position = QthConverter.qthToPosition(locator) ?: return false
saveGeoPos(position.latitude, position.longitude, 0.0, locator)
return true
}
private fun loadStationPosition(): GeoPos {
val latitude = (preferences.getString(keyLatitude, null) ?: "0.0").toDouble()
val longitude = (preferences.getString(keyLongitude, null) ?: "0.0").toDouble()
val altitude = (preferences.getString(keyAltitude, null) ?: "0.0").toDouble()
val qthLocator = preferences.getString(keyLocator, null) ?: "null"
val timestamp = preferences.getLong(keyLocTimestamp, 0L)
return GeoPos(latitude, longitude, altitude, qthLocator, timestamp)
}
private fun saveStationPosition(stationPos: GeoPos) = preferences.edit {
putString(keyLatitude, stationPos.latitude.toString())
putString(keyLongitude, stationPos.longitude.toString())
putString(keyAltitude, stationPos.altitude.toString())
putString(keyLocator, stationPos.qthLocator)
putLong(keyLocTimestamp, stationPos.timestamp)
_stationPosition.value = stationPos
}
private fun saveGeoPos(latitude: Double, longitude: Double, altitude: Double, locator: String) {
val newLat = latitude.round(4)
val newLon = longitude.round(4)
val newAlt = altitude.round(1)
val timestamp = System.currentTimeMillis()
Timber.d("Received new Position($newLat, $newLon, $newAlt) & Locator $locator")
saveStationPosition(GeoPos(newLat, newLon, newAlt, locator, timestamp))
}
//endregion
override fun saveEntriesSelection(catnums: List<Int>) {
val stringList = catnums.map { catnum -> catnum.toString() }
preferences.edit { putStringSet(keySelection, stringList.toSet()) }
}
override fun loadEntriesSelection(): List<Int> {
val catnums =
preferences.getStringSet(keySelection, emptySet())?.map { catnum -> catnum.toInt() }
return catnums?.sorted() ?: emptyList()
}
override fun saveSatType(type: String, catnums: List<Int>) {
val stringList = catnums.map { catnum -> catnum.toString() }
preferences.edit { putStringSet("type$type", stringList.toSet()) }
}
override fun loadSatType(type: String): List<Int> {
val catnums =
preferences.getStringSet("type$type", emptySet())?.map { catnum -> catnum.toInt() }
return catnums ?: emptyList()
}
override fun getHoursAhead(): Int {
return preferences.getInt(keyHoursAhead, 24)
}
override fun setHoursAhead(hoursAhead: Int) {
preferences.edit { putInt(keyHoursAhead, hoursAhead) }
}
override fun getMinElevation(): Double {
return preferences.getDouble(keyMinElevation, 16.0)
}
override fun setMinElevation(minElevation: Double) {
preferences.edit { putDouble(keyMinElevation, minElevation) }
}
override fun isUtcEnabled(): Boolean {
return preferences.getBoolean(keyTimeUTC, false)
}
override fun setUtcState(value: Boolean) {
preferences.edit { putBoolean(keyTimeUTC, value) }
}
override fun getLastUpdateTime(): Long {
return preferences.getLong(keyLastUpdateTime, 0L)
}
override fun setLastUpdateTime(updateTime: Long) {
preferences.edit { putLong(keyLastUpdateTime, updateTime) }
}
override fun isUpdateEnabled(): Boolean {
return preferences.getBoolean(keyAutoUpdateEnabled, true)
}
override fun setUpdateState(value: Boolean) {
preferences.edit { putBoolean(keyAutoUpdateEnabled, value) }
}
override fun isSensorEnabled(): Boolean {
return preferences.getBoolean(keyCompass, true)
}
override fun setSensorState(value: Boolean) {
preferences.edit { putBoolean(keyCompass, value) }
}
override fun isSweepEnabled(): Boolean {
return preferences.getBoolean(keyRadarSweep, true)
}
override fun setSweepState(value: Boolean) {
preferences.edit { putBoolean(keyRadarSweep, value) }
}
override fun saveModesSelection(modes: List<String>) {
preferences.edit { putStringSet(keyModes, modes.toSet()) }
}
override fun loadModesSelection(): List<String> {
return preferences.getStringSet(keyModes, null)?.toList()?.sorted() ?: emptyList()
}
override fun getRotatorEnabled(): Boolean {
return preferences.getBoolean(keyRotator, false)
}
override fun setRotatorEnabled(value: Boolean) {
preferences.edit { putBoolean(keyRotator, value) }
}
override fun getRotatorServer(): String {
return preferences.getString(keyRotatorAddress, null) ?: "127.0.0.1"
}
override fun setRotatorServer(value: String) {
preferences.edit { putString(keyRotatorAddress, value) }
}
override fun getRotatorPort(): String {
return preferences.getString(keyRotatorPort, null) ?: "4533"
}
override fun setRotatorPort(value: String) {
preferences.edit { putString(keyRotatorPort, value) }
}
override fun getBTEnabled(): Boolean {
return preferences.getBoolean(keyBTEnabled, false)
}
override fun setBTEnabled(value: Boolean) {
preferences.edit { putBoolean(keyBTEnabled, value) }
}
override fun getBTDeviceAddr(): String {
return preferences.getString(keyBTDeviceAddr, null) ?: "00:0C:BF:13:80:5D"
}
override fun setBTDeviceAddr(value: String) {
preferences.edit { putString(keyBTDeviceAddr, value) }
}
override fun getBTDeviceName(): String {
return preferences.getString(keyBTDeviceName, null) ?: "Default"
}
override fun setBTDeviceName(value: String) {
preferences.edit { putString(keyBTDeviceName, value) }
}
override fun getBTFormat(): String {
return preferences.getString(keyBTFormat, null) ?: "W\$AZ \$EL"
}
override fun setBTFormat(value: String) {
preferences.edit { putString(keyBTFormat, value) }
}
private fun SharedPreferences.getDouble(key: String, default: Double): Double {
return Double.fromBits(getLong(key, default.toRawBits()))
}
private fun SharedPreferences.Editor.putDouble(key: String, double: Double) {
putLong(key, double.toRawBits())
}
}
@@ -1,238 +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.framework
import android.content.SharedPreferences
import androidx.core.content.edit
import com.rtbishop.look4sat.domain.ISettingsSource
import com.rtbishop.look4sat.model.GeoPos
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.onCompletion
class SettingsSource(private val prefs: SharedPreferences) : ISettingsSource {
companion object {
const val keyModes = "satModes"
const val keyCompass = "compass"
const val keyRadarSweep = "radarSweep"
const val keyTimeUTC = "timeUTC"
const val keyLastUpdateTime = "lastUpdateTime"
const val keyAutoUpdateEnabled = "autoUpdateEnabled"
const val keyHoursAhead = "hoursAhead"
const val keyMinElevation = "minElevation"
const val keyRotator = "isRotatorEnabled"
const val keyRotatorAddress = "rotatorAddress"
const val keyRotatorPort = "rotatorPort"
const val keyBTEnabled = "isBTEnabled"
const val keyBTDeviceName = "BTDeviceName"
const val keyBTDeviceAddr = "BTDeviceAddr"
const val keyBTFormat = "BTFormat"
const val keyLatitude = "stationLat"
const val keyLongitude = "stationLon"
const val keyAltitude = "stationAlt"
const val keyLocator = "stationQTH"
const val keyLocTimestamp = "locTimestamp"
const val keySelection = "selection"
}
override fun loadStationPosition(): GeoPos {
val latitude = (prefs.getString(keyLatitude, null) ?: "0.0").toDouble()
val longitude = (prefs.getString(keyLongitude, null) ?: "0.0").toDouble()
val altitude = (prefs.getString(keyAltitude, null) ?: "0.0").toDouble()
val qthLocator = prefs.getString(keyLocator, null) ?: "null"
val timestamp = prefs.getLong(keyLocTimestamp, 0L)
return GeoPos(latitude, longitude, altitude, qthLocator, timestamp)
}
override fun saveStationPosition(stationPos: GeoPos) = prefs.edit {
putString(keyLatitude, stationPos.latitude.toString())
putString(keyLongitude, stationPos.longitude.toString())
putString(keyAltitude, stationPos.altitude.toString())
putString(keyLocator, stationPos.qthLocator)
putLong(keyLocTimestamp, stationPos.timestamp)
}
override fun saveEntriesSelection(catnums: List<Int>) {
val stringList = catnums.map { catnum -> catnum.toString() }
prefs.edit { putStringSet(keySelection, stringList.toSet()) }
}
override fun loadEntriesSelection(): List<Int> {
val catnums = prefs.getStringSet(keySelection, emptySet())?.map { catnum -> catnum.toInt() }
return catnums?.sorted() ?: emptyList()
}
override fun saveSatType(type: String, catnums: List<Int>) {
val stringList = catnums.map { catnum -> catnum.toString() }
prefs.edit { putStringSet("type$type", stringList.toSet()) }
}
override fun loadSatType(type: String): List<Int> {
val catnums = prefs.getStringSet("type$type", emptySet())?.map { catnum -> catnum.toInt() }
return catnums ?: emptyList()
}
override fun getHoursAhead(): Int {
return prefs.getInt(keyHoursAhead, 24)
}
override fun setHoursAhead(hoursAhead: Int) {
prefs.edit { putInt(keyHoursAhead, hoursAhead) }
}
override fun getMinElevation(): Double {
return prefs.getDouble(keyMinElevation, 16.0)
}
override fun setMinElevation(minElevation: Double) {
prefs.edit { putDouble(keyMinElevation, minElevation) }
}
override fun isUtcEnabled(): Boolean {
return prefs.getBoolean(keyTimeUTC, false)
}
override fun setUtcState(value: Boolean) {
prefs.edit { putBoolean(keyTimeUTC, value) }
}
override fun getLastUpdateTime(): Long {
return prefs.getLong(keyLastUpdateTime, 0L)
}
override fun setLastUpdateTime(updateTime: Long) {
prefs.edit { putLong(keyLastUpdateTime, updateTime) }
}
override fun isUpdateEnabled(): Boolean {
return prefs.getBoolean(keyAutoUpdateEnabled, true)
}
override fun setUpdateState(value: Boolean) {
prefs.edit { putBoolean(keyAutoUpdateEnabled, value) }
}
override fun isSensorEnabled(): Boolean {
return prefs.getBoolean(keyCompass, true)
}
override fun setSensorState(value: Boolean) {
prefs.edit { putBoolean(keyCompass, value) }
}
override fun isSweepEnabled(): Boolean {
return prefs.getBoolean(keyRadarSweep, true)
}
override fun setSweepState(value: Boolean) {
prefs.edit { putBoolean(keyRadarSweep, value) }
}
override fun saveModesSelection(modes: List<String>) {
prefs.edit { putStringSet(keyModes, modes.toSet()) }
}
override fun loadModesSelection(): List<String> {
return prefs.getStringSet(keyModes, null)?.toList()?.sorted() ?: emptyList()
}
override fun getRotatorEnabled(): Boolean {
return prefs.getBoolean(keyRotator, false)
}
override fun setRotatorEnabled(value: Boolean) {
prefs.edit { putBoolean(keyRotator, value) }
}
override fun getRotatorServer(): String {
return prefs.getString(keyRotatorAddress, null) ?: "127.0.0.1"
}
override fun setRotatorServer(value: String) {
prefs.edit { putString(keyRotatorAddress, value) }
}
override fun getRotatorPort(): String {
return prefs.getString(keyRotatorPort, null) ?: "4533"
}
override fun setRotatorPort(value: String) {
prefs.edit { putString(keyRotatorPort, value) }
}
override fun getBTEnabled(): Boolean {
return prefs.getBoolean(keyBTEnabled, false)
}
override fun setBTEnabled(value: Boolean) {
prefs.edit { putBoolean(keyBTEnabled, value) }
}
override fun getBTDeviceAddr(): String {
return prefs.getString(keyBTDeviceAddr, null) ?: "00:0C:BF:13:80:5D"
}
override fun setBTDeviceAddr(value: String) {
prefs.edit { putString(keyBTDeviceAddr, value) }
}
override fun getBTDeviceName(): String {
return prefs.getString(keyBTDeviceName, null) ?: "Default"
}
override fun setBTDeviceName(value: String) {
prefs.edit { putString(keyBTDeviceName, value) }
}
override fun getBTFormat(): String {
return prefs.getString(keyBTFormat, null) ?: "W\$AZ \$EL"
}
override fun setBTFormat(value: String) {
prefs.edit { putString(keyBTFormat, value) }
}
private fun SharedPreferences.getDouble(key: String, default: Double): Double {
return Double.fromBits(getLong(key, default.toRawBits()))
}
private fun SharedPreferences.Editor.putDouble(key: String, double: Double) {
putLong(key, double.toRawBits())
}
inline fun <reified T> SharedPreferences.observeKey(key: String, default: T): Flow<T> {
val flow = MutableStateFlow(getItem(key, default))
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, k ->
if (key == k) flow.value = getItem(key, default)
}
registerOnSharedPreferenceChangeListener(listener)
return flow.onCompletion { unregisterOnSharedPreferenceChangeListener(listener) }
}
inline fun <reified T> SharedPreferences.getItem(key: String, default: T): T {
return when (default) {
is Boolean -> getBoolean(key, default) as T
is Int -> getInt(key, default) as T
is Long -> getLong(key, default) as T
is Float -> getFloat(key, default) as T
is String -> getString(key, default) as T
else -> throw IllegalArgumentException("Could not handle ${T::class.java.name}")
}
}
}
@@ -20,11 +20,9 @@ package com.rtbishop.look4sat.presentation.entries
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.rtbishop.look4sat.domain.IDataRepository import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.ILocationSource import com.rtbishop.look4sat.domain.ISatelliteRepository
import com.rtbishop.look4sat.domain.ISatelliteManager import com.rtbishop.look4sat.domain.ISettingsRepository
import com.rtbishop.look4sat.domain.ISettingsSource
import com.rtbishop.look4sat.model.DataState import com.rtbishop.look4sat.model.DataState
import com.rtbishop.look4sat.model.GeoPos
import com.rtbishop.look4sat.model.SatItem import com.rtbishop.look4sat.model.SatItem
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -37,10 +35,9 @@ import javax.inject.Inject
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@HiltViewModel @HiltViewModel
class EntriesViewModel @Inject constructor( class EntriesViewModel @Inject constructor(
private val satelliteManager: ISatelliteManager, private val dataRepository: IDataRepository,
private val repository: IDataRepository, private val satelliteRepository: ISatelliteRepository,
private val settings: ISettingsSource, private val settingsRepository: ISettingsRepository
private val locationSource: ILocationSource
) : ViewModel() { ) : ViewModel() {
private val satType = MutableStateFlow("All") private val satType = MutableStateFlow("All")
@@ -53,12 +50,12 @@ class EntriesViewModel @Inject constructor(
itemsWithType.map { items -> filterByQuery(items, query) } itemsWithType.map { items -> filterByQuery(items, query) }
} }
val satData = itemsWithQuery.map { items -> DataState.Success(items) } val satData = itemsWithQuery.map { items -> DataState.Success(items) }
val satTypes: List<String> = settings.sourcesMap.keys.sorted() val satTypes = dataRepository.getSatelliteTypes()
init { init {
viewModelScope.launch { viewModelScope.launch {
delay(250) delay(250)
itemsFromRepo.value = loadEntriesWithSelection() itemsFromRepo.value = dataRepository.getEntriesWithSelection()
} }
} }
@@ -78,15 +75,14 @@ class EntriesViewModel @Inject constructor(
fun saveSelection() = viewModelScope.launch { fun saveSelection() = viewModelScope.launch {
val newSelection = itemsFromRepo.value.filter { it.isSelected }.map { it.catnum } val newSelection = itemsFromRepo.value.filter { it.isSelected }.map { it.catnum }
settings.saveEntriesSelection(newSelection) settingsRepository.saveEntriesSelection(newSelection)
val selectedIds = settings.loadEntriesSelection() val selectedIds = settingsRepository.loadEntriesSelection()
val satellites = repository.getEntriesWithIds(selectedIds) val satellites = dataRepository.getEntriesWithIds(selectedIds)
val stationPos = locationSource.stationPosition.value val stationPos = settingsRepository.stationPosition.value
val geoPos = GeoPos(stationPos.latitude, stationPos.longitude)
val timeRef = System.currentTimeMillis() val timeRef = System.currentTimeMillis()
val hoursAhead = settings.getHoursAhead() val hoursAhead = settingsRepository.getHoursAhead()
val minElev = settings.getMinElevation() val minElev = settingsRepository.getMinElevation()
satelliteManager.calculatePasses(satellites, geoPos, timeRef, hoursAhead, minElev) satelliteRepository.calculatePasses(satellites, stationPos, timeRef, hoursAhead, minElev)
} }
fun updateSelection(catNums: List<Int>, isSelected: Boolean) { fun updateSelection(catNums: List<Int>, isSelected: Boolean) {
@@ -99,14 +95,9 @@ class EntriesViewModel @Inject constructor(
} }
} }
private suspend fun loadEntriesWithSelection(): List<SatItem> {
val selectedIds = settings.loadEntriesSelection()
return repository.getEntriesWithModes().onEach { it.isSelected = it.catnum in selectedIds }
}
private fun filterByType(items: List<SatItem>, type: String): List<SatItem> { private fun filterByType(items: List<SatItem>, type: String): List<SatItem> {
if (type == "All") return items if (type == "All") return items
val catnums = settings.loadSatType(type) val catnums = settingsRepository.loadSatType(type)
if (catnums.isEmpty()) return items if (catnums.isEmpty()) return items
return items.filter { item -> item.catnum in catnums } return items.filter { item -> item.catnum in catnums }
} }
@@ -19,7 +19,10 @@ package com.rtbishop.look4sat.presentation.map
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.rtbishop.look4sat.domain.* import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.ISatelliteRepository
import com.rtbishop.look4sat.domain.ISettingsRepository
import com.rtbishop.look4sat.domain.Satellite
import com.rtbishop.look4sat.model.GeoPos import com.rtbishop.look4sat.model.GeoPos
import com.rtbishop.look4sat.model.SatPos import com.rtbishop.look4sat.model.SatPos
import com.rtbishop.look4sat.utility.QthConverter import com.rtbishop.look4sat.utility.QthConverter
@@ -38,15 +41,14 @@ import kotlin.math.min
@HiltViewModel @HiltViewModel
class MapViewModel @Inject constructor( class MapViewModel @Inject constructor(
private val repository: IDataRepository, private val dataRepository: IDataRepository,
private val satelliteManager: ISatelliteManager, private val satelliteRepository: ISatelliteRepository,
private val settings: ISettingsSource, private val settingsRepository: ISettingsRepository
private val locationSource: ILocationSource
) : ViewModel() { ) : ViewModel() {
private val stationPosNew = locationSource.stationPosition.value private val stationPosNew = settingsRepository.stationPosition.value
private val stationPosition = GeoPos(stationPosNew.latitude, stationPosNew.longitude) private val stationPosition = GeoPos(stationPosNew.latitude, stationPosNew.longitude)
private var allPasses = satelliteManager.getPasses() private var allPasses = satelliteRepository.getPasses()
private var allSatellites = listOf<Satellite>() private var allSatellites = listOf<Satellite>()
private var dataUpdateJob: Job? = null private var dataUpdateJob: Job? = null
private var dataUpdateRate = 1000L private var dataUpdateRate = 1000L
@@ -85,8 +87,8 @@ class MapViewModel @Inject constructor(
fun selectDefaultSatellite(catnum: Int) { fun selectDefaultSatellite(catnum: Int) {
viewModelScope.launch { viewModelScope.launch {
val selectedIds = settings.loadEntriesSelection() val selectedIds = settingsRepository.loadEntriesSelection()
repository.getEntriesWithIds(selectedIds).also { satellites -> dataRepository.getEntriesWithIds(selectedIds).also { satellites ->
if (satellites.isNotEmpty()) { if (satellites.isNotEmpty()) {
allSatellites = satellites allSatellites = satellites
if (catnum == -1) { if (catnum == -1) {
@@ -123,7 +125,7 @@ class MapViewModel @Inject constructor(
val currentTrack = mutableListOf<GeoPos>() val currentTrack = mutableListOf<GeoPos>()
val endDate = Date(date.time + (satellite.data.orbitalPeriod * 2.4 * 60000L).toLong()) val endDate = Date(date.time + (satellite.data.orbitalPeriod * 2.4 * 60000L).toLong())
var oldLongitude = 0.0 var oldLongitude = 0.0
satelliteManager.getTrack(satellite, pos, date.time, endDate.time).forEach { satPos -> satelliteRepository.getTrack(satellite, pos, date.time, endDate.time).forEach { satPos ->
val osmLat = clipLat(satPos.latitude.toDegrees()) val osmLat = clipLat(satPos.latitude.toDegrees())
val osmLon = clipLon(satPos.longitude.toDegrees()) val osmLon = clipLon(satPos.longitude.toDegrees())
val currentPosition = GeoPos(osmLat, osmLon) val currentPosition = GeoPos(osmLat, osmLon)
@@ -150,7 +152,7 @@ class MapViewModel @Inject constructor(
private suspend fun getPositions(satellites: List<Satellite>, pos: GeoPos, date: Date) { private suspend fun getPositions(satellites: List<Satellite>, pos: GeoPos, date: Date) {
val positions = mutableMapOf<Satellite, GeoPos>() val positions = mutableMapOf<Satellite, GeoPos>()
satellites.forEach { satellite -> satellites.forEach { satellite ->
val satPos = satelliteManager.getPosition(satellite, pos, date.time) val satPos = satelliteRepository.getPosition(satellite, pos, date.time)
val osmLat = clipLat(satPos.latitude.toDegrees()) val osmLat = clipLat(satPos.latitude.toDegrees())
val osmLon = clipLon(satPos.longitude.toDegrees()) val osmLon = clipLon(satPos.longitude.toDegrees())
positions[satellite] = GeoPos(osmLat, osmLon) positions[satellite] = GeoPos(osmLat, osmLon)
@@ -159,7 +161,7 @@ class MapViewModel @Inject constructor(
} }
private suspend fun getSatFootprint(satellite: Satellite, pos: GeoPos, date: Date) { private suspend fun getSatFootprint(satellite: Satellite, pos: GeoPos, date: Date) {
val satPos = satelliteManager.getPosition(satellite, pos, date.time) val satPos = satelliteRepository.getPosition(satellite, pos, date.time)
_footprint.emit(satPos) _footprint.emit(satPos)
} }
@@ -177,7 +179,7 @@ class MapViewModel @Inject constructor(
} }
} }
} }
val satPos = satelliteManager.getPosition(sat, pos, date.time) val satPos = satelliteRepository.getPosition(sat, pos, date.time)
val azimuth = satPos.azimuth.toDegrees() val azimuth = satPos.azimuth.toDegrees()
val elevation = satPos.elevation.toDegrees() val elevation = satPos.elevation.toDegrees()
val osmLat = clipLat(satPos.latitude.toDegrees()) val osmLat = clipLat(satPos.latitude.toDegrees())
@@ -22,7 +22,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.NearEarthSat import com.rtbishop.look4sat.domain.NearEarthSatellite
import com.rtbishop.look4sat.model.DataState import com.rtbishop.look4sat.model.DataState
import com.rtbishop.look4sat.model.OrbitalData import com.rtbishop.look4sat.model.OrbitalData
import com.rtbishop.look4sat.model.SatPass import com.rtbishop.look4sat.model.SatPass
@@ -100,7 +100,7 @@ private fun PassPreview() {
val data = OrbitalData( val data = OrbitalData(
"Satellite", 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 45000, 0.0 "Satellite", 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 45000, 0.0
) )
val satellite = NearEarthSat(data) val satellite = NearEarthSatellite(data)
val pass = SatPass(1L, 25.0, 10L, 75.0, 850, 45.0, satellite, 0.5f) val pass = SatPass(1L, 25.0, 10L, 75.0, 850, 45.0, satellite, 0.5f)
MainTheme { Pass(pass = pass, { _,_ -> }) } MainTheme { Pass(pass = pass, { _,_ -> }) }
} }
@@ -20,11 +20,9 @@ package com.rtbishop.look4sat.presentation.passes
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.rtbishop.look4sat.domain.IDataRepository import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.ILocationSource import com.rtbishop.look4sat.domain.ISatelliteRepository
import com.rtbishop.look4sat.domain.ISatelliteManager import com.rtbishop.look4sat.domain.ISettingsRepository
import com.rtbishop.look4sat.domain.ISettingsSource
import com.rtbishop.look4sat.model.DataState import com.rtbishop.look4sat.model.DataState
import com.rtbishop.look4sat.model.GeoPos
import com.rtbishop.look4sat.model.SatPass import com.rtbishop.look4sat.model.SatPass
import com.rtbishop.look4sat.utility.toTimerString import com.rtbishop.look4sat.utility.toTimerString
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@@ -35,10 +33,9 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class PassesViewModel @Inject constructor( class PassesViewModel @Inject constructor(
private val satelliteManager: ISatelliteManager, private val dataRepository: IDataRepository,
private val repository: IDataRepository, private val satelliteRepository: ISatelliteRepository,
private val settings: ISettingsSource, private val settingsRepository: ISettingsRepository
private val locationSource: ILocationSource
) : ViewModel() { ) : ViewModel() {
private val _passes = MutableStateFlow<DataState<List<SatPass>>>(DataState.Loading) private val _passes = MutableStateFlow<DataState<List<SatPass>>>(DataState.Loading)
@@ -48,18 +45,18 @@ class PassesViewModel @Inject constructor(
val passes: StateFlow<DataState<List<SatPass>>> = _passes val passes: StateFlow<DataState<List<SatPass>>> = _passes
val timerText: StateFlow<Triple<String, String, String>?> = _timerText val timerText: StateFlow<Triple<String, String, String>?> = _timerText
fun getHoursAhead() = settings.getHoursAhead() fun getHoursAhead() = settingsRepository.getHoursAhead()
fun getMinElevation() = settings.getMinElevation() fun getMinElevation() = settingsRepository.getMinElevation()
init { init {
viewModelScope.launch { viewModelScope.launch {
satelliteManager.calculatedPasses.collect { passes -> satelliteRepository.calculatedPasses.collect { passes ->
passesProcessing?.cancelAndJoin() passesProcessing?.cancelAndJoin()
passesProcessing = viewModelScope.launch { passesProcessing = viewModelScope.launch {
while (isActive) { while (isActive) {
val timeNow = System.currentTimeMillis() val timeNow = System.currentTimeMillis()
val newPasses = satelliteManager.processPasses(passes, timeNow) val newPasses = satelliteRepository.processPasses(passes, timeNow)
if (newPasses.isNotEmpty()) { if (newPasses.isNotEmpty()) {
try { try {
@@ -86,18 +83,18 @@ class PassesViewModel @Inject constructor(
} }
} }
viewModelScope.launch { viewModelScope.launch {
repository.updateState.collect { updateState -> dataRepository.updateState.collect { updateState ->
if (updateState is DataState.Success) calculatePasses() if (updateState is DataState.Success) calculatePasses()
} }
} }
calculatePasses() calculatePasses()
} }
fun shouldUseUTC() = settings.isUtcEnabled() fun shouldUseUTC() = settingsRepository.isUtcEnabled()
fun calculatePasses( fun calculatePasses(
hoursAhead: Int = 1, hoursAhead: Int = 1,
minElevation: Double = settings.getMinElevation(), minElevation: Double = settingsRepository.getMinElevation(),
timeRef: Long = System.currentTimeMillis(), timeRef: Long = System.currentTimeMillis(),
selection: List<Int>? = null selection: List<Int>? = null
) { ) {
@@ -105,15 +102,14 @@ class PassesViewModel @Inject constructor(
_passes.emit(DataState.Loading) _passes.emit(DataState.Loading)
// _timerText.postValue(timerDefaultText) // _timerText.postValue(timerDefaultText)
passesProcessing?.cancelAndJoin() passesProcessing?.cancelAndJoin()
selection?.let { items -> settings.saveEntriesSelection(items) } selection?.let { items -> settingsRepository.saveEntriesSelection(items) }
settings.setHoursAhead(hoursAhead) settingsRepository.setHoursAhead(hoursAhead)
settings.setMinElevation(minElevation) settingsRepository.setMinElevation(minElevation)
val stationPos = locationSource.stationPosition.value val stationPos = settingsRepository.stationPosition.value
val geoPos = GeoPos(stationPos.latitude, stationPos.longitude) val selectedIds = settingsRepository.loadEntriesSelection()
val selectedIds = settings.loadEntriesSelection() val satellites = dataRepository.getEntriesWithIds(selectedIds)
val satellites = repository.getEntriesWithIds(selectedIds) satelliteRepository.calculatePasses(
satelliteManager.calculatePasses( satellites, stationPos, timeRef, hoursAhead, minElevation
satellites, geoPos, timeRef, hoursAhead, minElevation
) )
} }
} }
@@ -26,7 +26,6 @@ import androidx.lifecycle.viewModelScope
import com.rtbishop.look4sat.domain.* import com.rtbishop.look4sat.domain.*
import com.rtbishop.look4sat.framework.BluetoothReporter import com.rtbishop.look4sat.framework.BluetoothReporter
import com.rtbishop.look4sat.framework.NetworkReporter import com.rtbishop.look4sat.framework.NetworkReporter
import com.rtbishop.look4sat.framework.OrientationSource
import com.rtbishop.look4sat.model.GeoPos import com.rtbishop.look4sat.model.GeoPos
import com.rtbishop.look4sat.model.SatPass import com.rtbishop.look4sat.model.SatPass
import com.rtbishop.look4sat.model.SatPos import com.rtbishop.look4sat.model.SatPos
@@ -42,27 +41,26 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class RadarViewModel @Inject constructor( class RadarViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val orientationSource: OrientationSource,
private val reporter: NetworkReporter,
private val bluetoothReporter: BluetoothReporter, private val bluetoothReporter: BluetoothReporter,
private val satManager: ISatelliteManager, private val networkReporter: NetworkReporter,
private val repository: IDataRepository, private val savedStateHandle: SavedStateHandle,
private val settings: ISettingsSource, private val dataRepository: IDataRepository,
private val locationSource: ILocationSource private val satelliteRepository: ISatelliteRepository,
private val sensorRepository: ISensorRepository,
private val settingsRepository: ISettingsRepository
) : ViewModel() { ) : ViewModel() {
private val stationPos = locationSource.stationPosition.value private val stationPos = settingsRepository.stationPosition.value
private val magDeclination = getMagDeclination(stationPos) private val magDeclination = getMagDeclination(stationPos)
val transmitters = mutableStateOf<List<SatRadio>>(emptyList()) val transmitters = mutableStateOf<List<SatRadio>>(emptyList())
val radarData = mutableStateOf<RadarData?>(null) val radarData = mutableStateOf<RadarData?>(null)
val orientation = mutableStateOf(orientationSource.orientation.value) val orientation = mutableStateOf(sensorRepository.orientation.value)
init { init {
if (settings.isSensorEnabled()) { if (settingsRepository.isSensorEnabled()) {
viewModelScope.launch { viewModelScope.launch {
orientationSource.startListening() sensorRepository.enableSensor()
orientationSource.orientation.collect { data -> sensorRepository.orientation.collect { data ->
orientation.value = Pair(data.first + magDeclination, data.second) orientation.value = Pair(data.first + magDeclination, data.second)
} }
} }
@@ -70,23 +68,24 @@ class RadarViewModel @Inject constructor(
} }
override fun onCleared() { override fun onCleared() {
orientationSource.stopListening() sensorRepository.disableSensor()
super.onCleared() super.onCleared()
} }
fun getPass() = flow { fun getPass() = flow {
val catNum = savedStateHandle.get<Int>("catNum") ?: 0 val catNum = savedStateHandle.get<Int>("catNum") ?: 0
val aosTime = savedStateHandle.get<Long>("aosTime") ?: 0L val aosTime = savedStateHandle.get<Long>("aosTime") ?: 0L
satManager.calculatedPasses.collect { passes -> satelliteRepository.calculatedPasses.collect { passes ->
val pass = passes.find { pass -> pass.catNum == catNum && pass.aosTime == aosTime } val pass = passes.find { pass -> pass.catNum == catNum && pass.aosTime == aosTime }
val currentPass = pass ?: passes.firstOrNull() val currentPass = pass ?: passes.firstOrNull()
currentPass?.let { satPass -> currentPass?.let { satPass ->
emit(satPass) emit(satPass)
val transmitters = repository.getRadiosWithId(satPass.catNum) val transmitters = dataRepository.getRadiosWithId(satPass.catNum)
viewModelScope.launch { viewModelScope.launch {
while (isActive) { while (isActive) {
val timeNow = System.currentTimeMillis() val timeNow = System.currentTimeMillis()
val satPos = satManager.getPosition(satPass.satellite, stationPos, timeNow) val satPos =
satelliteRepository.getPosition(satPass.satellite, stationPos, timeNow)
sendPassData(satPass, satPos, satPass.satellite) sendPassData(satPass, satPos, satPass.satellite)
sendPassDataBT(satPos) sendPassDataBT(satPos)
processRadios(transmitters, satPass.satellite, timeNow) processRadios(transmitters, satPass.satellite, timeNow)
@@ -97,9 +96,9 @@ class RadarViewModel @Inject constructor(
} }
} }
fun getUseCompass(): Boolean = settings.isSensorEnabled() fun getUseCompass(): Boolean = settingsRepository.isSensorEnabled()
fun getShowSweep(): Boolean = settings.isSweepEnabled() fun getShowSweep(): Boolean = settingsRepository.isSweepEnabled()
private fun getMagDeclination(geoPos: GeoPos, time: Long = System.currentTimeMillis()): Float { private fun getMagDeclination(geoPos: GeoPos, time: Long = System.currentTimeMillis()): Float {
val latitude = geoPos.latitude.toFloat() val latitude = geoPos.latitude.toFloat()
@@ -111,14 +110,16 @@ class RadarViewModel @Inject constructor(
viewModelScope.launch { viewModelScope.launch {
var track: List<SatPos> = emptyList() var track: List<SatPos> = emptyList()
if (!satPass.isDeepSpace) { if (!satPass.isDeepSpace) {
track = satManager.getTrack(satellite, stationPos, satPass.aosTime, satPass.losTime) track = satelliteRepository.getTrack(
satellite, stationPos, satPass.aosTime, satPass.losTime
)
} }
if (settings.getRotatorEnabled()) { if (settingsRepository.getRotatorEnabled()) {
val server = settings.getRotatorServer() val server = settingsRepository.getRotatorServer()
val port = settings.getRotatorPort().toInt() val port = settingsRepository.getRotatorPort().toInt()
val azimuth = satPos.azimuth.toDegrees().round(1) val azimuth = satPos.azimuth.toDegrees().round(1)
val elevation = satPos.elevation.toDegrees().round(1) val elevation = satPos.elevation.toDegrees().round(1)
reporter.reportRotation(server, port, azimuth, elevation) networkReporter.reportRotation(server, port, azimuth, elevation)
} }
radarData.value = RadarData(satPos, track) radarData.value = RadarData(satPos, track)
} }
@@ -126,10 +127,10 @@ class RadarViewModel @Inject constructor(
private fun sendPassDataBT(satPos: SatPos) { private fun sendPassDataBT(satPos: SatPos) {
viewModelScope.launch { viewModelScope.launch {
if (settings.getBTEnabled()) { if (settingsRepository.getBTEnabled()) {
val btDevice = settings.getBTDeviceAddr() val btDevice = settingsRepository.getBTDeviceAddr()
if (bluetoothReporter.isConnected()) { if (bluetoothReporter.isConnected()) {
val format = settings.getBTFormat() val format = settingsRepository.getBTFormat()
val azimuth = satPos.azimuth.toDegrees().round(0).toInt() val azimuth = satPos.azimuth.toDegrees().round(0).toInt()
val elevation = satPos.elevation.toDegrees().round(0).toInt() val elevation = satPos.elevation.toDegrees().round(0).toInt()
bluetoothReporter.reportRotation(format, azimuth, elevation) bluetoothReporter.reportRotation(format, azimuth, elevation)
@@ -144,7 +145,7 @@ class RadarViewModel @Inject constructor(
private fun processRadios(radios: List<SatRadio>, satellite: Satellite, time: Long) { private fun processRadios(radios: List<SatRadio>, satellite: Satellite, time: Long) {
viewModelScope.launch { viewModelScope.launch {
delay(125) delay(125)
val list = satManager.processRadios(satellite, stationPos, radios, time) val list = satelliteRepository.processRadios(satellite, stationPos, radios, time)
transmitters.value = list transmitters.value = list
} }
} }
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -165,9 +166,8 @@ private fun CardAbout(version: String, modifier: Modifier = Modifier) {
@Composable @Composable
private fun LocationCardPreview() = MainTheme { private fun LocationCardPreview() = MainTheme {
val stationPos = GeoPos(0.0, 0.0, 0.0, "IO91vl", 0L) val stationPos = GeoPos(0.0, 0.0, 0.0, "IO91vl", 0L)
LocationCard( val settings = LocationSettings(true, stationPos, 0, {}, { _, _ -> }, {}, {})
settings = LocationSettings(true, stationPos, {}, { _, _ -> }, { }), LocationCard(settings = settings, setGpsLoc = {}, togglePosDialog = {}) {}
setGpsLoc = {}, togglePosDialog = {}) {}
} }
@Composable @Composable
@@ -231,14 +231,21 @@ private fun LocationCard(
} }
} }
} }
if (settings.messageResId != 0) {
val context = LocalContext.current
val errorString = stringResource(id = settings.messageResId)
LaunchedEffect(key1 = settings.messageResId) {
showToast(context, errorString)
settings.dismissMessage()
}
}
} }
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
private fun DataCardPreview() = MainTheme { private fun DataCardPreview() = MainTheme {
DataCard(settings = DataSettings( val settings = DataSettings(true, 0L, 5000, 2500, {}, {}, {})
true, 0L, 5000, 2500, {}, { }, {}) DataCard(settings = settings) {}
) {}
} }
@Composable @Composable
@@ -335,10 +342,10 @@ private fun DataCard(settings: DataSettings, updateFromFile: () -> Unit) {
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
private fun OtherCardPreview() = MainTheme { private fun OtherCardPreview() = MainTheme {
OtherCard(settings = OtherSettings( val settings = OtherSettings(
getUtc = false, getUpdate = true, getSweep = true, getSensor = true, getUtc = false, getUpdate = true, getSweep = true, getSensor = true,
setUtc = {}, setUpdate = {}, setSweep = {}, setSensor = {}) setUtc = {}, setUpdate = {}, setSweep = {}, setSensor = {})
) OtherCard(settings = settings)
} }
@Composable @Composable
@@ -5,9 +5,11 @@ import com.rtbishop.look4sat.model.GeoPos
data class LocationSettings( data class LocationSettings(
val isUpdating: Boolean, val isUpdating: Boolean,
val stationPos: GeoPos, val stationPos: GeoPos,
val messageResId: Int,
val setGpsLoc: () -> Unit, val setGpsLoc: () -> Unit,
val setManualLoc: (Double, Double) -> Unit, val setManualLoc: (Double, Double) -> Unit,
val setQthLoc: (String) -> Unit val setQthLoc: (String) -> Unit,
val dismissMessage: () -> Unit
) )
data class DataSettings( data class DataSettings(
@@ -20,9 +20,9 @@ package com.rtbishop.look4sat.presentation.settings
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.IDataRepository import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.ILocationSource import com.rtbishop.look4sat.domain.ISettingsRepository
import com.rtbishop.look4sat.domain.ISettingsSource
import com.rtbishop.look4sat.model.DataState import com.rtbishop.look4sat.model.DataState
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
@@ -31,31 +31,32 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class SettingsViewModel @Inject constructor( class SettingsViewModel @Inject constructor(
private val locationSource: ILocationSource, private val dataRepository: IDataRepository,
private val repository: IDataRepository, private val settingsRepository: ISettingsRepository
private val settings: ISettingsSource
) : ViewModel() { ) : ViewModel() {
private val defaultLocationSettings = LocationSettings(false, private val defaultLocationSettings = LocationSettings(false,
locationSource.stationPosition.value, settingsRepository.stationPosition.value,
{ updateStationPosition() }, 0,
{ lat, lon -> locationSource.setGeoPosition(lat, lon) }, { setGpsPosition() },
{ locationSource.setQthPosition(it) }) { latitude, longitude -> setGeoPosition(latitude, longitude) },
{ locator -> setQthPosition(locator) },
{ dismissLocationMessage() })
val locationSettings = mutableStateOf(defaultLocationSettings) val locationSettings = mutableStateOf(defaultLocationSettings)
private val defaultDataSettings = DataSettings(false, private val defaultDataSettings = DataSettings(false,
settings.getLastUpdateTime(), settingsRepository.getLastUpdateTime(),
0, 0,
0, 0,
{ repository.updateFromWeb() }, { dataRepository.updateFromWeb() },
{ repository.updateFromFile(it) }, { dataRepository.updateFromFile(it) },
{ repository.clearAllData() }) { dataRepository.clearAllData() })
val dataSettings = mutableStateOf(defaultDataSettings) val dataSettings = mutableStateOf(defaultDataSettings)
private val defaultOtherSettings = OtherSettings(settings.isUtcEnabled(), private val defaultOtherSettings = OtherSettings(settingsRepository.isUtcEnabled(),
settings.isUpdateEnabled(), settingsRepository.isUpdateEnabled(),
settings.isSweepEnabled(), settingsRepository.isSweepEnabled(),
settings.isSensorEnabled(), settingsRepository.isSensorEnabled(),
{ setUtc(it) }, { setUtc(it) },
{ setUpdate(it) }, { setUpdate(it) },
{ setSweep(it) }, { setSweep(it) },
@@ -64,49 +65,81 @@ class SettingsViewModel @Inject constructor(
init { init {
viewModelScope.launch { viewModelScope.launch {
locationSource.stationPosition.collect { stationPos -> settingsRepository.stationPosition.collect { stationPos ->
locationSettings.value = locationSettings.value.copy( locationSettings.value = locationSettings.value.copy(
isUpdating = false, stationPos = stationPos isUpdating = false, stationPos = stationPos
) )
} }
} }
viewModelScope.launch { viewModelScope.launch {
repository.updateState.collect { dataRepository.updateState.collect {
val isUpdating = it is DataState.Loading val isUpdating = it is DataState.Loading
dataSettings.value = dataSettings.value.copy( dataSettings.value = dataSettings.value.copy(
isUpdating = isUpdating, lastUpdated = settings.getLastUpdateTime() isUpdating = isUpdating, lastUpdated = settingsRepository.getLastUpdateTime()
) )
} }
} }
viewModelScope.launch { viewModelScope.launch {
combine(repository.getEntriesTotal(), repository.getRadiosTotal()) { sats, radios -> combine(
dataRepository.getEntriesTotal(), dataRepository.getRadiosTotal()
) { sats, radios ->
dataSettings.value = dataSettings.value.copy(satsTotal = sats, radiosTotal = radios) dataSettings.value = dataSettings.value.copy(satsTotal = sats, radiosTotal = radios)
}.collect {} }.collect {}
} }
} }
private fun updateStationPosition() { private fun setGpsPosition() {
val isSuccessful = locationSource.setGpsPosition() if (settingsRepository.setGpsPosition()) {
locationSettings.value = locationSettings.value.copy(isUpdating = isSuccessful) val messageResId = R.string.location_success
locationSettings.value =
locationSettings.value.copy(isUpdating = true, messageResId = messageResId)
} else {
val errorResId = R.string.location_gps_error
locationSettings.value = locationSettings.value.copy(messageResId = errorResId)
}
}
private fun setGeoPosition(latitude: Double, longitude: Double) {
if (settingsRepository.setGeoPosition(latitude, longitude)) {
val messageResId = R.string.location_success
locationSettings.value = locationSettings.value.copy(messageResId = messageResId)
} else {
val errorResId = R.string.location_manual_error
locationSettings.value = locationSettings.value.copy(messageResId = errorResId)
}
}
private fun setQthPosition(locator: String) {
if (settingsRepository.setQthPosition(locator)) {
val messageResId = R.string.location_success
locationSettings.value = locationSettings.value.copy(messageResId = messageResId)
} else {
val errorResId = R.string.location_qth_error
locationSettings.value = locationSettings.value.copy(messageResId = errorResId)
}
}
private fun dismissLocationMessage() {
locationSettings.value = locationSettings.value.copy(messageResId = 0)
} }
private fun setUtc(value: Boolean) { private fun setUtc(value: Boolean) {
settings.setUtcState(value) settingsRepository.setUtcState(value)
otherSettings.value = otherSettings.value.copy(getUtc = value) otherSettings.value = otherSettings.value.copy(getUtc = value)
} }
private fun setUpdate(value: Boolean) { private fun setUpdate(value: Boolean) {
settings.setUpdateState(value) settingsRepository.setUpdateState(value)
otherSettings.value = otherSettings.value.copy(getUpdate = value) otherSettings.value = otherSettings.value.copy(getUpdate = value)
} }
private fun setSweep(value: Boolean) { private fun setSweep(value: Boolean) {
settings.setSweepState(value) settingsRepository.setSweepState(value)
otherSettings.value = otherSettings.value.copy(getSweep = value) otherSettings.value = otherSettings.value.copy(getSweep = value)
} }
private fun setSensor(value: Boolean) { private fun setSensor(value: Boolean) {
settings.setSensorState(value) settingsRepository.setSensorState(value)
otherSettings.value = otherSettings.value.copy(getSensor = value) otherSettings.value = otherSettings.value.copy(getSensor = value)
} }
@@ -26,56 +26,52 @@ import org.json.JSONObject
import java.io.InputStream import java.io.InputStream
import kotlin.math.pow import kotlin.math.pow
class DataParser(private val defaultDispatcher: CoroutineDispatcher) { class DataParser(private val dispatcher: CoroutineDispatcher) {
suspend fun parseCSVStream(csvStream: InputStream): List<OrbitalData> = suspend fun parseCSVStream(stream: InputStream): List<OrbitalData> = withContext(dispatcher) {
withContext(defaultDispatcher) { val parsedItems = mutableListOf<OrbitalData>()
val parsedItems = mutableListOf<OrbitalData>() stream.bufferedReader().useLines { lines ->
csvStream.bufferedReader().useLines { lines -> lines.forEachIndexed { index, line ->
lines.forEachIndexed { index, line -> if (index != 0) {
if (index != 0) { val values = line.split(",")
val values = line.split(",") parseCSV(values)?.let { tle -> parsedItems.add(tle) }
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 return@withContext parsedItems
} } catch (exception: Exception) {
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
}
}
return@withContext parsedItems 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? { 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 argper: Double = tle[2].substring(34, 42).toDouble()
val meanan: Double = tle[2].substring(43, 51).toDouble() val meanan: Double = tle[2].substring(43, 51).toDouble()
val catnum: Int = tle[1].substring(2, 7).trim().toInt() val catnum: Int = tle[1].substring(2, 7).trim().toInt()
val bstar: Double = 1.0e-5 * tle[1].substring(53, 59).toDouble() / val bstar: Double = 1.0e-5 * tle[1].substring(53, 59).toDouble() / 10.0.pow(
10.0.pow(tle[1].substring(60, 61).toDouble()) tle[1].substring(60, 61).toDouble()
)
return OrbitalData(name, epoch, meanmo, eccn, incl, raan, argper, meanan, catnum, bstar) return OrbitalData(name, epoch, meanmo, eccn, incl, raan, argper, meanan, catnum, bstar)
} catch (exception: Exception) { } catch (exception: Exception) {
return null return null
@@ -154,7 +151,7 @@ class DataParser(private val defaultDispatcher: CoroutineDispatcher) {
var dayOfYear = dayOfMonth var dayOfYear = dayOfMonth
// If leap year increment Feb days // If leap year increment Feb days
if (((year / 4 == 0) && (year / 100 != 0)) || (year / 400 == 0)) daysArray[1]++ 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 return dayOfYear
} }
} }
@@ -18,9 +18,10 @@
package com.rtbishop.look4sat.data package com.rtbishop.look4sat.data
import com.rtbishop.look4sat.domain.IDataRepository 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.DataState
import com.rtbishop.look4sat.model.SatEntry import com.rtbishop.look4sat.model.SatEntry
import com.rtbishop.look4sat.model.SatItem
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@@ -34,7 +35,7 @@ class DataRepository(
private val radioSource: IRadiosStorage, private val radioSource: IRadiosStorage,
private val remoteSource: INetworkSource, private val remoteSource: INetworkSource,
private val repositoryScope: CoroutineScope, private val repositoryScope: CoroutineScope,
private val settingsManager: ISettingsSource private val settingsRepository: ISettingsRepository
) : IDataRepository { ) : IDataRepository {
private val exceptionHandler = CoroutineExceptionHandler { _, exception -> private val exceptionHandler = CoroutineExceptionHandler { _, exception ->
@@ -48,10 +49,17 @@ class DataRepository(
override fun getRadiosTotal() = radioSource.getRadiosTotal() override fun getRadiosTotal() = radioSource.getRadiosTotal()
override fun getSatelliteTypes() = remoteSource.satelliteSourcesMap.keys.sorted()
override suspend fun getEntriesWithModes() = entrySource.getEntriesWithModes() override suspend fun getEntriesWithModes() = entrySource.getEntriesWithModes()
override suspend fun getEntriesWithIds(ids: List<Int>) = entrySource.getEntriesWithIds(ids) 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 suspend fun getRadiosWithId(id: Int) = radioSource.getRadiosWithId(id)
override fun updateFromFile(uri: String) { override fun updateFromFile(uri: String) {
@@ -69,7 +77,7 @@ class DataRepository(
_updateState.value = DataState.Loading _updateState.value = DataState.Loading
repositoryScope.launch(exceptionHandler) { repositoryScope.launch(exceptionHandler) {
val importedEntries = mutableListOf<SatEntry>() val importedEntries = mutableListOf<SatEntry>()
val sourcesMap = settingsManager.sourcesMap val sourcesMap = remoteSource.satelliteSourcesMap
val jobsMap = sourcesMap.mapValues { async { remoteSource.getDataStream(it.value) } } val jobsMap = sourcesMap.mapValues { async { remoteSource.getDataStream(it.value) } }
jobsMap.mapValues { job -> job.value.await() }.forEach { entry -> jobsMap.mapValues { job -> job.value.await() }.forEach { entry ->
entry.value?.let { stream -> entry.value?.let { stream ->
@@ -78,7 +86,7 @@ class DataRepository(
// parse tle stream // parse tle stream
val satellites = importSatellites(stream) val satellites = importSatellites(stream)
val catnums = satellites.map { it.data.catnum } val catnums = satellites.map { it.data.catnum }
settingsManager.saveSatType(type, catnums) settingsRepository.saveSatType(type, catnums)
importedEntries.addAll(satellites) importedEntries.addAll(satellites)
} }
"McCants", "Classified" -> { "McCants", "Classified" -> {
@@ -86,7 +94,7 @@ class DataRepository(
val unzipped = ZipInputStream(stream).apply { nextEntry } val unzipped = ZipInputStream(stream).apply { nextEntry }
val satellites = importSatellites(unzipped) val satellites = importSatellites(unzipped)
val catnums = satellites.map { it.data.catnum } val catnums = satellites.map { it.data.catnum }
settingsManager.saveSatType(type, catnums) settingsRepository.saveSatType(type, catnums)
importedEntries.addAll(satellites) importedEntries.addAll(satellites)
} }
else -> { else -> {
@@ -94,7 +102,7 @@ class DataRepository(
val parsed = dataParser.parseCSVStream(stream) val parsed = dataParser.parseCSVStream(stream)
val satellites = parsed.map { data -> SatEntry(data) } val satellites = parsed.map { data -> SatEntry(data) }
val catnums = satellites.map { it.data.catnum } val catnums = satellites.map { it.data.catnum }
settingsManager.saveSatType(type, catnums) settingsRepository.saveSatType(type, catnums)
importedEntries.addAll(satellites) importedEntries.addAll(satellites)
} }
} }
@@ -104,7 +112,7 @@ class DataRepository(
setUpdateSuccessful() setUpdateSuccessful()
} }
repositoryScope.launch(exceptionHandler) { repositoryScope.launch(exceptionHandler) {
remoteSource.getDataStream(remoteSource.radioApi)?.let { stream -> remoteSource.getDataStream(remoteSource.radioSourceUrl)?.let { stream ->
radioSource.insertRadios(dataParser.parseJSONStream(stream)) radioSource.insertRadios(dataParser.parseJSONStream(stream))
} }
} }
@@ -125,7 +133,7 @@ class DataRepository(
} }
private fun setUpdateSuccessful(updateTime: Long = System.currentTimeMillis()) { private fun setUpdateSuccessful(updateTime: Long = System.currentTimeMillis()) {
settingsManager.setLastUpdateTime(updateTime) settingsRepository.setLastUpdateTime(updateTime)
_updateState.value = DataState.Success(updateTime) _updateState.value = DataState.Success(updateTime)
} }
@@ -21,7 +21,36 @@ import java.io.InputStream
interface INetworkSource { 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? suspend fun getDataStream(url: String): InputStream?
} }
@@ -15,8 +15,10 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * 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.GeoPos
import com.rtbishop.look4sat.model.SatPass import com.rtbishop.look4sat.model.SatPass
import com.rtbishop.look4sat.model.SatPos import com.rtbishop.look4sat.model.SatPos
@@ -28,7 +30,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class SatelliteManager(private val defaultDispatcher: CoroutineDispatcher) : ISatelliteManager { class SatelliteRepository(private val dispatcher: CoroutineDispatcher) : ISatelliteRepository {
private var passes = listOf<SatPass>() private var passes = listOf<SatPass>()
private val _calculatedPasses = MutableSharedFlow<List<SatPass>>(replay = 1) 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 fun getPasses(): List<SatPass> = passes
override suspend fun getPosition(sat: Satellite, pos: GeoPos, time: Long): SatPos { 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( override suspend fun getTrack(
sat: Satellite, pos: GeoPos, start: Long, end: Long sat: Satellite, pos: GeoPos, start: Long, end: Long
): List<SatPos> { ): List<SatPos> {
return withContext(defaultDispatcher) { return withContext(dispatcher) {
val positions = mutableListOf<SatPos>() val positions = mutableListOf<SatPos>()
var currentTime = start var currentTime = start
while (currentTime < end) { while (currentTime < end) {
@@ -57,7 +59,7 @@ class SatelliteManager(private val defaultDispatcher: CoroutineDispatcher) : ISa
override suspend fun processRadios( override suspend fun processRadios(
sat: Satellite, pos: GeoPos, radios: List<SatRadio>, time: Long sat: Satellite, pos: GeoPos, radios: List<SatRadio>, time: Long
): List<SatRadio> { ): List<SatRadio> {
return withContext(defaultDispatcher) { return withContext(dispatcher) {
val satPos = sat.getPosition(pos, time) val satPos = sat.getPosition(pos, time)
val copiedList = radios.map { it.copy() } val copiedList = radios.map { it.copy() }
copiedList.forEach { transmitter -> 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> { override suspend fun processPasses(passList: List<SatPass>, time: Long): List<SatPass> {
return withContext(defaultDispatcher) { return withContext(dispatcher) {
passList.forEach { pass -> passList.forEach { pass ->
if (!pass.isDeepSpace) { if (!pass.isDeepSpace) {
val timeStart = pass.aosTime val timeStart = pass.aosTime
@@ -96,7 +98,7 @@ class SatelliteManager(private val defaultDispatcher: CoroutineDispatcher) : ISa
passes = newPasses passes = newPasses
_calculatedPasses.emit(newPasses) _calculatedPasses.emit(newPasses)
} else { } else {
withContext(defaultDispatcher) { withContext(dispatcher) {
val allPasses = mutableListOf<SatPass>() val allPasses = mutableListOf<SatPass>()
satList.forEach { satellite -> satList.forEach { satellite ->
allPasses.addAll(satellite.getPasses(pos, time, hoursAhead)) allPasses.addAll(satellite.getPasses(pos, time, hoursAhead))
@@ -20,7 +20,7 @@ package com.rtbishop.look4sat.domain
import com.rtbishop.look4sat.model.OrbitalData import com.rtbishop.look4sat.model.OrbitalData
import kotlin.math.* import kotlin.math.*
class DeepSpaceSat(data: OrbitalData) : Satellite(data) { class DeepSpaceSatellite(data: OrbitalData) : Satellite(data) {
private val c1: Double private val c1: Double
private val c4: Double private val c4: Double
@@ -31,10 +31,14 @@ interface IDataRepository {
fun getRadiosTotal(): Flow<Int> fun getRadiosTotal(): Flow<Int>
fun getSatelliteTypes(): List<String>
suspend fun getEntriesWithModes(): List<SatItem> suspend fun getEntriesWithModes(): List<SatItem>
suspend fun getEntriesWithIds(ids: List<Int>): List<Satellite> suspend fun getEntriesWithIds(ids: List<Int>): List<Satellite>
suspend fun getEntriesWithSelection(): List<SatItem>
suspend fun getRadiosWithId(id: Int): List<SatRadio> suspend fun getRadiosWithId(id: Int): List<SatRadio>
fun updateFromFile(uri: String) 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 com.rtbishop.look4sat.model.SatRadio
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
interface ISatelliteManager { interface ISatelliteRepository {
val calculatedPasses: SharedFlow<List<SatPass>> 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 com.rtbishop.look4sat.model.OrbitalData
import kotlin.math.* import kotlin.math.*
class NearEarthSat(data: OrbitalData) : Satellite(data) { class NearEarthSatellite(data: OrbitalData) : Satellite(data) {
private val aodp: Double private val aodp: Double
private val aycof: Double private val aycof: Double
@@ -110,8 +110,8 @@ abstract class Satellite(val data: OrbitalData) {
} }
private fun calculateSDP4orSGP4(tsince: Double) { private fun calculateSDP4orSGP4(tsince: Double) {
if (data.isDeepSpace) (this as DeepSpaceSat).calculateSDP4(tsince) if (data.isDeepSpace) (this as DeepSpaceSatellite).calculateSDP4(tsince)
else (this as NearEarthSat).calculateSGP4(tsince) else (this as NearEarthSatellite).calculateSGP4(tsince)
} }
// Converts the sat position and velocity vectors to km and km/sec // 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) // Space objects are classified as NearEarth (period < 225 min) or DeepSpace (period >= 225 min)
val isDeepSpace: Boolean = orbitalPeriod >= 225.0 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)
} }