Further refactoring of Look4Sat core module classes

This commit is contained in:
Arty Bishop committed 2021-09-15 09:58:40 +01:00
1 parent 2c757a6b35
commit bceeb01194
46 files changed
+603 -572

No files matched your search

@@ -22,8 +22,8 @@ import android.hardware.GeomagneticField
import android.location.LocationManager import android.location.LocationManager
import androidx.core.content.edit import androidx.core.content.edit
import com.rtbishop.look4sat.data.PreferencesSource import com.rtbishop.look4sat.data.PreferencesSource
import com.rtbishop.look4sat.domain.predict4kotlin.QthConverter import com.rtbishop.look4sat.domain.QthConverter
import com.rtbishop.look4sat.domain.predict4kotlin.StationPos import com.rtbishop.look4sat.domain.StationPos
import com.rtbishop.look4sat.utility.round import com.rtbishop.look4sat.utility.round
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.squareup.moshi.Types import com.squareup.moshi.Types
@@ -31,7 +31,6 @@ import javax.inject.Inject
class PreferencesProvider @Inject constructor( class PreferencesProvider @Inject constructor(
moshi: Moshi, moshi: Moshi,
private val qthConverter: QthConverter,
private val locationManager: LocationManager, private val locationManager: LocationManager,
private val preferences: SharedPreferences private val preferences: SharedPreferences
) : PreferencesSource { ) : PreferencesSource {
@@ -86,7 +85,7 @@ class PreferencesProvider @Inject constructor(
} }
override fun positionToQTH(lat: Double, lon: Double): String? { override fun positionToQTH(lat: Double, lon: Double): String? {
return qthConverter.positionToQTH(lat, lon) return QthConverter.positionToQTH(lat, lon)
} }
override fun loadStationPosition(): StationPos { override fun loadStationPosition(): StationPos {
@@ -123,7 +122,7 @@ class PreferencesProvider @Inject constructor(
} }
override fun updatePositionFromQTH(qthString: String): Boolean { override fun updatePositionFromQTH(qthString: String): Boolean {
val position = qthConverter.qthToPosition(qthString) ?: return false val position = QthConverter.qthToPosition(qthString) ?: return false
val stationPosition = StationPos(position.latitude, position.longitude, 0.0) val stationPosition = StationPos(position.latitude, position.longitude, 0.0)
saveStationPosition(stationPosition) saveStationPosition(stationPosition)
return true return true
@@ -17,12 +17,12 @@
*/ */
package com.rtbishop.look4sat.framework.api package com.rtbishop.look4sat.framework.api
import com.rtbishop.look4sat.data.SatDataRemoteSource import com.rtbishop.look4sat.data.RemoteDataSource
import com.rtbishop.look4sat.domain.model.SatTrans import com.rtbishop.look4sat.domain.SatTrans
import com.rtbishop.look4sat.utility.DataMapper import com.rtbishop.look4sat.utility.DataMapper
import java.io.InputStream import java.io.InputStream
class SatDataRemote(private val api: SatDataService) : SatDataRemoteSource { class RemoteSource(private val api: SatelliteService) : RemoteDataSource {
override suspend fun fetchDataStream(url: String): InputStream? { override suspend fun fetchDataStream(url: String): InputStream? {
return api.fetchFileByUrl(url).body()?.byteStream() return api.fetchFileByUrl(url).body()?.byteStream()
@@ -24,7 +24,7 @@ import retrofit2.http.GET
import retrofit2.http.Streaming import retrofit2.http.Streaming
import retrofit2.http.Url import retrofit2.http.Url
interface SatDataService { interface SatelliteService {
@Streaming @Streaming
@GET @GET
@@ -18,34 +18,34 @@
package com.rtbishop.look4sat.framework.db package com.rtbishop.look4sat.framework.db
import androidx.room.TypeConverter import androidx.room.TypeConverter
import com.rtbishop.look4sat.domain.predict4kotlin.Satellite import com.rtbishop.look4sat.domain.Satellite
import com.rtbishop.look4sat.domain.predict4kotlin.TLE import com.rtbishop.look4sat.domain.TLE
import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
object RoomConverters { object Converters {
private lateinit var tleAdapter: JsonAdapter<TLE> private lateinit var paramsAdapter: JsonAdapter<TLE>
fun initialize(moshi: Moshi) { fun initialize(moshi: Moshi) {
tleAdapter = moshi.adapter(TLE::class.java) paramsAdapter = moshi.adapter(TLE::class.java)
} }
@JvmStatic @JvmStatic
@TypeConverter @TypeConverter
fun tleToString(tle: TLE): String { fun tleToString(tle: TLE): String {
return tleAdapter.toJson(tle) return paramsAdapter.toJson(tle)
} }
@JvmStatic @JvmStatic
@TypeConverter @TypeConverter
fun tleFromString(string: String): TLE? { fun tleFromString(string: String): TLE? {
return tleAdapter.fromJson(string) return paramsAdapter.fromJson(string)
} }
@JvmStatic @JvmStatic
@TypeConverter @TypeConverter
fun satFromString(string: String): Satellite? { fun satFromString(string: String): Satellite? {
return tleAdapter.fromJson(string)?.createSat() return paramsAdapter.fromJson(string)?.createSat()
} }
} }
@@ -17,42 +17,42 @@
*/ */
package com.rtbishop.look4sat.framework.db package com.rtbishop.look4sat.framework.db
import com.rtbishop.look4sat.data.SatDataLocalSource import com.rtbishop.look4sat.data.LocalDataSource
import com.rtbishop.look4sat.domain.model.SatEntry import com.rtbishop.look4sat.domain.SatEntry
import com.rtbishop.look4sat.domain.model.SatItem import com.rtbishop.look4sat.domain.SatItem
import com.rtbishop.look4sat.domain.model.SatTrans import com.rtbishop.look4sat.domain.SatTrans
import com.rtbishop.look4sat.domain.predict4kotlin.Satellite import com.rtbishop.look4sat.domain.Satellite
import com.rtbishop.look4sat.utility.DataMapper import com.rtbishop.look4sat.utility.DataMapper
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
class SatDataLocal(private val satDataDao: SatDataDao) : SatDataLocalSource { class LocalSource(private val satelliteDao: SatelliteDao) : LocalDataSource {
override fun getSatItems(): Flow<List<SatItem>> { override fun getSatItems(): Flow<List<SatItem>> {
return satDataDao.getSatItems() return satelliteDao.getSatItems()
.map { satItems -> DataMapper.satItemsToDomainItems(satItems) } .map { satItems -> DataMapper.satItemsToDomainItems(satItems) }
} }
override suspend fun getSelectedSatellites(): List<Satellite> { override suspend fun getSelectedSatellites(): List<Satellite> {
return satDataDao.getSelectedSatellites() return satelliteDao.getSelectedSatellites()
} }
override suspend fun updateEntries(entries: List<SatEntry>) { override suspend fun updateEntries(entries: List<SatEntry>) {
val satEntries = entries.map { entry -> DataMapper.domainEntryToSatEntry(entry) } val satEntries = entries.map { entry -> DataMapper.domainEntryToSatEntry(entry) }
satDataDao.updateEntries(satEntries) satelliteDao.updateEntries(satEntries)
} }
override suspend fun updateEntriesSelection(catNums: List<Int>, isSelected: Boolean) { override suspend fun updateEntriesSelection(catNums: List<Int>, isSelected: Boolean) {
satDataDao.updateEntriesSelection(catNums, isSelected) satelliteDao.updateEntriesSelection(catNums, isSelected)
} }
override fun getSatTransmitters(catNum: Int): Flow<List<SatTrans>> { override fun getSatTransmitters(catNum: Int): Flow<List<SatTrans>> {
return satDataDao.getSatTransmitters(catNum) return satelliteDao.getSatTransmitters(catNum)
.map { satTransList -> DataMapper.satTransListToDomainTransList(satTransList) } .map { satTransList -> DataMapper.satTransListToDomainTransList(satTransList) }
} }
override suspend fun updateTransmitters(satelliteTrans: List<SatTrans>) { override suspend fun updateTransmitters(satelliteTrans: List<SatTrans>) {
val satTransList = DataMapper.domainTransListToSatTransList(satelliteTrans) val satTransList = DataMapper.domainTransListToSatTransList(satelliteTrans)
satDataDao.updateTransmitters(satTransList) satelliteDao.updateTransmitters(satTransList)
} }
} }
@@ -21,11 +21,11 @@ import androidx.room.*
import com.rtbishop.look4sat.framework.model.SatEntry import com.rtbishop.look4sat.framework.model.SatEntry
import com.rtbishop.look4sat.framework.model.SatItem import com.rtbishop.look4sat.framework.model.SatItem
import com.rtbishop.look4sat.framework.model.SatTrans import com.rtbishop.look4sat.framework.model.SatTrans
import com.rtbishop.look4sat.domain.predict4kotlin.Satellite import com.rtbishop.look4sat.domain.Satellite
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface SatDataDao { interface SatelliteDao {
@Transaction @Transaction
@Query("SELECT catNum, name, isSelected FROM entries ORDER BY name ASC") @Query("SELECT catNum, name, isSelected FROM entries ORDER BY name ASC")
@@ -24,8 +24,8 @@ import com.rtbishop.look4sat.framework.model.SatEntry
import com.rtbishop.look4sat.framework.model.SatTrans import com.rtbishop.look4sat.framework.model.SatTrans
@Database(entities = [SatEntry::class, SatTrans::class], version = 1, exportSchema = false) @Database(entities = [SatEntry::class, SatTrans::class], version = 1, exportSchema = false)
@TypeConverters(RoomConverters::class) @TypeConverters(Converters::class)
abstract class SatDataDb : RoomDatabase() { abstract class SatelliteDb : RoomDatabase() {
abstract fun satelliteDao(): SatDataDao abstract fun satelliteDao(): SatelliteDao
} }
@@ -19,7 +19,7 @@ package com.rtbishop.look4sat.framework.model
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.rtbishop.look4sat.domain.predict4kotlin.TLE import com.rtbishop.look4sat.domain.TLE
@Entity(tableName = "entries") @Entity(tableName = "entries")
data class SatEntry( data class SatEntry(
@@ -22,14 +22,13 @@ import android.content.SharedPreferences
import android.location.LocationManager import android.location.LocationManager
import androidx.room.Room import androidx.room.Room
import com.rtbishop.look4sat.data.* import com.rtbishop.look4sat.data.*
import com.rtbishop.look4sat.domain.predict4kotlin.QthConverter
import com.rtbishop.look4sat.framework.PreferencesProvider import com.rtbishop.look4sat.framework.PreferencesProvider
import com.rtbishop.look4sat.framework.api.SatDataRemote import com.rtbishop.look4sat.framework.api.RemoteSource
import com.rtbishop.look4sat.framework.api.SatDataService import com.rtbishop.look4sat.framework.api.SatelliteService
import com.rtbishop.look4sat.framework.db.RoomConverters import com.rtbishop.look4sat.framework.db.Converters
import com.rtbishop.look4sat.framework.db.SatDataDao import com.rtbishop.look4sat.framework.db.LocalSource
import com.rtbishop.look4sat.framework.db.SatDataDb import com.rtbishop.look4sat.framework.db.SatelliteDao
import com.rtbishop.look4sat.framework.db.SatDataLocal import com.rtbishop.look4sat.framework.db.SatelliteDb
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@@ -45,12 +44,7 @@ import javax.net.ssl.HostnameVerifier
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
object SatDataModule { object SatelliteDataModule {
@Provides
fun provideQthConverter(): QthConverter {
return QthConverter()
}
@Provides @Provides
fun provideMoshi(): Moshi { fun provideMoshi(): Moshi {
@@ -61,56 +55,52 @@ object SatDataModule {
@Singleton @Singleton
fun providePreferenceSource( fun providePreferenceSource(
moshi: Moshi, moshi: Moshi,
qthConverter: QthConverter,
locationManager: LocationManager, locationManager: LocationManager,
preferences: SharedPreferences preferences: SharedPreferences
): PreferencesSource { ): PreferencesSource {
return PreferencesProvider(moshi, qthConverter, locationManager, preferences) return PreferencesProvider(moshi, locationManager, preferences)
} }
@Provides @Provides
@Singleton @Singleton
fun providePassesRepo( fun providePassPredictor(@DefaultDispatcher dispatcher: CoroutineDispatcher): PassPredictor {
preferencesSource: PreferencesSource, return PassPredictor(dispatcher)
@DefaultDispatcher defaultDispatcher: CoroutineDispatcher
): SatPassRepository {
return SatPassRepository(preferencesSource, defaultDispatcher)
} }
@Provides @Provides
@Singleton @Singleton
fun provideSatelliteRepo( fun provideSatelliteRepo(
preferencesSource: PreferencesSource, preferencesSource: PreferencesSource,
satLocalSource: SatDataLocalSource, localDataSource: LocalDataSource,
satRemoteSource: SatDataRemoteSource, remoteDataSource: RemoteDataSource,
@IoDispatcher ioDispatcher: CoroutineDispatcher @IoDispatcher dispatcher: CoroutineDispatcher
): SatDataRepository { ): SatelliteRepo {
return SatDataRepository(preferencesSource, satLocalSource, satRemoteSource, ioDispatcher) return SatelliteRepo(preferencesSource, localDataSource, remoteDataSource, dispatcher)
} }
@Provides @Provides
fun provideLocalDataSource(satDataDao: SatDataDao): SatDataLocalSource { fun provideLocalDataSource(satelliteDao: SatelliteDao): LocalDataSource {
return SatDataLocal(satDataDao) return LocalSource(satelliteDao)
} }
@Provides @Provides
fun provideSatDataDao(db: SatDataDb): SatDataDao { fun provideSatelliteDao(db: SatelliteDb): SatelliteDao {
return db.satelliteDao() return db.satelliteDao()
} }
@Provides @Provides
fun provideSatelliteDb(@ApplicationContext context: Context, moshi: Moshi): SatDataDb { fun provideSatelliteDb(@ApplicationContext context: Context, moshi: Moshi): SatelliteDb {
RoomConverters.initialize(moshi) Converters.initialize(moshi)
return Room.databaseBuilder(context, SatDataDb::class.java, "SatelliteDb").build() return Room.databaseBuilder(context, SatelliteDb::class.java, "SatelliteDb").build()
} }
@Provides @Provides
fun provideRemoteDataSource(satDataService: SatDataService): SatDataRemoteSource { fun provideRemoteDataSource(satelliteService: SatelliteService): RemoteDataSource {
return SatDataRemote(satDataService) return RemoteSource(satelliteService)
} }
@Provides @Provides
fun provideSatDataService(): SatDataService { fun provideSatelliteService(): SatelliteService {
val verifier = HostnameVerifier { _, _ -> true } val verifier = HostnameVerifier { _, _ -> true }
val httpClient = OkHttpClient.Builder().hostnameVerifier(verifier).build() val httpClient = OkHttpClient.Builder().hostnameVerifier(verifier).build()
return Retrofit.Builder() return Retrofit.Builder()
@@ -118,6 +108,6 @@ object SatDataModule {
.client(httpClient) .client(httpClient)
.addConverterFactory(MoshiConverterFactory.create()) .addConverterFactory(MoshiConverterFactory.create())
.build() .build()
.create(SatDataService::class.java) .create(SatelliteService::class.java)
} }
} }
@@ -23,7 +23,7 @@ import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.rtbishop.look4sat.databinding.SatItemBinding import com.rtbishop.look4sat.databinding.SatItemBinding
import com.rtbishop.look4sat.domain.model.SatItem import com.rtbishop.look4sat.domain.SatItem
class SatItemAdapter : RecyclerView.Adapter<SatItemAdapter.SatItemHolder>() { class SatItemAdapter : RecyclerView.Adapter<SatItemAdapter.SatItemHolder>() {
@@ -28,7 +28,7 @@ import androidx.recyclerview.widget.SimpleItemAnimator
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.databinding.FragmentEntriesBinding import com.rtbishop.look4sat.databinding.FragmentEntriesBinding
import com.rtbishop.look4sat.domain.model.SatItem import com.rtbishop.look4sat.domain.SatItem
import com.rtbishop.look4sat.framework.model.Result import com.rtbishop.look4sat.framework.model.Result
import com.rtbishop.look4sat.utility.RecyclerDivider import com.rtbishop.look4sat.utility.RecyclerDivider
import com.rtbishop.look4sat.utility.getNavResult import com.rtbishop.look4sat.utility.getNavResult
@@ -22,8 +22,8 @@ import android.net.Uri
import android.widget.SearchView import android.widget.SearchView
import androidx.lifecycle.* import androidx.lifecycle.*
import com.rtbishop.look4sat.data.PreferencesSource import com.rtbishop.look4sat.data.PreferencesSource
import com.rtbishop.look4sat.data.SatDataRepository import com.rtbishop.look4sat.data.SatelliteRepo
import com.rtbishop.look4sat.domain.model.SatItem import com.rtbishop.look4sat.domain.SatItem
import com.rtbishop.look4sat.framework.model.Result import com.rtbishop.look4sat.framework.model.Result
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
@@ -35,13 +35,13 @@ import javax.inject.Inject
class SatItemViewModel @Inject constructor( class SatItemViewModel @Inject constructor(
private val preferencesSource: PreferencesSource, private val preferencesSource: PreferencesSource,
private val resolver: ContentResolver, private val resolver: ContentResolver,
private val satDataRepository: SatDataRepository, private val satelliteRepo: SatelliteRepo,
) : ViewModel(), SatItemAdapter.EntriesClickListener, SearchView.OnQueryTextListener { ) : ViewModel(), SatItemAdapter.EntriesClickListener, SearchView.OnQueryTextListener {
private val transModes = MutableLiveData(satDataRepository.loadSelectedModes()) private val transModes = MutableLiveData(satelliteRepo.loadSelectedModes())
private val currentQuery = MutableLiveData(String()) private val currentQuery = MutableLiveData(String())
private val itemsWithModes = transModes.switchMap { modes -> private val itemsWithModes = transModes.switchMap { modes ->
liveData { satDataRepository.getSatItems().collect { emit(filterByModes(it, modes)) } } liveData { satelliteRepo.getSatItems().collect { emit(filterByModes(it, modes)) } }
} }
private val itemsWithQuery = currentQuery.switchMap { query -> private val itemsWithQuery = currentQuery.switchMap { query ->
itemsWithModes.map { items -> Result.Success(filterByQuery(items, query)) } itemsWithModes.map { items -> Result.Success(filterByQuery(items, query)) }
@@ -57,7 +57,7 @@ class SatItemViewModel @Inject constructor(
_satData.value = Result.InProgress _satData.value = Result.InProgress
runCatching { runCatching {
resolver.openInputStream(uri)?.use { stream -> resolver.openInputStream(uri)?.use { stream ->
satDataRepository.updateEntriesFromFile(stream) satelliteRepo.updateEntriesFromFile(stream)
} }
}.onFailure { _satData.value = Result.Error(it) } }.onFailure { _satData.value = Result.Error(it) }
} }
@@ -68,7 +68,7 @@ class SatItemViewModel @Inject constructor(
_satData.value = Result.InProgress _satData.value = Result.InProgress
try { try {
preferencesSource.saveTleSources(sources) preferencesSource.saveTleSources(sources)
satDataRepository.updateEntriesFromWeb(sources) satelliteRepo.updateEntriesFromWeb(sources)
} catch (exception: Exception) { } catch (exception: Exception) {
_satData.value = Result.Error(exception) _satData.value = Result.Error(exception)
} }
@@ -84,12 +84,12 @@ class SatItemViewModel @Inject constructor(
} }
fun loadSelectedModes(): List<String> { fun loadSelectedModes(): List<String> {
return satDataRepository.loadSelectedModes() return satelliteRepo.loadSelectedModes()
} }
fun saveSelectedModes(modes: List<String>) { fun saveSelectedModes(modes: List<String>) {
transModes.value = modes transModes.value = modes
satDataRepository.saveSelectedModes(modes) satelliteRepo.saveSelectedModes(modes)
} }
override fun onQueryTextSubmit(query: String): Boolean { override fun onQueryTextSubmit(query: String): Boolean {
@@ -102,7 +102,7 @@ class SatItemViewModel @Inject constructor(
} }
override fun updateSelection(catNums: List<Int>, isSelected: Boolean) { override fun updateSelection(catNums: List<Int>, isSelected: Boolean) {
viewModelScope.launch { satDataRepository.updateEntriesSelection(catNums, isSelected) } viewModelScope.launch { satelliteRepo.updateEntriesSelection(catNums, isSelected) }
} }
private fun filterByModes(items: List<SatItem>, modes: List<String>): List<SatItem> { private fun filterByModes(items: List<SatItem>, modes: List<String>): List<SatItem> {
@@ -29,9 +29,9 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.databinding.FragmentMapBinding import com.rtbishop.look4sat.databinding.FragmentMapBinding
import com.rtbishop.look4sat.domain.predict4kotlin.GeoPos import com.rtbishop.look4sat.domain.GeoPos
import com.rtbishop.look4sat.domain.predict4kotlin.Satellite import com.rtbishop.look4sat.domain.Satellite
import com.rtbishop.look4sat.framework.model.SatData import com.rtbishop.look4sat.domain.SatData
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.osmdroid.config.Configuration import org.osmdroid.config.Configuration
import org.osmdroid.tileprovider.tilesource.TileSourceFactory import org.osmdroid.tileprovider.tilesource.TileSourceFactory
@@ -89,7 +89,7 @@ class SatMapFragment : Fragment(R.layout.fragment_map) {
} }
private fun setupObservers(binding: FragmentMapBinding) { private fun setupObservers(binding: FragmentMapBinding) {
viewModel.stationPos.observe(viewLifecycleOwner, { renderStationPos(it, binding) }) viewModel.stationPosLiveData.observe(viewLifecycleOwner, { renderStationPos(it, binding) })
viewModel.satPositions.observe(viewLifecycleOwner, { renderSatPositions(it, binding) }) viewModel.satPositions.observe(viewLifecycleOwner, { renderSatPositions(it, binding) })
viewModel.satTrack.observe(viewLifecycleOwner, { renderSatTrack(it, binding) }) viewModel.satTrack.observe(viewLifecycleOwner, { renderSatTrack(it, binding) })
viewModel.satFootprint.observe(viewLifecycleOwner, { renderSatFootprint(it, binding) }) viewModel.satFootprint.observe(viewLifecycleOwner, { renderSatFootprint(it, binding) })
@@ -19,11 +19,12 @@ package com.rtbishop.look4sat.presentation.satMapScreen
import androidx.lifecycle.* import androidx.lifecycle.*
import com.rtbishop.look4sat.data.PreferencesSource import com.rtbishop.look4sat.data.PreferencesSource
import com.rtbishop.look4sat.data.SatDataRepository import com.rtbishop.look4sat.data.SatelliteRepo
import com.rtbishop.look4sat.domain.predict4kotlin.GeoPos import com.rtbishop.look4sat.data.PassPredictor
import com.rtbishop.look4sat.domain.predict4kotlin.Satellite import com.rtbishop.look4sat.domain.GeoPos
import com.rtbishop.look4sat.domain.predict4kotlin.StationPos import com.rtbishop.look4sat.domain.Satellite
import com.rtbishop.look4sat.framework.model.SatData import com.rtbishop.look4sat.domain.StationPos
import com.rtbishop.look4sat.domain.SatData
import com.rtbishop.look4sat.injection.DefaultDispatcher import com.rtbishop.look4sat.injection.DefaultDispatcher
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.* import kotlinx.coroutines.*
@@ -35,19 +36,20 @@ import kotlin.math.sqrt
@HiltViewModel @HiltViewModel
class SatMapViewModel @Inject constructor( class SatMapViewModel @Inject constructor(
private val satDataRepository: SatDataRepository, private val satelliteRepo: SatelliteRepo,
private val preferencesSource: PreferencesSource, private val passPredictor: PassPredictor,
private val preferences: PreferencesSource,
@DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher
) : ViewModel() { ) : ViewModel() {
private val gsp = preferencesSource.loadStationPosition() private val stationPos = preferences.loadStationPosition()
private var dataUpdateJob: Job? = null private var dataUpdateJob: Job? = null
private var allSatList = listOf<Satellite>() private var allSatList = listOf<Satellite>()
private lateinit var selectedSat: Satellite private lateinit var selectedSat: Satellite
val stationPos = liveData { val stationPosLiveData = liveData {
val osmLat = clipLat(gsp.latitude) val osmLat = clipLat(stationPos.latitude)
val osmLon = clipLon(gsp.longitude) val osmLon = clipLon(stationPos.longitude)
emit(GeoPos(osmLat, osmLon)) emit(GeoPos(osmLat, osmLon))
} }
@@ -65,7 +67,7 @@ class SatMapViewModel @Inject constructor(
init { init {
viewModelScope.launch { viewModelScope.launch {
satDataRepository.getSelectedSatellites().also { selectedSatellites -> satelliteRepo.getSelectedSatellites().also { selectedSatellites ->
if (selectedSatellites.isNotEmpty()) { if (selectedSatellites.isNotEmpty()) {
allSatList = selectedSatellites allSatList = selectedSatellites
selectSatellite(selectedSatellites.first()) selectSatellite(selectedSatellites.first())
@@ -75,7 +77,7 @@ class SatMapViewModel @Inject constructor(
} }
fun shouldUseTextLabels(): Boolean { fun shouldUseTextLabels(): Boolean {
return preferencesSource.shouldUseTextLabels() return preferences.shouldUseTextLabels()
} }
fun scrollSelection(decrement: Boolean) { fun scrollSelection(decrement: Boolean) {
@@ -97,12 +99,12 @@ class SatMapViewModel @Inject constructor(
dataUpdateJob?.cancelAndJoin() dataUpdateJob?.cancelAndJoin()
dataUpdateJob = launch { dataUpdateJob = launch {
val dateNow = Date() val dateNow = Date()
setSelectedSatTrack(selectedSat, gsp, dateNow) setSelectedSatTrack(selectedSat, stationPos, dateNow)
while (isActive) { while (isActive) {
dateNow.time = System.currentTimeMillis() dateNow.time = System.currentTimeMillis()
setSatPositions(allSatList, gsp, dateNow) setSatPositions(allSatList, stationPos, dateNow)
setSelectedSatFootprint(selectedSat, gsp, dateNow) setSelectedSatFootprint(selectedSat, stationPos, dateNow)
setSelectedSatData(selectedSat, gsp, dateNow) setSelectedSatData(selectedSat, stationPos, dateNow)
delay(updateFreq) delay(updateFreq)
} }
} }
@@ -113,7 +115,7 @@ class SatMapViewModel @Inject constructor(
withContext(defaultDispatcher) { withContext(defaultDispatcher) {
val satPositions = mutableMapOf<Satellite, GeoPos>() val satPositions = mutableMapOf<Satellite, GeoPos>()
list.forEach { satellite -> list.forEach { satellite ->
val satPos = satellite.getPredictor(gsp).getSatPos(date) val satPos = satellite.getPosition(gsp, date.time)
val osmLat = clipLat(Math.toDegrees(satPos.latitude)) val osmLat = clipLat(Math.toDegrees(satPos.latitude))
val osmLon = clipLon(Math.toDegrees(satPos.longitude)) val osmLon = clipLon(Math.toDegrees(satPos.longitude))
satPositions[satellite] = GeoPos(osmLat, osmLon) satPositions[satellite] = GeoPos(osmLat, osmLon)
@@ -127,7 +129,7 @@ class SatMapViewModel @Inject constructor(
val satTracks = mutableListOf<List<GeoPos>>() val satTracks = mutableListOf<List<GeoPos>>()
val currentTrack = mutableListOf<GeoPos>() val currentTrack = mutableListOf<GeoPos>()
var oldLongitude = 0.0 var oldLongitude = 0.0
sat.getPredictor(gsp).getPositions(date, 15, 0, 2.4).forEach { satPos -> passPredictor.getPositions(sat, gsp, date, 15, 0, 2.4).forEach { satPos ->
val osmLat = clipLat(Math.toDegrees(satPos.latitude)) val osmLat = clipLat(Math.toDegrees(satPos.latitude))
val osmLon = clipLon(Math.toDegrees(satPos.longitude)) val osmLon = clipLon(Math.toDegrees(satPos.longitude))
val currentPosition = GeoPos(osmLat, osmLon) val currentPosition = GeoPos(osmLat, osmLon)
@@ -154,7 +156,7 @@ class SatMapViewModel @Inject constructor(
private suspend fun setSelectedSatFootprint(sat: Satellite, gsp: StationPos, date: Date) { private suspend fun setSelectedSatFootprint(sat: Satellite, gsp: StationPos, date: Date) {
withContext(defaultDispatcher) { withContext(defaultDispatcher) {
val satFootprint = sat.getPosition(gsp, date).getRangeCircle().map { rangePos -> val satFootprint = sat.getPosition(gsp, date.time).getRangeCircle().map { rangePos ->
val osmLat = clipLat(rangePos.latitude) val osmLat = clipLat(rangePos.latitude)
val osmLon = clipLon(rangePos.longitude) val osmLon = clipLon(rangePos.longitude)
GeoPos(osmLat, osmLon) GeoPos(osmLat, osmLon)
@@ -165,12 +167,11 @@ class SatMapViewModel @Inject constructor(
private suspend fun setSelectedSatData(sat: Satellite, gsp: StationPos, date: Date) { private suspend fun setSelectedSatData(sat: Satellite, gsp: StationPos, date: Date) {
withContext(defaultDispatcher) { withContext(defaultDispatcher) {
val satPos = sat.getPredictor(gsp).getSatPos(date) val satPos = sat.getPosition(gsp, date.time)
val osmLat = clipLat(Math.toDegrees(satPos.latitude)) val osmLat = clipLat(Math.toDegrees(satPos.latitude))
val osmLon = clipLon(Math.toDegrees(satPos.longitude)) val osmLon = clipLon(Math.toDegrees(satPos.longitude))
val osmPos = GeoPos(osmLat, osmLon) val osmPos = GeoPos(osmLat, osmLon)
val qthLoc = val qthLoc = preferences.positionToQTH(osmPos.latitude, osmPos.longitude) ?: "-- --"
preferencesSource.positionToQTH(osmPos.latitude, osmPos.longitude) ?: "-- --"
val velocity = getOrbitalVelocity(satPos.altitude) val velocity = getOrbitalVelocity(satPos.altitude)
val satData = SatData( val satData = SatData(
sat, sat.params.catnum, sat.params.name, satPos.range, sat, sat.params.catnum, sat.params.name, satPos.range,
@@ -27,7 +27,7 @@ import androidx.recyclerview.widget.SimpleItemAnimator
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.data.PreferencesSource import com.rtbishop.look4sat.data.PreferencesSource
import com.rtbishop.look4sat.databinding.FragmentPolarBinding import com.rtbishop.look4sat.databinding.FragmentPolarBinding
import com.rtbishop.look4sat.domain.predict4kotlin.SatPass import com.rtbishop.look4sat.domain.SatPass
import com.rtbishop.look4sat.utility.RecyclerDivider import com.rtbishop.look4sat.utility.RecyclerDivider
import com.rtbishop.look4sat.utility.navigateSafe import com.rtbishop.look4sat.utility.navigateSafe
import com.rtbishop.look4sat.utility.toTimerString import com.rtbishop.look4sat.utility.toTimerString
@@ -39,7 +39,7 @@ import javax.inject.Inject
class PassInfoFragment : Fragment(R.layout.fragment_polar) { class PassInfoFragment : Fragment(R.layout.fragment_polar) {
@Inject @Inject
lateinit var preferencesSource: PreferencesSource lateinit var preferences: PreferencesSource
private val viewModel: PassInfoViewModel by viewModels() private val viewModel: PassInfoViewModel by viewModels()
private var passInfoView: PassInfoView? = null private var passInfoView: PassInfoView? = null
@@ -72,10 +72,12 @@ class PassInfoFragment : Fragment(R.layout.fragment_polar) {
private fun setupObservers(satTransAdapter: SatTransAdapter, binding: FragmentPolarBinding) { private fun setupObservers(satTransAdapter: SatTransAdapter, binding: FragmentPolarBinding) {
val catNum = requireArguments().getInt("catNum") val catNum = requireArguments().getInt("catNum")
val aosTime = requireArguments().getLong("aosTime") val aosTime = requireArguments().getLong("aosTime")
val stationPos = preferences.loadStationPosition()
viewModel.getPass(catNum, aosTime).observe(viewLifecycleOwner) { pass -> viewModel.getPass(catNum, aosTime).observe(viewLifecycleOwner) { pass ->
passInfoView = PassInfoView(requireContext()).apply { passInfoView = PassInfoView(requireContext()).apply {
setShowAim(preferencesSource.shouldUseCompass()) setShowAim(preferences.shouldUseCompass())
setPass(pass) setPass(pass)
setStationPos(stationPos)
} }
binding.frame.addView(passInfoView) binding.frame.addView(passInfoView)
observeTransmitters(pass, satTransAdapter, binding) observeTransmitters(pass, satTransAdapter, binding)
@@ -107,7 +109,8 @@ class PassInfoFragment : Fragment(R.layout.fragment_polar) {
private fun setPassText(satPass: SatPass, binding: FragmentPolarBinding) { private fun setPassText(satPass: SatPass, binding: FragmentPolarBinding) {
val dateNow = Date() val dateNow = Date()
val timeNow = System.currentTimeMillis() val timeNow = System.currentTimeMillis()
val satPos = satPass.predictor.getSatPos(dateNow) val stationPos = preferences.loadStationPosition()
val satPos = satPass.satellite.getPosition(stationPos, dateNow.time)
val polarAz = getString(R.string.pat_azimuth) val polarAz = getString(R.string.pat_azimuth)
val polarEl = getString(R.string.pat_elevation) val polarEl = getString(R.string.pat_elevation)
val polarRng = getString(R.string.pat_distance) val polarRng = getString(R.string.pat_distance)
@@ -22,7 +22,8 @@ import android.graphics.*
import android.view.View import android.view.View
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.predict4kotlin.SatPass import com.rtbishop.look4sat.domain.SatPass
import com.rtbishop.look4sat.domain.StationPos
import java.util.* import java.util.*
import kotlin.math.cos import kotlin.math.cos
import kotlin.math.min import kotlin.math.min
@@ -37,6 +38,7 @@ class PassInfoView(context: Context) : View(context) {
private val radarRadius = radarWidth * 0.48f private val radarRadius = radarWidth * 0.48f
private val piDiv2 = Math.PI / 2.0 private val piDiv2 = Math.PI / 2.0
private val strokeSize = scale * 2f private val strokeSize = scale * 2f
private var stationPos: StationPos = StationPos(0.0, 0.0, 0.0)
private var radarColor = ContextCompat.getColor(context, R.color.greyLight) private var radarColor = ContextCompat.getColor(context, R.color.greyLight)
private var radarCircleNum = 3 private var radarCircleNum = 3
@@ -101,6 +103,10 @@ class PassInfoView(context: Context) : View(context) {
this.satPass = satPass this.satPass = satPass
} }
fun setStationPos(stationPos: StationPos) {
this.stationPos = stationPos
}
fun setShowAim(showAim: Boolean) { fun setShowAim(showAim: Boolean) {
shouldShowAim = showAim shouldShowAim = showAim
} }
@@ -170,7 +176,7 @@ class PassInfoView(context: Context) : View(context) {
} }
private fun drawSatellite(canvas: Canvas, satPass: SatPass) { private fun drawSatellite(canvas: Canvas, satPass: SatPass) {
val satPos = satPass.predictor.getSatPos(Date()) val satPos = satPass.satellite.getPosition(stationPos, Date().time)
if (satPos.elevation > 0) { if (satPos.elevation > 0) {
val satX = sph2CartX(satPos.azimuth, satPos.elevation, radarRadius.toDouble()) val satX = sph2CartX(satPos.azimuth, satPos.elevation, radarRadius.toDouble())
val satY = sph2CartY(satPos.azimuth, satPos.elevation, radarRadius.toDouble()) val satY = sph2CartY(satPos.azimuth, satPos.elevation, radarRadius.toDouble())
@@ -202,7 +208,7 @@ class PassInfoView(context: Context) : View(context) {
private fun createPassTrajectory(satPass: SatPass) { private fun createPassTrajectory(satPass: SatPass) {
var currentTime = satPass.aosTime var currentTime = satPass.aosTime
while (currentTime < satPass.losTime) { while (currentTime < satPass.losTime) {
val satPos = satPass.predictor.getSatPos(Date(currentTime)) val satPos = satPass.satellite.getPosition(stationPos, currentTime)
val passX = sph2CartX(satPos.azimuth, satPos.elevation, radarRadius.toDouble()) val passX = sph2CartX(satPos.azimuth, satPos.elevation, radarRadius.toDouble())
val passY = sph2CartY(satPos.azimuth, satPos.elevation, radarRadius.toDouble()) val passY = sph2CartY(satPos.azimuth, satPos.elevation, radarRadius.toDouble())
if (currentTime == satPass.aosTime) { if (currentTime == satPass.aosTime) {
@@ -18,12 +18,12 @@
package com.rtbishop.look4sat.presentation.satPassInfoScreen package com.rtbishop.look4sat.presentation.satPassInfoScreen
import androidx.lifecycle.* import androidx.lifecycle.*
import com.rtbishop.look4sat.data.SatPassRepository import com.rtbishop.look4sat.data.PassPredictor
import com.rtbishop.look4sat.data.PreferencesSource import com.rtbishop.look4sat.data.PreferencesSource
import com.rtbishop.look4sat.data.SatDataRepository import com.rtbishop.look4sat.data.SatelliteRepo
import com.rtbishop.look4sat.injection.IoDispatcher import com.rtbishop.look4sat.injection.IoDispatcher
import com.rtbishop.look4sat.domain.model.SatTrans import com.rtbishop.look4sat.domain.SatTrans
import com.rtbishop.look4sat.domain.predict4kotlin.SatPass import com.rtbishop.look4sat.domain.SatPass
import com.rtbishop.look4sat.framework.OrientationProvider import com.rtbishop.look4sat.framework.OrientationProvider
import com.rtbishop.look4sat.utility.round import com.rtbishop.look4sat.utility.round
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@@ -37,19 +37,20 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class PassInfoViewModel @Inject constructor( class PassInfoViewModel @Inject constructor(
private val orientationProvider: OrientationProvider, private val orientationProvider: OrientationProvider,
private val preferencesSource: PreferencesSource, private val preferences: PreferencesSource,
private val satPassRepository: SatPassRepository, private val passPredictor: PassPredictor,
private val satDataRepository: SatDataRepository, private val satelliteRepo: SatelliteRepo,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher @IoDispatcher private val ioDispatcher: CoroutineDispatcher
) : ViewModel(), OrientationProvider.OrientationListener { ) : ViewModel(), OrientationProvider.OrientationListener {
private val stationPos = preferences.loadStationPosition()
private val _transmitters = MutableLiveData<List<SatTrans>>() private val _transmitters = MutableLiveData<List<SatTrans>>()
private val _orientation = MutableLiveData<Triple<Float, Float, Float>>() private val _orientation = MutableLiveData<Triple<Float, Float, Float>>()
val transmitters: LiveData<List<SatTrans>> = _transmitters val transmitters: LiveData<List<SatTrans>> = _transmitters
val orientation: LiveData<Triple<Float, Float, Float>> = _orientation val orientation: LiveData<Triple<Float, Float, Float>> = _orientation
fun getPass(catNum: Int, aosTime: Long) = liveData { fun getPass(catNum: Int, aosTime: Long) = liveData {
satPassRepository.passes.collect { passes -> passPredictor.passes.collect { passes ->
val pass = passes.find { it.catNum == catNum && it.aosTime == aosTime } val pass = passes.find { it.catNum == catNum && it.aosTime == aosTime }
pass?.let { satPass -> pass?.let { satPass ->
processTransmitters(satPass) processTransmitters(satPass)
@@ -60,27 +61,27 @@ class PassInfoViewModel @Inject constructor(
} }
fun enableSensor() { fun enableSensor() {
if (preferencesSource.shouldUseCompass()) orientationProvider.startListening(this) if (preferences.shouldUseCompass()) orientationProvider.startListening(this)
} }
fun disableSensor() { fun disableSensor() {
if (preferencesSource.shouldUseCompass()) orientationProvider.stopListening() if (preferences.shouldUseCompass()) orientationProvider.stopListening()
} }
override fun onOrientationChanged(azimuth: Float, pitch: Float, roll: Float) { override fun onOrientationChanged(azimuth: Float, pitch: Float, roll: Float) {
_orientation.value = Triple(azimuth + preferencesSource.getMagDeclination(), pitch, roll) _orientation.value = Triple(azimuth + preferences.getMagDeclination(), pitch, roll)
} }
private fun initRotatorControl(satPass: SatPass) { private fun initRotatorControl(satPass: SatPass) {
viewModelScope.launch { viewModelScope.launch {
val rotatorPrefs = preferencesSource.getRotatorServer() val rotatorPrefs = preferences.getRotatorServer()
if (rotatorPrefs != null) { if (rotatorPrefs != null) {
runCatching { runCatching {
withContext(ioDispatcher) { withContext(ioDispatcher) {
val socket = Socket(rotatorPrefs.first, rotatorPrefs.second) val socket = Socket(rotatorPrefs.first, rotatorPrefs.second)
val writer = socket.getOutputStream().bufferedWriter() val writer = socket.getOutputStream().bufferedWriter()
while (isActive) { while (isActive) {
val satPos = satPass.predictor.getSatPos(Date()) val satPos = satPass.satellite.getPosition(stationPos, Date().time)
val azimuth = Math.toDegrees(satPos.azimuth).round(1) val azimuth = Math.toDegrees(satPos.azimuth).round(1)
val elevation = Math.toDegrees(satPos.elevation).round(1) val elevation = Math.toDegrees(satPos.elevation).round(1)
writer.write("\\set_pos $azimuth $elevation") writer.write("\\set_pos $azimuth $elevation")
@@ -97,16 +98,16 @@ class PassInfoViewModel @Inject constructor(
private fun processTransmitters(satPass: SatPass) { private fun processTransmitters(satPass: SatPass) {
viewModelScope.launch { viewModelScope.launch {
satDataRepository.getSatTransmitters(satPass.catNum).collect { transList -> satelliteRepo.getSatTransmitters(satPass.catNum).collect { transList ->
while (isActive) { while (isActive) {
val timeNow = Date() val satPos = satPass.satellite.getPosition(stationPos, Date().time)
val copiedList = transList.map { it.copy() } val copiedList = transList.map { it.copy() }
copiedList.forEach { transmitter -> copiedList.forEach { transmitter ->
transmitter.downlink?.let { transmitter.downlink?.let {
transmitter.downlink = satPass.predictor.getDownlinkFreq(it, timeNow) transmitter.downlink = satPos.getDownlinkFreq(it)
} }
transmitter.uplink?.let { transmitter.uplink?.let {
transmitter.uplink = satPass.predictor.getUplinkFreq(it, timeNow) transmitter.uplink = satPos.getUplinkFreq(it)
} }
} }
_transmitters.postValue(copiedList.map { it.copy() }) _transmitters.postValue(copiedList.map { it.copy() })
@@ -115,4 +116,4 @@ class PassInfoViewModel @Inject constructor(
} }
} }
} }
} }
@@ -24,7 +24,7 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.databinding.ItemTransBinding import com.rtbishop.look4sat.databinding.ItemTransBinding
import com.rtbishop.look4sat.domain.model.SatTrans import com.rtbishop.look4sat.domain.SatTrans
import java.util.* import java.util.*
class SatTransAdapter : RecyclerView.Adapter<SatTransAdapter.TransHolder>() { class SatTransAdapter : RecyclerView.Adapter<SatTransAdapter.TransHolder>() {
@@ -25,7 +25,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.databinding.ItemPassGeoBinding import com.rtbishop.look4sat.databinding.ItemPassGeoBinding
import com.rtbishop.look4sat.databinding.ItemPassLeoBinding import com.rtbishop.look4sat.databinding.ItemPassLeoBinding
import com.rtbishop.look4sat.domain.predict4kotlin.SatPass import com.rtbishop.look4sat.domain.SatPass
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@@ -29,7 +29,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.SimpleItemAnimator import androidx.recyclerview.widget.SimpleItemAnimator
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.databinding.FragmentPassesBinding import com.rtbishop.look4sat.databinding.FragmentPassesBinding
import com.rtbishop.look4sat.domain.predict4kotlin.SatPass import com.rtbishop.look4sat.domain.SatPass
import com.rtbishop.look4sat.framework.model.Result import com.rtbishop.look4sat.framework.model.Result
import com.rtbishop.look4sat.utility.RecyclerDivider import com.rtbishop.look4sat.utility.RecyclerDivider
import com.rtbishop.look4sat.utility.navigateSafe import com.rtbishop.look4sat.utility.navigateSafe
@@ -22,9 +22,9 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.rtbishop.look4sat.data.PreferencesSource import com.rtbishop.look4sat.data.PreferencesSource
import com.rtbishop.look4sat.data.SatDataRepository import com.rtbishop.look4sat.data.SatelliteRepo
import com.rtbishop.look4sat.data.SatPassRepository import com.rtbishop.look4sat.data.PassPredictor
import com.rtbishop.look4sat.domain.predict4kotlin.SatPass import com.rtbishop.look4sat.domain.SatPass
import com.rtbishop.look4sat.framework.model.Result import com.rtbishop.look4sat.framework.model.Result
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.* import kotlinx.coroutines.*
@@ -33,8 +33,8 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class SatPassViewModel @Inject constructor( class SatPassViewModel @Inject constructor(
private val satDataRepository: SatDataRepository, private val satelliteRepo: SatelliteRepo,
private val satPassRepository: SatPassRepository, private val passPredictor: PassPredictor,
private val preferencesSource: PreferencesSource private val preferencesSource: PreferencesSource
) : ViewModel() { ) : ViewModel() {
@@ -48,13 +48,14 @@ class SatPassViewModel @Inject constructor(
if (preferencesSource.isSetupDone()) { if (preferencesSource.isSetupDone()) {
viewModelScope.launch { viewModelScope.launch {
_passes.postValue(Result.InProgress) _passes.postValue(Result.InProgress)
satPassRepository.triggerCalculation(satDataRepository.getSelectedSatellites()) val stationPos = preferencesSource.loadStationPosition()
passPredictor.triggerCalculation(satelliteRepo.getSelectedSatellites(), stationPos)
} }
} else { } else {
_isFirstLaunchDone.value = false _isFirstLaunchDone.value = false
} }
viewModelScope.launch { viewModelScope.launch {
satPassRepository.passes.collect { passes -> passPredictor.passes.collect { passes ->
passesProcessing?.cancelAndJoin() passesProcessing?.cancelAndJoin()
passesProcessing = viewModelScope.launch { tickPasses(passes) } passesProcessing = viewModelScope.launch { tickPasses(passes) }
} }
@@ -66,9 +67,10 @@ class SatPassViewModel @Inject constructor(
viewModelScope.launch { viewModelScope.launch {
_passes.postValue(Result.InProgress) _passes.postValue(Result.InProgress)
val defaultCatNums = listOf(43700, 25544, 25338, 28654, 33591, 40069, 27607, 24278) val defaultCatNums = listOf(43700, 25544, 25338, 28654, 33591, 40069, 27607, 24278)
satDataRepository.updateEntriesFromWeb(preferencesSource.loadDefaultSources()) val stationPos = preferencesSource.loadStationPosition()
satDataRepository.updateEntriesSelection(defaultCatNums, true) satelliteRepo.updateEntriesFromWeb(preferencesSource.loadDefaultSources())
satPassRepository.forceCalculation(satDataRepository.getSelectedSatellites()) satelliteRepo.updateEntriesSelection(defaultCatNums, true)
passPredictor.forceCalculation(satelliteRepo.getSelectedSatellites(), stationPos)
preferencesSource.setSetupDone() preferencesSource.setSetupDone()
_isFirstLaunchDone.value = true _isFirstLaunchDone.value = true
} }
@@ -78,7 +80,8 @@ class SatPassViewModel @Inject constructor(
viewModelScope.launch { viewModelScope.launch {
_passes.postValue(Result.InProgress) _passes.postValue(Result.InProgress)
passesProcessing?.cancelAndJoin() passesProcessing?.cancelAndJoin()
satPassRepository.forceCalculation(satDataRepository.getSelectedSatellites()) val stationPos = preferencesSource.loadStationPosition()
passPredictor.forceCalculation(satelliteRepo.getSelectedSatellites(), stationPos)
} }
} }
@@ -20,9 +20,9 @@ package com.rtbishop.look4sat.utility
import com.rtbishop.look4sat.framework.model.SatEntry import com.rtbishop.look4sat.framework.model.SatEntry
import com.rtbishop.look4sat.framework.model.SatItem import com.rtbishop.look4sat.framework.model.SatItem
import com.rtbishop.look4sat.framework.model.SatTrans import com.rtbishop.look4sat.framework.model.SatTrans
import com.rtbishop.look4sat.domain.model.SatEntry as DomainEntry import com.rtbishop.look4sat.domain.SatEntry as DomainEntry
import com.rtbishop.look4sat.domain.model.SatItem as DomainItem import com.rtbishop.look4sat.domain.SatItem as DomainItem
import com.rtbishop.look4sat.domain.model.SatTrans as DomainTrans import com.rtbishop.look4sat.domain.SatTrans as DomainTrans
object DataMapper { object DataMapper {
@@ -17,13 +17,13 @@
*/ */
package com.rtbishop.look4sat.data package com.rtbishop.look4sat.data
import com.rtbishop.look4sat.domain.model.SatEntry import com.rtbishop.look4sat.domain.SatEntry
import com.rtbishop.look4sat.domain.model.SatItem import com.rtbishop.look4sat.domain.SatItem
import com.rtbishop.look4sat.domain.model.SatTrans import com.rtbishop.look4sat.domain.SatTrans
import com.rtbishop.look4sat.domain.predict4kotlin.Satellite import com.rtbishop.look4sat.domain.Satellite
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface SatDataLocalSource { interface LocalDataSource {
fun getSatItems(): Flow<List<SatItem>> fun getSatItems(): Flow<List<SatItem>>
@@ -0,0 +1,388 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.data
import com.rtbishop.look4sat.domain.*
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.withContext
import java.util.*
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
import kotlin.math.sqrt
class PassPredictor(private val defaultDispatcher: CoroutineDispatcher) {
// Radar view - beacon
private val _skyPosition = MutableSharedFlow<SatPos>(replay = 1)
val skyPosition: SharedFlow<SatPos> = _skyPosition
suspend fun getSkyPosition(pass: SatPass, stationPos: StationPos, date: Date) {
withContext(defaultDispatcher) {
_skyPosition.emit(pass.satellite.getPosition(stationPos, date.time))
}
}
// Radar view - track
private val _skyTrack = MutableSharedFlow<List<SatPos>>(replay = 1)
val skyTrack: SharedFlow<List<SatPos>> = _skyTrack
suspend fun getSkyTrack(pass: SatPass, stationPos: StationPos) {
withContext(defaultDispatcher) {
val positions = mutableListOf<SatPos>()
var currentTime = pass.aosTime
while (currentTime < pass.losTime) {
positions.add(pass.satellite.getPosition(stationPos, currentTime))
currentTime += 15000
}
_skyTrack.emit(positions)
}
}
// Map view - beacons
private val _mapPositions = MutableSharedFlow<Map<Satellite, GeoPos>>(replay = 1)
val mapPositions: SharedFlow<Map<Satellite, GeoPos>> = _mapPositions
suspend fun getMapPositions(satellites: List<Satellite>, stationPos: StationPos, date: Date) {
withContext(defaultDispatcher) {
val positions = mutableMapOf<Satellite, GeoPos>()
satellites.forEach { satellite ->
val satPos = satellite.getPosition(stationPos, date.time)
val osmLat = clipLat(Math.toDegrees(satPos.latitude))
val osmLon = clipLon(Math.toDegrees(satPos.longitude))
positions[satellite] = GeoPos(osmLat, osmLon)
}
_mapPositions.emit(positions)
}
}
// Map view - track
private val _mapTrack = MutableSharedFlow<List<List<GeoPos>>>(replay = 1)
val mapTrack: SharedFlow<List<List<GeoPos>>> = _mapTrack
suspend fun getMapTrack(satellite: Satellite, stationPos: StationPos, date: Date) {
withContext(defaultDispatcher) {
val positions = mutableListOf<SatPos>()
val orbitalPeriod = satellite.getQuarterOrbitMin() * 4
val endTime = date.time + (orbitalPeriod * 2.4 * 60L * 1000L).toLong()
var currentTime = date.time
while (currentTime < endTime) {
positions.add(satellite.getPosition(stationPos, currentTime))
currentTime += 15000
}
val satTracks = mutableListOf<List<GeoPos>>()
val currentTrack = mutableListOf<GeoPos>()
var oldLongitude = 0.0
positions.forEach { satPos ->
val osmLat = clipLat(Math.toDegrees(satPos.latitude))
val osmLon = clipLon(Math.toDegrees(satPos.longitude))
val currentPos = GeoPos(osmLat, osmLon)
if (oldLongitude < -170.0 && currentPos.longitude > 170.0) {
// adding left terminal position
currentTrack.add(GeoPos(osmLat, -180.0))
satTracks.add(mutableListOf<GeoPos>().apply { addAll(currentTrack) })
currentTrack.clear()
} else if (oldLongitude > 170.0 && currentPos.longitude < -170.0) {
// adding right terminal position
currentTrack.add(GeoPos(osmLat, 180.0))
satTracks.add(mutableListOf<GeoPos>().apply { addAll(currentTrack) })
currentTrack.clear()
}
oldLongitude = currentPos.longitude
currentTrack.add(currentPos)
}
satTracks.add(currentTrack)
_mapTrack.emit(satTracks)
}
}
private val _mapFootprint = MutableSharedFlow<List<GeoPos>>(replay = 1)
val mapFootprint: SharedFlow<List<GeoPos>> = _mapFootprint
suspend fun setSelectedSatFootprint(satellite: Satellite, stationPos: StationPos, date: Date) {
withContext(defaultDispatcher) {
val satPos = satellite.getPosition(stationPos, date.time)
val satFootprint = satPos.getRangeCircle().map { groundPos ->
val osmLat = clipLat(groundPos.latitude)
val osmLon = clipLon(groundPos.longitude)
GeoPos(osmLat, osmLon)
}
_mapFootprint.emit(satFootprint)
}
}
private val _satData = MutableSharedFlow<SatData>(replay = 1)
val satData: SharedFlow<SatData> = _satData
suspend fun setSelectedSatData(satellite: Satellite, stationPos: StationPos, date: Date) {
withContext(defaultDispatcher) {
val satPos = satellite.getPosition(stationPos, date.time)
val osmLat = clipLat(Math.toDegrees(satPos.latitude))
val osmLon = clipLon(Math.toDegrees(satPos.longitude))
val osmPos = GeoPos(osmLat, osmLon)
val qthLoc = QthConverter.positionToQTH(osmPos.latitude, osmPos.longitude) ?: "-- --"
val velocity = getOrbitalVelocity(satPos.altitude)
val satData = SatData(
satellite, satellite.params.catnum, satellite.params.name, satPos.range,
satPos.altitude, velocity, qthLoc, osmPos
)
_satData.emit(satData)
}
}
private fun getOrbitalVelocity(altitude: Double): Double {
val earthG = 6.674 * 10.0.pow(-11)
val earthM = 5.98 * 10.0.pow(24)
val radius = 6.37 * 10.0.pow(6) + altitude * 10.0.pow(3)
return sqrt(earthG * earthM / radius) / 1000
}
private fun clipLat(pLatitude: Double): Double {
return clip(pLatitude, -85.05, 85.05)
}
private fun clipLon(pLongitude: Double): Double {
var result = pLongitude
while (result < -180.0) result += 360.0
while (result > 180.0) result -= 360.0
return clip(result, -180.0, 180.0)
}
private fun clip(currentValue: Double, minValue: Double, maxValue: Double): Double {
return min(max(currentValue, minValue), maxValue)
}
fun getPositions(
satellite: Satellite,
stationPos: StationPos,
refDate: Date,
stepSec: Int,
minBefore: Int,
orbits: Double
): List<SatPos> {
val positions = mutableListOf<SatPos>()
val orbitalPeriod = 24 * 60 / satellite.params.meanmo
val endDate = Date(refDate.time + (orbitalPeriod * orbits * 60L * 1000L).toLong())
val startDate = Date(refDate.time - minBefore * 60L * 1000L)
var currentDate = startDate
while (currentDate.before(endDate)) {
val satPos = satellite.getPosition(stationPos, currentDate.time)
positions.add(satPos)
currentDate = Date(currentDate.time + stepSec * 1000)
}
return positions
}
// List view - passes
private val _passes = MutableSharedFlow<List<SatPass>>(replay = 1)
private var selectedSatellites = emptyList<Satellite>()
val passes: SharedFlow<List<SatPass>> = _passes
suspend fun triggerCalculation(
satellites: List<Satellite>,
stationPos: StationPos,
refDate: Date = Date(),
hoursAhead: Int = 8
) {
if (satellites.isEmpty()) {
_passes.emit(emptyList())
} else {
val oldCatNums = selectedSatellites.map { it.params.catnum }
val newCatNums = satellites.map { it.params.catnum }
if (oldCatNums != newCatNums) forceCalculation(
satellites,
stationPos,
refDate,
hoursAhead
)
}
}
suspend fun forceCalculation(
satellites: List<Satellite>,
stationPos: StationPos,
refDate: Date = Date(),
hoursAhead: Int = 8
) {
if (satellites.isEmpty()) {
_passes.emit(emptyList())
} else {
withContext(defaultDispatcher) {
val allPasses = mutableListOf<SatPass>()
selectedSatellites = satellites
satellites.forEach { satellite ->
val passes = getPasses(satellite, stationPos, refDate, hoursAhead)
allPasses.addAll(passes)
}
_passes.emit(filterPasses(allPasses, refDate))
}
}
}
private fun getPasses(
satellite: Satellite,
stationPos: StationPos,
date: Date,
hoursAhead: Int
): List<SatPass> {
val passes = mutableListOf<SatPass>()
val endDate = Date(date.time + hoursAhead * 60L * 60L * 1000L)
val quarterOrbitMin = satellite.getQuarterOrbitMin()
var startDate = date
var shouldWindBack = true
var lastAosDate: Date
var count = 0
if (satellite.willBeSeen(stationPos)) {
if (satellite.params.isDeepspace) {
passes.add(nextDeepSpacePass(satellite, stationPos, date))
} else {
do {
if (count > 0) shouldWindBack = false
val pass = nextNearEarthPass(satellite, stationPos, startDate, shouldWindBack)
lastAosDate = Date(pass.aosTime)
passes.add(pass)
startDate = Date(pass.losTime + (quarterOrbitMin * 3) * 60L * 1000L)
count++
} while (lastAosDate < endDate)
}
}
return passes
}
private fun filterPasses(
passes: List<SatPass>,
refDate: Date,
hoursAhead: Int = 8,
minElevation: Double = 16.0
): List<SatPass> {
val timeFuture = refDate.time + (hoursAhead * 3600 * 1000)
return passes.filter { it.losTime > refDate.time }
.filter { it.aosTime < timeFuture }
.filter { it.maxElevation > minElevation }
.sortedBy { it.aosTime }
}
private fun nextDeepSpacePass(
satellite: Satellite,
stationPos: StationPos,
refDate: Date
): SatPass {
val satPos = satellite.getPosition(stationPos, refDate.time)
val aos = Date(refDate.time - 24 * 60L * 60L * 1000L).time
val los = Date(refDate.time + 24 * 60L * 60L * 1000L).time
val tca = Date((aos + los) / 2).time
val az = Math.toDegrees(satPos.azimuth)
val elev = Math.toDegrees(satPos.elevation)
val alt = satPos.altitude
return SatPass(aos, az, los, az, tca, az, alt, elev, satellite)
}
private fun nextNearEarthPass(
satellite: Satellite,
stationPos: StationPos,
refDate: Date,
windBack: Boolean = false
): SatPass {
val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")).apply {
clear()
timeInMillis = refDate.time
}
val quarterOrbitMin = satellite.getQuarterOrbitMin()
var elevation: Double
var maxElevation = 0.0
var alt = 0.0
var tcaAz = 0.0
// wind back time 1/4 of an orbit
if (windBack) calendar.add(Calendar.MINUTE, -quarterOrbitMin)
var satPos = satellite.getPosition(stationPos, calendar.time.time)
if (satPos.elevation > 0.0) {
// move forward in 30 second intervals until the sat goes below the horizon
do {
calendar.add(Calendar.SECOND, 30)
satPos = satellite.getPosition(stationPos, calendar.time.time)
} while (satPos.elevation > 0.0)
// move forward 3/4 of an orbit
calendar.add(Calendar.MINUTE, quarterOrbitMin * 3)
}
// find the next time sat comes above the horizon
do {
calendar.add(Calendar.SECOND, 60)
satPos = satellite.getPosition(stationPos, calendar.time.time)
elevation = satPos.elevation
if (elevation > maxElevation) {
maxElevation = elevation
alt = satPos.altitude
tcaAz = Math.toDegrees(satPos.azimuth)
}
} while (satPos.elevation < 0.0)
// refine to 3 seconds
calendar.add(Calendar.SECOND, -60)
do {
calendar.add(Calendar.SECOND, 3)
satPos = satellite.getPosition(stationPos, calendar.time.time)
elevation = satPos.elevation
if (elevation > maxElevation) {
maxElevation = elevation
alt = satPos.altitude
tcaAz = Math.toDegrees(satPos.azimuth)
}
} while (satPos.elevation < 0.0)
val aos = satPos.time
val aosAz = Math.toDegrees(satPos.azimuth)
// find when sat goes below
do {
calendar.add(Calendar.SECOND, 30)
satPos = satellite.getPosition(stationPos, calendar.time.time)
elevation = satPos.elevation
if (elevation > maxElevation) {
maxElevation = elevation
alt = satPos.altitude
tcaAz = Math.toDegrees(satPos.azimuth)
}
} while (satPos.elevation > 0.0)
// refine to 3 seconds
calendar.add(Calendar.SECOND, -30)
do {
calendar.add(Calendar.SECOND, 3)
satPos = satellite.getPosition(stationPos, calendar.time.time)
elevation = satPos.elevation
if (elevation > maxElevation) {
maxElevation = elevation
alt = satPos.altitude
tcaAz = Math.toDegrees(satPos.azimuth)
}
} while (satPos.elevation > 0.0)
val los = satPos.time
val losAz = Math.toDegrees(satPos.azimuth)
val tca = Date((aos + los) / 2).time
val elev = Math.toDegrees(maxElevation)
return SatPass(aos, aosAz, los, losAz, tca, tcaAz, alt, elev, satellite)
}
}
@@ -17,7 +17,7 @@
*/ */
package com.rtbishop.look4sat.data package com.rtbishop.look4sat.data
import com.rtbishop.look4sat.domain.predict4kotlin.StationPos import com.rtbishop.look4sat.domain.StationPos
interface PreferencesSource { interface PreferencesSource {
@@ -17,10 +17,10 @@
*/ */
package com.rtbishop.look4sat.data package com.rtbishop.look4sat.data
import com.rtbishop.look4sat.domain.model.SatTrans import com.rtbishop.look4sat.domain.SatTrans
import java.io.InputStream import java.io.InputStream
interface SatDataRemoteSource { interface RemoteDataSource {
suspend fun fetchDataStream(url: String): InputStream? suspend fun fetchDataStream(url: String): InputStream?
@@ -1,71 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.data
import com.rtbishop.look4sat.domain.predict4kotlin.SatPass
import com.rtbishop.look4sat.domain.predict4kotlin.Satellite
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.withContext
import java.util.*
class SatPassRepository(
private val preferencesSource: PreferencesSource,
private val defaultDispatcher: CoroutineDispatcher
) {
private val _passes = MutableSharedFlow<List<SatPass>>(replay = 1)
private var selectedSatellites = emptyList<Satellite>()
val passes: SharedFlow<List<SatPass>> = _passes
suspend fun triggerCalculation(satellites: List<Satellite>, refDate: Date = Date()) {
if (satellites.isEmpty()) {
_passes.emit(emptyList())
} else {
val oldCatNums = selectedSatellites.map { it.params.catnum }
val newCatNums = satellites.map { it.params.catnum }
if (oldCatNums != newCatNums) forceCalculation(satellites, refDate)
}
}
suspend fun forceCalculation(satellites: List<Satellite>, refDate: Date = Date()) {
if (satellites.isEmpty()) {
_passes.emit(emptyList())
} else {
withContext(defaultDispatcher) {
val allPasses = mutableListOf<SatPass>()
selectedSatellites = satellites
satellites.forEach { satellite -> allPasses.addAll(getPasses(satellite, refDate)) }
_passes.emit(filterPasses(allPasses, refDate))
}
}
}
private fun getPasses(satellite: Satellite, refDate: Date): List<SatPass> {
val predictor = satellite.getPredictor(preferencesSource.loadStationPosition())
return predictor.getPasses(refDate, preferencesSource.getHoursAhead(), true)
}
private fun filterPasses(passes: List<SatPass>, refDate: Date): List<SatPass> {
val timeFuture = refDate.time + (preferencesSource.getHoursAhead() * 3600 * 1000)
return passes.filter { it.losTime > refDate.time }
.filter { it.aosTime < timeFuture }
.filter { it.maxElevation > preferencesSource.getMinElevation() }
.sortedBy { it.aosTime }
}
}
@@ -1,86 +0,0 @@
package com.rtbishop.look4sat.data
import com.rtbishop.look4sat.domain.predict4kotlin.*
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.withContext
import java.util.*
class SatPredictor(
private val preferencesSource: PreferencesSource,
private val defaultDispatcher: CoroutineDispatcher
) {
// Multi Sat passes
private val _passes = MutableSharedFlow<List<SatPass>>(replay = 1)
private var selectedSatellites = emptyList<Satellite>()
val passes: SharedFlow<List<SatPass>> = _passes
suspend fun triggerCalculation(satellites: List<Satellite>, refDate: Date = Date()) {
if (satellites.isEmpty()) {
_passes.emit(emptyList())
} else {
val oldCatNums = selectedSatellites.map { it.params.catnum }
val newCatNums = satellites.map { it.params.catnum }
if (oldCatNums != newCatNums) forceCalculation(satellites, refDate)
}
}
suspend fun forceCalculation(satellites: List<Satellite>, refDate: Date = Date()) {
if (satellites.isEmpty()) {
_passes.emit(emptyList())
} else {
withContext(defaultDispatcher) {
val allPasses = mutableListOf<SatPass>()
selectedSatellites = satellites
satellites.forEach { satellite -> allPasses.addAll(getPasses(satellite, refDate)) }
_passes.emit(filterPasses(allPasses, refDate))
}
}
}
private fun getPasses(satellite: Satellite, refDate: Date): List<SatPass> {
val predictor = satellite.getPredictor(preferencesSource.loadStationPosition())
return predictor.getPasses(refDate, preferencesSource.getHoursAhead(), true)
}
private fun filterPasses(passes: List<SatPass>, refDate: Date): List<SatPass> {
val timeFuture = refDate.time + (preferencesSource.getHoursAhead() * 3600 * 1000)
return passes.filter { it.losTime > refDate.time }
.filter { it.aosTime < timeFuture }
.filter { it.maxElevation > preferencesSource.getMinElevation() }
.sortedBy { it.aosTime }
}
// Single Sat radar
// Constant calculation of beacon Az/El point
private val _beaconPoint = MutableSharedFlow<SkyPos>(replay = 1)
val beaconPoint: SharedFlow<SkyPos> = _beaconPoint
suspend fun calculateSatBeacon(satPass: SatPass, stationPos: StationPos, date: Date) {
withContext(defaultDispatcher) {
val satPos = satPass.satellite.getPosition(stationPos, date)
_beaconPoint.emit(SkyPos(satPos.azimuth, satPos.elevation))
}
}
// Single calculation of beacon Az/El points list
private val _trajectoryPoints = MutableSharedFlow<List<SkyPos>>(replay = 1)
val trajectoryPoints: SharedFlow<List<SkyPos>> = _trajectoryPoints
suspend fun calculateSatTrajectory(satPass: SatPass, stationPos: StationPos) {
withContext(defaultDispatcher) {
val trajectoryPoints = mutableListOf<SkyPos>()
var currentTime = satPass.aosTime
while (currentTime < satPass.losTime) {
val satPos = satPass.satellite.getPosition(stationPos, Date(currentTime))
trajectoryPoints.add(SkyPos(satPos.azimuth, satPos.elevation))
currentTime += 15000
}
_trajectoryPoints.emit(trajectoryPoints)
}
}
// Multi Sat map
}
@@ -17,11 +17,11 @@
*/ */
package com.rtbishop.look4sat.data package com.rtbishop.look4sat.data
import com.rtbishop.look4sat.domain.model.SatEntry import com.rtbishop.look4sat.domain.SatEntry
import com.rtbishop.look4sat.domain.model.SatItem import com.rtbishop.look4sat.domain.SatItem
import com.rtbishop.look4sat.domain.model.SatTrans import com.rtbishop.look4sat.domain.SatTrans
import com.rtbishop.look4sat.domain.predict4kotlin.Satellite import com.rtbishop.look4sat.domain.Satellite
import com.rtbishop.look4sat.domain.predict4kotlin.TLE import com.rtbishop.look4sat.domain.TLE
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -30,10 +30,10 @@ import kotlinx.coroutines.withContext
import java.io.InputStream import java.io.InputStream
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
class SatDataRepository( class SatelliteRepo(
private val preferencesSource: PreferencesSource, private val preferencesSource: PreferencesSource,
private val satLocalSource: SatDataLocalSource, private val localDataSource: LocalDataSource,
private val satRemoteSource: SatDataRemoteSource, private val remoteDataSource: RemoteDataSource,
private val ioDispatcher: CoroutineDispatcher private val ioDispatcher: CoroutineDispatcher
) { ) {
@@ -46,19 +46,19 @@ class SatDataRepository(
} }
fun getSatItems(): Flow<List<SatItem>> { fun getSatItems(): Flow<List<SatItem>> {
return satLocalSource.getSatItems() return localDataSource.getSatItems()
} }
fun getSatTransmitters(catNum: Int): Flow<List<SatTrans>> { fun getSatTransmitters(catNum: Int): Flow<List<SatTrans>> {
return satLocalSource.getSatTransmitters(catNum) return localDataSource.getSatTransmitters(catNum)
} }
suspend fun getSelectedSatellites(): List<Satellite> { suspend fun getSelectedSatellites(): List<Satellite> {
return satLocalSource.getSelectedSatellites() return localDataSource.getSelectedSatellites()
} }
suspend fun updateEntriesFromFile(stream: InputStream) = withContext(ioDispatcher) { suspend fun updateEntriesFromFile(stream: InputStream) = withContext(ioDispatcher) {
satLocalSource.updateEntries(importSatEntries(stream)) localDataSource.updateEntries(importSatEntries(stream))
} }
suspend fun updateEntriesFromWeb(sources: List<String>) { suspend fun updateEntriesFromWeb(sources: List<String>) {
@@ -67,7 +67,7 @@ class SatDataRepository(
val streams = mutableListOf<InputStream>() val streams = mutableListOf<InputStream>()
val entries = mutableListOf<SatEntry>() val entries = mutableListOf<SatEntry>()
sources.forEach { source -> sources.forEach { source ->
satRemoteSource.fetchDataStream(source)?.let { stream -> remoteDataSource.fetchDataStream(source)?.let { stream ->
if (source.contains(".zip", true)) { if (source.contains(".zip", true)) {
val zipStream = ZipInputStream(stream).apply { nextEntry } val zipStream = ZipInputStream(stream).apply { nextEntry }
streams.add(zipStream) streams.add(zipStream)
@@ -77,17 +77,17 @@ class SatDataRepository(
} }
} }
streams.forEach { stream -> entries.addAll(importSatEntries(stream)) } streams.forEach { stream -> entries.addAll(importSatEntries(stream)) }
satLocalSource.updateEntries(entries) localDataSource.updateEntries(entries)
} }
launch(ioDispatcher) { launch(ioDispatcher) {
val transmitters = satRemoteSource.fetchTransmitters().filter { it.isAlive } val transmitters = remoteDataSource.fetchTransmitters().filter { it.isAlive }
satLocalSource.updateTransmitters(transmitters) localDataSource.updateTransmitters(transmitters)
} }
} }
} }
suspend fun updateEntriesSelection(catNums: List<Int>, isSelected: Boolean) { suspend fun updateEntriesSelection(catNums: List<Int>, isSelected: Boolean) {
satLocalSource.updateEntriesSelection(catNums, isSelected) localDataSource.updateEntriesSelection(catNums, isSelected)
} }
private fun importSatEntries(stream: InputStream): List<SatEntry> { private fun importSatEntries(stream: InputStream): List<SatEntry> {
@@ -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.predict4kotlin package com.rtbishop.look4sat.domain
import kotlin.math.* import kotlin.math.*
@@ -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.predict4kotlin package com.rtbishop.look4sat.domain
data class GeoPos(val latitude: Double, val longitude: Double) data class GeoPos(val latitude: Double, val longitude: Double)
@@ -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.predict4kotlin package com.rtbishop.look4sat.domain
import kotlin.math.* import kotlin.math.*
@@ -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.predict4kotlin package com.rtbishop.look4sat.domain
import kotlin.math.round import kotlin.math.round
class QthConverter { object QthConverter {
fun qthToPosition(qthString: String): GeoPos? { fun qthToPosition(qthString: String): GeoPos? {
val trimmedQth = qthString.take(6) val trimmedQth = qthString.take(6)
@@ -15,10 +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.framework.model package com.rtbishop.look4sat.domain
import com.rtbishop.look4sat.domain.predict4kotlin.GeoPos
import com.rtbishop.look4sat.domain.predict4kotlin.Satellite
data class SatData( data class SatData(
val pass: Satellite, val pass: Satellite,
@@ -15,9 +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.domain
import com.rtbishop.look4sat.domain.predict4kotlin.TLE
data class SatEntry( data class SatEntry(
val tle: TLE, val tle: TLE,
@@ -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.domain
data class SatItem( data class SatItem(
val catNum: Int, val catNum: Int,
@@ -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.predict4kotlin package com.rtbishop.look4sat.domain
data class SatPass( data class SatPass(
val aosTime: Long, val aosTime: Long,
@@ -26,8 +26,7 @@ data class SatPass(
val tcaAzimuth: Double, val tcaAzimuth: Double,
val altitude: Double, val altitude: Double,
val maxElevation: Double, val maxElevation: Double,
val satellite: Satellite, val satellite: Satellite
val predictor: PassPredictor,
) { ) {
val catNum: Int = satellite.params.catnum val catNum: Int = satellite.params.catnum
val name: String = satellite.params.name val name: String = satellite.params.name
@@ -15,9 +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.domain.predict4kotlin package com.rtbishop.look4sat.domain
import java.util.*
import kotlin.math.* import kotlin.math.*
data class SatPos( data class SatPos(
@@ -29,7 +28,7 @@ data class SatPos(
var range: Double = 0.0, var range: Double = 0.0,
var rangeRate: Double = 0.0, var rangeRate: Double = 0.0,
var theta: Double = 0.0, var theta: Double = 0.0,
var time: Date = Date() var time: Long = 0L
) { ) {
private val earthRadiusKm = 6378.16 private val earthRadiusKm = 6378.16
private val speedOfLight = 2.99792458E8 private val speedOfLight = 2.99792458E8
@@ -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.domain
data class SatTrans( data class SatTrans(
val uuid: String, val uuid: String,
@@ -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.predict4kotlin package com.rtbishop.look4sat.domain
import java.util.* import java.util.*
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
@@ -41,6 +41,10 @@ abstract class Satellite(val params: TLE) {
var s4 = 0.0 var s4 = 0.0
val xke = 7.43669161E-2 val xke = 7.43669161E-2
fun getQuarterOrbitMin(): Int {
return (24.0 * 60.0 / params.meanmo / 4.0).toInt()
}
fun willBeSeen(pos: StationPos): Boolean { fun willBeSeen(pos: StationPos): Boolean {
return if (params.meanmo < 1e-8) false else { return if (params.meanmo < 1e-8) false else {
var lin = params.incl var lin = params.incl
@@ -51,11 +55,7 @@ abstract class Satellite(val params: TLE) {
} }
} }
fun getPredictor(pos: StationPos): PassPredictor { fun getPosition(pos: StationPos, time: Long): SatPos {
return PassPredictor(this, pos)
}
fun getPosition(pos: StationPos, time: Date): SatPos {
val satPos = SatPos() val satPos = SatPos()
// Date/time at which the position and velocity were calculated // Date/time at which the position and velocity were calculated
val julUTC = calcCurrentDaynum(time) + 2444238.5 val julUTC = calcCurrentDaynum(time) + 2444238.5
@@ -76,8 +76,7 @@ abstract class Satellite(val params: TLE) {
} }
// Read the system clock and return the number of days since 31Dec79 00:00:00 UTC (daynum 0) // Read the system clock and return the number of days since 31Dec79 00:00:00 UTC (daynum 0)
private fun calcCurrentDaynum(date: Date): Double { private fun calcCurrentDaynum(now: Long): Double {
val now = date.time
val sgp4Epoch = Calendar.getInstance(TimeZone.getTimeZone("UTC:UTC")) val sgp4Epoch = Calendar.getInstance(TimeZone.getTimeZone("UTC:UTC"))
sgp4Epoch.clear() sgp4Epoch.clear()
sgp4Epoch[1979, 11, 31, 0, 0] = 0 sgp4Epoch[1979, 11, 31, 0, 0] = 0
@@ -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.predict4kotlin package com.rtbishop.look4sat.domain
data class StationPos( data class StationPos(
val latitude: Double, val latitude: Double,
@@ -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.predict4kotlin package com.rtbishop.look4sat.domain
import java.io.InputStream import java.io.InputStream
import kotlin.math.pow import kotlin.math.pow
@@ -1,173 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 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.predict4kotlin
import java.util.*
class PassPredictor(private val satellite: Satellite, private val stationPos: StationPos) {
private val oneQuarterOrbitMin = (24.0 * 60.0 / satellite.params.meanmo / 4.0).toInt()
private val speedOfLight = 2.99792458E8
fun getDownlinkFreq(freq: Long, date: Date): Long {
val rangeRate = getSatPos(date).rangeRate
return (freq.toDouble() * (speedOfLight - rangeRate * 1000.0) / speedOfLight).toLong()
}
fun getUplinkFreq(freq: Long, date: Date): Long {
val rangeRate = getSatPos(date).rangeRate
return (freq.toDouble() * (speedOfLight + rangeRate * 1000.0) / speedOfLight).toLong()
}
fun getSatPos(date: Date): SatPos {
return satellite.getPosition(stationPos, date)
}
fun getPositions(refDate: Date, stepSec: Int, minBefore: Int, orbits: Double): List<SatPos> {
val positions = mutableListOf<SatPos>()
val orbitalPeriod = 24 * 60 / satellite.params.meanmo
val endDate = Date(refDate.time + (orbitalPeriod * orbits * 60L * 1000L).toLong())
val startDate = Date(refDate.time - minBefore * 60L * 1000L)
var currentDate = startDate
while (currentDate.before(endDate)) {
positions.add(getSatPos(currentDate))
currentDate = Date(currentDate.time + stepSec * 1000)
}
return positions
}
fun getPasses(refDate: Date, hoursAhead: Int, windBack: Boolean): List<SatPass> {
val passes = mutableListOf<SatPass>()
val endDate = Date(refDate.time + hoursAhead * 60L * 60L * 1000L)
var startDate = refDate
var shouldWindBack = windBack
var lastAosDate: Date
var count = 0
if (satellite.willBeSeen(stationPos)) {
if (satellite.params.isDeepspace) {
passes.add(nextDeepSpacePass(refDate))
} else {
do {
if (count > 0) shouldWindBack = false
val pass = nextNearEarthPass(startDate, shouldWindBack)
lastAosDate = Date(pass.aosTime)
passes.add(pass)
startDate = Date(pass.losTime + (oneQuarterOrbitMin * 3) * 60L * 1000L)
count++
} while (lastAosDate < endDate)
}
}
return passes
}
private fun nextDeepSpacePass(refDate: Date): SatPass {
val satPos = getSatPos(refDate)
val aos = Date(refDate.time - 24 * 60L * 60L * 1000L).time
val los = Date(refDate.time + 24 * 60L * 60L * 1000L).time
val tca = Date((aos + los) / 2).time
val az = Math.toDegrees(satPos.azimuth)
val elev = Math.toDegrees(satPos.elevation)
val alt = satPos.altitude
return SatPass(aos, az, los, az, tca, az, alt, elev, satellite, this)
}
private fun nextNearEarthPass(refDate: Date, windBack: Boolean = false): SatPass {
val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")).apply {
clear()
timeInMillis = refDate.time
}
var elevation: Double
var maxElevation = 0.0
var alt = 0.0
var tcaAz = 0.0
// wind back time 1/4 of an orbit
if (windBack) calendar.add(Calendar.MINUTE, -oneQuarterOrbitMin)
var satPos = getSatPos(calendar.time)
if (satPos.elevation > 0.0) {
// move forward in 30 second intervals until the sat goes below the horizon
do {
calendar.add(Calendar.SECOND, 30)
satPos = getSatPos(calendar.time)
} while (satPos.elevation > 0.0)
// move forward 3/4 of an orbit
calendar.add(Calendar.MINUTE, oneQuarterOrbitMin * 3)
}
// find the next time sat comes above the horizon
do {
calendar.add(Calendar.SECOND, 60)
satPos = getSatPos(calendar.time)
elevation = satPos.elevation
if (elevation > maxElevation) {
maxElevation = elevation
alt = satPos.altitude
tcaAz = Math.toDegrees(satPos.azimuth)
}
} while (satPos.elevation < 0.0)
// refine to 3 seconds
calendar.add(Calendar.SECOND, -60)
do {
calendar.add(Calendar.SECOND, 3)
satPos = getSatPos(calendar.time)
elevation = satPos.elevation
if (elevation > maxElevation) {
maxElevation = elevation
alt = satPos.altitude
tcaAz = Math.toDegrees(satPos.azimuth)
}
} while (satPos.elevation < 0.0)
val aos = satPos.time.time
val aosAz = Math.toDegrees(satPos.azimuth)
// find when sat goes below
do {
calendar.add(Calendar.SECOND, 30)
satPos = getSatPos(calendar.time)
elevation = satPos.elevation
if (elevation > maxElevation) {
maxElevation = elevation
alt = satPos.altitude
tcaAz = Math.toDegrees(satPos.azimuth)
}
} while (satPos.elevation > 0.0)
// refine to 3 seconds
calendar.add(Calendar.SECOND, -30)
do {
calendar.add(Calendar.SECOND, 3)
satPos = getSatPos(calendar.time)
elevation = satPos.elevation
if (elevation > maxElevation) {
maxElevation = elevation
alt = satPos.altitude
tcaAz = Math.toDegrees(satPos.azimuth)
}
} while (satPos.elevation > 0.0)
val los = satPos.time.time
val losAz = Math.toDegrees(satPos.azimuth)
val tca = Date((aos + los) / 2).time
val elev = Math.toDegrees(maxElevation)
return SatPass(aos, aosAz, los, losAz, tca, tcaAz, alt, elev, satellite, this)
}
}
@@ -1,20 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 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.predict4kotlin
data class SkyPos(val azimuth: Double, val elevation: Double)
@@ -1,6 +1,6 @@
package com.rtbishop.look4sat package com.rtbishop.look4sat
import com.rtbishop.look4sat.domain.predict4kotlin.TLE import com.rtbishop.look4sat.domain.TLE
import org.junit.Test import org.junit.Test
class Look4SatTest { class Look4SatTest {
@@ -17,44 +17,42 @@
*/ */
package com.rtbishop.look4sat package com.rtbishop.look4sat
import com.rtbishop.look4sat.domain.predict4kotlin.QthConverter import com.rtbishop.look4sat.domain.QthConverter
import org.junit.Test import org.junit.Test
class QthConverterTest { class QthConverterTest {
private val qthConverter = QthConverter()
@Test @Test
fun `Given valid QTH returns correct POS`() { fun `Given valid QTH returns correct POS`() {
var result = qthConverter.qthToPosition("io91VL39FX") var result = QthConverter.qthToPosition("io91VL39FX")
assert(result?.latitude == 51.4792 && result.longitude == -0.2083) assert(result?.latitude == 51.4792 && result.longitude == -0.2083)
result = qthConverter.qthToPosition("JN58TD") result = QthConverter.qthToPosition("JN58TD")
assert(result?.latitude == 48.1458 && result.longitude == 11.6250) assert(result?.latitude == 48.1458 && result.longitude == 11.6250)
result = qthConverter.qthToPosition("gf15vc") result = QthConverter.qthToPosition("gf15vc")
assert(result?.latitude == -34.8958 && result.longitude == -56.2083) assert(result?.latitude == -34.8958 && result.longitude == -56.2083)
result = qthConverter.qthToPosition("fm18LW") result = QthConverter.qthToPosition("fm18LW")
assert(result?.latitude == 38.9375 && result.longitude == -77.0417) assert(result?.latitude == 38.9375 && result.longitude == -77.0417)
} }
@Test @Test
fun `Given invalid QTH returns null`() { fun `Given invalid QTH returns null`() {
var result = qthConverter.qthToPosition("ZZ00zz") var result = QthConverter.qthToPosition("ZZ00zz")
assert(result == null) assert(result == null)
result = qthConverter.qthToPosition("JN58tz") result = QthConverter.qthToPosition("JN58tz")
assert(result == null) assert(result == null)
} }
@Test @Test
fun `Given valid POS returns correct QTH`() { fun `Given valid POS returns correct QTH`() {
assert(qthConverter.positionToQTH(51.4878, -0.2146) == "IO91vl") assert(QthConverter.positionToQTH(51.4878, -0.2146) == "IO91vl")
assert(qthConverter.positionToQTH(48.1466, 11.6083) == "JN58td") assert(QthConverter.positionToQTH(48.1466, 11.6083) == "JN58td")
assert(qthConverter.positionToQTH(-34.91, -56.2116) == "GF15vc") assert(QthConverter.positionToQTH(-34.91, -56.2116) == "GF15vc")
assert(qthConverter.positionToQTH(38.92, -77.065) == "FM18lw") assert(QthConverter.positionToQTH(38.92, -77.065) == "FM18lw")
} }
@Test @Test
fun `Given invalid POS returns null`() { fun `Given invalid POS returns null`() {
assert(qthConverter.positionToQTH(91.0542, -170.1142) == null) assert(QthConverter.positionToQTH(91.0542, -170.1142) == null)
assert(qthConverter.positionToQTH(89.0542, -240.1142) == null) assert(QthConverter.positionToQTH(89.0542, -240.1142) == null)
} }
} }