mirror of
https://github.com/rt-bishop/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
Further refactoring of Look4Sat core module classes
This commit is contained in:
1 parent
2c757a6b35
commit
bceeb01194
46 files changed
+603
-572
No files matched your search
+5
-5
@@ -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 {
|
||||
|
||||
|
||||
+2
-2
@@ -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
-17
@@ -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> {
|
||||
+1
-1
@@ -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.*
|
||||
|
||||
+1
-1
@@ -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)
|
||||
+1
-1
@@ -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.*
|
||||
|
||||
+2
-2
@@ -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)
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
data class SatData(
|
||||
val pass: Satellite,
|
||||
val catNum: Int,
|
||||
val name: String,
|
||||
val range: Double,
|
||||
val altitude: Double,
|
||||
val velocity: Double,
|
||||
val qthLoc: String,
|
||||
val osmPos: GeoPos
|
||||
)
|
||||
+1
-3
@@ -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,
|
||||
+1
-1
@@ -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,
|
||||
+2
-3
@@ -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
|
||||
+2
-3
@@ -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
|
||||
+1
-1
@@ -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,
|
||||
+7
-8
@@ -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
|
||||
+1
-1
@@ -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,
|
||||
+1
-1
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user