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 androidx.core.content.edit
import com.rtbishop.look4sat.data.PreferencesSource
import com.rtbishop.look4sat.domain.predict4kotlin.QthConverter
import com.rtbishop.look4sat.domain.predict4kotlin.StationPos
import com.rtbishop.look4sat.domain.QthConverter
import com.rtbishop.look4sat.domain.StationPos
import com.rtbishop.look4sat.utility.round
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
@@ -31,7 +31,6 @@ import javax.inject.Inject
class PreferencesProvider @Inject constructor(
moshi: Moshi,
private val qthConverter: QthConverter,
private val locationManager: LocationManager,
private val preferences: SharedPreferences
) : PreferencesSource {
@@ -86,7 +85,7 @@ class PreferencesProvider @Inject constructor(
}
override fun positionToQTH(lat: Double, lon: Double): String? {
return qthConverter.positionToQTH(lat, lon)
return QthConverter.positionToQTH(lat, lon)
}
override fun loadStationPosition(): StationPos {
@@ -123,7 +122,7 @@ class PreferencesProvider @Inject constructor(
}
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)
saveStationPosition(stationPosition)
return true
@@ -17,12 +17,12 @@
*/
package com.rtbishop.look4sat.framework.api
import com.rtbishop.look4sat.data.SatDataRemoteSource
import com.rtbishop.look4sat.domain.model.SatTrans
import com.rtbishop.look4sat.data.RemoteDataSource
import com.rtbishop.look4sat.domain.SatTrans
import com.rtbishop.look4sat.utility.DataMapper
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? {
return api.fetchFileByUrl(url).body()?.byteStream()
@@ -24,7 +24,7 @@ import retrofit2.http.GET
import retrofit2.http.Streaming
import retrofit2.http.Url
interface SatDataService {
interface SatelliteService {
@Streaming
@GET
@@ -18,34 +18,34 @@
package com.rtbishop.look4sat.framework.db
import androidx.room.TypeConverter
import com.rtbishop.look4sat.domain.predict4kotlin.Satellite
import com.rtbishop.look4sat.domain.predict4kotlin.TLE
import com.rtbishop.look4sat.domain.Satellite
import com.rtbishop.look4sat.domain.TLE
import com.squareup.moshi.JsonAdapter
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) {
tleAdapter = moshi.adapter(TLE::class.java)
paramsAdapter = moshi.adapter(TLE::class.java)
}
@JvmStatic
@TypeConverter
fun tleToString(tle: TLE): String {
return tleAdapter.toJson(tle)
return paramsAdapter.toJson(tle)
}
@JvmStatic
@TypeConverter
fun tleFromString(string: String): TLE? {
return tleAdapter.fromJson(string)
return paramsAdapter.fromJson(string)
}
@JvmStatic
@TypeConverter
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
import com.rtbishop.look4sat.data.SatDataLocalSource
import com.rtbishop.look4sat.domain.model.SatEntry
import com.rtbishop.look4sat.domain.model.SatItem
import com.rtbishop.look4sat.domain.model.SatTrans
import com.rtbishop.look4sat.domain.predict4kotlin.Satellite
import com.rtbishop.look4sat.data.LocalDataSource
import com.rtbishop.look4sat.domain.SatEntry
import com.rtbishop.look4sat.domain.SatItem
import com.rtbishop.look4sat.domain.SatTrans
import com.rtbishop.look4sat.domain.Satellite
import com.rtbishop.look4sat.utility.DataMapper
import kotlinx.coroutines.flow.Flow
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>> {
return satDataDao.getSatItems()
return satelliteDao.getSatItems()
.map { satItems -> DataMapper.satItemsToDomainItems(satItems) }
}
override suspend fun getSelectedSatellites(): List<Satellite> {
return satDataDao.getSelectedSatellites()
return satelliteDao.getSelectedSatellites()
}
override suspend fun updateEntries(entries: List<SatEntry>) {
val satEntries = entries.map { entry -> DataMapper.domainEntryToSatEntry(entry) }
satDataDao.updateEntries(satEntries)
satelliteDao.updateEntries(satEntries)
}
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>> {
return satDataDao.getSatTransmitters(catNum)
return satelliteDao.getSatTransmitters(catNum)
.map { satTransList -> DataMapper.satTransListToDomainTransList(satTransList) }
}
override suspend fun updateTransmitters(satelliteTrans: List<SatTrans>) {
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.SatItem
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
@Dao
interface SatDataDao {
interface SatelliteDao {
@Transaction
@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
@Database(entities = [SatEntry::class, SatTrans::class], version = 1, exportSchema = false)
@TypeConverters(RoomConverters::class)
abstract class SatDataDb : RoomDatabase() {
@TypeConverters(Converters::class)
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.PrimaryKey
import com.rtbishop.look4sat.domain.predict4kotlin.TLE
import com.rtbishop.look4sat.domain.TLE
@Entity(tableName = "entries")
data class SatEntry(
@@ -22,14 +22,13 @@ import android.content.SharedPreferences
import android.location.LocationManager
import androidx.room.Room
import com.rtbishop.look4sat.data.*
import com.rtbishop.look4sat.domain.predict4kotlin.QthConverter
import com.rtbishop.look4sat.framework.PreferencesProvider
import com.rtbishop.look4sat.framework.api.SatDataRemote
import com.rtbishop.look4sat.framework.api.SatDataService
import com.rtbishop.look4sat.framework.db.RoomConverters
import com.rtbishop.look4sat.framework.db.SatDataDao
import com.rtbishop.look4sat.framework.db.SatDataDb
import com.rtbishop.look4sat.framework.db.SatDataLocal
import com.rtbishop.look4sat.framework.api.RemoteSource
import com.rtbishop.look4sat.framework.api.SatelliteService
import com.rtbishop.look4sat.framework.db.Converters
import com.rtbishop.look4sat.framework.db.LocalSource
import com.rtbishop.look4sat.framework.db.SatelliteDao
import com.rtbishop.look4sat.framework.db.SatelliteDb
import com.squareup.moshi.Moshi
import dagger.Module
import dagger.Provides
@@ -45,12 +44,7 @@ import javax.net.ssl.HostnameVerifier
@Module
@InstallIn(SingletonComponent::class)
object SatDataModule {
@Provides
fun provideQthConverter(): QthConverter {
return QthConverter()
}
object SatelliteDataModule {
@Provides
fun provideMoshi(): Moshi {
@@ -61,56 +55,52 @@ object SatDataModule {
@Singleton
fun providePreferenceSource(
moshi: Moshi,
qthConverter: QthConverter,
locationManager: LocationManager,
preferences: SharedPreferences
): PreferencesSource {
return PreferencesProvider(moshi, qthConverter, locationManager, preferences)
return PreferencesProvider(moshi, locationManager, preferences)
}
@Provides
@Singleton
fun providePassesRepo(
preferencesSource: PreferencesSource,
@DefaultDispatcher defaultDispatcher: CoroutineDispatcher
): SatPassRepository {
return SatPassRepository(preferencesSource, defaultDispatcher)
fun providePassPredictor(@DefaultDispatcher dispatcher: CoroutineDispatcher): PassPredictor {
return PassPredictor(dispatcher)
}
@Provides
@Singleton
fun provideSatelliteRepo(
preferencesSource: PreferencesSource,
satLocalSource: SatDataLocalSource,
satRemoteSource: SatDataRemoteSource,
@IoDispatcher ioDispatcher: CoroutineDispatcher
): SatDataRepository {
return SatDataRepository(preferencesSource, satLocalSource, satRemoteSource, ioDispatcher)
localDataSource: LocalDataSource,
remoteDataSource: RemoteDataSource,
@IoDispatcher dispatcher: CoroutineDispatcher
): SatelliteRepo {
return SatelliteRepo(preferencesSource, localDataSource, remoteDataSource, dispatcher)
}
@Provides
fun provideLocalDataSource(satDataDao: SatDataDao): SatDataLocalSource {
return SatDataLocal(satDataDao)
fun provideLocalDataSource(satelliteDao: SatelliteDao): LocalDataSource {
return LocalSource(satelliteDao)
}
@Provides
fun provideSatDataDao(db: SatDataDb): SatDataDao {
fun provideSatelliteDao(db: SatelliteDb): SatelliteDao {
return db.satelliteDao()
}
@Provides
fun provideSatelliteDb(@ApplicationContext context: Context, moshi: Moshi): SatDataDb {
RoomConverters.initialize(moshi)
return Room.databaseBuilder(context, SatDataDb::class.java, "SatelliteDb").build()
fun provideSatelliteDb(@ApplicationContext context: Context, moshi: Moshi): SatelliteDb {
Converters.initialize(moshi)
return Room.databaseBuilder(context, SatelliteDb::class.java, "SatelliteDb").build()
}
@Provides
fun provideRemoteDataSource(satDataService: SatDataService): SatDataRemoteSource {
return SatDataRemote(satDataService)
fun provideRemoteDataSource(satelliteService: SatelliteService): RemoteDataSource {
return RemoteSource(satelliteService)
}
@Provides
fun provideSatDataService(): SatDataService {
fun provideSatelliteService(): SatelliteService {
val verifier = HostnameVerifier { _, _ -> true }
val httpClient = OkHttpClient.Builder().hostnameVerifier(verifier).build()
return Retrofit.Builder()
@@ -118,6 +108,6 @@ object SatDataModule {
.client(httpClient)
.addConverterFactory(MoshiConverterFactory.create())
.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.RecyclerView
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>() {
@@ -28,7 +28,7 @@ import androidx.recyclerview.widget.SimpleItemAnimator
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.rtbishop.look4sat.R
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.utility.RecyclerDivider
import com.rtbishop.look4sat.utility.getNavResult
@@ -22,8 +22,8 @@ import android.net.Uri
import android.widget.SearchView
import androidx.lifecycle.*
import com.rtbishop.look4sat.data.PreferencesSource
import com.rtbishop.look4sat.data.SatDataRepository
import com.rtbishop.look4sat.domain.model.SatItem
import com.rtbishop.look4sat.data.SatelliteRepo
import com.rtbishop.look4sat.domain.SatItem
import com.rtbishop.look4sat.framework.model.Result
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.collect
@@ -35,13 +35,13 @@ import javax.inject.Inject
class SatItemViewModel @Inject constructor(
private val preferencesSource: PreferencesSource,
private val resolver: ContentResolver,
private val satDataRepository: SatDataRepository,
private val satelliteRepo: SatelliteRepo,
) : ViewModel(), SatItemAdapter.EntriesClickListener, SearchView.OnQueryTextListener {
private val transModes = MutableLiveData(satDataRepository.loadSelectedModes())
private val transModes = MutableLiveData(satelliteRepo.loadSelectedModes())
private val currentQuery = MutableLiveData(String())
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 ->
itemsWithModes.map { items -> Result.Success(filterByQuery(items, query)) }
@@ -57,7 +57,7 @@ class SatItemViewModel @Inject constructor(
_satData.value = Result.InProgress
runCatching {
resolver.openInputStream(uri)?.use { stream ->
satDataRepository.updateEntriesFromFile(stream)
satelliteRepo.updateEntriesFromFile(stream)
}
}.onFailure { _satData.value = Result.Error(it) }
}
@@ -68,7 +68,7 @@ class SatItemViewModel @Inject constructor(
_satData.value = Result.InProgress
try {
preferencesSource.saveTleSources(sources)
satDataRepository.updateEntriesFromWeb(sources)
satelliteRepo.updateEntriesFromWeb(sources)
} catch (exception: Exception) {
_satData.value = Result.Error(exception)
}
@@ -84,12 +84,12 @@ class SatItemViewModel @Inject constructor(
}
fun loadSelectedModes(): List<String> {
return satDataRepository.loadSelectedModes()
return satelliteRepo.loadSelectedModes()
}
fun saveSelectedModes(modes: List<String>) {
transModes.value = modes
satDataRepository.saveSelectedModes(modes)
satelliteRepo.saveSelectedModes(modes)
}
override fun onQueryTextSubmit(query: String): Boolean {
@@ -102,7 +102,7 @@ class SatItemViewModel @Inject constructor(
}
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> {
@@ -29,9 +29,9 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.databinding.FragmentMapBinding
import com.rtbishop.look4sat.domain.predict4kotlin.GeoPos
import com.rtbishop.look4sat.domain.predict4kotlin.Satellite
import com.rtbishop.look4sat.framework.model.SatData
import com.rtbishop.look4sat.domain.GeoPos
import com.rtbishop.look4sat.domain.Satellite
import com.rtbishop.look4sat.domain.SatData
import dagger.hilt.android.AndroidEntryPoint
import org.osmdroid.config.Configuration
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
@@ -89,7 +89,7 @@ class SatMapFragment : Fragment(R.layout.fragment_map) {
}
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.satTrack.observe(viewLifecycleOwner, { renderSatTrack(it, binding) })
viewModel.satFootprint.observe(viewLifecycleOwner, { renderSatFootprint(it, binding) })
@@ -19,11 +19,12 @@ package com.rtbishop.look4sat.presentation.satMapScreen
import androidx.lifecycle.*
import com.rtbishop.look4sat.data.PreferencesSource
import com.rtbishop.look4sat.data.SatDataRepository
import com.rtbishop.look4sat.domain.predict4kotlin.GeoPos
import com.rtbishop.look4sat.domain.predict4kotlin.Satellite
import com.rtbishop.look4sat.domain.predict4kotlin.StationPos
import com.rtbishop.look4sat.framework.model.SatData
import com.rtbishop.look4sat.data.SatelliteRepo
import com.rtbishop.look4sat.data.PassPredictor
import com.rtbishop.look4sat.domain.GeoPos
import com.rtbishop.look4sat.domain.Satellite
import com.rtbishop.look4sat.domain.StationPos
import com.rtbishop.look4sat.domain.SatData
import com.rtbishop.look4sat.injection.DefaultDispatcher
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.*
@@ -35,19 +36,20 @@ import kotlin.math.sqrt
@HiltViewModel
class SatMapViewModel @Inject constructor(
private val satDataRepository: SatDataRepository,
private val preferencesSource: PreferencesSource,
private val satelliteRepo: SatelliteRepo,
private val passPredictor: PassPredictor,
private val preferences: PreferencesSource,
@DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher
) : ViewModel() {
private val gsp = preferencesSource.loadStationPosition()
private val stationPos = preferences.loadStationPosition()
private var dataUpdateJob: Job? = null
private var allSatList = listOf<Satellite>()
private lateinit var selectedSat: Satellite
val stationPos = liveData {
val osmLat = clipLat(gsp.latitude)
val osmLon = clipLon(gsp.longitude)
val stationPosLiveData = liveData {
val osmLat = clipLat(stationPos.latitude)
val osmLon = clipLon(stationPos.longitude)
emit(GeoPos(osmLat, osmLon))
}
@@ -65,7 +67,7 @@ class SatMapViewModel @Inject constructor(
init {
viewModelScope.launch {
satDataRepository.getSelectedSatellites().also { selectedSatellites ->
satelliteRepo.getSelectedSatellites().also { selectedSatellites ->
if (selectedSatellites.isNotEmpty()) {
allSatList = selectedSatellites
selectSatellite(selectedSatellites.first())
@@ -75,7 +77,7 @@ class SatMapViewModel @Inject constructor(
}
fun shouldUseTextLabels(): Boolean {
return preferencesSource.shouldUseTextLabels()
return preferences.shouldUseTextLabels()
}
fun scrollSelection(decrement: Boolean) {
@@ -97,12 +99,12 @@ class SatMapViewModel @Inject constructor(
dataUpdateJob?.cancelAndJoin()
dataUpdateJob = launch {
val dateNow = Date()
setSelectedSatTrack(selectedSat, gsp, dateNow)
setSelectedSatTrack(selectedSat, stationPos, dateNow)
while (isActive) {
dateNow.time = System.currentTimeMillis()
setSatPositions(allSatList, gsp, dateNow)
setSelectedSatFootprint(selectedSat, gsp, dateNow)
setSelectedSatData(selectedSat, gsp, dateNow)
setSatPositions(allSatList, stationPos, dateNow)
setSelectedSatFootprint(selectedSat, stationPos, dateNow)
setSelectedSatData(selectedSat, stationPos, dateNow)
delay(updateFreq)
}
}
@@ -113,7 +115,7 @@ class SatMapViewModel @Inject constructor(
withContext(defaultDispatcher) {
val satPositions = mutableMapOf<Satellite, GeoPos>()
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 osmLon = clipLon(Math.toDegrees(satPos.longitude))
satPositions[satellite] = GeoPos(osmLat, osmLon)
@@ -127,7 +129,7 @@ class SatMapViewModel @Inject constructor(
val satTracks = mutableListOf<List<GeoPos>>()
val currentTrack = mutableListOf<GeoPos>()
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 osmLon = clipLon(Math.toDegrees(satPos.longitude))
val currentPosition = GeoPos(osmLat, osmLon)
@@ -154,7 +156,7 @@ class SatMapViewModel @Inject constructor(
private suspend fun setSelectedSatFootprint(sat: Satellite, gsp: StationPos, date: Date) {
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 osmLon = clipLon(rangePos.longitude)
GeoPos(osmLat, osmLon)
@@ -165,12 +167,11 @@ class SatMapViewModel @Inject constructor(
private suspend fun setSelectedSatData(sat: Satellite, gsp: StationPos, date: Date) {
withContext(defaultDispatcher) {
val satPos = sat.getPredictor(gsp).getSatPos(date)
val satPos = sat.getPosition(gsp, date.time)
val osmLat = clipLat(Math.toDegrees(satPos.latitude))
val osmLon = clipLon(Math.toDegrees(satPos.longitude))
val osmPos = GeoPos(osmLat, osmLon)
val qthLoc =
preferencesSource.positionToQTH(osmPos.latitude, osmPos.longitude) ?: "-- --"
val qthLoc = preferences.positionToQTH(osmPos.latitude, osmPos.longitude) ?: "-- --"
val velocity = getOrbitalVelocity(satPos.altitude)
val satData = SatData(
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.data.PreferencesSource
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.navigateSafe
import com.rtbishop.look4sat.utility.toTimerString
@@ -39,7 +39,7 @@ import javax.inject.Inject
class PassInfoFragment : Fragment(R.layout.fragment_polar) {
@Inject
lateinit var preferencesSource: PreferencesSource
lateinit var preferences: PreferencesSource
private val viewModel: PassInfoViewModel by viewModels()
private var passInfoView: PassInfoView? = null
@@ -72,10 +72,12 @@ class PassInfoFragment : Fragment(R.layout.fragment_polar) {
private fun setupObservers(satTransAdapter: SatTransAdapter, binding: FragmentPolarBinding) {
val catNum = requireArguments().getInt("catNum")
val aosTime = requireArguments().getLong("aosTime")
val stationPos = preferences.loadStationPosition()
viewModel.getPass(catNum, aosTime).observe(viewLifecycleOwner) { pass ->
passInfoView = PassInfoView(requireContext()).apply {
setShowAim(preferencesSource.shouldUseCompass())
setShowAim(preferences.shouldUseCompass())
setPass(pass)
setStationPos(stationPos)
}
binding.frame.addView(passInfoView)
observeTransmitters(pass, satTransAdapter, binding)
@@ -107,7 +109,8 @@ class PassInfoFragment : Fragment(R.layout.fragment_polar) {
private fun setPassText(satPass: SatPass, binding: FragmentPolarBinding) {
val dateNow = Date()
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 polarEl = getString(R.string.pat_elevation)
val polarRng = getString(R.string.pat_distance)
@@ -22,7 +22,8 @@ import android.graphics.*
import android.view.View
import androidx.core.content.ContextCompat
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 kotlin.math.cos
import kotlin.math.min
@@ -37,6 +38,7 @@ class PassInfoView(context: Context) : View(context) {
private val radarRadius = radarWidth * 0.48f
private val piDiv2 = Math.PI / 2.0
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 radarCircleNum = 3
@@ -101,6 +103,10 @@ class PassInfoView(context: Context) : View(context) {
this.satPass = satPass
}
fun setStationPos(stationPos: StationPos) {
this.stationPos = stationPos
}
fun setShowAim(showAim: Boolean) {
shouldShowAim = showAim
}
@@ -170,7 +176,7 @@ class PassInfoView(context: Context) : View(context) {
}
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) {
val satX = sph2CartX(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) {
var currentTime = satPass.aosTime
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 passY = sph2CartY(satPos.azimuth, satPos.elevation, radarRadius.toDouble())
if (currentTime == satPass.aosTime) {
@@ -18,12 +18,12 @@
package com.rtbishop.look4sat.presentation.satPassInfoScreen
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.SatDataRepository
import com.rtbishop.look4sat.data.SatelliteRepo
import com.rtbishop.look4sat.injection.IoDispatcher
import com.rtbishop.look4sat.domain.model.SatTrans
import com.rtbishop.look4sat.domain.predict4kotlin.SatPass
import com.rtbishop.look4sat.domain.SatTrans
import com.rtbishop.look4sat.domain.SatPass
import com.rtbishop.look4sat.framework.OrientationProvider
import com.rtbishop.look4sat.utility.round
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -37,19 +37,20 @@ import javax.inject.Inject
@HiltViewModel
class PassInfoViewModel @Inject constructor(
private val orientationProvider: OrientationProvider,
private val preferencesSource: PreferencesSource,
private val satPassRepository: SatPassRepository,
private val satDataRepository: SatDataRepository,
private val preferences: PreferencesSource,
private val passPredictor: PassPredictor,
private val satelliteRepo: SatelliteRepo,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
) : ViewModel(), OrientationProvider.OrientationListener {
private val stationPos = preferences.loadStationPosition()
private val _transmitters = MutableLiveData<List<SatTrans>>()
private val _orientation = MutableLiveData<Triple<Float, Float, Float>>()
val transmitters: LiveData<List<SatTrans>> = _transmitters
val orientation: LiveData<Triple<Float, Float, Float>> = _orientation
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 }
pass?.let { satPass ->
processTransmitters(satPass)
@@ -60,27 +61,27 @@ class PassInfoViewModel @Inject constructor(
}
fun enableSensor() {
if (preferencesSource.shouldUseCompass()) orientationProvider.startListening(this)
if (preferences.shouldUseCompass()) orientationProvider.startListening(this)
}
fun disableSensor() {
if (preferencesSource.shouldUseCompass()) orientationProvider.stopListening()
if (preferences.shouldUseCompass()) orientationProvider.stopListening()
}
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) {
viewModelScope.launch {
val rotatorPrefs = preferencesSource.getRotatorServer()
val rotatorPrefs = preferences.getRotatorServer()
if (rotatorPrefs != null) {
runCatching {
withContext(ioDispatcher) {
val socket = Socket(rotatorPrefs.first, rotatorPrefs.second)
val writer = socket.getOutputStream().bufferedWriter()
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 elevation = Math.toDegrees(satPos.elevation).round(1)
writer.write("\\set_pos $azimuth $elevation")
@@ -97,16 +98,16 @@ class PassInfoViewModel @Inject constructor(
private fun processTransmitters(satPass: SatPass) {
viewModelScope.launch {
satDataRepository.getSatTransmitters(satPass.catNum).collect { transList ->
satelliteRepo.getSatTransmitters(satPass.catNum).collect { transList ->
while (isActive) {
val timeNow = Date()
val satPos = satPass.satellite.getPosition(stationPos, Date().time)
val copiedList = transList.map { it.copy() }
copiedList.forEach { transmitter ->
transmitter.downlink?.let {
transmitter.downlink = satPass.predictor.getDownlinkFreq(it, timeNow)
transmitter.downlink = satPos.getDownlinkFreq(it)
}
transmitter.uplink?.let {
transmitter.uplink = satPass.predictor.getUplinkFreq(it, timeNow)
transmitter.uplink = satPos.getUplinkFreq(it)
}
}
_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 com.rtbishop.look4sat.R
import com.rtbishop.look4sat.databinding.ItemTransBinding
import com.rtbishop.look4sat.domain.model.SatTrans
import com.rtbishop.look4sat.domain.SatTrans
import java.util.*
class SatTransAdapter : RecyclerView.Adapter<SatTransAdapter.TransHolder>() {
@@ -25,7 +25,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.databinding.ItemPassGeoBinding
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.util.*
@@ -29,7 +29,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.SimpleItemAnimator
import com.rtbishop.look4sat.R
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.utility.RecyclerDivider
import com.rtbishop.look4sat.utility.navigateSafe
@@ -22,9 +22,9 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.rtbishop.look4sat.data.PreferencesSource
import com.rtbishop.look4sat.data.SatDataRepository
import com.rtbishop.look4sat.data.SatPassRepository
import com.rtbishop.look4sat.domain.predict4kotlin.SatPass
import com.rtbishop.look4sat.data.SatelliteRepo
import com.rtbishop.look4sat.data.PassPredictor
import com.rtbishop.look4sat.domain.SatPass
import com.rtbishop.look4sat.framework.model.Result
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.*
@@ -33,8 +33,8 @@ import javax.inject.Inject
@HiltViewModel
class SatPassViewModel @Inject constructor(
private val satDataRepository: SatDataRepository,
private val satPassRepository: SatPassRepository,
private val satelliteRepo: SatelliteRepo,
private val passPredictor: PassPredictor,
private val preferencesSource: PreferencesSource
) : ViewModel() {
@@ -48,13 +48,14 @@ class SatPassViewModel @Inject constructor(
if (preferencesSource.isSetupDone()) {
viewModelScope.launch {
_passes.postValue(Result.InProgress)
satPassRepository.triggerCalculation(satDataRepository.getSelectedSatellites())
val stationPos = preferencesSource.loadStationPosition()
passPredictor.triggerCalculation(satelliteRepo.getSelectedSatellites(), stationPos)
}
} else {
_isFirstLaunchDone.value = false
}
viewModelScope.launch {
satPassRepository.passes.collect { passes ->
passPredictor.passes.collect { passes ->
passesProcessing?.cancelAndJoin()
passesProcessing = viewModelScope.launch { tickPasses(passes) }
}
@@ -66,9 +67,10 @@ class SatPassViewModel @Inject constructor(
viewModelScope.launch {
_passes.postValue(Result.InProgress)
val defaultCatNums = listOf(43700, 25544, 25338, 28654, 33591, 40069, 27607, 24278)
satDataRepository.updateEntriesFromWeb(preferencesSource.loadDefaultSources())
satDataRepository.updateEntriesSelection(defaultCatNums, true)
satPassRepository.forceCalculation(satDataRepository.getSelectedSatellites())
val stationPos = preferencesSource.loadStationPosition()
satelliteRepo.updateEntriesFromWeb(preferencesSource.loadDefaultSources())
satelliteRepo.updateEntriesSelection(defaultCatNums, true)
passPredictor.forceCalculation(satelliteRepo.getSelectedSatellites(), stationPos)
preferencesSource.setSetupDone()
_isFirstLaunchDone.value = true
}
@@ -78,7 +80,8 @@ class SatPassViewModel @Inject constructor(
viewModelScope.launch {
_passes.postValue(Result.InProgress)
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.SatItem
import com.rtbishop.look4sat.framework.model.SatTrans
import com.rtbishop.look4sat.domain.model.SatEntry as DomainEntry
import com.rtbishop.look4sat.domain.model.SatItem as DomainItem
import com.rtbishop.look4sat.domain.model.SatTrans as DomainTrans
import com.rtbishop.look4sat.domain.SatEntry as DomainEntry
import com.rtbishop.look4sat.domain.SatItem as DomainItem
import com.rtbishop.look4sat.domain.SatTrans as DomainTrans
object DataMapper {
@@ -17,13 +17,13 @@
*/
package com.rtbishop.look4sat.data
import com.rtbishop.look4sat.domain.model.SatEntry
import com.rtbishop.look4sat.domain.model.SatItem
import com.rtbishop.look4sat.domain.model.SatTrans
import com.rtbishop.look4sat.domain.predict4kotlin.Satellite
import com.rtbishop.look4sat.domain.SatEntry
import com.rtbishop.look4sat.domain.SatItem
import com.rtbishop.look4sat.domain.SatTrans
import com.rtbishop.look4sat.domain.Satellite
import kotlinx.coroutines.flow.Flow
interface SatDataLocalSource {
interface LocalDataSource {
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
import com.rtbishop.look4sat.domain.predict4kotlin.StationPos
import com.rtbishop.look4sat.domain.StationPos
interface PreferencesSource {
@@ -17,10 +17,10 @@
*/
package com.rtbishop.look4sat.data
import com.rtbishop.look4sat.domain.model.SatTrans
import com.rtbishop.look4sat.domain.SatTrans
import java.io.InputStream
interface SatDataRemoteSource {
interface RemoteDataSource {
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
import com.rtbishop.look4sat.domain.model.SatEntry
import com.rtbishop.look4sat.domain.model.SatItem
import com.rtbishop.look4sat.domain.model.SatTrans
import com.rtbishop.look4sat.domain.predict4kotlin.Satellite
import com.rtbishop.look4sat.domain.predict4kotlin.TLE
import com.rtbishop.look4sat.domain.SatEntry
import com.rtbishop.look4sat.domain.SatItem
import com.rtbishop.look4sat.domain.SatTrans
import com.rtbishop.look4sat.domain.Satellite
import com.rtbishop.look4sat.domain.TLE
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
@@ -30,10 +30,10 @@ import kotlinx.coroutines.withContext
import java.io.InputStream
import java.util.zip.ZipInputStream
class SatDataRepository(
class SatelliteRepo(
private val preferencesSource: PreferencesSource,
private val satLocalSource: SatDataLocalSource,
private val satRemoteSource: SatDataRemoteSource,
private val localDataSource: LocalDataSource,
private val remoteDataSource: RemoteDataSource,
private val ioDispatcher: CoroutineDispatcher
) {
@@ -46,19 +46,19 @@ class SatDataRepository(
}
fun getSatItems(): Flow<List<SatItem>> {
return satLocalSource.getSatItems()
return localDataSource.getSatItems()
}
fun getSatTransmitters(catNum: Int): Flow<List<SatTrans>> {
return satLocalSource.getSatTransmitters(catNum)
return localDataSource.getSatTransmitters(catNum)
}
suspend fun getSelectedSatellites(): List<Satellite> {
return satLocalSource.getSelectedSatellites()
return localDataSource.getSelectedSatellites()
}
suspend fun updateEntriesFromFile(stream: InputStream) = withContext(ioDispatcher) {
satLocalSource.updateEntries(importSatEntries(stream))
localDataSource.updateEntries(importSatEntries(stream))
}
suspend fun updateEntriesFromWeb(sources: List<String>) {
@@ -67,7 +67,7 @@ class SatDataRepository(
val streams = mutableListOf<InputStream>()
val entries = mutableListOf<SatEntry>()
sources.forEach { source ->
satRemoteSource.fetchDataStream(source)?.let { stream ->
remoteDataSource.fetchDataStream(source)?.let { stream ->
if (source.contains(".zip", true)) {
val zipStream = ZipInputStream(stream).apply { nextEntry }
streams.add(zipStream)
@@ -77,17 +77,17 @@ class SatDataRepository(
}
}
streams.forEach { stream -> entries.addAll(importSatEntries(stream)) }
satLocalSource.updateEntries(entries)
localDataSource.updateEntries(entries)
}
launch(ioDispatcher) {
val transmitters = satRemoteSource.fetchTransmitters().filter { it.isAlive }
satLocalSource.updateTransmitters(transmitters)
val transmitters = remoteDataSource.fetchTransmitters().filter { it.isAlive }
localDataSource.updateTransmitters(transmitters)
}
}
}
suspend fun updateEntriesSelection(catNums: List<Int>, isSelected: Boolean) {
satLocalSource.updateEntriesSelection(catNums, isSelected)
localDataSource.updateEntriesSelection(catNums, isSelected)
}
private fun importSatEntries(stream: InputStream): List<SatEntry> {
@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.predict4kotlin
package com.rtbishop.look4sat.domain
import kotlin.math.*
@@ -15,6 +15,6 @@
* 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
package com.rtbishop.look4sat.domain
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
* 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.*
@@ -15,11 +15,11 @@
* 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
package com.rtbishop.look4sat.domain
import kotlin.math.round
class QthConverter {
object QthConverter {
fun qthToPosition(qthString: String): GeoPos? {
val trimmedQth = qthString.take(6)
@@ -15,10 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.framework.model
import com.rtbishop.look4sat.domain.predict4kotlin.GeoPos
import com.rtbishop.look4sat.domain.predict4kotlin.Satellite
package com.rtbishop.look4sat.domain
data class SatData(
val pass: Satellite,
@@ -15,9 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.model
import com.rtbishop.look4sat.domain.predict4kotlin.TLE
package com.rtbishop.look4sat.domain
data class SatEntry(
val tle: TLE,
@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.model
package com.rtbishop.look4sat.domain
data class SatItem(
val catNum: Int,
@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.predict4kotlin
package com.rtbishop.look4sat.domain
data class SatPass(
val aosTime: Long,
@@ -26,8 +26,7 @@ data class SatPass(
val tcaAzimuth: Double,
val altitude: Double,
val maxElevation: Double,
val satellite: Satellite,
val predictor: PassPredictor,
val satellite: Satellite
) {
val catNum: Int = satellite.params.catnum
val name: String = satellite.params.name
@@ -15,9 +15,8 @@
* 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
package com.rtbishop.look4sat.domain
import java.util.*
import kotlin.math.*
data class SatPos(
@@ -29,7 +28,7 @@ data class SatPos(
var range: Double = 0.0,
var rangeRate: Double = 0.0,
var theta: Double = 0.0,
var time: Date = Date()
var time: Long = 0L
) {
private val earthRadiusKm = 6378.16
private val speedOfLight = 2.99792458E8
@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.model
package com.rtbishop.look4sat.domain
data class SatTrans(
val uuid: String,
@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.predict4kotlin
package com.rtbishop.look4sat.domain
import java.util.*
import java.util.concurrent.atomic.AtomicReference
@@ -41,6 +41,10 @@ abstract class Satellite(val params: TLE) {
var s4 = 0.0
val xke = 7.43669161E-2
fun getQuarterOrbitMin(): Int {
return (24.0 * 60.0 / params.meanmo / 4.0).toInt()
}
fun willBeSeen(pos: StationPos): Boolean {
return if (params.meanmo < 1e-8) false else {
var lin = params.incl
@@ -51,11 +55,7 @@ abstract class Satellite(val params: TLE) {
}
}
fun getPredictor(pos: StationPos): PassPredictor {
return PassPredictor(this, pos)
}
fun getPosition(pos: StationPos, time: Date): SatPos {
fun getPosition(pos: StationPos, time: Long): SatPos {
val satPos = SatPos()
// Date/time at which the position and velocity were calculated
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)
private fun calcCurrentDaynum(date: Date): Double {
val now = date.time
private fun calcCurrentDaynum(now: Long): Double {
val sgp4Epoch = Calendar.getInstance(TimeZone.getTimeZone("UTC:UTC"))
sgp4Epoch.clear()
sgp4Epoch[1979, 11, 31, 0, 0] = 0
@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.predict4kotlin
package com.rtbishop.look4sat.domain
data class StationPos(
val latitude: Double,
@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.predict4kotlin
package com.rtbishop.look4sat.domain
import java.io.InputStream
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
import com.rtbishop.look4sat.domain.predict4kotlin.TLE
import com.rtbishop.look4sat.domain.TLE
import org.junit.Test
class Look4SatTest {
@@ -17,44 +17,42 @@
*/
package com.rtbishop.look4sat
import com.rtbishop.look4sat.domain.predict4kotlin.QthConverter
import com.rtbishop.look4sat.domain.QthConverter
import org.junit.Test
class QthConverterTest {
private val qthConverter = QthConverter()
@Test
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)
result = qthConverter.qthToPosition("JN58TD")
result = QthConverter.qthToPosition("JN58TD")
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)
result = qthConverter.qthToPosition("fm18LW")
result = QthConverter.qthToPosition("fm18LW")
assert(result?.latitude == 38.9375 && result.longitude == -77.0417)
}
@Test
fun `Given invalid QTH returns null`() {
var result = qthConverter.qthToPosition("ZZ00zz")
var result = QthConverter.qthToPosition("ZZ00zz")
assert(result == null)
result = qthConverter.qthToPosition("JN58tz")
result = QthConverter.qthToPosition("JN58tz")
assert(result == null)
}
@Test
fun `Given valid POS returns correct QTH`() {
assert(qthConverter.positionToQTH(51.4878, -0.2146) == "IO91vl")
assert(qthConverter.positionToQTH(48.1466, 11.6083) == "JN58td")
assert(qthConverter.positionToQTH(-34.91, -56.2116) == "GF15vc")
assert(qthConverter.positionToQTH(38.92, -77.065) == "FM18lw")
assert(QthConverter.positionToQTH(51.4878, -0.2146) == "IO91vl")
assert(QthConverter.positionToQTH(48.1466, 11.6083) == "JN58td")
assert(QthConverter.positionToQTH(-34.91, -56.2116) == "GF15vc")
assert(QthConverter.positionToQTH(38.92, -77.065) == "FM18lw")
}
@Test
fun `Given invalid POS returns null`() {
assert(qthConverter.positionToQTH(91.0542, -170.1142) == null)
assert(qthConverter.positionToQTH(89.0542, -240.1142) == null)
assert(QthConverter.positionToQTH(91.0542, -170.1142) == null)
assert(QthConverter.positionToQTH(89.0542, -240.1142) == null)
}
}