From a82915c0ca43d471210560aa1668eab17dbaf3a4 Mon Sep 17 00:00:00 2001 From: Arty Bishop Date: Thu, 16 Sep 2021 10:03:58 +0100 Subject: [PATCH] Further changes to Look4Sat Predictor.kt core class --- .../look4sat/injection/SatelliteDataModule.kt | 5 +- .../satMapScreen/SatMapViewModel.kt | 161 ++++---- .../satPassInfoScreen/PassInfoViewModel.kt | 6 +- .../satPassScreen/SatPassViewModel.kt | 12 +- .../rtbishop/look4sat/data/PassPredictor.kt | 388 ------------------ .../com/rtbishop/look4sat/domain/Predictor.kt | 214 ++++++++++ .../com/rtbishop/look4sat/domain/SatPos.kt | 7 + .../com/rtbishop/look4sat/domain/Satellite.kt | 17 +- 8 files changed, 314 insertions(+), 496 deletions(-) delete mode 100644 core/src/main/java/com/rtbishop/look4sat/data/PassPredictor.kt create mode 100644 core/src/main/java/com/rtbishop/look4sat/domain/Predictor.kt diff --git a/app/src/main/java/com/rtbishop/look4sat/injection/SatelliteDataModule.kt b/app/src/main/java/com/rtbishop/look4sat/injection/SatelliteDataModule.kt index c7ddaaf6..44f63fdb 100644 --- a/app/src/main/java/com/rtbishop/look4sat/injection/SatelliteDataModule.kt +++ b/app/src/main/java/com/rtbishop/look4sat/injection/SatelliteDataModule.kt @@ -22,6 +22,7 @@ import android.content.SharedPreferences import android.location.LocationManager import androidx.room.Room import com.rtbishop.look4sat.data.* +import com.rtbishop.look4sat.domain.Predictor import com.rtbishop.look4sat.framework.PreferencesProvider import com.rtbishop.look4sat.framework.api.RemoteSource import com.rtbishop.look4sat.framework.api.SatelliteService @@ -63,8 +64,8 @@ object SatelliteDataModule { @Provides @Singleton - fun providePassPredictor(@DefaultDispatcher dispatcher: CoroutineDispatcher): PassPredictor { - return PassPredictor(dispatcher) + fun providePredictor(@DefaultDispatcher dispatcher: CoroutineDispatcher): Predictor { + return Predictor(dispatcher) } @Provides diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/satMapScreen/SatMapViewModel.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/satMapScreen/SatMapViewModel.kt index 4b049e57..67883477 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/satMapScreen/SatMapViewModel.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/satMapScreen/SatMapViewModel.kt @@ -18,28 +18,22 @@ package com.rtbishop.look4sat.presentation.satMapScreen import androidx.lifecycle.* +import com.rtbishop.look4sat.domain.Predictor import com.rtbishop.look4sat.data.PreferencesSource 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 com.rtbishop.look4sat.domain.* import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.* -import org.osmdroid.views.MapView import java.util.* import javax.inject.Inject -import kotlin.math.pow -import kotlin.math.sqrt +import kotlin.math.max +import kotlin.math.min @HiltViewModel class SatMapViewModel @Inject constructor( private val satelliteRepo: SatelliteRepo, - private val passPredictor: PassPredictor, + private val predictor: Predictor, private val preferences: PreferencesSource, - @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher ) : ViewModel() { private val stationPos = preferences.loadStationPosition() @@ -99,100 +93,93 @@ class SatMapViewModel @Inject constructor( dataUpdateJob?.cancelAndJoin() dataUpdateJob = launch { val dateNow = Date() - setSelectedSatTrack(selectedSat, stationPos, dateNow) + getSatTrack(selectedSat, stationPos, dateNow) while (isActive) { dateNow.time = System.currentTimeMillis() - setSatPositions(allSatList, stationPos, dateNow) - setSelectedSatFootprint(selectedSat, stationPos, dateNow) - setSelectedSatData(selectedSat, stationPos, dateNow) + getPositions(allSatList, stationPos, dateNow) + getSatFootprint(selectedSat, stationPos, dateNow) + getSatData(selectedSat, stationPos, dateNow) delay(updateFreq) } } } } - private suspend fun setSatPositions(list: List, gsp: StationPos, date: Date) { - withContext(defaultDispatcher) { - val satPositions = mutableMapOf() - list.forEach { satellite -> - 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) - } - _satPositions.postValue(satPositions) - } - } - - private suspend fun setSelectedSatTrack(sat: Satellite, gsp: StationPos, date: Date) { - withContext(defaultDispatcher) { - val satTracks = mutableListOf>() - val currentTrack = mutableListOf() - var oldLongitude = 0.0 - 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) - if (oldLongitude < -170.0 && currentPosition.longitude > 170.0) { - // adding left terminal position - currentTrack.add(GeoPos(osmLat, -180.0)) - val finishedTrack = mutableListOf().apply { addAll(currentTrack) } - satTracks.add(finishedTrack) - currentTrack.clear() - } else if (oldLongitude > 170.0 && currentPosition.longitude < -170.0) { - // adding right terminal position - currentTrack.add(GeoPos(osmLat, 180.0)) - val finishedTrack = mutableListOf().apply { addAll(currentTrack) } - satTracks.add(finishedTrack) - currentTrack.clear() - } - oldLongitude = currentPosition.longitude - currentTrack.add(currentPosition) - } - satTracks.add(currentTrack) - _satTrack.postValue(satTracks) - } - } - - private suspend fun setSelectedSatFootprint(sat: Satellite, gsp: StationPos, date: Date) { - withContext(defaultDispatcher) { - val satFootprint = sat.getPosition(gsp, date.time).getRangeCircle().map { rangePos -> - val osmLat = clipLat(rangePos.latitude) - val osmLon = clipLon(rangePos.longitude) - GeoPos(osmLat, osmLon) - } - _satFootprint.postValue(satFootprint) - } - } - - private suspend fun setSelectedSatData(sat: Satellite, gsp: StationPos, date: Date) { - withContext(defaultDispatcher) { - val satPos = sat.getPosition(gsp, date.time) + private suspend fun getSatTrack(satellite: Satellite, pos: StationPos, date: Date) { + val satTracks = mutableListOf>() + val currentTrack = mutableListOf() + val endDate = Date(date.time + (satellite.orbitalPeriod * 2.4 * 60000L).toLong()) + var oldLongitude = 0.0 + predictor.getSatTrack(satellite, pos, date, endDate).forEach { satPos -> val osmLat = clipLat(Math.toDegrees(satPos.latitude)) val osmLon = clipLon(Math.toDegrees(satPos.longitude)) - val osmPos = GeoPos(osmLat, osmLon) - 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, - satPos.altitude, velocity, qthLoc, osmPos - ) - this@SatMapViewModel._satData.postValue(satData) + val currentPosition = GeoPos(osmLat, osmLon) + if (oldLongitude < -170.0 && currentPosition.longitude > 170.0) { + // adding left terminal position + currentTrack.add(GeoPos(osmLat, -180.0)) + val finishedTrack = mutableListOf().apply { addAll(currentTrack) } + satTracks.add(finishedTrack) + currentTrack.clear() + } else if (oldLongitude > 170.0 && currentPosition.longitude < -170.0) { + // adding right terminal position + currentTrack.add(GeoPos(osmLat, 180.0)) + val finishedTrack = mutableListOf().apply { addAll(currentTrack) } + satTracks.add(finishedTrack) + currentTrack.clear() + } + oldLongitude = currentPosition.longitude + currentTrack.add(currentPosition) } + satTracks.add(currentTrack) + _satTrack.postValue(satTracks) + } + + private suspend fun getPositions(satellites: List, pos: StationPos, date: Date) { + val positions = mutableMapOf() + satellites.forEach { satellite -> + val satPos = predictor.getSatPos(satellite, pos, date) + val osmLat = clipLat(Math.toDegrees(satPos.latitude)) + val osmLon = clipLon(Math.toDegrees(satPos.longitude)) + positions[satellite] = GeoPos(osmLat, osmLon) + } + _satPositions.postValue(positions) + } + + private suspend fun getSatFootprint(satellite: Satellite, pos: StationPos, date: Date) { + val satPos = predictor.getSatPos(satellite, pos, date) + val satFootprint = satPos.getRangeCircle().map { rangePos -> + val osmLat = clipLat(rangePos.latitude) + val osmLon = clipLon(rangePos.longitude) + GeoPos(osmLat, osmLon) + } + _satFootprint.postValue(satFootprint) + } + + private suspend fun getSatData(satellite: Satellite, pos: StationPos, date: Date) { + val satPos = predictor.getSatPos(satellite, pos, date) + 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 satData = SatData( + satellite, satellite.params.catnum, satellite.params.name, satPos.range, + satPos.altitude, satPos.getOrbitalVelocity(), qthLoc, osmPos + ) + _satData.postValue(satData) } private fun clipLat(latitude: Double): Double { - return MapView.getTileSystem().cleanLatitude(latitude) + return clip(latitude, -85.05, 85.05) } private fun clipLon(longitude: Double): Double { - return MapView.getTileSystem().cleanLongitude(longitude) + var result = longitude + while (result < -180.0) result += 360.0 + while (result > 180.0) result -= 360.0 + return clip(result, -180.0, 180.0) } - 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 clip(currentValue: Double, minValue: Double, maxValue: Double): Double { + return min(max(currentValue, minValue), maxValue) } } diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/satPassInfoScreen/PassInfoViewModel.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/satPassInfoScreen/PassInfoViewModel.kt index a94b7307..d9fc80ac 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/satPassInfoScreen/PassInfoViewModel.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/satPassInfoScreen/PassInfoViewModel.kt @@ -18,7 +18,7 @@ package com.rtbishop.look4sat.presentation.satPassInfoScreen import androidx.lifecycle.* -import com.rtbishop.look4sat.data.PassPredictor +import com.rtbishop.look4sat.domain.Predictor import com.rtbishop.look4sat.data.PreferencesSource import com.rtbishop.look4sat.data.SatelliteRepo import com.rtbishop.look4sat.injection.IoDispatcher @@ -38,7 +38,7 @@ import javax.inject.Inject class PassInfoViewModel @Inject constructor( private val orientationProvider: OrientationProvider, private val preferences: PreferencesSource, - private val passPredictor: PassPredictor, + private val predictor: Predictor, private val satelliteRepo: SatelliteRepo, @IoDispatcher private val ioDispatcher: CoroutineDispatcher ) : ViewModel(), OrientationProvider.OrientationListener { @@ -50,7 +50,7 @@ class PassInfoViewModel @Inject constructor( val orientation: LiveData> = _orientation fun getPass(catNum: Int, aosTime: Long) = liveData { - passPredictor.passes.collect { passes -> + predictor.passes.collect { passes -> val pass = passes.find { it.catNum == catNum && it.aosTime == aosTime } pass?.let { satPass -> processTransmitters(satPass) diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/satPassScreen/SatPassViewModel.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/satPassScreen/SatPassViewModel.kt index b81ebb7e..9a3743d9 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/satPassScreen/SatPassViewModel.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/satPassScreen/SatPassViewModel.kt @@ -23,7 +23,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.rtbishop.look4sat.data.PreferencesSource import com.rtbishop.look4sat.data.SatelliteRepo -import com.rtbishop.look4sat.data.PassPredictor +import com.rtbishop.look4sat.domain.Predictor import com.rtbishop.look4sat.domain.SatPass import com.rtbishop.look4sat.framework.model.Result import dagger.hilt.android.lifecycle.HiltViewModel @@ -34,7 +34,7 @@ import javax.inject.Inject @HiltViewModel class SatPassViewModel @Inject constructor( private val satelliteRepo: SatelliteRepo, - private val passPredictor: PassPredictor, + private val predictor: Predictor, private val preferencesSource: PreferencesSource ) : ViewModel() { @@ -49,13 +49,13 @@ class SatPassViewModel @Inject constructor( viewModelScope.launch { _passes.postValue(Result.InProgress) val stationPos = preferencesSource.loadStationPosition() - passPredictor.triggerCalculation(satelliteRepo.getSelectedSatellites(), stationPos) + predictor.triggerCalculation(satelliteRepo.getSelectedSatellites(), stationPos) } } else { _isFirstLaunchDone.value = false } viewModelScope.launch { - passPredictor.passes.collect { passes -> + predictor.passes.collect { passes -> passesProcessing?.cancelAndJoin() passesProcessing = viewModelScope.launch { tickPasses(passes) } } @@ -70,7 +70,7 @@ class SatPassViewModel @Inject constructor( val stationPos = preferencesSource.loadStationPosition() satelliteRepo.updateEntriesFromWeb(preferencesSource.loadDefaultSources()) satelliteRepo.updateEntriesSelection(defaultCatNums, true) - passPredictor.forceCalculation(satelliteRepo.getSelectedSatellites(), stationPos) + predictor.forceCalculation(satelliteRepo.getSelectedSatellites(), stationPos) preferencesSource.setSetupDone() _isFirstLaunchDone.value = true } @@ -81,7 +81,7 @@ class SatPassViewModel @Inject constructor( _passes.postValue(Result.InProgress) passesProcessing?.cancelAndJoin() val stationPos = preferencesSource.loadStationPosition() - passPredictor.forceCalculation(satelliteRepo.getSelectedSatellites(), stationPos) + predictor.forceCalculation(satelliteRepo.getSelectedSatellites(), stationPos) } } diff --git a/core/src/main/java/com/rtbishop/look4sat/data/PassPredictor.kt b/core/src/main/java/com/rtbishop/look4sat/data/PassPredictor.kt deleted file mode 100644 index 5e2b1b06..00000000 --- a/core/src/main/java/com/rtbishop/look4sat/data/PassPredictor.kt +++ /dev/null @@ -1,388 +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 . - */ -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(replay = 1) - val skyPosition: SharedFlow = _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>(replay = 1) - val skyTrack: SharedFlow> = _skyTrack - - suspend fun getSkyTrack(pass: SatPass, stationPos: StationPos) { - withContext(defaultDispatcher) { - val positions = mutableListOf() - 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>(replay = 1) - val mapPositions: SharedFlow> = _mapPositions - - suspend fun getMapPositions(satellites: List, stationPos: StationPos, date: Date) { - withContext(defaultDispatcher) { - val positions = mutableMapOf() - 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>>(replay = 1) - val mapTrack: SharedFlow>> = _mapTrack - - suspend fun getMapTrack(satellite: Satellite, stationPos: StationPos, date: Date) { - withContext(defaultDispatcher) { - val positions = mutableListOf() - 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>() - val currentTrack = mutableListOf() - 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().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().apply { addAll(currentTrack) }) - currentTrack.clear() - } - oldLongitude = currentPos.longitude - currentTrack.add(currentPos) - } - satTracks.add(currentTrack) - _mapTrack.emit(satTracks) - } - } - - private val _mapFootprint = MutableSharedFlow>(replay = 1) - val mapFootprint: SharedFlow> = _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(replay = 1) - val satData: SharedFlow = _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 { - val positions = mutableListOf() - 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>(replay = 1) - private var selectedSatellites = emptyList() - val passes: SharedFlow> = _passes - - suspend fun triggerCalculation( - satellites: List, - 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, - stationPos: StationPos, - refDate: Date = Date(), - hoursAhead: Int = 8 - ) { - if (satellites.isEmpty()) { - _passes.emit(emptyList()) - } else { - withContext(defaultDispatcher) { - val allPasses = mutableListOf() - 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 { - val passes = mutableListOf() - 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, - refDate: Date, - hoursAhead: Int = 8, - minElevation: Double = 16.0 - ): List { - 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) - } -} diff --git a/core/src/main/java/com/rtbishop/look4sat/domain/Predictor.kt b/core/src/main/java/com/rtbishop/look4sat/domain/Predictor.kt new file mode 100644 index 00000000..fe097675 --- /dev/null +++ b/core/src/main/java/com/rtbishop/look4sat/domain/Predictor.kt @@ -0,0 +1,214 @@ +/* + * 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 . + */ +package com.rtbishop.look4sat.domain + +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.* + +class Predictor(private val predictorDispatcher: CoroutineDispatcher) { + + private val _passes = MutableSharedFlow>(replay = 1) + private var selectedSatIds = emptyList() + val passes: SharedFlow> = _passes + + suspend fun getSatPos(sat: Satellite, pos: StationPos, date: Date): SatPos = + withContext(predictorDispatcher) { + return@withContext sat.getPosition(pos, date.time) + } + + suspend fun getSatTrack(sat: Satellite, pos: StationPos, start: Date, end: Date): List = + withContext(predictorDispatcher) { + val positions = mutableListOf() + var currentTime = start.time + while (currentTime < end.time) { + positions.add(sat.getPosition(pos, currentTime)) + currentTime += 15000 + } + return@withContext positions + } + + suspend fun triggerCalculation( + satellites: List, + pos: StationPos, + date: Date = Date(), + hoursAhead: Int = 8, + minElevation: Double = 16.0 + ) { + if (satellites.isEmpty()) { + _passes.emit(emptyList()) + } else { + val newCatNums = satellites.map { it.params.catnum } + if (selectedSatIds != newCatNums) { + forceCalculation(satellites, pos, date, hoursAhead, minElevation) + } + } + } + + suspend fun forceCalculation( + satellites: List, + pos: StationPos, + date: Date = Date(), + hoursAhead: Int = 8, + minElevation: Double = 16.0 + ) { + if (satellites.isEmpty()) { + _passes.emit(emptyList()) + } else { + withContext(predictorDispatcher) { + val allPasses = mutableListOf() + selectedSatIds = satellites.map { it.params.catnum } + satellites.forEach { satellite -> + allPasses.addAll(satellite.getPasses(pos, date, hoursAhead)) + } + _passes.emit(allPasses.filter(date, hoursAhead, minElevation)) + } + } + } + + private fun Satellite.getPasses(pos: StationPos, date: Date, hours: Int): List { + val passes = mutableListOf() + val endDate = Date(date.time + hours * 60L * 60L * 1000L) + val quarterOrbitMin = (this.orbitalPeriod / 4.0).toInt() + var startDate = date + var shouldRewind = true + var lastAosDate: Date + var count = 0 + if (this.willBeSeen(pos)) { + if (this.params.isDeepspace) { + passes.add(getGeoPass(this, pos, date)) + } else { + do { + if (count > 0) shouldRewind = false + val pass = getLeoPass(this, pos, startDate, shouldRewind) + lastAosDate = Date(pass.aosTime) + passes.add(pass) + startDate = Date(pass.losTime + (quarterOrbitMin * 3) * 60L * 1000L) + count++ + } while (lastAosDate < endDate) + } + } + return passes + } + + private fun List.filter(date: Date, hoursAhead: Int, minElev: Double): List { + val timeFuture = date.time + (hoursAhead * 60L * 60L * 1000L) + return this.filter { it.losTime > date.time } + .filter { it.aosTime < timeFuture } + .filter { it.maxElevation > minElev } + .sortedBy { it.aosTime } + } + + private fun getGeoPass(sat: Satellite, pos: StationPos, date: Date): SatPass { + val satPos = sat.getPosition(pos, date.time) + val aos = Date(date.time - 24 * 60L * 60L * 1000L).time + val los = Date(date.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, sat) + } + + private fun getLeoPass(sat: Satellite, pos: StationPos, date: Date, rewind: Boolean): SatPass { + val quarterOrbitMin = (sat.orbitalPeriod / 4.0).toInt() + val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")).apply { + clear() + timeInMillis = date.time + } + var elevation: Double + var maxElevation = 0.0 + var alt = 0.0 + var tcaAz = 0.0 + // rewind 1/4 of an orbit + if (rewind) calendar.add(Calendar.MINUTE, -quarterOrbitMin) + + var satPos = sat.getPosition(pos, 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 = sat.getPosition(pos, 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 = sat.getPosition(pos, 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 = sat.getPosition(pos, 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 = sat.getPosition(pos, 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 = sat.getPosition(pos, 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, sat) + } +} diff --git a/core/src/main/java/com/rtbishop/look4sat/domain/SatPos.kt b/core/src/main/java/com/rtbishop/look4sat/domain/SatPos.kt index 0969f65c..ed89a4dc 100644 --- a/core/src/main/java/com/rtbishop/look4sat/domain/SatPos.kt +++ b/core/src/main/java/com/rtbishop/look4sat/domain/SatPos.kt @@ -41,6 +41,13 @@ data class SatPos( return (freq.toDouble() * (speedOfLight + this.rangeRate * 1000.0) / speedOfLight).toLong() } + fun getOrbitalVelocity(): 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 + } + fun getRangeCircle(): List { val positions = mutableListOf() val lat = this.latitude diff --git a/core/src/main/java/com/rtbishop/look4sat/domain/Satellite.kt b/core/src/main/java/com/rtbishop/look4sat/domain/Satellite.kt index fc4ec662..0279716e 100644 --- a/core/src/main/java/com/rtbishop/look4sat/domain/Satellite.kt +++ b/core/src/main/java/com/rtbishop/look4sat/domain/Satellite.kt @@ -31,26 +31,24 @@ abstract class Satellite(val params: TLE) { private val position = Vector4() private val velocity = Vector4() private var perigee = 0.0 + val orbitalPeriod = 24 * 60 / params.meanmo val earthRadius = 6378.137 val j3Harmonic = -2.53881E-6 val twoPi = Math.PI * 2.0 val twoThirds = 2.0 / 3.0 + val xke = 7.43669161E-2 val ck2 = 5.413079E-4 val ck4 = 6.209887E-7 var qoms24 = 0.0 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 - if (lin >= 90.0) lin = 180.0 - lin + return if (params.meanmo < 1e-8) false + else { val sma = 331.25 * exp(ln(1440.0 / params.meanmo) * (2.0 / 3.0)) val apogee = sma * (1.0 + params.eccn) - earthRadius + var lin = params.incl + if (lin >= 90.0) lin = 180.0 - lin acos(earthRadius / (apogee + earthRadius)) + lin * deg2Rad > abs(pos.latitude * deg2Rad) } } @@ -71,8 +69,7 @@ abstract class Satellite(val params: TLE) { // Angles in rads, dist in km, vel in km/S. Calculate sat Az, El, Range and Range-rate. calculateObs(julUTC, position, velocity, pos, squintVector, satPos) calculateLatLonAlt(julUTC, satPos, position) - satPos.time = time - return satPos + return satPos.apply { this.time = time } } // Read the system clock and return the number of days since 31Dec79 00:00:00 UTC (daynum 0)