mirror of
https://github.com/rt-bishop/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
Further changes to Look4Sat Predictor.kt core class
This commit is contained in:
1 parent
bceeb01194
commit
a82915c0ca
8 files changed
+314
-496
No files matched your search
@@ -22,6 +22,7 @@ import android.content.SharedPreferences
|
|||||||
import android.location.LocationManager
|
import android.location.LocationManager
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import com.rtbishop.look4sat.data.*
|
import com.rtbishop.look4sat.data.*
|
||||||
|
import com.rtbishop.look4sat.domain.Predictor
|
||||||
import com.rtbishop.look4sat.framework.PreferencesProvider
|
import com.rtbishop.look4sat.framework.PreferencesProvider
|
||||||
import com.rtbishop.look4sat.framework.api.RemoteSource
|
import com.rtbishop.look4sat.framework.api.RemoteSource
|
||||||
import com.rtbishop.look4sat.framework.api.SatelliteService
|
import com.rtbishop.look4sat.framework.api.SatelliteService
|
||||||
@@ -63,8 +64,8 @@ object SatelliteDataModule {
|
|||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providePassPredictor(@DefaultDispatcher dispatcher: CoroutineDispatcher): PassPredictor {
|
fun providePredictor(@DefaultDispatcher dispatcher: CoroutineDispatcher): Predictor {
|
||||||
return PassPredictor(dispatcher)
|
return Predictor(dispatcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|||||||
+74
-87
@@ -18,28 +18,22 @@
|
|||||||
package com.rtbishop.look4sat.presentation.satMapScreen
|
package com.rtbishop.look4sat.presentation.satMapScreen
|
||||||
|
|
||||||
import androidx.lifecycle.*
|
import androidx.lifecycle.*
|
||||||
|
import com.rtbishop.look4sat.domain.Predictor
|
||||||
import com.rtbishop.look4sat.data.PreferencesSource
|
import com.rtbishop.look4sat.data.PreferencesSource
|
||||||
import com.rtbishop.look4sat.data.SatelliteRepo
|
import com.rtbishop.look4sat.data.SatelliteRepo
|
||||||
import com.rtbishop.look4sat.data.PassPredictor
|
import com.rtbishop.look4sat.domain.*
|
||||||
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 dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import org.osmdroid.views.MapView
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.pow
|
import kotlin.math.max
|
||||||
import kotlin.math.sqrt
|
import kotlin.math.min
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SatMapViewModel @Inject constructor(
|
class SatMapViewModel @Inject constructor(
|
||||||
private val satelliteRepo: SatelliteRepo,
|
private val satelliteRepo: SatelliteRepo,
|
||||||
private val passPredictor: PassPredictor,
|
private val predictor: Predictor,
|
||||||
private val preferences: PreferencesSource,
|
private val preferences: PreferencesSource,
|
||||||
@DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher
|
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val stationPos = preferences.loadStationPosition()
|
private val stationPos = preferences.loadStationPosition()
|
||||||
@@ -99,100 +93,93 @@ class SatMapViewModel @Inject constructor(
|
|||||||
dataUpdateJob?.cancelAndJoin()
|
dataUpdateJob?.cancelAndJoin()
|
||||||
dataUpdateJob = launch {
|
dataUpdateJob = launch {
|
||||||
val dateNow = Date()
|
val dateNow = Date()
|
||||||
setSelectedSatTrack(selectedSat, stationPos, dateNow)
|
getSatTrack(selectedSat, stationPos, dateNow)
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
dateNow.time = System.currentTimeMillis()
|
dateNow.time = System.currentTimeMillis()
|
||||||
setSatPositions(allSatList, stationPos, dateNow)
|
getPositions(allSatList, stationPos, dateNow)
|
||||||
setSelectedSatFootprint(selectedSat, stationPos, dateNow)
|
getSatFootprint(selectedSat, stationPos, dateNow)
|
||||||
setSelectedSatData(selectedSat, stationPos, dateNow)
|
getSatData(selectedSat, stationPos, dateNow)
|
||||||
delay(updateFreq)
|
delay(updateFreq)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun setSatPositions(list: List<Satellite>, gsp: StationPos, date: Date) {
|
private suspend fun getSatTrack(satellite: Satellite, pos: StationPos, date: Date) {
|
||||||
withContext(defaultDispatcher) {
|
val satTracks = mutableListOf<List<GeoPos>>()
|
||||||
val satPositions = mutableMapOf<Satellite, GeoPos>()
|
val currentTrack = mutableListOf<GeoPos>()
|
||||||
list.forEach { satellite ->
|
val endDate = Date(date.time + (satellite.orbitalPeriod * 2.4 * 60000L).toLong())
|
||||||
val satPos = satellite.getPosition(gsp, date.time)
|
var oldLongitude = 0.0
|
||||||
val osmLat = clipLat(Math.toDegrees(satPos.latitude))
|
predictor.getSatTrack(satellite, pos, date, endDate).forEach { satPos ->
|
||||||
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<List<GeoPos>>()
|
|
||||||
val currentTrack = mutableListOf<GeoPos>()
|
|
||||||
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<GeoPos>().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<GeoPos>().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)
|
|
||||||
val osmLat = clipLat(Math.toDegrees(satPos.latitude))
|
val osmLat = clipLat(Math.toDegrees(satPos.latitude))
|
||||||
val osmLon = clipLon(Math.toDegrees(satPos.longitude))
|
val osmLon = clipLon(Math.toDegrees(satPos.longitude))
|
||||||
val osmPos = GeoPos(osmLat, osmLon)
|
val currentPosition = GeoPos(osmLat, osmLon)
|
||||||
val qthLoc = preferences.positionToQTH(osmPos.latitude, osmPos.longitude) ?: "-- --"
|
if (oldLongitude < -170.0 && currentPosition.longitude > 170.0) {
|
||||||
val velocity = getOrbitalVelocity(satPos.altitude)
|
// adding left terminal position
|
||||||
val satData = SatData(
|
currentTrack.add(GeoPos(osmLat, -180.0))
|
||||||
sat, sat.params.catnum, sat.params.name, satPos.range,
|
val finishedTrack = mutableListOf<GeoPos>().apply { addAll(currentTrack) }
|
||||||
satPos.altitude, velocity, qthLoc, osmPos
|
satTracks.add(finishedTrack)
|
||||||
)
|
currentTrack.clear()
|
||||||
this@SatMapViewModel._satData.postValue(satData)
|
} else if (oldLongitude > 170.0 && currentPosition.longitude < -170.0) {
|
||||||
|
// adding right terminal position
|
||||||
|
currentTrack.add(GeoPos(osmLat, 180.0))
|
||||||
|
val finishedTrack = mutableListOf<GeoPos>().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<Satellite>, pos: StationPos, date: Date) {
|
||||||
|
val positions = mutableMapOf<Satellite, GeoPos>()
|
||||||
|
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 {
|
private fun clipLat(latitude: Double): Double {
|
||||||
return MapView.getTileSystem().cleanLatitude(latitude)
|
return clip(latitude, -85.05, 85.05)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun clipLon(longitude: Double): Double {
|
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 {
|
private fun clip(currentValue: Double, minValue: Double, maxValue: Double): Double {
|
||||||
val earthG = 6.674 * 10.0.pow(-11)
|
return min(max(currentValue, minValue), maxValue)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+3
-3
@@ -18,7 +18,7 @@
|
|||||||
package com.rtbishop.look4sat.presentation.satPassInfoScreen
|
package com.rtbishop.look4sat.presentation.satPassInfoScreen
|
||||||
|
|
||||||
import androidx.lifecycle.*
|
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.PreferencesSource
|
||||||
import com.rtbishop.look4sat.data.SatelliteRepo
|
import com.rtbishop.look4sat.data.SatelliteRepo
|
||||||
import com.rtbishop.look4sat.injection.IoDispatcher
|
import com.rtbishop.look4sat.injection.IoDispatcher
|
||||||
@@ -38,7 +38,7 @@ import javax.inject.Inject
|
|||||||
class PassInfoViewModel @Inject constructor(
|
class PassInfoViewModel @Inject constructor(
|
||||||
private val orientationProvider: OrientationProvider,
|
private val orientationProvider: OrientationProvider,
|
||||||
private val preferences: PreferencesSource,
|
private val preferences: PreferencesSource,
|
||||||
private val passPredictor: PassPredictor,
|
private val predictor: Predictor,
|
||||||
private val satelliteRepo: SatelliteRepo,
|
private val satelliteRepo: SatelliteRepo,
|
||||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
|
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
|
||||||
) : ViewModel(), OrientationProvider.OrientationListener {
|
) : ViewModel(), OrientationProvider.OrientationListener {
|
||||||
@@ -50,7 +50,7 @@ class PassInfoViewModel @Inject constructor(
|
|||||||
val orientation: LiveData<Triple<Float, Float, Float>> = _orientation
|
val orientation: LiveData<Triple<Float, Float, Float>> = _orientation
|
||||||
|
|
||||||
fun getPass(catNum: Int, aosTime: Long) = liveData {
|
fun getPass(catNum: Int, aosTime: Long) = liveData {
|
||||||
passPredictor.passes.collect { passes ->
|
predictor.passes.collect { passes ->
|
||||||
val pass = passes.find { it.catNum == catNum && it.aosTime == aosTime }
|
val pass = passes.find { it.catNum == catNum && it.aosTime == aosTime }
|
||||||
pass?.let { satPass ->
|
pass?.let { satPass ->
|
||||||
processTransmitters(satPass)
|
processTransmitters(satPass)
|
||||||
|
|||||||
+6
-6
@@ -23,7 +23,7 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.rtbishop.look4sat.data.PreferencesSource
|
import com.rtbishop.look4sat.data.PreferencesSource
|
||||||
import com.rtbishop.look4sat.data.SatelliteRepo
|
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.domain.SatPass
|
||||||
import com.rtbishop.look4sat.framework.model.Result
|
import com.rtbishop.look4sat.framework.model.Result
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
@@ -34,7 +34,7 @@ import javax.inject.Inject
|
|||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SatPassViewModel @Inject constructor(
|
class SatPassViewModel @Inject constructor(
|
||||||
private val satelliteRepo: SatelliteRepo,
|
private val satelliteRepo: SatelliteRepo,
|
||||||
private val passPredictor: PassPredictor,
|
private val predictor: Predictor,
|
||||||
private val preferencesSource: PreferencesSource
|
private val preferencesSource: PreferencesSource
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
@@ -49,13 +49,13 @@ class SatPassViewModel @Inject constructor(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_passes.postValue(Result.InProgress)
|
_passes.postValue(Result.InProgress)
|
||||||
val stationPos = preferencesSource.loadStationPosition()
|
val stationPos = preferencesSource.loadStationPosition()
|
||||||
passPredictor.triggerCalculation(satelliteRepo.getSelectedSatellites(), stationPos)
|
predictor.triggerCalculation(satelliteRepo.getSelectedSatellites(), stationPos)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_isFirstLaunchDone.value = false
|
_isFirstLaunchDone.value = false
|
||||||
}
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
passPredictor.passes.collect { passes ->
|
predictor.passes.collect { passes ->
|
||||||
passesProcessing?.cancelAndJoin()
|
passesProcessing?.cancelAndJoin()
|
||||||
passesProcessing = viewModelScope.launch { tickPasses(passes) }
|
passesProcessing = viewModelScope.launch { tickPasses(passes) }
|
||||||
}
|
}
|
||||||
@@ -70,7 +70,7 @@ class SatPassViewModel @Inject constructor(
|
|||||||
val stationPos = preferencesSource.loadStationPosition()
|
val stationPos = preferencesSource.loadStationPosition()
|
||||||
satelliteRepo.updateEntriesFromWeb(preferencesSource.loadDefaultSources())
|
satelliteRepo.updateEntriesFromWeb(preferencesSource.loadDefaultSources())
|
||||||
satelliteRepo.updateEntriesSelection(defaultCatNums, true)
|
satelliteRepo.updateEntriesSelection(defaultCatNums, true)
|
||||||
passPredictor.forceCalculation(satelliteRepo.getSelectedSatellites(), stationPos)
|
predictor.forceCalculation(satelliteRepo.getSelectedSatellites(), stationPos)
|
||||||
preferencesSource.setSetupDone()
|
preferencesSource.setSetupDone()
|
||||||
_isFirstLaunchDone.value = true
|
_isFirstLaunchDone.value = true
|
||||||
}
|
}
|
||||||
@@ -81,7 +81,7 @@ class SatPassViewModel @Inject constructor(
|
|||||||
_passes.postValue(Result.InProgress)
|
_passes.postValue(Result.InProgress)
|
||||||
passesProcessing?.cancelAndJoin()
|
passesProcessing?.cancelAndJoin()
|
||||||
val stationPos = preferencesSource.loadStationPosition()
|
val stationPos = preferencesSource.loadStationPosition()
|
||||||
passPredictor.forceCalculation(satelliteRepo.getSelectedSatellites(), stationPos)
|
predictor.forceCalculation(satelliteRepo.getSelectedSatellites(), stationPos)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 <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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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<List<SatPass>>(replay = 1)
|
||||||
|
private var selectedSatIds = emptyList<Int>()
|
||||||
|
val passes: SharedFlow<List<SatPass>> = _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<SatPos> =
|
||||||
|
withContext(predictorDispatcher) {
|
||||||
|
val positions = mutableListOf<SatPos>()
|
||||||
|
var currentTime = start.time
|
||||||
|
while (currentTime < end.time) {
|
||||||
|
positions.add(sat.getPosition(pos, currentTime))
|
||||||
|
currentTime += 15000
|
||||||
|
}
|
||||||
|
return@withContext positions
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun triggerCalculation(
|
||||||
|
satellites: List<Satellite>,
|
||||||
|
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<Satellite>,
|
||||||
|
pos: StationPos,
|
||||||
|
date: Date = Date(),
|
||||||
|
hoursAhead: Int = 8,
|
||||||
|
minElevation: Double = 16.0
|
||||||
|
) {
|
||||||
|
if (satellites.isEmpty()) {
|
||||||
|
_passes.emit(emptyList())
|
||||||
|
} else {
|
||||||
|
withContext(predictorDispatcher) {
|
||||||
|
val allPasses = mutableListOf<SatPass>()
|
||||||
|
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<SatPass> {
|
||||||
|
val passes = mutableListOf<SatPass>()
|
||||||
|
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<SatPass>.filter(date: Date, hoursAhead: Int, minElev: Double): List<SatPass> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,6 +41,13 @@ data class SatPos(
|
|||||||
return (freq.toDouble() * (speedOfLight + this.rangeRate * 1000.0) / speedOfLight).toLong()
|
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<GeoPos> {
|
fun getRangeCircle(): List<GeoPos> {
|
||||||
val positions = mutableListOf<GeoPos>()
|
val positions = mutableListOf<GeoPos>()
|
||||||
val lat = this.latitude
|
val lat = this.latitude
|
||||||
|
|||||||
@@ -31,26 +31,24 @@ abstract class Satellite(val params: TLE) {
|
|||||||
private val position = Vector4()
|
private val position = Vector4()
|
||||||
private val velocity = Vector4()
|
private val velocity = Vector4()
|
||||||
private var perigee = 0.0
|
private var perigee = 0.0
|
||||||
|
val orbitalPeriod = 24 * 60 / params.meanmo
|
||||||
val earthRadius = 6378.137
|
val earthRadius = 6378.137
|
||||||
val j3Harmonic = -2.53881E-6
|
val j3Harmonic = -2.53881E-6
|
||||||
val twoPi = Math.PI * 2.0
|
val twoPi = Math.PI * 2.0
|
||||||
val twoThirds = 2.0 / 3.0
|
val twoThirds = 2.0 / 3.0
|
||||||
|
val xke = 7.43669161E-2
|
||||||
val ck2 = 5.413079E-4
|
val ck2 = 5.413079E-4
|
||||||
val ck4 = 6.209887E-7
|
val ck4 = 6.209887E-7
|
||||||
var qoms24 = 0.0
|
var qoms24 = 0.0
|
||||||
var s4 = 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 {
|
fun willBeSeen(pos: StationPos): Boolean {
|
||||||
return if (params.meanmo < 1e-8) false else {
|
return if (params.meanmo < 1e-8) false
|
||||||
var lin = params.incl
|
else {
|
||||||
if (lin >= 90.0) lin = 180.0 - lin
|
|
||||||
val sma = 331.25 * exp(ln(1440.0 / params.meanmo) * (2.0 / 3.0))
|
val sma = 331.25 * exp(ln(1440.0 / params.meanmo) * (2.0 / 3.0))
|
||||||
val apogee = sma * (1.0 + params.eccn) - earthRadius
|
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)
|
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.
|
// 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)
|
calculateObs(julUTC, position, velocity, pos, squintVector, satPos)
|
||||||
calculateLatLonAlt(julUTC, satPos, position)
|
calculateLatLonAlt(julUTC, satPos, position)
|
||||||
satPos.time = time
|
return satPos.apply { this.time = time }
|
||||||
return satPos
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the system clock and return the number of days since 31Dec79 00:00:00 UTC (daynum 0)
|
// Read the system clock and return the number of days since 31Dec79 00:00:00 UTC (daynum 0)
|
||||||
|
|||||||
Reference in new issue
Block a user