Major classes refactoring and code cleanup

This commit is contained in:
Arty Bishop committed 2025-12-27 15:01:33 +00:00
1 parent 6572dd4eab
commit f7f317cf8b
67 files changed
+610 -754

No files matched your search

@@ -118,10 +118,10 @@
} }
], ],
"primaryKey": { "primaryKey": {
"autoGenerate": false,
"columnNames": [ "columnNames": [
"catnum" "catnum"
], ]
"autoGenerate": false
}, },
"indices": [], "indices": [],
"foreignKeys": [] "foreignKeys": []
@@ -186,10 +186,10 @@
} }
], ],
"primaryKey": { "primaryKey": {
"autoGenerate": false,
"columnNames": [ "columnNames": [
"uuid" "uuid"
], ]
"autoGenerate": false
}, },
"indices": [], "indices": [],
"foreignKeys": [] "foreignKeys": []
+2 -2
View File
@@ -12,7 +12,7 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<application <application
android:name=".presentation.MainApplication" android:name=".MainApplication"
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
@@ -24,7 +24,7 @@
android:theme="@style/Theme.Look4Sat.Main" android:theme="@style/Theme.Look4Sat.Main"
tools:targetApi="31"> tools:targetApi="31">
<activity <activity
android:name=".presentation.MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:theme="@style/Theme.Look4Sat.SplashScreen"> android:theme="@style/Theme.Look4Sat.SplashScreen">
<intent-filter> <intent-filter>
@@ -0,0 +1,47 @@
/*
* 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
import android.content.Context
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import com.rtbishop.look4sat.presentation.MainScreen
import com.rtbishop.look4sat.presentation.MainTheme
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
installSplashScreen()
super.onCreate(savedInstanceState)
setContent { MainTheme { MainScreen() } }
}
override fun attachBaseContext(newBase: Context?) {
val newConfig = Configuration(newBase?.resources?.configuration)
newConfig.fontScale = 1.0f
applyOverrideConfiguration(newConfig)
super.attachBaseContext(newBase)
}
}
@@ -15,12 +15,11 @@
* 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.presentation package com.rtbishop.look4sat
import android.app.Application import android.app.Application
import com.rtbishop.look4sat.BuildConfig
import com.rtbishop.look4sat.domain.IDataRepository import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.framework.SettingsManager import com.rtbishop.look4sat.domain.ISettingsSource
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
@@ -31,10 +30,10 @@ import javax.inject.Inject
class MainApplication : Application() { class MainApplication : Application() {
@Inject @Inject
lateinit var repository: IDataRepository lateinit var dataRepository: IDataRepository
@Inject @Inject
lateinit var settingsManager: SettingsManager lateinit var settingsSource: ISettingsSource
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@@ -45,12 +44,12 @@ class MainApplication : Application() {
} }
private fun checkAutoUpdate() { private fun checkAutoUpdate() {
if (settingsManager.isUpdateEnabled()) { if (settingsSource.isUpdateEnabled()) {
val timeDelta = System.currentTimeMillis() - settingsManager.getLastUpdateTime() val timeDelta = System.currentTimeMillis() - settingsSource.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())}")
repository.updateFromWeb() dataRepository.updateFromWeb()
} }
} }
} }
@@ -0,0 +1,102 @@
/*
* 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
import android.bluetooth.BluetoothManager
import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorManager
import android.location.LocationManager
import androidx.room.Room
import com.rtbishop.look4sat.data.DataParser
import com.rtbishop.look4sat.data.DataRepository
import com.rtbishop.look4sat.domain.*
import com.rtbishop.look4sat.framework.*
import com.rtbishop.look4sat.framework.data.*
import com.rtbishop.look4sat.framework.data.local.EntriesStorage
import com.rtbishop.look4sat.framework.data.local.RadiosStorage
import com.rtbishop.look4sat.framework.data.remote.FileSource
import com.rtbishop.look4sat.framework.data.remote.NetworkSource
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object MainModule {
@Provides
@Singleton
fun provideBluetoothReporter(@ApplicationContext context: Context): BluetoothReporter {
val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
return BluetoothReporter(manager, CoroutineScope(Dispatchers.IO))
}
@Provides
@Singleton
fun provideNetworkReporter(): NetworkReporter = NetworkReporter(CoroutineScope(Dispatchers.IO))
@Provides
@Singleton
fun provideDataRepository(
@ApplicationContext context: Context, settings: ISettingsSource
): IDataRepository {
val db = Room.databaseBuilder(context, MainDatabase::class.java, "Look4SatDb")
.addMigrations(MIGRATION_1_2).fallbackToDestructiveMigration().build()
val parser = DataParser(Dispatchers.Default)
val fileSource = FileSource(context.contentResolver, Dispatchers.IO)
val entries = EntriesStorage(db.entriesDao())
val radios = RadiosStorage(db.radiosDao())
val remoteSource = NetworkSource(Dispatchers.IO)
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
return DataRepository(parser, fileSource, entries, radios, remoteSource, scope, settings)
}
@Provides
@Singleton
fun provideLocationSource(
@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 sensor = manager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
return OrientationSource(manager, sensor)
}
@Provides
@Singleton
fun provideSatelliteManager(): ISatelliteManager = SatelliteManager(Dispatchers.Default)
@Provides
@Singleton
fun provideSettingsSource(@ApplicationContext context: Context): ISettingsSource {
return SettingsSource(context.getSharedPreferences("default", Context.MODE_PRIVATE))
}
}
@@ -1,4 +1,4 @@
package com.rtbishop.look4sat.presentation.radarScreen package com.rtbishop.look4sat.framework
import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothManager
import android.util.Log import android.util.Log
@@ -9,7 +9,10 @@ import java.io.OutputStream
import java.util.* import java.util.*
import kotlin.math.abs import kotlin.math.abs
class BTReporter(private val manager: BluetoothManager, private val reporterScope: CoroutineScope) { class BluetoothReporter(
private val manager: BluetoothManager,
private val reporterScope: CoroutineScope
) {
private val tag = "BTReporter" private val tag = "BTReporter"
private val sppid: UUID = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb") private val sppid: UUID = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb")
@@ -1,139 +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.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.Bundle
import androidx.core.content.ContextCompat
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.ILocationManager
import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.utility.QthConverter
import com.rtbishop.look4sat.utility.round
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class LocationManager @Inject constructor(
@ApplicationContext private val context: Context,
private val manager: LocationManager,
private val settings: ISettingsManager,
) : LocationListener, ILocationManager {
private val providerDef = LocationManager.PASSIVE_PROVIDER
private val providerNet = LocationManager.NETWORK_PROVIDER
private val providerGps = LocationManager.GPS_PROVIDER
private val locationCoarse = Manifest.permission.ACCESS_COARSE_LOCATION
private val locationFine = Manifest.permission.ACCESS_FINE_LOCATION
private val _stationPosition = MutableStateFlow<DataState<GeoPos>>(DataState.Handled)
private var currentLocator = settings.loadStationLocator()
private var currentPosition = settings.loadStationPosition()
override val stationPosition: StateFlow<DataState<GeoPos>> = _stationPosition
override fun getStationLocator(): String = currentLocator
override fun getStationPosition(): GeoPos = currentPosition
override fun setStationPosition(latitude: Double, longitude: Double) {
if (QthConverter.isValidPosition(latitude, longitude)) {
val tempLon = if (longitude > 180.0) longitude - 180 else longitude
val newLat = latitude.round(4)
val newLon = tempLon.round(4)
QthConverter.positionToQth(newLat, newLon)?.let { locator ->
currentLocator = locator
currentPosition = GeoPos(newLat, newLon)
settings.saveStationLocator(locator)
settings.saveStationPosition(currentPosition)
_stationPosition.value = DataState.Success(currentPosition)
}
} else {
_stationPosition.value =
DataState.Error(context.getString(R.string.location_manual_error))
}
}
override fun setPositionFromGps() {
val result = ContextCompat.checkSelfPermission(context, locationFine)
if (manager.isProviderEnabled(providerGps) && result == PackageManager.PERMISSION_GRANTED) {
val location = manager.getLastKnownLocation(providerDef)
if (location != null) {
setStationPosition(location.latitude, location.longitude)
} else {
_stationPosition.value = DataState.Loading
manager.requestLocationUpdates(providerGps, 0L, 0f, this)
}
} else {
_stationPosition.value = DataState.Error(context.getString(R.string.location_null))
}
}
override fun setPositionFromNet() {
val result = ContextCompat.checkSelfPermission(context, locationCoarse)
if (manager.isProviderEnabled(providerNet) && result == PackageManager.PERMISSION_GRANTED) {
val location = manager.getLastKnownLocation(providerDef)
if (location != null) {
setStationPosition(location.latitude, location.longitude)
} else {
_stationPosition.value = DataState.Loading
manager.requestLocationUpdates(providerNet, 0L, 0f, this)
}
} else {
_stationPosition.value = DataState.Error(context.getString(R.string.location_null))
}
}
override fun setPositionFromQth(locator: String) {
val position = QthConverter.qthToPosition(locator)
if (position != null) {
currentLocator = locator
currentPosition = position
settings.saveStationLocator(locator)
settings.saveStationPosition(currentPosition)
_stationPosition.value = DataState.Success(currentPosition)
} else {
_stationPosition.value = DataState.Error(context.getString(R.string.location_qth_error))
}
}
override fun setPositionHandled() {
_stationPosition.value = DataState.Handled
}
override fun onLocationChanged(location: Location) {
manager.removeUpdates(this)
setStationPosition(location.latitude, location.longitude)
}
override fun onProviderEnabled(provider: String) {}
override fun onProviderDisabled(provider: String) {}
@Deprecated("Deprecated in Java")
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
}
@@ -0,0 +1,87 @@
/*
* 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.Location
import android.location.LocationManager
import androidx.core.location.LocationListenerCompat
import androidx.core.location.LocationManagerCompat
import com.rtbishop.look4sat.domain.ILocationSource
import com.rtbishop.look4sat.domain.ISettingsSource
import com.rtbishop.look4sat.model.StationPos
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
class LocationSource(
private val manager: LocationManager,
private val settings: ISettingsSource,
) : ILocationSource, LocationListenerCompat {
private val _stationPosition = MutableStateFlow(settings.loadStationPosition())
override val stationPosition: StateFlow<StationPos> = _stationPosition
override fun setGpsPosition(): Boolean {
if (!LocationManagerCompat.isLocationEnabled(manager)) return false
forceLocationUpdate(LocationManager.GPS_PROVIDER)
forceLocationUpdate(LocationManager.NETWORK_PROVIDER)
return true
}
override fun setGeoPosition(latitude: Double, longitude: Double): Boolean {
manager.removeUpdates(this)
val newLongitude = if (longitude > 180.0) longitude - 180 else longitude
val locator = QthConverter.positionToQth(latitude, newLongitude) ?: return false
saveStationPosition(latitude, newLongitude, locator)
return true
}
override fun setQthPosition(locator: String): Boolean {
val position = QthConverter.qthToPosition(locator) ?: return false
saveStationPosition(position.lat, position.lon, locator)
return true
}
override fun onLocationChanged(location: Location) {
setGeoPosition(location.latitude, location.longitude)
}
private fun forceLocationUpdate(provider: String) {
try {
val location = manager.getLastKnownLocation(LocationManager.PASSIVE_PROVIDER)
if (location == null || System.currentTimeMillis() - location.time > 600000) {
Timber.d("Requesting $provider location update")
manager.requestLocationUpdates(provider, 0L, 0f, this)
} else {
setGeoPosition(location.latitude, location.longitude)
}
} catch (exception: SecurityException) {
Timber.d("No permissions were given")
}
}
private fun saveStationPosition(latitude: Double, longitude: Double, locator: String) {
val newLat = latitude.round(4)
val newLon = longitude.round(4)
Timber.d("Received new Position($newLat, $newLon) & Locator $locator")
_stationPosition.value = StationPos(newLat, newLon, locator, System.currentTimeMillis())
settings.saveStationPosition(stationPosition.value)
}
}
@@ -15,7 +15,7 @@
* 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.utility package com.rtbishop.look4sat.framework
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.nio.ByteBuffer import java.nio.ByteBuffer
@@ -25,7 +25,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class DataReporter(private val reporterScope: CoroutineScope) { class NetworkReporter(private val reporterScope: CoroutineScope) {
private var rotationSocketChannel: SocketChannel? = null private var rotationSocketChannel: SocketChannel? = null
private var rotationReporting: Job? = null private var rotationReporting: Job? = null
@@ -21,47 +21,36 @@ 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.predict.RAD2DEG import com.rtbishop.look4sat.domain.RAD2DEG
import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow
import javax.inject.Singleton import kotlinx.coroutines.flow.StateFlow
import kotlin.math.round import kotlin.math.round
@Singleton class OrientationSource(
class OrientationManager @Inject constructor(private val sensorManager: SensorManager) : private val manager: SensorManager, private val sensor: Sensor?
SensorEventListener { ) : SensorEventListener {
interface OrientationListener {
fun onOrientationChanged(azimuth: Float, pitch: Float, roll: Float)
}
private val sensor: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
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 var orientationListener: OrientationListener? = null
fun startListening(listener: OrientationListener) { private val _orientation = MutableStateFlow(Pair(0f, 0f))
if (sensor != null && orientationListener !== listener) { val orientation: StateFlow<Pair<Float, Float>> = _orientation
orientationListener = listener
sensorManager.registerListener(this, sensor, 16000) fun startListening() = sensor?.let {
} else return manager.registerListener(this, it, 16000)
} }
fun stopListening() { fun stopListening() = manager.unregisterListener(this)
sensorManager.unregisterListener(this)
orientationListener = null
}
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) { override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
sensorAccuracy = accuracy sensorAccuracy = accuracy
} }
override fun onSensorChanged(event: SensorEvent) { override fun onSensorChanged(event: SensorEvent) = when {
when { sensorAccuracy == SensorManager.SENSOR_STATUS_UNRELIABLE -> {}
orientationListener == null -> return event.sensor == sensor -> updateOrientation(event.values)
sensorAccuracy == SensorManager.SENSOR_STATUS_UNRELIABLE -> return else -> {}
event.sensor == sensor -> updateOrientation(event.values)
}
} }
private fun updateOrientation(rotationVector: FloatArray) { private fun updateOrientation(rotationVector: FloatArray) {
@@ -69,11 +58,11 @@ class OrientationManager @Inject constructor(private val sensorManager: SensorMa
SensorManager.getOrientation(rotationMatrix, orientationValues) SensorManager.getOrientation(rotationMatrix, orientationValues)
val azimuth = (orientationValues[0] * RAD2DEG).toFloat() val azimuth = (orientationValues[0] * RAD2DEG).toFloat()
val pitch = (orientationValues[1] * RAD2DEG).toFloat() val pitch = (orientationValues[1] * RAD2DEG).toFloat()
val roll = (orientationValues[2] * RAD2DEG).toFloat() // val roll = (orientationValues[2] * RAD2DEG).toFloat()
val magneticAzimuth = (azimuth + 360f) % 360f val magneticAzimuth = (azimuth + 360f) % 360f
val roundedAzimuth = round(magneticAzimuth * 10) / 10 val roundedAzimuth = round(magneticAzimuth * 10) / 10
val roundedPitch = round(pitch * 10) / 10 val roundedPitch = round(pitch * 10) / 10
val roundedRoll = round(roll * 10) / 10 // val roundedRoll = round(roll * 10) / 10
orientationListener?.onOrientationChanged(roundedAzimuth, roundedPitch, roundedRoll) _orientation.value = Pair(roundedAzimuth, roundedPitch)
} }
} }
@@ -19,13 +19,10 @@ package com.rtbishop.look4sat.framework
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.core.content.edit import androidx.core.content.edit
import com.rtbishop.look4sat.domain.ISettingsManager import com.rtbishop.look4sat.domain.ISettingsSource
import com.rtbishop.look4sat.domain.predict.GeoPos import com.rtbishop.look4sat.model.StationPos
import javax.inject.Inject
import javax.inject.Singleton
@Singleton class SettingsSource(private val prefs: SharedPreferences) : ISettingsSource {
class SettingsManager @Inject constructor(private val prefs: SharedPreferences) : ISettingsManager {
companion object { companion object {
const val keyModes = "satModes" const val keyModes = "satModes"
@@ -46,29 +43,23 @@ class SettingsManager @Inject constructor(private val prefs: SharedPreferences)
const val keyLatitude = "stationLat" const val keyLatitude = "stationLat"
const val keyLongitude = "stationLon" const val keyLongitude = "stationLon"
const val keyLocator = "stationQTH" const val keyLocator = "stationQTH"
const val keyLocTimestamp = "locTimestamp"
const val keySelection = "selection" const val keySelection = "selection"
} }
override fun loadStationLocator(): String { override fun loadStationPosition(): StationPos {
return prefs.getString(keyLocator, null) ?: "" val latitude = prefs.getString(keyLatitude, null) ?: "0.0"
val longitude = prefs.getString(keyLongitude, null) ?: "0.0"
val qthLocator = prefs.getString(keyLocator, null) ?: ""
val timestamp = prefs.getLong(keyLocTimestamp, 0L)
return StationPos(latitude.toDouble(), longitude.toDouble(), qthLocator, timestamp)
} }
override fun saveStationLocator(locator: String) { override fun saveStationPosition(stationPos: StationPos) = prefs.edit {
prefs.edit { putString(keyLocator, locator) } putString(keyLatitude, stationPos.latitude.toString())
} putString(keyLongitude, stationPos.longitude.toString())
putString(keyLocator, stationPos.qthLocator)
override fun loadStationPosition(): GeoPos { putLong(keyLocTimestamp, stationPos.timestamp)
val defaultSP = "0.0"
val latitude = prefs.getString(keyLatitude, null) ?: defaultSP
val longitude = prefs.getString(keyLongitude, null) ?: defaultSP
return GeoPos(latitude.toDouble(), longitude.toDouble())
}
override fun saveStationPosition(position: GeoPos) {
prefs.edit {
putString(keyLatitude, position.lat.toString())
putString(keyLongitude, position.lon.toString())
}
} }
override fun saveEntriesSelection(catnums: List<Int>) { override fun saveEntriesSelection(catnums: List<Int>) {
@@ -27,7 +27,7 @@ import com.rtbishop.look4sat.framework.model.SatEntry
import com.rtbishop.look4sat.framework.model.SatRadio import com.rtbishop.look4sat.framework.model.SatRadio
@Database(entities = [SatEntry::class, SatRadio::class], version = 2, exportSchema = true) @Database(entities = [SatEntry::class, SatRadio::class], version = 2, exportSchema = true)
abstract class LocalDatabase : RoomDatabase() { abstract class MainDatabase : RoomDatabase() {
abstract fun entriesDao(): EntriesDao abstract fun entriesDao(): EntriesDao
@@ -15,17 +15,17 @@
* 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.framework.data package com.rtbishop.look4sat.framework.data.local
import com.rtbishop.look4sat.domain.data.ILocalEntrySource import com.rtbishop.look4sat.data.IEntriesStorage
import com.rtbishop.look4sat.domain.predict.Satellite import com.rtbishop.look4sat.domain.Satellite
import com.rtbishop.look4sat.framework.data.dao.EntriesDao import com.rtbishop.look4sat.framework.data.dao.EntriesDao
import com.rtbishop.look4sat.domain.model.SatEntry as DomainEntry
import com.rtbishop.look4sat.domain.model.SatItem as DomainItem
import com.rtbishop.look4sat.framework.model.SatEntry as FrameworkEntry import com.rtbishop.look4sat.framework.model.SatEntry as FrameworkEntry
import com.rtbishop.look4sat.framework.model.SatItem as FrameworkItem import com.rtbishop.look4sat.framework.model.SatItem as FrameworkItem
import com.rtbishop.look4sat.model.SatEntry as DomainEntry
import com.rtbishop.look4sat.model.SatItem as DomainItem
class LocalEntrySource(private val entriesDao: EntriesDao) : ILocalEntrySource { class EntriesStorage(private val entriesDao: EntriesDao) : IEntriesStorage {
override fun getEntriesTotal() = entriesDao.getEntriesTotal() override fun getEntriesTotal() = entriesDao.getEntriesTotal()
@@ -15,14 +15,14 @@
* 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.framework.data package com.rtbishop.look4sat.framework.data.local
import com.rtbishop.look4sat.domain.model.SatRadio as DomainRadio import com.rtbishop.look4sat.data.IRadiosStorage
import com.rtbishop.look4sat.framework.model.SatRadio as FrameworkRadio
import com.rtbishop.look4sat.domain.data.ILocalRadioSource
import com.rtbishop.look4sat.framework.data.dao.RadiosDao import com.rtbishop.look4sat.framework.data.dao.RadiosDao
import com.rtbishop.look4sat.framework.model.SatRadio as FrameworkRadio
import com.rtbishop.look4sat.model.SatRadio as DomainRadio
class LocalRadioSource(private val radiosDao: RadiosDao) : ILocalRadioSource { class RadiosStorage(private val radiosDao: RadiosDao) : IRadiosStorage {
override fun getRadiosTotal() = radiosDao.getRadiosTotal() override fun getRadiosTotal() = radiosDao.getRadiosTotal()
@@ -31,7 +31,6 @@ class LocalRadioSource(private val radiosDao: RadiosDao) : ILocalRadioSource {
} }
override suspend fun insertRadios(radios: List<DomainRadio>) { override suspend fun insertRadios(radios: List<DomainRadio>) {
radiosDao.deleteRadios()
radiosDao.insertRadios(radios.toFrameworkRadios()) radiosDao.insertRadios(radios.toFrameworkRadios())
} }
@@ -15,22 +15,21 @@
* 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.framework.data package com.rtbishop.look4sat.framework.data.remote
import android.content.ContentResolver import android.content.ContentResolver
import android.net.Uri import android.net.Uri
import com.rtbishop.look4sat.domain.data.IFileDataSource import com.rtbishop.look4sat.data.IFileSource
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.InputStream import java.io.InputStream
class FileDataSource( class FileSource(
private val contentResolver: ContentResolver, private val contentResolver: ContentResolver,
private val ioDispatcher: CoroutineDispatcher private val coroutineDispatcher: CoroutineDispatcher
) : IFileDataSource { ) : IFileSource {
@Suppress("BlockingMethodInNonBlockingContext")
override suspend fun getDataStream(uri: String): InputStream? { override suspend fun getDataStream(uri: String): InputStream? {
return withContext(ioDispatcher) { contentResolver.openInputStream(Uri.parse(uri)) } return withContext(coroutineDispatcher) { contentResolver.openInputStream(Uri.parse(uri)) }
} }
} }
@@ -15,25 +15,17 @@
* 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.framework.data package com.rtbishop.look4sat.framework.data.remote
import com.rtbishop.look4sat.domain.data.IRemoteDataSource import com.rtbishop.look4sat.data.INetworkSource
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.InputStream import java.io.InputStream
import java.net.URL
class RemoteDataSource( class NetworkSource(private val coroutineDispatcher: CoroutineDispatcher) : INetworkSource {
private val httpClient: OkHttpClient, private val ioDispatcher: CoroutineDispatcher
) : IRemoteDataSource {
override suspend fun getDataStream(url: String): InputStream? = withContext(ioDispatcher) { override suspend fun getDataStream(url: String): InputStream? {
try { return withContext(coroutineDispatcher) { URL(url).openStream() }
val request = Request.Builder().url(url).build()
httpClient.newCall(request).execute().body?.byteStream()
} catch (exception: Exception) {
null
}
} }
} }
@@ -19,7 +19,7 @@ package com.rtbishop.look4sat.framework.model
import androidx.room.Embedded import androidx.room.Embedded
import androidx.room.Entity import androidx.room.Entity
import com.rtbishop.look4sat.domain.predict.OrbitalData import com.rtbishop.look4sat.model.OrbitalData
@Entity(tableName = "entries", primaryKeys = ["catnum"]) @Entity(tableName = "entries", primaryKeys = ["catnum"])
data class SatEntry(@Embedded val data: OrbitalData, var comment: String? = null) data class SatEntry(@Embedded val data: OrbitalData, var comment: String? = null)
@@ -1,59 +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.injection
import android.bluetooth.BluetoothManager
import android.content.Context
import android.content.SharedPreferences
import android.hardware.SensorManager
import android.location.LocationManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideBluetoothManager(@ApplicationContext context: Context): BluetoothManager {
return context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
}
@Provides
@Singleton
fun provideLocationManager(@ApplicationContext context: Context): LocationManager {
return context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
}
@Provides
@Singleton
fun provideSensorManager(@ApplicationContext context: Context): SensorManager {
return context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
}
@Provides
@Singleton
fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences {
return context.getSharedPreferences("default", Context.MODE_PRIVATE)
}
}
@@ -1,93 +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.injection
import android.bluetooth.BluetoothManager
import android.content.Context
import androidx.room.Room
import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.ILocationManager
import com.rtbishop.look4sat.domain.ISatelliteManager
import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.domain.data.DataRepository
import com.rtbishop.look4sat.domain.predict.SatelliteManager
import com.rtbishop.look4sat.framework.LocationManager
import com.rtbishop.look4sat.framework.SettingsManager
import com.rtbishop.look4sat.framework.data.FileDataSource
import com.rtbishop.look4sat.framework.data.LocalDatabase
import com.rtbishop.look4sat.framework.data.LocalEntrySource
import com.rtbishop.look4sat.framework.data.LocalRadioSource
import com.rtbishop.look4sat.framework.data.MIGRATION_1_2
import com.rtbishop.look4sat.framework.data.RemoteDataSource
import com.rtbishop.look4sat.presentation.radarScreen.BTReporter
import com.rtbishop.look4sat.utility.DataParser
import com.rtbishop.look4sat.utility.DataReporter
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import okhttp3.OkHttpClient
@Module
@InstallIn(SingletonComponent::class)
object BaseModule {
@Provides
@Singleton
fun provideDataRepository(
@ApplicationContext context: Context,
settings: ISettingsManager
): IDataRepository {
val db = Room.databaseBuilder(context, LocalDatabase::class.java, "Look4SatDb")
.addMigrations(MIGRATION_1_2).fallbackToDestructiveMigration().build()
val parser = DataParser(Dispatchers.Default)
val fileSource = FileDataSource(context.contentResolver, Dispatchers.IO)
val entries = LocalEntrySource(db.entriesDao())
val radios = LocalRadioSource(db.radiosDao())
val remoteSource = RemoteDataSource(OkHttpClient.Builder().build(), Dispatchers.IO)
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
return DataRepository(parser, fileSource, entries, radios, remoteSource, scope, settings)
}
@Provides
@Singleton
fun provideDataReporter(): DataReporter = DataReporter(CoroutineScope(Dispatchers.IO))
@Provides
@Singleton
fun provideBTReporter(manager: BluetoothManager): BTReporter {
return BTReporter(manager, CoroutineScope(Dispatchers.IO))
}
@Provides
@Singleton
fun provideLocationManager(manager: LocationManager): ILocationManager = manager
@Provides
@Singleton
fun provideSatelliteManager(): ISatelliteManager = SatelliteManager(Dispatchers.Default)
@Provides
@Singleton
fun provideSettingsManager(manager: SettingsManager): ISettingsManager = manager
}
@@ -1,29 +1,5 @@
/*
* 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.presentation package com.rtbishop.look4sat.presentation
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@@ -32,7 +8,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
@@ -41,31 +16,11 @@ import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument import androidx.navigation.navArgument
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.presentation.entriesScreen.EntriesScreen import com.rtbishop.look4sat.presentation.entries.EntriesScreen
import com.rtbishop.look4sat.presentation.mapScreen.MapScreen import com.rtbishop.look4sat.presentation.map.MapScreen
import com.rtbishop.look4sat.presentation.passesScreen.PassesScreen import com.rtbishop.look4sat.presentation.passes.PassesScreen
import com.rtbishop.look4sat.presentation.radarScreen.RadarScreen import com.rtbishop.look4sat.presentation.radar.RadarScreen
import com.rtbishop.look4sat.presentation.settingsScreen.SettingsScreen import com.rtbishop.look4sat.presentation.settings.SettingsScreen
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@SuppressLint("SourceLockedOrientationActivity")
override fun onCreate(savedInstanceState: Bundle?) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
installSplashScreen()
super.onCreate(savedInstanceState)
setContent { MainTheme { MainScreen() } }
}
override fun attachBaseContext(newBase: Context?) {
val newConfig = Configuration(newBase?.resources?.configuration)
newConfig.fontScale = 1.0f
applyOverrideConfiguration(newConfig)
super.attachBaseContext(newBase)
}
}
sealed class Screen(var title: String, var icon: Int, var route: String) { sealed class Screen(var title: String, var icon: Int, var route: String) {
object Entries : Screen("Entries", R.drawable.ic_entries, "entries") object Entries : Screen("Entries", R.drawable.ic_entries, "entries")
@@ -77,7 +32,7 @@ sealed class Screen(var title: String, var icon: Int, var route: String) {
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun MainScreen(navController: NavHostController = rememberNavController()) { fun MainScreen(navController: NavHostController = rememberNavController()) {
Scaffold(bottomBar = { MainNavBar(navController = navController) }) { innerPadding -> Scaffold(bottomBar = { MainNavBar(navController = navController) }) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) { MainNavGraph(navController) } Box(modifier = Modifier.padding(innerPadding)) { MainNavGraph(navController) }
} }
@@ -15,7 +15,7 @@
* 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.presentation.passesScreen package com.rtbishop.look4sat.presentation.dialogs
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -1,4 +1,4 @@
package com.rtbishop.look4sat.presentation.settingsScreen package com.rtbishop.look4sat.presentation.dialogs
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -1,4 +1,4 @@
package com.rtbishop.look4sat.presentation.passesScreen package com.rtbishop.look4sat.presentation.dialogs
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@@ -1,4 +1,4 @@
package com.rtbishop.look4sat.presentation.settingsScreen package com.rtbishop.look4sat.presentation.dialogs
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -1,4 +1,4 @@
package com.rtbishop.look4sat.presentation.entriesScreen package com.rtbishop.look4sat.presentation.dialogs
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@@ -1,4 +1,4 @@
package com.rtbishop.look4sat.presentation.entriesScreen package com.rtbishop.look4sat.presentation.entries
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
@@ -28,11 +28,12 @@ import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.model.DataState import com.rtbishop.look4sat.model.DataState
import com.rtbishop.look4sat.domain.model.SatItem import com.rtbishop.look4sat.model.SatItem
import com.rtbishop.look4sat.presentation.Screen
import com.rtbishop.look4sat.presentation.CardLoadingIndicator import com.rtbishop.look4sat.presentation.CardLoadingIndicator
import com.rtbishop.look4sat.presentation.MainTheme import com.rtbishop.look4sat.presentation.MainTheme
import com.rtbishop.look4sat.presentation.Screen
import com.rtbishop.look4sat.presentation.dialogs.TypesDialog
import com.rtbishop.look4sat.presentation.onClick import com.rtbishop.look4sat.presentation.onClick
@Composable @Composable
@@ -15,15 +15,17 @@
* 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.presentation.entriesScreen 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.ISatelliteManager import com.rtbishop.look4sat.domain.ISatelliteManager
import com.rtbishop.look4sat.domain.ISettingsManager import com.rtbishop.look4sat.domain.ISettingsSource
import com.rtbishop.look4sat.domain.model.DataState import com.rtbishop.look4sat.model.DataState
import com.rtbishop.look4sat.domain.model.SatItem import com.rtbishop.look4sat.model.GeoPos
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
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -37,7 +39,8 @@ import javax.inject.Inject
class EntriesViewModel @Inject constructor( class EntriesViewModel @Inject constructor(
private val satelliteManager: ISatelliteManager, private val satelliteManager: ISatelliteManager,
private val repository: IDataRepository, private val repository: IDataRepository,
private val settings: ISettingsManager private val settings: ISettingsSource,
private val locationSource: ILocationSource
) : ViewModel() { ) : ViewModel() {
private val satType = MutableStateFlow("All") private val satType = MutableStateFlow("All")
@@ -78,11 +81,12 @@ class EntriesViewModel @Inject constructor(
settings.saveEntriesSelection(newSelection) settings.saveEntriesSelection(newSelection)
val selectedIds = settings.loadEntriesSelection() val selectedIds = settings.loadEntriesSelection()
val satellites = repository.getEntriesWithIds(selectedIds) val satellites = repository.getEntriesWithIds(selectedIds)
val stationPos = settings.loadStationPosition() val stationPos = locationSource.stationPosition.value
val geoPos = GeoPos(stationPos.latitude, stationPos.longitude)
val timeRef = System.currentTimeMillis() val timeRef = System.currentTimeMillis()
val hoursAhead = settings.getHoursAhead() val hoursAhead = settings.getHoursAhead()
val minElev = settings.getMinElevation() val minElev = settings.getMinElevation()
satelliteManager.calculatePasses(satellites, stationPos, timeRef, hoursAhead, minElev) satelliteManager.calculatePasses(satellites, geoPos, timeRef, hoursAhead, minElev)
} }
fun updateSelection(catNums: List<Int>, isSelected: Boolean) { fun updateSelection(catNums: List<Int>, isSelected: Boolean) {
@@ -15,9 +15,9 @@
* 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.presentation.mapScreen package com.rtbishop.look4sat.presentation.map
import com.rtbishop.look4sat.domain.predict.GeoPos import com.rtbishop.look4sat.model.GeoPos
data class MapData( data class MapData(
val catNum: Int, val catNum: Int,
@@ -1,5 +1,6 @@
package com.rtbishop.look4sat.presentation.mapScreen package com.rtbishop.look4sat.presentation.map
import android.content.Context
import android.graphics.* import android.graphics.*
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
@@ -22,9 +23,9 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.predict.GeoPos import com.rtbishop.look4sat.domain.Satellite
import com.rtbishop.look4sat.domain.predict.SatPos import com.rtbishop.look4sat.model.GeoPos
import com.rtbishop.look4sat.domain.predict.Satellite import com.rtbishop.look4sat.model.SatPos
import org.osmdroid.config.Configuration import org.osmdroid.config.Configuration
import org.osmdroid.tileprovider.tilesource.TileSourceFactory import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.util.GeoPoint import org.osmdroid.util.GeoPoint
@@ -60,7 +61,8 @@ private val labelRect = Rect()
@Composable @Composable
fun MapScreen(viewModel: MapViewModel = hiltViewModel()) { fun MapScreen(viewModel: MapViewModel = hiltViewModel()) {
val context = LocalContext.current val context = LocalContext.current
Configuration.getInstance().load(context, viewModel.getPreferences()) val preferences = context.getSharedPreferences("default", Context.MODE_PRIVATE)
Configuration.getInstance().load(context, preferences)
viewModel.selectDefaultSatellite(-1) viewModel.selectDefaultSatellite(-1)
val stationPos = viewModel.stationPos.collectAsState(initial = null) val stationPos = viewModel.stationPos.collectAsState(initial = null)
val positions = viewModel.positions.collectAsState(initial = null) val positions = viewModel.positions.collectAsState(initial = null)
@@ -15,17 +15,13 @@
* 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.presentation.mapScreen package com.rtbishop.look4sat.presentation.map
import android.content.SharedPreferences
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.*
import com.rtbishop.look4sat.domain.ISatelliteManager import com.rtbishop.look4sat.model.GeoPos
import com.rtbishop.look4sat.domain.ISettingsManager import com.rtbishop.look4sat.model.SatPos
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.utility.QthConverter import com.rtbishop.look4sat.utility.QthConverter
import com.rtbishop.look4sat.utility.toDegrees import com.rtbishop.look4sat.utility.toDegrees
import com.rtbishop.look4sat.utility.toTimerString import com.rtbishop.look4sat.utility.toTimerString
@@ -42,13 +38,14 @@ import kotlin.math.min
@HiltViewModel @HiltViewModel
class MapViewModel @Inject constructor( class MapViewModel @Inject constructor(
private val preferences: SharedPreferences,
private val repository: IDataRepository, private val repository: IDataRepository,
private val satelliteManager: ISatelliteManager, private val satelliteManager: ISatelliteManager,
private val settings: ISettingsManager private val settings: ISettingsSource,
private val locationSource: ILocationSource
) : ViewModel() { ) : ViewModel() {
private val stationPosition = settings.loadStationPosition() private val stationPosNew = locationSource.stationPosition.value
private val stationPosition = GeoPos(stationPosNew.latitude, stationPosNew.longitude)
private var allPasses = satelliteManager.getPasses() private var allPasses = satelliteManager.getPasses()
private var allSatellites = listOf<Satellite>() private var allSatellites = listOf<Satellite>()
private var dataUpdateJob: Job? = null private var dataUpdateJob: Job? = null
@@ -73,8 +70,6 @@ class MapViewModel @Inject constructor(
private val _positions = MutableSharedFlow<Map<Satellite, GeoPos>>() private val _positions = MutableSharedFlow<Map<Satellite, GeoPos>>()
val positions: SharedFlow<Map<Satellite, GeoPos>> = _positions val positions: SharedFlow<Map<Satellite, GeoPos>> = _positions
fun getPreferences() = preferences
fun scrollSelection(decrement: Boolean) { fun scrollSelection(decrement: Boolean) {
if (allSatellites.isNotEmpty()) { if (allSatellites.isNotEmpty()) {
val index = allSatellites.indexOf(selectedSatellite) val index = allSatellites.indexOf(selectedSatellite)
@@ -1,4 +1,4 @@
package com.rtbishop.look4sat.presentation.passesScreen package com.rtbishop.look4sat.presentation.passes
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
@@ -23,12 +23,12 @@ import androidx.compose.ui.unit.*
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.model.DataState import com.rtbishop.look4sat.domain.NearEarthSat
import com.rtbishop.look4sat.domain.predict.NearEarthSat import com.rtbishop.look4sat.model.DataState
import com.rtbishop.look4sat.domain.predict.OrbitalData import com.rtbishop.look4sat.model.OrbitalData
import com.rtbishop.look4sat.domain.predict.SatPass import com.rtbishop.look4sat.model.SatPass
import com.rtbishop.look4sat.presentation.Screen import com.rtbishop.look4sat.presentation.*
import com.rtbishop.look4sat.presentation.MainTheme import com.rtbishop.look4sat.presentation.dialogs.FilterDialog
import com.rtbishop.look4sat.presentation.pullRefresh.PullRefreshIndicator import com.rtbishop.look4sat.presentation.pullRefresh.PullRefreshIndicator
import com.rtbishop.look4sat.presentation.pullRefresh.PullRefreshState import com.rtbishop.look4sat.presentation.pullRefresh.PullRefreshState
import com.rtbishop.look4sat.presentation.pullRefresh.pullRefresh import com.rtbishop.look4sat.presentation.pullRefresh.pullRefresh
@@ -15,15 +15,17 @@
* 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.presentation.passesScreen 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.ISatelliteManager import com.rtbishop.look4sat.domain.ISatelliteManager
import com.rtbishop.look4sat.domain.ISettingsManager import com.rtbishop.look4sat.domain.ISettingsSource
import com.rtbishop.look4sat.domain.model.DataState import com.rtbishop.look4sat.model.DataState
import com.rtbishop.look4sat.domain.predict.SatPass import com.rtbishop.look4sat.model.GeoPos
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
import kotlinx.coroutines.* import kotlinx.coroutines.*
@@ -35,7 +37,8 @@ import javax.inject.Inject
class PassesViewModel @Inject constructor( class PassesViewModel @Inject constructor(
private val satelliteManager: ISatelliteManager, private val satelliteManager: ISatelliteManager,
private val repository: IDataRepository, private val repository: IDataRepository,
private val settings: ISettingsManager private val settings: ISettingsSource,
private val locationSource: ILocationSource
) : ViewModel() { ) : ViewModel() {
private val _passes = MutableStateFlow<DataState<List<SatPass>>>(DataState.Loading) private val _passes = MutableStateFlow<DataState<List<SatPass>>>(DataState.Loading)
@@ -105,11 +108,12 @@ class PassesViewModel @Inject constructor(
selection?.let { items -> settings.saveEntriesSelection(items) } selection?.let { items -> settings.saveEntriesSelection(items) }
settings.setHoursAhead(hoursAhead) settings.setHoursAhead(hoursAhead)
settings.setMinElevation(minElevation) settings.setMinElevation(minElevation)
val stationPos = settings.loadStationPosition() val stationPos = locationSource.stationPosition.value
val geoPos = GeoPos(stationPos.latitude, stationPos.longitude)
val selectedIds = settings.loadEntriesSelection() val selectedIds = settings.loadEntriesSelection()
val satellites = repository.getEntriesWithIds(selectedIds) val satellites = repository.getEntriesWithIds(selectedIds)
satelliteManager.calculatePasses( satelliteManager.calculatePasses(
satellites, stationPos, timeRef, hoursAhead, minElevation satellites, geoPos, timeRef, hoursAhead, minElevation
) )
} }
} }
@@ -15,8 +15,8 @@
* 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.presentation.radarScreen package com.rtbishop.look4sat.presentation.radar
import com.rtbishop.look4sat.domain.predict.SatPos import com.rtbishop.look4sat.model.SatPos
data class RadarData(val satPos: SatPos, val satTrack: List<SatPos> = emptyList()) data class RadarData(val satPos: SatPos, val satTrack: List<SatPos> = emptyList())
@@ -1,4 +1,4 @@
package com.rtbishop.look4sat.presentation.radarScreen package com.rtbishop.look4sat.presentation.radar
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@@ -18,15 +18,12 @@ import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.model.SatRadio import com.rtbishop.look4sat.model.SatRadio
@Composable @Composable
fun RadarScreen(navController: NavController) { fun RadarScreen(navController: NavController) {
val viewModel: RadarViewModel = hiltViewModel() val viewModel: RadarViewModel = hiltViewModel()
val currentPass = viewModel.getPass().collectAsState(null) val currentPass = viewModel.getPass().collectAsState(null)
val radarData = viewModel.radarData.collectAsState()
val transmitters = viewModel.transmitters.collectAsState()
val orientation = viewModel.orientation.collectAsState()
Column(modifier = Modifier.padding(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { Column(modifier = Modifier.padding(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
ElevatedCard(modifier = Modifier.height(52.dp)) { ElevatedCard(modifier = Modifier.height(52.dp)) {
@@ -62,20 +59,18 @@ fun RadarScreen(navController: NavController) {
ElevatedCard(modifier = Modifier ElevatedCard(modifier = Modifier
.fillMaxSize() .fillMaxSize()
.weight(1f)) { .weight(1f)) {
radarData.value?.let { data -> viewModel.radarData.value?.let { data ->
orientation.value?.let { triple -> RadarViewCompose(
RadarViewCompose( item = data.satPos,
item = data.satPos, items = data.satTrack,
items = data.satTrack, azimElev = viewModel.orientation.value
orientation = triple )
)
}
} }
} }
ElevatedCard(modifier = Modifier ElevatedCard(modifier = Modifier
.fillMaxSize() .fillMaxSize()
.weight(1f)) { .weight(1f)) {
TransmittersList(transmitters = transmitters.value) TransmittersList(transmitters = viewModel.transmitters.value)
} }
} }
} }
@@ -161,15 +156,9 @@ private fun TransmitterItem(radio: SatRadio) {
// setShowAim(viewModel.getUseCompass()) // setShowAim(viewModel.getUseCompass())
// setScanning(viewModel.getShowSweep()) // setScanning(viewModel.getShowSweep())
// } // }
// radarCard.addView(radarView)
// viewModel.radarData.observe(viewLifecycleOwner) { passData -> // viewModel.radarData.observe(viewLifecycleOwner) { passData ->
// radarView?.setPosition(passData.satPos)
// radarView?.setPositions(passData.satTrack)
// setPassText(pass, passData.satPos) // setPassText(pass, passData.satPos)
// } // }
// viewModel.orientation.observe(viewLifecycleOwner) { value ->
// radarView?.setOrientation(value.first, value.second, value.third)
// }
// } // }
// } // }
//} //}
@@ -1,6 +1,6 @@
@file:OptIn(ExperimentalTextApi::class) @file:OptIn(ExperimentalTextApi::class)
package com.rtbishop.look4sat.presentation.radarScreen package com.rtbishop.look4sat.presentation.radar
import androidx.compose.animation.core.* import androidx.compose.animation.core.*
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
@@ -14,14 +14,14 @@ import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.* import androidx.compose.ui.graphics.drawscope.*
import androidx.compose.ui.text.* import androidx.compose.ui.text.*
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.rtbishop.look4sat.domain.predict.PI_2 import com.rtbishop.look4sat.domain.PI_2
import com.rtbishop.look4sat.domain.predict.SatPos import com.rtbishop.look4sat.model.SatPos
import com.rtbishop.look4sat.utility.toRadians import com.rtbishop.look4sat.utility.toRadians
import kotlin.math.cos import kotlin.math.cos
import kotlin.math.sin import kotlin.math.sin
@Composable @Composable
fun RadarViewCompose(item: SatPos, items: List<SatPos>, orientation: Triple<Float, Float, Float>) { fun RadarViewCompose(item: SatPos, items: List<SatPos>, azimElev: Pair<Float, Float>) {
val radarColor = Color(0xFFDCDCDC) val radarColor = Color(0xFFDCDCDC)
val primaryColor = Color(0xFFFFE082) val primaryColor = Color(0xFFFFE082)
val secondaryColor = Color(0xFFDC0000) val secondaryColor = Color(0xFFDC0000)
@@ -41,14 +41,14 @@ fun RadarViewCompose(item: SatPos, items: List<SatPos>, orientation: Triple<Floa
trackEffect.value = createTrackEffect(trackPath.value) trackEffect.value = createTrackEffect(trackPath.value)
trackCreated.value = true trackCreated.value = true
} }
rotate(-orientation.first) { rotate(-azimElev.first) {
drawSweep(center, sweepDegrees.value, radius, primaryColor) drawSweep(center, sweepDegrees.value, radius, primaryColor)
drawRadar(radius, radarColor, strokeWidth, 3) drawRadar(radius, radarColor, strokeWidth, 3)
drawInfo(radius, primaryColor, measurer, 3) drawInfo(radius, primaryColor, measurer, 3)
translate(center.x, center.y) { translate(center.x, center.y) {
drawTrack(trackPath.value, trackEffect.value, secondaryColor, primaryColor) drawTrack(trackPath.value, trackEffect.value, secondaryColor, primaryColor)
if (item.elevation > 0) drawPosition(item, radius, animScale.value, primaryColor) if (item.elevation > 0) drawPosition(item, radius, animScale.value, primaryColor)
drawAim(orientation.first, orientation.second, radius, strokeWidth, secondaryColor) drawAim(azimElev.first, azimElev.second, radius, strokeWidth, secondaryColor)
} }
sweepDegrees.value = (sweepDegrees.value + 360 / 12.0f / 60) % 360 sweepDegrees.value = (sweepDegrees.value + 360 / 12.0f / 60) % 360
} }
@@ -15,29 +15,26 @@
* 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.presentation.radarScreen package com.rtbishop.look4sat.presentation.radar
import android.hardware.GeomagneticField import android.hardware.GeomagneticField
import android.util.Log import android.util.Log
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
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.*
import com.rtbishop.look4sat.domain.ISatelliteManager import com.rtbishop.look4sat.framework.BluetoothReporter
import com.rtbishop.look4sat.domain.ISettingsManager import com.rtbishop.look4sat.framework.NetworkReporter
import com.rtbishop.look4sat.domain.model.SatRadio import com.rtbishop.look4sat.framework.OrientationSource
import com.rtbishop.look4sat.domain.predict.GeoPos import com.rtbishop.look4sat.model.GeoPos
import com.rtbishop.look4sat.domain.predict.SatPass import com.rtbishop.look4sat.model.SatPass
import com.rtbishop.look4sat.domain.predict.SatPos import com.rtbishop.look4sat.model.SatPos
import com.rtbishop.look4sat.domain.predict.Satellite import com.rtbishop.look4sat.model.SatRadio
import com.rtbishop.look4sat.framework.OrientationManager
import com.rtbishop.look4sat.utility.DataReporter
import com.rtbishop.look4sat.utility.round import com.rtbishop.look4sat.utility.round
import com.rtbishop.look4sat.utility.toDegrees import com.rtbishop.look4sat.utility.toDegrees
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -46,28 +43,34 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class RadarViewModel @Inject constructor( class RadarViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle, private val savedStateHandle: SavedStateHandle,
private val orientationManager: OrientationManager, private val orientationSource: OrientationSource,
private val reporter: DataReporter, private val reporter: NetworkReporter,
private val btReporter: BTReporter, private val bluetoothReporter: BluetoothReporter,
private val satManager: ISatelliteManager, private val satManager: ISatelliteManager,
private val repository: IDataRepository, private val repository: IDataRepository,
private val settings: ISettingsManager private val settings: ISettingsSource,
) : ViewModel(), OrientationManager.OrientationListener { private val locationSource: ILocationSource
) : ViewModel() {
private val stationPos = settings.loadStationPosition() private val stationPos = locationSource.stationPosition.value.toGeoPos()
private val _passData = MutableStateFlow<RadarData?>(null) private val magDeclination = getMagDeclination(stationPos)
private val _transmitters = MutableStateFlow<List<SatRadio>>(emptyList()) val transmitters = mutableStateOf<List<SatRadio>>(emptyList())
private val _orientation = MutableStateFlow<Triple<Float, Float, Float>?>(null) val radarData = mutableStateOf<RadarData?>(null)
val radarData: StateFlow<RadarData?> = _passData val orientation = mutableStateOf(orientationSource.orientation.value)
val transmitters: StateFlow<List<SatRadio>> = _transmitters
val orientation: StateFlow<Triple<Float, Float, Float>?> = _orientation
init { init {
enableSensor() if (settings.isSensorEnabled()) {
viewModelScope.launch {
orientationSource.startListening()
orientationSource.orientation.collect { data ->
orientation.value = Pair(data.first + magDeclination, data.second)
}
}
}
} }
override fun onCleared() { override fun onCleared() {
disableSensor() orientationSource.stopListening()
super.onCleared() super.onCleared()
} }
@@ -94,22 +97,10 @@ class RadarViewModel @Inject constructor(
} }
} }
fun enableSensor() {
if (settings.isSensorEnabled()) orientationManager.startListening(this)
}
fun disableSensor() {
if (settings.isSensorEnabled()) orientationManager.stopListening()
}
fun getUseCompass(): Boolean = settings.isSensorEnabled() fun getUseCompass(): Boolean = settings.isSensorEnabled()
fun getShowSweep(): Boolean = settings.isSweepEnabled() fun getShowSweep(): Boolean = settings.isSweepEnabled()
override fun onOrientationChanged(azimuth: Float, pitch: Float, roll: Float) {
_orientation.value = Triple(azimuth + getMagDeclination(stationPos), pitch, roll)
}
private fun getMagDeclination(geoPos: GeoPos, time: Long = System.currentTimeMillis()): Float { private fun getMagDeclination(geoPos: GeoPos, time: Long = System.currentTimeMillis()): Float {
val latitude = geoPos.lat.toFloat() val latitude = geoPos.lat.toFloat()
val longitude = geoPos.lon.toFloat() val longitude = geoPos.lon.toFloat()
@@ -129,7 +120,7 @@ class RadarViewModel @Inject constructor(
val elevation = satPos.elevation.toDegrees().round(1) val elevation = satPos.elevation.toDegrees().round(1)
reporter.reportRotation(server, port, azimuth, elevation) reporter.reportRotation(server, port, azimuth, elevation)
} }
_passData.value = RadarData(satPos, track) radarData.value = RadarData(satPos, track)
} }
} }
@@ -137,14 +128,14 @@ class RadarViewModel @Inject constructor(
viewModelScope.launch { viewModelScope.launch {
if (settings.getBTEnabled()) { if (settings.getBTEnabled()) {
val btDevice = settings.getBTDeviceAddr() val btDevice = settings.getBTDeviceAddr()
if (btReporter.isConnected()) { if (bluetoothReporter.isConnected()) {
val format = settings.getBTFormat() val format = settings.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()
btReporter.reportRotation(format, azimuth, elevation) bluetoothReporter.reportRotation(format, azimuth, elevation)
} else if (!btReporter.isConnecting()) { } else if (!bluetoothReporter.isConnecting()) {
Log.i("BTReporter", "BTReporter: Attempting to connect...") Log.i("BTReporter", "BTReporter: Attempting to connect...")
btReporter.connectBTDevice(btDevice) bluetoothReporter.connectBTDevice(btDevice)
} }
} }
} }
@@ -154,7 +145,7 @@ class RadarViewModel @Inject constructor(
viewModelScope.launch { viewModelScope.launch {
delay(125) delay(125)
val list = satManager.processRadios(satellite, stationPos, radios, time) val list = satManager.processRadios(satellite, stationPos, radios, time)
_transmitters.value = list transmitters.value = list
} }
} }
} }
@@ -1,4 +1,4 @@
package com.rtbishop.look4sat.presentation.settingsScreen package com.rtbishop.look4sat.presentation.settings
import android.Manifest import android.Manifest
import android.os.Build import android.os.Build
@@ -26,6 +26,8 @@ import com.rtbishop.look4sat.BuildConfig
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.presentation.CardButton import com.rtbishop.look4sat.presentation.CardButton
import com.rtbishop.look4sat.presentation.MainTheme import com.rtbishop.look4sat.presentation.MainTheme
import com.rtbishop.look4sat.presentation.dialogs.LocatorDialog
import com.rtbishop.look4sat.presentation.dialogs.PositionDialog
import com.rtbishop.look4sat.presentation.gotoUrl import com.rtbishop.look4sat.presentation.gotoUrl
import com.rtbishop.look4sat.presentation.showToast import com.rtbishop.look4sat.presentation.showToast
@@ -57,8 +59,8 @@ fun SettingsScreen(navController: NavController) {
val locationPermFine = Manifest.permission.ACCESS_FINE_LOCATION val locationPermFine = Manifest.permission.ACCESS_FINE_LOCATION
val locationRequest = rememberLauncherForActivityResult(locationContract) { permissions -> val locationRequest = rememberLauncherForActivityResult(locationContract) { permissions ->
when { when {
permissions[locationPermFine] == true -> viewModel.locationSettings.value.setGpsLoc permissions[locationPermFine] == true -> viewModel.locationSettings.value.setGpsLoc()
permissions[locationPermCoarse] == true -> viewModel.locationSettings.value.setGpsLoc permissions[locationPermCoarse] == true -> viewModel.locationSettings.value.setGpsLoc()
else -> showToast(context, locationError) else -> showToast(context, locationError)
} }
} }
@@ -1,4 +1,4 @@
package com.rtbishop.look4sat.presentation.settingsScreen package com.rtbishop.look4sat.presentation.settings
data class LocationSettings( data class LocationSettings(
val getUpdating: Boolean, val getUpdating: Boolean,
@@ -15,15 +15,15 @@
* 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.presentation.settingsScreen 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.domain.IDataRepository import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.ILocationManager import com.rtbishop.look4sat.domain.ILocationSource
import com.rtbishop.look4sat.domain.ISettingsManager import com.rtbishop.look4sat.domain.ISettingsSource
import com.rtbishop.look4sat.domain.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
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -31,21 +31,20 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class SettingsViewModel @Inject constructor( class SettingsViewModel @Inject constructor(
private val locationManager: ILocationManager, private val locationSource: ILocationSource,
private val repository: IDataRepository, private val repository: IDataRepository,
private val settings: ISettingsManager private val settings: ISettingsSource
) : ViewModel() { ) : ViewModel() {
private val defaultLocationSettings = LocationSettings(false, private val defaultLocationSettings = LocationSettings(false,
settings.getLastUpdateTime(), locationSource.stationPosition.value.timestamp,
locationManager.getStationPosition().lat, locationSource.stationPosition.value.latitude,
locationManager.getStationPosition().lon, locationSource.stationPosition.value.longitude,
settings.loadStationLocator(), locationSource.stationPosition.value.qthLocator,
{ locationManager.setPositionFromGps() }, { locationSource.setGpsPosition() },
{ lat, lon -> locationManager.setStationPosition(lat, lon) }, { lat, lon -> locationSource.setGeoPosition(lat, lon) },
{ locationManager.setPositionFromQth(it) }) { locationSource.setQthPosition(it) })
var locationSettings = mutableStateOf(defaultLocationSettings) val locationSettings = mutableStateOf(defaultLocationSettings)
private set
private val defaultDataSettings = DataSettings(false, private val defaultDataSettings = DataSettings(false,
settings.getLastUpdateTime(), settings.getLastUpdateTime(),
@@ -54,8 +53,7 @@ class SettingsViewModel @Inject constructor(
{ repository.updateFromWeb() }, { repository.updateFromWeb() },
{ repository.updateFromFile(it) }, { repository.updateFromFile(it) },
{ repository.clearAllData() }) { repository.clearAllData() })
var dataSettings = mutableStateOf(defaultDataSettings) val dataSettings = mutableStateOf(defaultDataSettings)
private set
private val defaultOtherSettings = OtherSettings(settings.isUtcEnabled(), private val defaultOtherSettings = OtherSettings(settings.isUtcEnabled(),
settings.isUpdateEnabled(), settings.isUpdateEnabled(),
@@ -65,8 +63,7 @@ class SettingsViewModel @Inject constructor(
{ setUpdate(it) }, { setUpdate(it) },
{ setSweep(it) }, { setSweep(it) },
{ setSensor(it) }) { setSensor(it) })
var otherSettings = mutableStateOf(defaultOtherSettings) val otherSettings = mutableStateOf(defaultOtherSettings)
private set
init { init {
viewModelScope.launch { viewModelScope.launch {
@@ -81,9 +78,7 @@ class SettingsViewModel @Inject constructor(
combine(repository.getEntriesTotal(), repository.getRadiosTotal()) { sats, radios -> combine(repository.getEntriesTotal(), repository.getRadiosTotal()) { sats, radios ->
dataSettings.value = dataSettings.value =
dataSettings.value.copy(getSatellites = sats, getRadios = radios) dataSettings.value.copy(getSatellites = sats, getRadios = radios)
}.collect { }.collect {}
}
} }
} }
@@ -15,50 +15,52 @@
* 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.utility package com.rtbishop.look4sat.data
import com.rtbishop.look4sat.domain.model.SatRadio import com.rtbishop.look4sat.model.OrbitalData
import com.rtbishop.look4sat.domain.predict.OrbitalData import com.rtbishop.look4sat.model.SatRadio
import java.io.InputStream
import kotlin.math.pow
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import java.io.InputStream
import kotlin.math.pow
class DataParser(private val defaultDispatcher: CoroutineDispatcher) { class DataParser(private val defaultDispatcher: CoroutineDispatcher) {
suspend fun parseCSVStream(csvStream: InputStream): List<OrbitalData> = withContext(defaultDispatcher) { suspend fun parseCSVStream(csvStream: InputStream): List<OrbitalData> =
val parsedItems = mutableListOf<OrbitalData>() withContext(defaultDispatcher) {
csvStream.bufferedReader().useLines { lines -> val parsedItems = mutableListOf<OrbitalData>()
lines.forEachIndexed { index, line -> csvStream.bufferedReader().useLines { lines ->
if (index != 0) { lines.forEachIndexed { index, line ->
val values = line.split(",") if (index != 0) {
parseCSV(values)?.let { tle -> parsedItems.add(tle) } val values = line.split(",")
parseCSV(values)?.let { tle -> parsedItems.add(tle) }
}
} }
} }
return@withContext parsedItems
} }
return@withContext parsedItems
}
suspend fun parseTLEStream(tleStream: InputStream): List<OrbitalData> = withContext(defaultDispatcher) { suspend fun parseTLEStream(tleStream: InputStream): List<OrbitalData> =
val tleStrings = mutableListOf(String(), String(), String()) withContext(defaultDispatcher) {
val parsedItems = mutableListOf<OrbitalData>() val tleStrings = mutableListOf(String(), String(), String())
var lineIndex = 0 val parsedItems = mutableListOf<OrbitalData>()
tleStream.bufferedReader().forEachLine { line -> var lineIndex = 0
tleStrings[lineIndex] = line tleStream.bufferedReader().forEachLine { line ->
if (lineIndex < 2) { tleStrings[lineIndex] = line
lineIndex++ if (lineIndex < 2) {
} else { lineIndex++
val isLineOneValid = tleStrings[1].substring(0, 1) == "1" } else {
val isLineTwoValid = tleStrings[2].substring(0, 1) == "2" val isLineOneValid = tleStrings[1].substring(0, 1) == "1"
if (!isLineOneValid && !isLineTwoValid) return@forEachLine val isLineTwoValid = tleStrings[2].substring(0, 1) == "2"
parseTLE(tleStrings)?.let { tle -> parsedItems.add(tle) } if (!isLineOneValid && !isLineTwoValid) return@forEachLine
lineIndex = 0 parseTLE(tleStrings)?.let { tle -> parsedItems.add(tle) }
lineIndex = 0
}
} }
return@withContext parsedItems
} }
return@withContext parsedItems
}
suspend fun parseJSONStream(jsonStream: InputStream): List<SatRadio> { suspend fun parseJSONStream(jsonStream: InputStream): List<SatRadio> {
return withContext(defaultDispatcher) { return withContext(defaultDispatcher) {
@@ -76,10 +78,6 @@ class DataParser(private val defaultDispatcher: CoroutineDispatcher) {
} }
} }
fun isLeapYear(year: Int): Boolean {
return ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)
}
private fun parseCSV(values: List<String>): OrbitalData? { private fun parseCSV(values: List<String>): OrbitalData? {
try { try {
val name = values[0] val name = values[0]
@@ -122,8 +120,8 @@ 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 = val bstar: Double = 1.0e-5 * tle[1].substring(53, 59).toDouble() /
1.0e-5 * tle[1].substring(53, 59).toDouble() / 10.0.pow(tle[1].substring(60, 61).toDouble()) 10.0.pow(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
@@ -155,10 +153,8 @@ class DataParser(private val defaultDispatcher: CoroutineDispatcher) {
val daysArray = arrayOf(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) val daysArray = arrayOf(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
var dayOfYear = dayOfMonth var dayOfYear = dayOfMonth
// If leap year increment Feb days // If leap year increment Feb days
if (isLeapYear(year)) daysArray[1]++ if (((year / 4 == 0) && (year / 100 != 0)) || (year / 400 == 0)) daysArray[1]++
for (i in 0 until month - 1) { for (i in 0 until month - 1) { dayOfYear += daysArray[i] }
dayOfYear += daysArray[i]
}
return dayOfYear return dayOfYear
} }
} }
@@ -15,13 +15,12 @@
* 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.data package com.rtbishop.look4sat.data
import com.rtbishop.look4sat.domain.IDataRepository import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.ISettingsManager import com.rtbishop.look4sat.domain.ISettingsSource
import com.rtbishop.look4sat.domain.model.DataState import com.rtbishop.look4sat.model.DataState
import com.rtbishop.look4sat.domain.model.SatEntry import com.rtbishop.look4sat.model.SatEntry
import com.rtbishop.look4sat.utility.DataParser
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
@@ -30,16 +29,16 @@ import java.util.zip.ZipInputStream
class DataRepository( class DataRepository(
private val dataParser: DataParser, private val dataParser: DataParser,
private val fileSource: IFileDataSource, private val fileSource: IFileSource,
private val entrySource: ILocalEntrySource, private val entrySource: IEntriesStorage,
private val radioSource: ILocalRadioSource, private val radioSource: IRadiosStorage,
private val remoteSource: IRemoteDataSource, private val remoteSource: INetworkSource,
private val repositoryScope: CoroutineScope, private val repositoryScope: CoroutineScope,
private val settingsManager: ISettingsManager private val settingsManager: ISettingsSource
) : IDataRepository { ) : IDataRepository {
private val exceptionHandler = CoroutineExceptionHandler { _, exception -> private val exceptionHandler = CoroutineExceptionHandler { _, exception ->
_updateState.value = DataState.Error(exception.message) _updateState.value = DataState.Exception(exception.message)
} }
private val updateStateDelay = 875L private val updateStateDelay = 875L
private val _updateState = MutableStateFlow<DataState<Long>>(DataState.Handled) private val _updateState = MutableStateFlow<DataState<Long>>(DataState.Handled)
@@ -15,14 +15,14 @@
* 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.data package com.rtbishop.look4sat.data
import com.rtbishop.look4sat.domain.model.SatEntry import com.rtbishop.look4sat.domain.Satellite
import com.rtbishop.look4sat.domain.model.SatItem import com.rtbishop.look4sat.model.SatEntry
import com.rtbishop.look4sat.domain.predict.Satellite import com.rtbishop.look4sat.model.SatItem
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface ILocalEntrySource { interface IEntriesStorage {
fun getEntriesTotal(): Flow<Int> fun getEntriesTotal(): Flow<Int>
@@ -15,11 +15,11 @@
* 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.data package com.rtbishop.look4sat.data
import java.io.InputStream import java.io.InputStream
interface IFileDataSource { interface IFileSource {
suspend fun getDataStream(uri: String): InputStream? suspend fun getDataStream(uri: String): InputStream?
} }
@@ -15,11 +15,11 @@
* 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.data package com.rtbishop.look4sat.data
import java.io.InputStream import java.io.InputStream
interface IRemoteDataSource { interface INetworkSource {
val radioApi: String get() = "https://db.satnogs.org/api/transmitters/?format=json&status=active" val radioApi: String get() = "https://db.satnogs.org/api/transmitters/?format=json&status=active"
@@ -15,12 +15,12 @@
* 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.data package com.rtbishop.look4sat.data
import com.rtbishop.look4sat.domain.model.SatRadio import com.rtbishop.look4sat.model.SatRadio
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface ILocalRadioSource { interface IRadiosStorage {
fun getRadiosTotal(): Flow<Int> fun getRadiosTotal(): Flow<Int>
@@ -15,8 +15,9 @@
* 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.predict package com.rtbishop.look4sat.domain
import com.rtbishop.look4sat.model.OrbitalData
import kotlin.math.* import kotlin.math.*
class DeepSpaceSat(data: OrbitalData) : Satellite(data) { class DeepSpaceSat(data: OrbitalData) : Satellite(data) {
@@ -17,10 +17,9 @@
*/ */
package com.rtbishop.look4sat.domain package com.rtbishop.look4sat.domain
import com.rtbishop.look4sat.domain.model.DataState import com.rtbishop.look4sat.model.DataState
import com.rtbishop.look4sat.domain.model.SatItem import com.rtbishop.look4sat.model.SatItem
import com.rtbishop.look4sat.domain.model.SatRadio import com.rtbishop.look4sat.model.SatRadio
import com.rtbishop.look4sat.domain.predict.Satellite
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@@ -1,41 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain
import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.predict.GeoPos
import kotlinx.coroutines.flow.SharedFlow
interface ILocationManager {
val stationPosition: SharedFlow<DataState<GeoPos>>
fun getStationLocator(): String
fun getStationPosition(): GeoPos
fun setStationPosition(latitude: Double, longitude: Double)
fun setPositionFromGps()
fun setPositionFromNet()
fun setPositionFromQth(locator: String)
fun setPositionHandled()
}
@@ -0,0 +1,32 @@
/*
* 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.StationPos
import kotlinx.coroutines.flow.StateFlow
interface ILocationSource {
val stationPosition: StateFlow<StationPos>
fun setGpsPosition(): Boolean
fun setGeoPosition(latitude: Double, longitude: Double): Boolean
fun setQthPosition(locator: String): Boolean
}
@@ -1,10 +1,9 @@
package com.rtbishop.look4sat.domain package com.rtbishop.look4sat.domain
import com.rtbishop.look4sat.domain.model.SatRadio import com.rtbishop.look4sat.model.GeoPos
import com.rtbishop.look4sat.domain.predict.GeoPos import com.rtbishop.look4sat.model.SatPass
import com.rtbishop.look4sat.domain.predict.SatPass import com.rtbishop.look4sat.model.SatPos
import com.rtbishop.look4sat.domain.predict.SatPos import com.rtbishop.look4sat.model.SatRadio
import com.rtbishop.look4sat.domain.predict.Satellite
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
interface ISatelliteManager { interface ISatelliteManager {
@@ -17,9 +17,9 @@
*/ */
package com.rtbishop.look4sat.domain package com.rtbishop.look4sat.domain
import com.rtbishop.look4sat.domain.predict.GeoPos import com.rtbishop.look4sat.model.StationPos
interface ISettingsManager { interface ISettingsSource {
val sourcesMap: Map<String, String> val sourcesMap: Map<String, String>
get() = mapOf( get() = mapOf(
@@ -51,13 +51,9 @@ interface ISettingsManager {
"Amsat" to "https://amsat.org/tle/current/nasabare.txt" "Amsat" to "https://amsat.org/tle/current/nasabare.txt"
) )
fun loadStationLocator(): String fun loadStationPosition(): StationPos
fun saveStationLocator(locator: String) fun saveStationPosition(stationPos: StationPos)
fun loadStationPosition(): GeoPos
fun saveStationPosition(position: GeoPos)
fun getHoursAhead(): Int fun getHoursAhead(): Int
@@ -15,8 +15,9 @@
* 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.predict package com.rtbishop.look4sat.domain
import com.rtbishop.look4sat.model.OrbitalData
import kotlin.math.* import kotlin.math.*
class NearEarthSat(data: OrbitalData) : Satellite(data) { class NearEarthSat(data: OrbitalData) : Satellite(data) {
@@ -15,8 +15,11 @@
* 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.predict package com.rtbishop.look4sat.domain
import com.rtbishop.look4sat.model.GeoPos
import com.rtbishop.look4sat.model.OrbitalData
import com.rtbishop.look4sat.model.SatPos
import kotlin.math.* import kotlin.math.*
const val ASTRONOMICAL_UNIT = 1.49597870691E8 const val ASTRONOMICAL_UNIT = 1.49597870691E8
@@ -15,10 +15,12 @@
* 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.predict package com.rtbishop.look4sat.domain
import com.rtbishop.look4sat.domain.ISatelliteManager import com.rtbishop.look4sat.model.GeoPos
import com.rtbishop.look4sat.domain.model.SatRadio import com.rtbishop.look4sat.model.SatPass
import com.rtbishop.look4sat.model.SatPos
import com.rtbishop.look4sat.model.SatRadio
import com.rtbishop.look4sat.utility.round import com.rtbishop.look4sat.utility.round
import com.rtbishop.look4sat.utility.toDegrees import com.rtbishop.look4sat.utility.toDegrees
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
@@ -15,11 +15,12 @@
* 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.model package com.rtbishop.look4sat.model
sealed class DataState<out T> { sealed class DataState<out T> {
data class Success<out T>(val data: T) : DataState<T>() data class Success<out T>(val data: T) : DataState<T>()
data class Error(val message: String?) : DataState<Nothing>() data class Error(val resId: Int?) : DataState<Nothing>()
data class Exception(val message: String?) : DataState<Nothing>()
object Handled : DataState<Nothing>() object Handled : DataState<Nothing>()
object Loading : DataState<Nothing>() object Loading : DataState<Nothing>()
} }
@@ -15,6 +15,6 @@
* 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.predict package com.rtbishop.look4sat.model
data class GeoPos(val lat: Double, val lon: Double, val alt: Double = 0.0, val name: String? = null) data class GeoPos(val lat: Double, val lon: Double, val alt: Double = 0.0, val name: String? = null)
@@ -15,7 +15,9 @@
* 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.predict package com.rtbishop.look4sat.model
import com.rtbishop.look4sat.domain.*
data class OrbitalData( data class OrbitalData(
val name: String, val name: String,
@@ -15,8 +15,6 @@
* 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.model package com.rtbishop.look4sat.model
import com.rtbishop.look4sat.domain.predict.OrbitalData
data class SatEntry(val data: OrbitalData, var comment: String? = null) data class SatEntry(val data: OrbitalData, var comment: String? = null)
@@ -15,7 +15,7 @@
* 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.model package com.rtbishop.look4sat.model
data class SatItem( data class SatItem(
val catnum: Int, val catnum: Int,
@@ -15,7 +15,9 @@
* 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.predict package com.rtbishop.look4sat.model
import com.rtbishop.look4sat.domain.Satellite
data class SatPass( data class SatPass(
val aosTime: Long = 0L, val aosTime: Long = 0L,
@@ -15,8 +15,12 @@
* 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.predict package com.rtbishop.look4sat.model
import com.rtbishop.look4sat.domain.DEG2RAD
import com.rtbishop.look4sat.domain.EARTH_RADIUS
import com.rtbishop.look4sat.domain.RAD2DEG
import com.rtbishop.look4sat.domain.SPEED_OF_LIGHT
import kotlin.math.* import kotlin.math.*
data class SatPos( data class SatPos(
@@ -15,7 +15,7 @@
* 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.model package com.rtbishop.look4sat.model
data class SatRadio( data class SatRadio(
val uuid: String, val uuid: String,
@@ -0,0 +1,7 @@
package com.rtbishop.look4sat.model
data class StationPos(
val latitude: Double, val longitude: Double, val qthLocator: String, val timestamp: Long
) {
fun toGeoPos(): GeoPos = GeoPos(latitude, longitude)
}
@@ -17,8 +17,8 @@
*/ */
package com.rtbishop.look4sat.utility package com.rtbishop.look4sat.utility
import com.rtbishop.look4sat.domain.predict.DEG2RAD import com.rtbishop.look4sat.domain.DEG2RAD
import com.rtbishop.look4sat.domain.predict.RAD2DEG import com.rtbishop.look4sat.domain.RAD2DEG
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
fun Long.toTimerString(): String { fun Long.toTimerString(): String {
@@ -64,7 +64,7 @@ fun Double.toRadians(): Double = this * DEG2RAD
//} //}
fun String.isValidIPv4(): Boolean { fun String.isValidIPv4(): Boolean {
val ip4 = "^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(\\.(?!\$)|\$)){4}\$" val ip4 = "^((\\d|[1-9]\\d|1\\d\\d|2[0-4]\\d|25[0-5])(\\.(?!\$)|\$)){4}\$"
return this.matches(ip4.toRegex()) return this.matches(ip4.toRegex())
} }
@@ -17,7 +17,7 @@
*/ */
package com.rtbishop.look4sat.utility package com.rtbishop.look4sat.utility
import com.rtbishop.look4sat.domain.predict.GeoPos import com.rtbishop.look4sat.model.GeoPos
object QthConverter { object QthConverter {
@@ -35,27 +35,27 @@ object QthConverter {
return GeoPos(latitude, longitude) return GeoPos(latitude, longitude)
} }
fun positionToQth(lat: Double, lon: Double): String? { fun positionToQth(latitude: Double, longitude: Double): String? {
if (!isValidPosition(lat, lon)) return null if (!isValidPosition(latitude, longitude)) return null
val tempLon = if (lon > 180.0) lon - 180 else lon val tempLon = if (longitude > 180.0) longitude - 180 else longitude
val upper = "ABCDEFGHIJKLMNOPQRSTUVWX" val upper = "ABCDEFGHIJKLMNOPQRSTUVWX"
val lower = "abcdefghijklmnopqrstuvwx" val lower = "abcdefghijklmnopqrstuvwx"
val longitude = tempLon + 180 val newLongitude = tempLon + 180
val latitude = lat + 90 val newLatitude = latitude + 90
val lonFirst = upper[(longitude / 20).toInt()] val lonFirst = upper[(newLongitude / 20).toInt()]
val latFirst = upper[(latitude / 10).toInt()] val latFirst = upper[(newLatitude / 10).toInt()]
val lonSecond = ((longitude / 2) % 10).toInt().toString() val lonSecond = ((newLongitude / 2) % 10).toInt().toString()
val latSecond = (latitude % 10).toInt().toString() val latSecond = (newLatitude % 10).toInt().toString()
val lonThird = lower[((longitude % 2) * 12).toInt()] val lonThird = lower[((newLongitude % 2) * 12).toInt()]
val latThird = lower[((latitude % 1) * 24).toInt()] val latThird = lower[((newLatitude % 1) * 24).toInt()]
return "$lonFirst$latFirst$lonSecond$latSecond$lonThird$latThird" return "$lonFirst$latFirst$lonSecond$latSecond$lonThird$latThird"
} }
fun isValidPosition(lat: Double, lon: Double): Boolean { private fun isValidPosition(lat: Double, lon: Double): Boolean {
return (lat >= -90.0 && lat <= 90.0) && (lon >= -180.0 && lon <= 360.0) return (lat >= -90.0 && lat <= 90.0) && (lon >= -180.0 && lon <= 360.0)
} }
private fun isValidLocator(locator: String): Boolean { private fun isValidLocator(locator: String): Boolean {
return locator.matches("[a-xA-X][a-xA-X][0-9][0-9][a-xA-X][a-xA-X]".toRegex()) return locator.matches("[a-xA-X][a-xA-X]\\d\\d[a-xA-X][a-xA-X]".toRegex())
} }
} }
@@ -17,7 +17,7 @@
*/ */
package com.rtbishop.look4sat package com.rtbishop.look4sat
import com.rtbishop.look4sat.utility.DataParser import com.rtbishop.look4sat.data.DataParser
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest