mirror of
https://github.com/rt-bishop/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
Moved and renamed several classes
This commit is contained in:
1 parent
20bf1cedd0
commit
29c3b2f2f1
29 files changed
+3
-3
No files matched your search
@@ -1 +0,0 @@
|
||||
/build
|
||||
@@ -1,21 +0,0 @@
|
||||
plugins {
|
||||
id "kotlin"
|
||||
}
|
||||
|
||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0"
|
||||
implementation "org.json:json:20211205"
|
||||
|
||||
testImplementation "junit:junit:4.13.2"
|
||||
testImplementation "org.mockito:mockito-core:4.3.1"
|
||||
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0"
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 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.model.SatRadio
|
||||
import com.rtbishop.look4sat.domain.predict.OrbitalData
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.InputStream
|
||||
import kotlin.math.pow
|
||||
|
||||
class DataParser(private val parserDispatcher: CoroutineDispatcher) {
|
||||
|
||||
suspend fun parseCSVStream(csvStream: InputStream): List<OrbitalData> = withContext(parserDispatcher) {
|
||||
val parsedItems = mutableListOf<OrbitalData>()
|
||||
csvStream.bufferedReader().useLines { lines ->
|
||||
lines.forEachIndexed { index, line ->
|
||||
if (index != 0) {
|
||||
val values = line.split(",")
|
||||
parseCSV(values)?.let { tle -> parsedItems.add(tle) }
|
||||
}
|
||||
}
|
||||
}
|
||||
return@withContext parsedItems
|
||||
}
|
||||
|
||||
suspend fun parseTLEStream(tleStream: InputStream): List<OrbitalData> = withContext(parserDispatcher) {
|
||||
val tleStrings = mutableListOf(String(), String(), String())
|
||||
val parsedItems = mutableListOf<OrbitalData>()
|
||||
var lineIndex = 0
|
||||
tleStream.bufferedReader().forEachLine { line ->
|
||||
tleStrings[lineIndex] = line
|
||||
if (lineIndex < 2) {
|
||||
lineIndex++
|
||||
} else {
|
||||
val isLineOneValid = tleStrings[1].substring(0, 1) == "1"
|
||||
val isLineTwoValid = tleStrings[2].substring(0, 1) == "2"
|
||||
if (!isLineOneValid && !isLineTwoValid) return@forEachLine
|
||||
parseTLE(tleStrings)?.let { tle -> parsedItems.add(tle) }
|
||||
lineIndex = 0
|
||||
}
|
||||
}
|
||||
return@withContext parsedItems
|
||||
}
|
||||
|
||||
suspend fun parseJSONStream(jsonStream: InputStream): List<SatRadio> {
|
||||
return withContext(parserDispatcher) {
|
||||
val parsedItems = mutableListOf<SatRadio>()
|
||||
try {
|
||||
val jsonArray = JSONArray(jsonStream.bufferedReader().readText())
|
||||
for (index in 0 until jsonArray.length()) {
|
||||
val jsonObject = jsonArray.getJSONObject(index)
|
||||
parseJSON(jsonObject)?.let { parsedItems.add(it) }
|
||||
}
|
||||
return@withContext parsedItems
|
||||
} catch (exception: Exception) {
|
||||
return@withContext parsedItems
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseCSV(values: List<String>): OrbitalData? {
|
||||
try {
|
||||
val name = values[0]
|
||||
val year = values[2].substring(0, 4)
|
||||
val month = values[2].substring(5, 7)
|
||||
val dayOfMonth = values[2].substring(8, 10)
|
||||
val dayInt = getDayOfYear(year.toInt(), month.toInt(), dayOfMonth.toInt())
|
||||
val day = if (dayInt < 10) "00$dayInt" else if (dayInt < 100) "0$dayInt" else "$dayInt"
|
||||
val hour = values[2].substring(11, 13).toInt() * 3600000 // ms in one hour
|
||||
val min = values[2].substring(14, 16).toInt() * 60000 // ms in one minute
|
||||
val sec = values[2].substring(17, 19).toInt() * 1000 // ms in one second
|
||||
val ms = values[2].substring(20, 26).toInt() / 1000.0 // microseconds to ms
|
||||
val frac = ((hour + min + sec + ms) / 86400000.0).toString()
|
||||
val epoch = "${year.substring(2)}$day${frac.substring(1)}".toDouble()
|
||||
val meanmo = values[3].toDouble()
|
||||
val eccn = values[4].toDouble()
|
||||
val incl = values[5].toDouble()
|
||||
val raan = values[6].toDouble()
|
||||
val argper = values[7].toDouble()
|
||||
val meanan = values[8].toDouble()
|
||||
val catnum = values[11].toInt()
|
||||
val bstar = values[14].toDouble()
|
||||
return OrbitalData(name, epoch, meanmo, eccn, incl, raan, argper, meanan, catnum, bstar)
|
||||
} catch (exception: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseTLE(tle: List<String>): OrbitalData? {
|
||||
if (tle[1].substring(0, 1) != "1" && tle[2].substring(0, 1) != "2") {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
val name: String = tle[0].trim()
|
||||
val epoch: Double = tle[1].substring(18, 32).toDouble()
|
||||
val meanmo: Double = tle[2].substring(52, 63).toDouble()
|
||||
val eccn: Double = tle[2].substring(26, 33).toDouble() / 10000000.0
|
||||
val incl: Double = tle[2].substring(8, 16).toDouble()
|
||||
val raan: Double = tle[2].substring(17, 25).toDouble()
|
||||
val argper: Double = tle[2].substring(34, 42).toDouble()
|
||||
val meanan: Double = tle[2].substring(43, 51).toDouble()
|
||||
val catnum: Int = tle[1].substring(2, 7).trim().toInt()
|
||||
val bstar: Double = 1.0e-5 * tle[1].substring(53, 59).toDouble() /
|
||||
10.0.pow(tle[1].substring(60, 61).toDouble())
|
||||
return OrbitalData(name, epoch, meanmo, eccn, incl, raan, argper, meanan, catnum, bstar)
|
||||
} catch (exception: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseJSON(json: JSONObject): SatRadio? {
|
||||
try {
|
||||
val uuid = json.getString("uuid")
|
||||
val info = json.getString("description")
|
||||
val isAlive = json.getBoolean("alive")
|
||||
val downlink = if (json.isNull("downlink_low")) null
|
||||
else json.getLong("downlink_low")
|
||||
val uplink = if (json.isNull("uplink_low")) null
|
||||
else json.getLong("uplink_low")
|
||||
val mode = if (json.isNull("mode")) null
|
||||
else json.getString("mode")
|
||||
val isInverted = json.getBoolean("invert")
|
||||
val catnum = if (json.isNull("norad_cat_id")) null
|
||||
else json.getInt("norad_cat_id")
|
||||
return SatRadio(uuid, info, isAlive, downlink, uplink, mode, isInverted, catnum)
|
||||
} catch (exception: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDayOfYear(year: Int, month: Int, dayOfMonth: Int): Int {
|
||||
if (month == 1) return dayOfMonth
|
||||
val daysArray = arrayOf(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
|
||||
var dayOfYear = dayOfMonth
|
||||
// If leap year increment Feb days
|
||||
if (((year / 4 == 0) && (year / 100 != 0)) || (year / 400 == 0)) daysArray[1]++
|
||||
for (i in 0 until month - 1) { dayOfYear += daysArray[i] }
|
||||
return dayOfYear
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 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 java.io.InputStream
|
||||
|
||||
interface IProvider {
|
||||
|
||||
val radioApi: String get() = "https://db.satnogs.org/api/transmitters/?format=json"
|
||||
|
||||
suspend fun getLocalFileStream(uri: String): InputStream?
|
||||
|
||||
suspend fun getRemoteFileStream(url: String): InputStream?
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 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.model.SatEntry
|
||||
import com.rtbishop.look4sat.domain.model.SatItem
|
||||
import com.rtbishop.look4sat.domain.model.SatRadio
|
||||
import com.rtbishop.look4sat.domain.predict.Satellite
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface IStorage {
|
||||
|
||||
fun getEntriesTotal(): Flow<Int>
|
||||
|
||||
fun getRadiosTotal(): Flow<Int>
|
||||
|
||||
suspend fun getEntriesWithModes(): List<SatItem>
|
||||
|
||||
suspend fun getEntriesWithIds(ids: List<Int>): List<Satellite>
|
||||
|
||||
suspend fun getRadiosWithId(id: Int): List<SatRadio>
|
||||
|
||||
suspend fun insertEntries(entries: List<SatEntry>)
|
||||
|
||||
suspend fun insertRadios(radios: List<SatRadio>)
|
||||
|
||||
suspend fun clearAllData()
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 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.IRepository
|
||||
import com.rtbishop.look4sat.domain.model.DataState
|
||||
import com.rtbishop.look4sat.domain.model.SatEntry
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import java.io.InputStream
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
class Repository(
|
||||
private val dataParser: DataParser,
|
||||
private val storage: IStorage,
|
||||
private val provider: IProvider,
|
||||
private val repoScope: CoroutineScope
|
||||
) : IRepository {
|
||||
|
||||
private val exceptionHandler = CoroutineExceptionHandler { _, exception ->
|
||||
_updateState.value = DataState.Error(exception.message)
|
||||
}
|
||||
private val updateStateDelay = 875L
|
||||
private val _updateState = MutableStateFlow<DataState<Long>>(DataState.Handled)
|
||||
override val updateState: StateFlow<DataState<Long>> = _updateState
|
||||
|
||||
override fun getEntriesTotal() = storage.getEntriesTotal()
|
||||
|
||||
override fun getRadiosTotal() = storage.getRadiosTotal()
|
||||
|
||||
override suspend fun getEntriesWithModes() = storage.getEntriesWithModes()
|
||||
|
||||
override suspend fun getEntriesWithIds(ids: List<Int>) = storage.getEntriesWithIds(ids)
|
||||
|
||||
override suspend fun getRadiosWithId(id: Int) = storage.getRadiosWithId(id)
|
||||
|
||||
override fun updateFromFile(uri: String) {
|
||||
repoScope.launch(exceptionHandler) {
|
||||
_updateState.value = DataState.Loading
|
||||
provider.getLocalFileStream(uri)?.let { fileStream ->
|
||||
delay(updateStateDelay)
|
||||
storage.insertEntries(importSatellites(fileStream))
|
||||
}
|
||||
_updateState.value = DataState.Success(0L)
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateFromWeb(urls: List<String>) {
|
||||
_updateState.value = DataState.Loading
|
||||
repoScope.launch(exceptionHandler) {
|
||||
val jobsMap = mutableMapOf<String, Deferred<InputStream?>>()
|
||||
val streamsMap = mutableMapOf<String, InputStream?>()
|
||||
val streams = mutableListOf<InputStream>()
|
||||
val entries = mutableListOf<SatEntry>()
|
||||
urls.forEach { jobsMap[it] = async { provider.getRemoteFileStream(it) } }
|
||||
jobsMap.forEach { job -> streamsMap[job.key] = job.value.await() }
|
||||
streamsMap.forEach { stream ->
|
||||
stream.value?.let { inputStream ->
|
||||
when {
|
||||
stream.key.contains("=csv", true) -> {
|
||||
val tles = dataParser.parseCSVStream(inputStream)
|
||||
entries.addAll(tles.map { tle -> SatEntry(tle) })
|
||||
}
|
||||
stream.key.contains(".zip", true) -> {
|
||||
streams.add(ZipInputStream(inputStream).apply { nextEntry })
|
||||
}
|
||||
else -> streams.add(inputStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
streams.forEach { stream -> entries.addAll(importSatellites(stream)) }
|
||||
storage.insertEntries(entries)
|
||||
}
|
||||
repoScope.launch(exceptionHandler) {
|
||||
provider.getRemoteFileStream(provider.radioApi)?.let { stream ->
|
||||
storage.insertRadios(dataParser.parseJSONStream(stream))
|
||||
_updateState.value = DataState.Success(0L)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setUpdateStateHandled() {
|
||||
_updateState.value = DataState.Handled
|
||||
}
|
||||
|
||||
override fun clearAllData() {
|
||||
repoScope.launch {
|
||||
_updateState.value = DataState.Loading
|
||||
delay(updateStateDelay)
|
||||
storage.clearAllData()
|
||||
_updateState.value = DataState.Success(0L)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun importSatellites(stream: InputStream): List<SatEntry> {
|
||||
return dataParser.parseTLEStream(stream).map { tle -> SatEntry(tle) }
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 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 kotlinx.coroutines.*
|
||||
import java.net.InetSocketAddress
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.channels.SocketChannel
|
||||
|
||||
class DataReporter(reporterDispatcher: CoroutineDispatcher) {
|
||||
|
||||
private val reporterScope = CoroutineScope(reporterDispatcher)
|
||||
private var rotationSocketChannel: SocketChannel? = null
|
||||
private var rotationReporting: Job? = null
|
||||
|
||||
// private var frequencySocketChannel: SocketChannel? = null
|
||||
// private var frequencyReporting: Job? = null
|
||||
//
|
||||
// fun reportFrequency(server: String, port: Int, frequency: Long) {
|
||||
// frequencyReporting = reporterScope.launch {
|
||||
// runCatching {
|
||||
// if (frequencySocketChannel == null) {
|
||||
// frequencySocketChannel = SocketChannel.open(InetSocketAddress(server, port))
|
||||
// } else {
|
||||
// val buffer = ByteBuffer.wrap("\\set_freq $frequency\n".toByteArray())
|
||||
// frequencySocketChannel?.write(buffer)
|
||||
// }
|
||||
// }.onFailure { error ->
|
||||
// println(error.localizedMessage)
|
||||
// frequencySocketChannel = null
|
||||
// frequencyReporting?.cancelAndJoin()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
fun reportRotation(server: String, port: Int, azimuth: Double, elevation: Double) {
|
||||
rotationReporting = reporterScope.launch {
|
||||
runCatching {
|
||||
if (rotationSocketChannel == null) {
|
||||
rotationSocketChannel = SocketChannel.open(InetSocketAddress(server, port))
|
||||
} else {
|
||||
val buffer = ByteBuffer.wrap("\\set_pos $azimuth $elevation\n".toByteArray())
|
||||
rotationSocketChannel?.write(buffer)
|
||||
}
|
||||
}.onFailure { error ->
|
||||
println(error.localizedMessage)
|
||||
rotationSocketChannel = null
|
||||
rotationReporting?.cancelAndJoin()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 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 java.net.InetSocketAddress
|
||||
import java.net.Socket
|
||||
import java.security.MessageDigest
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.regex.Pattern
|
||||
|
||||
fun Long.toTimerString(): String {
|
||||
val format = "%02d:%02d:%02d"
|
||||
val hours = TimeUnit.MILLISECONDS.toHours(this)
|
||||
val minutes = TimeUnit.MILLISECONDS.toMinutes(this) % 60
|
||||
val seconds = TimeUnit.MILLISECONDS.toSeconds(this) % 60
|
||||
return String.format(format, hours, minutes, seconds)
|
||||
}
|
||||
|
||||
fun Double.round(decimals: Int): Double {
|
||||
var multiplier = 1.0
|
||||
repeat(decimals) { multiplier *= 10 }
|
||||
return kotlin.math.round(this * multiplier) / multiplier
|
||||
}
|
||||
|
||||
fun String.getHash(type: String = "SHA-256"): String {
|
||||
val hexChars = "0123456789ABCDEF"
|
||||
val bytes = MessageDigest.getInstance(type).digest(this.toByteArray())
|
||||
val result = StringBuilder(bytes.size * 2)
|
||||
bytes.forEach {
|
||||
val i = it.toInt()
|
||||
result.append(hexChars[i shr 4 and 0x0f])
|
||||
result.append(hexChars[i and 0x0f])
|
||||
}
|
||||
return result.toString()
|
||||
}
|
||||
|
||||
fun String.isValidEmail(): Boolean {
|
||||
val expression = "^[\\w.-]+@([\\w\\-]+\\.)+[A-Z]{2,8}$"
|
||||
val pattern = Pattern.compile(expression, Pattern.CASE_INSENSITIVE)
|
||||
return pattern.matcher(this).matches()
|
||||
}
|
||||
|
||||
fun String.isValidIPv4(): Boolean {
|
||||
val ip4 = "^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(\\.(?!\$)|\$)){4}\$"
|
||||
return this.matches(ip4.toRegex())
|
||||
}
|
||||
|
||||
fun String.isValidPort(): Boolean {
|
||||
return this.isNotEmpty() && this.toInt() in 1024..65535
|
||||
}
|
||||
|
||||
fun ping(hostname: String, port: Int): Int {
|
||||
val start = System.currentTimeMillis()
|
||||
val socket = Socket()
|
||||
try {
|
||||
socket.connect(InetSocketAddress(hostname, port), 5000)
|
||||
socket.close()
|
||||
} catch (exception: Exception) {
|
||||
exception.printStackTrace()
|
||||
println("Failed to ping: $hostname")
|
||||
return Int.MAX_VALUE
|
||||
}
|
||||
return (System.currentTimeMillis() - start).toInt()
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 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.model.DataState
|
||||
import com.rtbishop.look4sat.domain.predict.GeoPos
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
|
||||
interface ILocationHandler {
|
||||
|
||||
val stationPosition: SharedFlow<DataState<GeoPos>>
|
||||
|
||||
fun getStationLocator(): String
|
||||
|
||||
fun getStationPosition(): GeoPos
|
||||
|
||||
fun setStationPosition(latitude: Double, longitude: Double)
|
||||
|
||||
fun setPositionFromGps()
|
||||
|
||||
fun setPositionFromNet()
|
||||
|
||||
fun setPositionFromQth(locator: String)
|
||||
|
||||
fun setPositionHandled()
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 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.model.DataState
|
||||
import com.rtbishop.look4sat.domain.model.SatItem
|
||||
import com.rtbishop.look4sat.domain.model.SatRadio
|
||||
import com.rtbishop.look4sat.domain.predict.Satellite
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface IRepository {
|
||||
|
||||
val updateState: StateFlow<DataState<Long>>
|
||||
|
||||
fun getEntriesTotal(): Flow<Int>
|
||||
|
||||
fun getRadiosTotal(): Flow<Int>
|
||||
|
||||
suspend fun getEntriesWithModes(): List<SatItem>
|
||||
|
||||
suspend fun getEntriesWithIds(ids: List<Int>): List<Satellite>
|
||||
|
||||
suspend fun getRadiosWithId(id: Int): List<SatRadio>
|
||||
|
||||
fun updateFromFile(uri: String)
|
||||
|
||||
fun updateFromWeb(urls: List<String>)
|
||||
|
||||
fun setUpdateStateHandled()
|
||||
|
||||
fun clearAllData()
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 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.predict.GeoPos
|
||||
|
||||
interface ISettings {
|
||||
|
||||
val defaultSources: List<String>
|
||||
get() = listOf(
|
||||
"https://www.prismnet.com/~mmccants/tles/inttles.zip",
|
||||
"https://www.prismnet.com/~mmccants/tles/classfd.zip",
|
||||
"https://celestrak.com/NORAD/elements/gp.php?GROUP=active&FORMAT=csv",
|
||||
"https://amsat.org/tle/current/nasabare.txt"
|
||||
)
|
||||
|
||||
fun loadStationLocator(): String
|
||||
|
||||
fun saveStationLocator(locator: String)
|
||||
|
||||
fun loadStationPosition(): GeoPos
|
||||
|
||||
fun saveStationPosition(position: GeoPos)
|
||||
|
||||
fun getHoursAhead(): Int
|
||||
|
||||
fun setHoursAhead(hoursAhead: Int)
|
||||
|
||||
fun getMinElevation(): Double
|
||||
|
||||
fun setMinElevation(minElevation: Double)
|
||||
|
||||
fun getUseUTC(): Boolean
|
||||
|
||||
fun setUseUTC(value: Boolean)
|
||||
|
||||
fun getUseCompass(): Boolean
|
||||
|
||||
fun setUseCompass(value: Boolean)
|
||||
|
||||
fun getShowSweep(): Boolean
|
||||
|
||||
fun setShowSweep(value: Boolean)
|
||||
|
||||
fun saveModesSelection(modes: List<String>)
|
||||
|
||||
fun loadModesSelection(): List<String>
|
||||
|
||||
fun saveEntriesSelection(catnums: List<Int>)
|
||||
|
||||
fun loadEntriesSelection(): List<Int>
|
||||
|
||||
fun getRotatorEnabled(): Boolean
|
||||
|
||||
fun setRotatorEnabled(value: Boolean)
|
||||
|
||||
fun getRotatorServer(): String
|
||||
|
||||
fun setRotatorServer(value: String)
|
||||
|
||||
fun getRotatorPort(): String
|
||||
|
||||
fun setRotatorPort(value: String)
|
||||
|
||||
fun loadDataSources(): List<String>
|
||||
|
||||
fun saveDataSources(sources: List<String>)
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 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.predict.GeoPos
|
||||
|
||||
object QthConverter {
|
||||
|
||||
fun qthToPosition(locator: String): GeoPos? {
|
||||
val trimmedQth = locator.take(6)
|
||||
if (!isValidLocator(trimmedQth)) return null
|
||||
val lonFirst = (trimmedQth[0].uppercaseChar().code - 65) * 20
|
||||
val latFirst = (trimmedQth[1].uppercaseChar().code - 65) * 10
|
||||
val lonSecond = trimmedQth[2].toString().toInt() * 2
|
||||
val latSecond = trimmedQth[3].toString().toInt()
|
||||
val lonThird = (((trimmedQth[4].lowercaseChar().code - 97) / 12.0) + (1.0 / 24.0)) - 180
|
||||
val latThird = (((trimmedQth[5].lowercaseChar().code - 97) / 24.0) + (1.0 / 48.0)) - 90
|
||||
val longitude = (lonFirst + lonSecond + lonThird).round(4)
|
||||
val latitude = (latFirst + latSecond + latThird).round(4)
|
||||
return GeoPos(latitude, longitude)
|
||||
}
|
||||
|
||||
fun positionToQth(lat: Double, lon: Double): String? {
|
||||
if (!isValidPosition(lat, lon)) return null
|
||||
val tempLon = if (lon > 180.0) lon - 180 else lon
|
||||
val upper = "ABCDEFGHIJKLMNOPQRSTUVWX"
|
||||
val lower = "abcdefghijklmnopqrstuvwx"
|
||||
val longitude = tempLon + 180
|
||||
val latitude = lat + 90
|
||||
val lonFirst = upper[(longitude / 20).toInt()]
|
||||
val latFirst = upper[(latitude / 10).toInt()]
|
||||
val lonSecond = ((longitude / 2) % 10).toInt().toString()
|
||||
val latSecond = (latitude % 10).toInt().toString()
|
||||
val lonThird = lower[((longitude % 2) * 12).toInt()]
|
||||
val latThird = lower[((latitude % 1) * 24).toInt()]
|
||||
return "$lonFirst$latFirst$lonSecond$latSecond$lonThird$latThird"
|
||||
}
|
||||
|
||||
fun isValidPosition(lat: Double, lon: Double): Boolean {
|
||||
return (lat >= -90.0 && lat <= 90.0) && (lon >= -180.0 && lon <= 360.0)
|
||||
}
|
||||
|
||||
fun isValidLocator(locator: String): Boolean {
|
||||
return locator.matches("[a-xA-X][a-xA-X][0-9][0-9][a-xA-X][a-xA-X]".toRegex())
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 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.model
|
||||
|
||||
sealed class DataState<out T> {
|
||||
data class Success<out T>(val data: T) : DataState<T>()
|
||||
data class Error(val message: String?) : DataState<Nothing>()
|
||||
object Handled : DataState<Nothing>()
|
||||
object Loading : DataState<Nothing>()
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 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.model
|
||||
|
||||
import com.rtbishop.look4sat.domain.predict.OrbitalData
|
||||
|
||||
data class SatEntry(val data: OrbitalData, var comment: String? = null)
|
||||
@@ -1,25 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 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.model
|
||||
|
||||
data class SatItem(
|
||||
val catnum: Int,
|
||||
val name: String,
|
||||
val modes: List<String>,
|
||||
var isSelected: Boolean
|
||||
)
|
||||
@@ -1,30 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 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.model
|
||||
|
||||
data class SatRadio(
|
||||
val uuid: String,
|
||||
val info: String,
|
||||
val isAlive: Boolean,
|
||||
var downlink: Long?,
|
||||
var uplink: Long?,
|
||||
val mode: String?,
|
||||
val isInverted: Boolean,
|
||||
val catnum: Int?,
|
||||
var comment: String? = null
|
||||
)
|
||||
@@ -1,849 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 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.predict
|
||||
|
||||
import kotlin.math.*
|
||||
|
||||
class DeepSpaceSat(data: OrbitalData) : Satellite(data) {
|
||||
|
||||
private val c1: Double
|
||||
private val c4: Double
|
||||
private val x1mth2: Double
|
||||
private val x3thm1: Double
|
||||
private val xlcof: Double
|
||||
private val xnodcf: Double
|
||||
private val t2cof: Double
|
||||
private val aycof: Double
|
||||
private val x7thm1: Double
|
||||
private val deep: DeepSpaceCalculator
|
||||
private val dsv = DeepSpaceValueObject()
|
||||
|
||||
init {
|
||||
// Recover original mean motion (xnodp) and semimajor axis (aodp) from input elements
|
||||
val a1 = (xke / super.data.xno).pow(twoThirds)
|
||||
dsv.cosio = cos(super.data.xincl)
|
||||
dsv.theta2 = dsv.cosio * dsv.cosio
|
||||
x3thm1 = 3.0 * dsv.theta2 - 1
|
||||
dsv.eosq = super.data.eccn * super.data.eccn
|
||||
dsv.betao2 = 1.0 - dsv.eosq
|
||||
dsv.betao = sqrt(dsv.betao2)
|
||||
val del1 = 1.5 * ck2 * x3thm1 / (a1 * a1 * dsv.betao * dsv.betao2)
|
||||
val ao = a1 * (1.0 - del1 * (0.5 * twoThirds + del1 * (1.0 + 134.0 / 81.0 * del1)))
|
||||
val delo = 1.5 * ck2 * x3thm1 / (ao * ao * dsv.betao * dsv.betao2)
|
||||
dsv.xnodp = super.data.xno / (1.0 + delo)
|
||||
dsv.aodp = ao / (1.0 - delo)
|
||||
// For perigee below 156 km, the values of S and QOMS2T are altered
|
||||
setPerigee((dsv.aodp * (1.0 - super.data.eccn) - 1.0) * earthRadius)
|
||||
val pinvsq = invert(dsv.aodp * dsv.aodp * dsv.betao2 * dsv.betao2)
|
||||
dsv.sing = sin(super.data.omegao)
|
||||
dsv.cosg = cos(super.data.omegao)
|
||||
val tsi = invert(dsv.aodp - s4)
|
||||
val eta = dsv.aodp * super.data.eccn * tsi
|
||||
val etasq = eta * eta
|
||||
val eeta = super.data.eccn * eta
|
||||
val psisq = abs(1.0 - etasq)
|
||||
val coef = qoms24 * tsi.pow(4.0)
|
||||
val coef1 = coef / psisq.pow(3.5)
|
||||
val c2 = coef1 * dsv.xnodp * (dsv.aodp * (1.0 + 1.5 * etasq + eeta * (4.0 + etasq))
|
||||
+ 0.75 * ck2 * tsi / psisq * x3thm1 * (8.0 + 3.0 * etasq * (8.0 + etasq)))
|
||||
c1 = super.data.bstar * c2
|
||||
dsv.sinio = sin(super.data.xincl)
|
||||
val a3ovk2 = -j3Harmonic / ck2
|
||||
x1mth2 = 1.0 - dsv.theta2
|
||||
c4 =
|
||||
2 * dsv.xnodp * coef1 * dsv.aodp * dsv.betao2 * (eta * (2.0 + 0.5 * etasq) + super.data.eccn
|
||||
* (0.5 + 2 * etasq) - 2 * ck2 * tsi / (dsv.aodp * psisq)
|
||||
* (-3 * x3thm1 * (1.0 - 2 * eeta + etasq * (1.5 - 0.5 * eeta)) + (0.75 * x1mth2
|
||||
* (2.0 * etasq - eeta * (1.0 + etasq)) * cos(2.0 * super.data.omegao))))
|
||||
val theta4 = dsv.theta2 * dsv.theta2
|
||||
val temp1 = 3.0 * ck2 * pinvsq * dsv.xnodp
|
||||
val temp2 = temp1 * ck2 * pinvsq
|
||||
val temp3 = 1.25 * ck4 * pinvsq * pinvsq * dsv.xnodp
|
||||
dsv.xmdot =
|
||||
dsv.xnodp + 0.5 * temp1 * dsv.betao * x3thm1 + 0.0625 * temp2 * dsv.betao * (13 - 78 * dsv.theta2 + 137 * theta4)
|
||||
val x1m5th = 1.0 - 5 * dsv.theta2
|
||||
dsv.omgdot =
|
||||
-0.5 * temp1 * x1m5th + 0.0625 * temp2 * (7.0 - 114 * dsv.theta2 + 395 * theta4) + temp3 * (3.0 - 36 * dsv.theta2 + 49 * theta4)
|
||||
val xhdot1 = -temp1 * dsv.cosio
|
||||
dsv.xnodot =
|
||||
xhdot1 + (0.5 * temp2 * (4.0 - 19 * dsv.theta2) + 2 * temp3 * (3.0 - 7 * dsv.theta2)) * dsv.cosio
|
||||
xnodcf = 3.5 * dsv.betao2 * xhdot1 * c1
|
||||
t2cof = 1.5 * c1
|
||||
xlcof = 0.125 * a3ovk2 * dsv.sinio * (3.0 + 5 * dsv.cosio) / (1.0 + dsv.cosio)
|
||||
aycof = 0.25 * a3ovk2 * dsv.sinio
|
||||
x7thm1 = 7.0 * dsv.theta2 - 1
|
||||
deep = DeepSpaceCalculator(dsv)
|
||||
}
|
||||
|
||||
internal fun calculateSDP4(tSince: Double) {
|
||||
synchronized(this) {
|
||||
val temp = DoubleArray(12)
|
||||
val xmdf = data.xmo + dsv.xmdot * tSince
|
||||
val tsq = tSince * tSince
|
||||
val templ = t2cof * tsq
|
||||
dsv.xll = xmdf + dsv.xnodp * templ
|
||||
dsv.omgadf = data.omegao + dsv.omgdot * tSince
|
||||
val xnoddf = data.xnodeo + dsv.xnodot * tSince
|
||||
dsv.xnode = xnoddf + xnodcf * tsq
|
||||
val tempa = 1.0 - c1 * tSince
|
||||
val tempe = data.bstar * c4 * tSince
|
||||
dsv.xn = dsv.xnodp
|
||||
dsv.t = tSince
|
||||
deep.dpsec(data)
|
||||
val a = (xke / dsv.xn).pow(twoThirds) * tempa * tempa
|
||||
dsv.em = dsv.em - tempe
|
||||
deep.dpper()
|
||||
val xl = dsv.xll + dsv.omgadf + dsv.xnode
|
||||
val beta = sqrt(1.0 - dsv.em * dsv.em)
|
||||
dsv.xn = xke / a.pow(1.5)
|
||||
|
||||
// Long period periodics
|
||||
val axn = dsv.em * cos(dsv.omgadf)
|
||||
temp[0] = invert(a * beta * beta)
|
||||
val xll = temp[0] * xlcof * axn
|
||||
val aynl = temp[0] * aycof
|
||||
val xlt = xl + xll
|
||||
val ayn = dsv.em * sin(dsv.omgadf) + aynl
|
||||
|
||||
// Solve Kepler's equation
|
||||
val capu = mod2PI(xlt - dsv.xnode)
|
||||
temp[2] = capu
|
||||
converge(temp, axn, ayn, capu)
|
||||
calculatePosAndVel(temp, a, axn, ayn)
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculatePosAndVel(temp: DoubleArray, a: Double, axn: Double, ayn: Double) {
|
||||
val ecose = temp[5] + temp[6]
|
||||
val esine = temp[3] - temp[4]
|
||||
val elsq = axn * axn + ayn * ayn
|
||||
temp[0] = 1.0 - elsq
|
||||
val pl = a * temp[0]
|
||||
temp[9] = a * (1.0 - ecose)
|
||||
temp[1] = invert(temp[9])
|
||||
temp[10] = xke * sqrt(a) * esine * temp[1]
|
||||
temp[11] = xke * sqrt(pl) * temp[1]
|
||||
temp[2] = a * temp[1]
|
||||
val betal = sqrt(temp[0])
|
||||
temp[3] = invert(1.0 + betal)
|
||||
val cosu = temp[2] * (temp[8] - axn + ayn * esine * temp[3])
|
||||
val sinu = temp[2] * (temp[7] - ayn - axn * esine * temp[3])
|
||||
val u = atan2(sinu, cosu)
|
||||
val sin2u = 2.0 * sinu * cosu
|
||||
val cos2u = 2.0 * cosu * cosu - 1
|
||||
temp[0] = invert(pl)
|
||||
temp[1] = ck2 * temp[0]
|
||||
temp[2] = temp[1] * temp[0]
|
||||
|
||||
// Update for short periodics
|
||||
val rk = temp[9] * (1.0 - 1.5 * temp[2] * betal * x3thm1) + 0.5 * temp[1] * x1mth2 * cos2u
|
||||
val uk = u - 0.25 * temp[2] * x7thm1 * sin2u
|
||||
val xnodek = dsv.xnode + 1.5 * temp[2] * dsv.cosio * sin2u
|
||||
val xinck = dsv.xinc + 1.5 * temp[2] * dsv.cosio * dsv.sinio * cos2u
|
||||
val rdotk = temp[10] - dsv.xn * temp[1] * x1mth2 * sin2u
|
||||
val rfdotk = temp[11] + dsv.xn * temp[1] * (x1mth2 * cos2u + 1.5 * x3thm1)
|
||||
super.calculatePosAndVel(rk, uk, xnodek, xinck, rdotk, rfdotk)
|
||||
}
|
||||
|
||||
inner class DeepSpaceValueObject {
|
||||
var eosq = 0.0
|
||||
var sinio = 0.0
|
||||
var cosio = 0.0
|
||||
var betao = 0.0
|
||||
var aodp = 0.0
|
||||
var theta2 = 0.0
|
||||
var sing = 0.0
|
||||
var cosg = 0.0
|
||||
var betao2 = 0.0
|
||||
var xmdot = 0.0
|
||||
var omgdot = 0.0
|
||||
var xnodot = 0.0
|
||||
var xnodp = 0.0
|
||||
|
||||
// Used by dpsec and dpper parts of Deep()
|
||||
var xll = 0.0
|
||||
var omgadf = 0.0
|
||||
var xnode = 0.0
|
||||
var em = 0.0
|
||||
var xinc = 0.0
|
||||
var xn = 0.0
|
||||
var t = 0.0
|
||||
|
||||
// Used by thetg and Deep()
|
||||
var ds50 = 0.0
|
||||
}
|
||||
|
||||
inner class DeepSpaceCalculator(private val dsv: DeepSpaceValueObject) {
|
||||
|
||||
private val zSinis = 3.9785416E-1
|
||||
private val zSings = -9.8088458E-1
|
||||
private val zNs = 1.19459E-5
|
||||
private val c1ss = 2.9864797E-6
|
||||
private val zEs = 1.675E-2
|
||||
private val zNl = 1.5835218E-4
|
||||
private val c1l = 4.7968065E-7
|
||||
private val zEl = 5.490E-2
|
||||
private val root22 = 1.7891679E-6
|
||||
private val root32 = 3.7393792E-7
|
||||
private val root44 = 7.3636953E-9
|
||||
private val root52 = 1.1428639E-7
|
||||
private val root54 = 2.1765803E-9
|
||||
private val tHdt = 4.3752691E-3
|
||||
private val q22 = 1.7891679E-6
|
||||
private val q31 = 2.1460748E-6
|
||||
private val q33 = 2.2123015E-7
|
||||
private val g22 = 5.7686396
|
||||
private val g32 = 9.5240898E-1
|
||||
private val g44 = 1.8014998
|
||||
private val g52 = 1.0508330
|
||||
private val g54 = 4.4108898
|
||||
|
||||
private val thgr: Double
|
||||
private val xnq: Double
|
||||
private val xqncl: Double
|
||||
private val omegaq: Double
|
||||
private var zmol = 0.0
|
||||
private var zmos = 0.0
|
||||
|
||||
// Many fields below cannot be final because they are iteratively refined
|
||||
private var savtsn = 0.0
|
||||
private var ee2 = 0.0
|
||||
private var e3 = 0.0
|
||||
private var xi2 = 0.0
|
||||
private var xl2 = 0.0
|
||||
private var xl3 = 0.0
|
||||
private var xl4 = 0.0
|
||||
private var xgh2 = 0.0
|
||||
private var xgh3 = 0.0
|
||||
private var xgh4 = 0.0
|
||||
private var xh2 = 0.0
|
||||
private var xh3 = 0.0
|
||||
private var sse = 0.0
|
||||
private var ssi = 0.0
|
||||
private var ssg = 0.0
|
||||
private var xi3 = 0.0
|
||||
private var se2 = 0.0
|
||||
private var si2 = 0.0
|
||||
private var sl2 = 0.0
|
||||
private var sgh2 = 0.0
|
||||
private var sh2 = 0.0
|
||||
private var se3 = 0.0
|
||||
private var si3 = 0.0
|
||||
private var sl3 = 0.0
|
||||
private var sgh3 = 0.0
|
||||
private var sh3 = 0.0
|
||||
private var sl4 = 0.0
|
||||
private var sgh4 = 0.0
|
||||
private var ssl = 0.0
|
||||
private var ssh = 0.0
|
||||
private var d3210 = 0.0
|
||||
private var d3222 = 0.0
|
||||
private var d4410 = 0.0
|
||||
private var d4422 = 0.0
|
||||
private var d5220 = 0.0
|
||||
private var d5232 = 0.0
|
||||
private var d5421 = 0.0
|
||||
private var d5433 = 0.0
|
||||
private var del1 = 0.0
|
||||
private var del2 = 0.0
|
||||
private var del3 = 0.0
|
||||
private var fasx2 = 0.0
|
||||
private var fasx4 = 0.0
|
||||
private var fasx6 = 0.0
|
||||
private var xlamo = 0.0
|
||||
private val xfact: Double
|
||||
private var xni: Double
|
||||
private var atime: Double
|
||||
private val stepp: Double
|
||||
private val stepn: Double
|
||||
private val step2: Double
|
||||
private var preep = 0.0
|
||||
private var pl = 0.0
|
||||
private var sghs = 0.0
|
||||
private var xli: Double
|
||||
private var d2201 = 0.0
|
||||
private var d2211 = 0.0
|
||||
private var sghl = 0.0
|
||||
private var sh1 = 0.0
|
||||
private var pinc = 0.0
|
||||
private var pe = 0.0
|
||||
private var shs = 0.0
|
||||
private var zsingl = 0.0
|
||||
private var zcosgl = 0.0
|
||||
private var zsinhl = 0.0
|
||||
private var zcoshl = 0.0
|
||||
private var zsinil = 0.0
|
||||
private var zcosil = 0.0
|
||||
private var a1 = 0.0
|
||||
private var a2 = 0.0
|
||||
private var a3 = 0.0
|
||||
private var a4 = 0.0
|
||||
private var a5 = 0.0
|
||||
private var a6 = 0.0
|
||||
private var a7 = 0.0
|
||||
private var a8 = 0.0
|
||||
private var a9 = 0.0
|
||||
private var a10 = 0.0
|
||||
private var ainv2 = 0.0
|
||||
private var alfdp = 0.0
|
||||
private val aqnv: Double
|
||||
private var sgh = 0.0
|
||||
private var sini2 = 0.0
|
||||
private var sinis = 0.0
|
||||
private var sinok = 0.0
|
||||
private var sh = 0.0
|
||||
private var si = 0.0
|
||||
private var sil = 0.0
|
||||
private val day: Double
|
||||
private var betdp = 0.0
|
||||
private var dalf = 0.0
|
||||
private var bfact = 0.0
|
||||
private var c = 0.0
|
||||
private var cc = 0.0
|
||||
private var cosis = 0.0
|
||||
private var cosok = 0.0
|
||||
private val cosq: Double
|
||||
private var ctem = 0.0
|
||||
private var f322 = 0.0
|
||||
private var zx = 0.0
|
||||
private var zy = 0.0
|
||||
private var dbet = 0.0
|
||||
private var dls = 0.0
|
||||
private var eoc = 0.0
|
||||
private val eq: Double
|
||||
private var f2 = 0.0
|
||||
private var f220 = 0.0
|
||||
private var f221 = 0.0
|
||||
private var f3 = 0.0
|
||||
private var f311 = 0.0
|
||||
private var f321 = 0.0
|
||||
private var xnoh = 0.0
|
||||
private var f330 = 0.0
|
||||
private var f441 = 0.0
|
||||
private var f442 = 0.0
|
||||
private var f522 = 0.0
|
||||
private var f523 = 0.0
|
||||
private var f542 = 0.0
|
||||
private var f543 = 0.0
|
||||
private var g200 = 0.0
|
||||
private var g201 = 0.0
|
||||
private var g211 = 0.0
|
||||
private var pgh = 0.0
|
||||
private var ph = 0.0
|
||||
private var s1 = 0.0
|
||||
private var s2 = 0.0
|
||||
private var s3 = 0.0
|
||||
private var s4 = 0.0
|
||||
private var s5 = 0.0
|
||||
private var s6 = 0.0
|
||||
private var s7 = 0.0
|
||||
private var se = 0.0
|
||||
private var sel = 0.0
|
||||
private var ses = 0.0
|
||||
private var xls = 0.0
|
||||
private var g300 = 0.0
|
||||
private var g310 = 0.0
|
||||
private var g322 = 0.0
|
||||
private var g410 = 0.0
|
||||
private var g422 = 0.0
|
||||
private var g520 = 0.0
|
||||
private var g521 = 0.0
|
||||
private var g532 = 0.0
|
||||
private var g533 = 0.0
|
||||
private var gam = 0.0
|
||||
private val sinq: Double
|
||||
private var sinzf = 0.0
|
||||
private var sis = 0.0
|
||||
private var sl = 0.0
|
||||
private var sll = 0.0
|
||||
private var sls = 0.0
|
||||
private var stem = 0.0
|
||||
private var temp = 0.0
|
||||
private var temp1 = 0.0
|
||||
private var x1 = 0.0
|
||||
private var x2 = 0.0
|
||||
private var x2li = 0.0
|
||||
private var x2omi = 0.0
|
||||
private var x3 = 0.0
|
||||
private var x4 = 0.0
|
||||
private var x5 = 0.0
|
||||
private var x6 = 0.0
|
||||
private var x7 = 0.0
|
||||
private var x8 = 0.0
|
||||
private var xl = 0.0
|
||||
private var xldot = 0.0
|
||||
private val xmao: Double
|
||||
private var xnddt = 0.0
|
||||
private var xndot = 0.0
|
||||
private var xno2 = 0.0
|
||||
private var xnodce = 0.0
|
||||
private var xnoi = 0.0
|
||||
private var xomi = 0.0
|
||||
private val xpidot: Double
|
||||
private var z1 = 0.0
|
||||
private var z11 = 0.0
|
||||
private var z12 = 0.0
|
||||
private var z13 = 0.0
|
||||
private var z2 = 0.0
|
||||
private var z21 = 0.0
|
||||
private var z22 = 0.0
|
||||
private var z23 = 0.0
|
||||
private var z3 = 0.0
|
||||
private var z31 = 0.0
|
||||
private var z32 = 0.0
|
||||
private var z33 = 0.0
|
||||
private var ze = 0.0
|
||||
private var zf = 0.0
|
||||
private var zm = 0.0
|
||||
private var zn = 0.0
|
||||
private var zsing = 0.0
|
||||
private var zsinh = 0.0
|
||||
private var zsini = 0.0
|
||||
private var zcosg = 0.0
|
||||
private var zcosh = 0.0
|
||||
private var zcosi = 0.0
|
||||
private var delt = 0.0
|
||||
private var ft = 0.0
|
||||
private var resonance: Boolean
|
||||
private var synchronous: Boolean
|
||||
private var doLoop = false
|
||||
private var epochRestart = false
|
||||
|
||||
init {
|
||||
thgr = thetaG(data.epoch)
|
||||
eq = data.eccn
|
||||
xnq = dsv.xnodp
|
||||
aqnv = invert(dsv.aodp)
|
||||
xqncl = data.xincl
|
||||
xmao = data.xmo
|
||||
xpidot = dsv.omgdot + dsv.xnodot
|
||||
sinq = sin(data.xnodeo)
|
||||
cosq = cos(data.xnodeo)
|
||||
omegaq = data.omegao
|
||||
// Initialize lunar solar terms, days since 1900 Jan 0.5
|
||||
day = dsv.ds50 + 18261.5
|
||||
if (abs(day - preep) > 1.0E-6) {
|
||||
preep = day
|
||||
xnodce = 4.5236020 - 9.2422029E-4 * day
|
||||
stem = sin(xnodce)
|
||||
ctem = cos(xnodce)
|
||||
zcosil = 0.91375164 - 0.03568096 * ctem
|
||||
zsinil = sqrt(1.0 - zcosil * zcosil)
|
||||
zsinhl = 0.089683511 * stem / zsinil
|
||||
zcoshl = sqrt(1.0 - zsinhl * zsinhl)
|
||||
c = 4.7199672 + 0.22997150 * day
|
||||
gam = 5.8351514 + 0.0019443680 * day
|
||||
zmol = mod2PI(c - gam)
|
||||
zx = 0.39785416 * stem / zsinil
|
||||
zy = zcoshl * ctem + 0.91744867 * zsinhl * stem
|
||||
zx = atan2(zx, zy)
|
||||
zx = gam + zx - xnodce
|
||||
zcosgl = cos(zx)
|
||||
zsingl = sin(zx)
|
||||
zmos = mod2PI(6.2565837 + 0.017201977 * day)
|
||||
} else {
|
||||
zmol = 0.0
|
||||
zmos = 0.0
|
||||
}
|
||||
doSolarTerms()
|
||||
|
||||
// Geopotential resonance initialization for 12 hour orbits
|
||||
resonance = false
|
||||
synchronous = false
|
||||
if (!(xnq < 0.0052359877 && xnq > 0.0034906585)) {
|
||||
if (xnq < 0.00826 || xnq > 0.00924)
|
||||
if (eq < 0.5)
|
||||
// calculateResonance
|
||||
resonance = true
|
||||
eoc = eq * dsv.eosq
|
||||
g201 = -0.306 - (eq - 0.64) * 0.440
|
||||
if (eq <= 0.65) {
|
||||
g211 = 3.616 - 13.247 * eq + 16.290 * dsv.eosq
|
||||
g310 = -19.302 + 117.390 * eq - 228.419 * dsv.eosq + 156.591 * eoc
|
||||
g322 = -18.9068 + 109.7927 * eq - 214.6334 * dsv.eosq + 146.5816 * eoc
|
||||
g410 = -41.122 + 242.694 * eq - 471.094 * dsv.eosq + 313.953 * eoc
|
||||
g422 = -146.407 + 841.880 * eq - 1629.014 * dsv.eosq + 1083.435 * eoc
|
||||
g520 = -532.114 + 3017.977 * eq - 5740 * dsv.eosq + 3708.276 * eoc
|
||||
} else {
|
||||
g211 = -72.099 + 331.819 * eq - 508.738 * dsv.eosq + 266.724 * eoc
|
||||
g310 = -346.844 + 1582.851 * eq - 2415.925 * dsv.eosq + 1246.113 * eoc
|
||||
g322 = -342.585 + 1554.908 * eq - 2366.899 * dsv.eosq + 1215.972 * eoc
|
||||
g410 = -1052.797 + 4758.686 * eq - 7193.992 * dsv.eosq + 3651.957 * eoc
|
||||
g422 = -3581.69 + 16178.11 * eq - 24462.77 * dsv.eosq + 12422.52 * eoc
|
||||
g520 =
|
||||
if (eq <= 0.715) 1464.74 - 4664.75 * eq + 3763.64 * dsv.eosq
|
||||
else -5149.66 + 29936.92 * eq - 54087.36 * dsv.eosq + 31324.56 * eoc
|
||||
}
|
||||
if (eq < 0.7) {
|
||||
g533 = -919.2277 + 4988.61 * eq - 9064.77 * dsv.eosq + 5542.21 * eoc
|
||||
g521 = -822.71072 + 4568.6173 * eq - 8491.4146 * dsv.eosq + 5337.524 * eoc
|
||||
g532 = -853.666 + 4690.25 * eq - 8624.77 * dsv.eosq + 5341.4 * eoc
|
||||
} else {
|
||||
g533 = -37995.78 + 161616.52 * eq - 229838.2 * dsv.eosq + 109377.94 * eoc
|
||||
g521 = -51752.104 + 218913.95 * eq - 309468.16 * dsv.eosq + 146349.42 * eoc
|
||||
g532 = -40023.88 + 170470.89 * eq - 242699.48 * dsv.eosq + 115605.82 * eoc
|
||||
}
|
||||
sini2 = dsv.sinio * dsv.sinio
|
||||
f220 = 0.75 * (1.0 + 2 * dsv.cosio + dsv.theta2)
|
||||
f221 = 1.5 * sini2
|
||||
f321 = 1.875 * dsv.sinio * (1.0 - 2 * dsv.cosio - 3.0 * dsv.theta2)
|
||||
f322 = -1.875 * dsv.sinio * (1.0 + 2 * dsv.cosio - 3.0 * dsv.theta2)
|
||||
f441 = 35 * sini2 * f220
|
||||
f442 = 39.3750 * sini2 * sini2
|
||||
f522 =
|
||||
9.84375 * dsv.sinio * (sini2 * (1.0 - 2 * dsv.cosio - 5 * dsv.theta2) + 0.33333333 * (-2 + 4 * dsv.cosio + 6 * dsv.theta2))
|
||||
f523 =
|
||||
dsv.sinio * (4.92187512 * sini2 * (-2 - 4 * dsv.cosio + 10 * dsv.theta2) + 6.56250012 * (1.0 + 2 * dsv.cosio - 3.0 * dsv.theta2))
|
||||
f542 =
|
||||
29.53125 * dsv.sinio * (2.0 - 8 * dsv.cosio + dsv.theta2 * (-12 + 8 * dsv.cosio + 10 * dsv.theta2))
|
||||
f543 =
|
||||
29.53125 * dsv.sinio * (-2 - 8 * dsv.cosio + dsv.theta2 * (12 + 8 * dsv.cosio - 10 * dsv.theta2))
|
||||
xno2 = xnq * xnq
|
||||
ainv2 = aqnv * aqnv
|
||||
temp1 = 3.0 * xno2 * ainv2
|
||||
temp = temp1 * root22
|
||||
d2201 = temp * f220 * g201
|
||||
d2211 = temp * f221 * g211
|
||||
temp1 *= aqnv
|
||||
temp = temp1 * root32
|
||||
d3210 = temp * f321 * g310
|
||||
d3222 = temp * f322 * g322
|
||||
temp1 *= aqnv
|
||||
temp = 2.0 * temp1 * root44
|
||||
d4410 = temp * f441 * g410
|
||||
d4422 = temp * f442 * g422
|
||||
temp1 *= aqnv
|
||||
temp = temp1 * root52
|
||||
d5220 = temp * f522 * g520
|
||||
d5232 = temp * f523 * g532
|
||||
temp = 2.0 * temp1 * root54
|
||||
d5421 = temp * f542 * g521
|
||||
d5433 = temp * f543 * g533
|
||||
xlamo = xmao + data.xnodeo + data.xnodeo - thgr - thgr
|
||||
bfact = dsv.xmdot + dsv.xnodot + dsv.xnodot - tHdt - tHdt
|
||||
bfact += ssl + ssh + ssh
|
||||
} else {
|
||||
// Init synchronous resonance terms
|
||||
resonance = true
|
||||
synchronous = true
|
||||
g200 = 1.0 + dsv.eosq * (-2.5 + 0.8125 * dsv.eosq)
|
||||
g310 = 1.0 + 2 * dsv.eosq
|
||||
g300 = 1.0 + dsv.eosq * (-6 + 6.60937 * dsv.eosq)
|
||||
f220 = 0.75 * (1.0 + dsv.cosio) * (1.0 + dsv.cosio)
|
||||
f311 =
|
||||
0.9375 * dsv.sinio * dsv.sinio * (1.0 + 3.0 * dsv.cosio) - 0.75 * (1.0 + dsv.cosio)
|
||||
f330 = 1.0 + dsv.cosio
|
||||
f330 *= 1.875 * f330 * f330
|
||||
del1 = 3.0 * xnq * xnq * aqnv * aqnv
|
||||
del2 = 2.0 * del1 * f220 * g200 * q22
|
||||
del3 = 3.0 * del1 * f330 * g300 * q33 * aqnv
|
||||
del1 *= f311 * g310 * q31 * aqnv
|
||||
fasx2 = 0.13130908
|
||||
fasx4 = 2.8843198
|
||||
fasx6 = 0.37448087
|
||||
xlamo = xmao + data.xnodeo + data.omegao - thgr
|
||||
bfact = dsv.xmdot + xpidot - tHdt
|
||||
bfact += ssl + ssg + ssh
|
||||
}
|
||||
xfact = bfact - xnq
|
||||
// Init integrator
|
||||
xli = xlamo
|
||||
xni = xnq
|
||||
atime = 0.0
|
||||
stepp = 720.0
|
||||
stepn = -720.0
|
||||
step2 = 259200.0
|
||||
}
|
||||
|
||||
// Entrance for lunar-solar periodics
|
||||
fun dpper() {
|
||||
sinis = sin(dsv.xinc)
|
||||
cosis = cos(dsv.xinc)
|
||||
if (abs(savtsn - dsv.t) >= 30) {
|
||||
savtsn = dsv.t
|
||||
zm = zmos + zNs * dsv.t
|
||||
zf = zm + 2 * zEs * sin(zm)
|
||||
sinzf = sin(zf)
|
||||
f2 = 0.5 * sinzf * sinzf - 0.25
|
||||
f3 = -0.5 * sinzf * cos(zf)
|
||||
ses = se2 * f2 + se3 * f3
|
||||
sis = si2 * f2 + si3 * f3
|
||||
sls = sl2 * f2 + sl3 * f3 + sl4 * sinzf
|
||||
sghs = sgh2 * f2 + sgh3 * f3 + sgh4 * sinzf
|
||||
shs = sh2 * f2 + sh3 * f3
|
||||
zm = zmol + zNl * dsv.t
|
||||
zf = zm + 2 * zEl * sin(zm)
|
||||
sinzf = sin(zf)
|
||||
f2 = 0.5 * sinzf * sinzf - 0.25
|
||||
f3 = -0.5 * sinzf * cos(zf)
|
||||
sel = ee2 * f2 + e3 * f3
|
||||
sil = xi2 * f2 + xi3 * f3
|
||||
sll = xl2 * f2 + xl3 * f3 + xl4 * sinzf
|
||||
sghl = xgh2 * f2 + xgh3 * f3 + xgh4 * sinzf
|
||||
sh1 = xh2 * f2 + xh3 * f3
|
||||
pe = ses + sel
|
||||
pinc = sis + sil
|
||||
pl = sls + sll
|
||||
}
|
||||
pgh = sghs + sghl
|
||||
ph = shs + sh1
|
||||
dsv.xinc = dsv.xinc + pinc
|
||||
dsv.em = dsv.em + pe
|
||||
if (xqncl >= 0.2) {
|
||||
/* Apply periodics directly */
|
||||
ph /= dsv.sinio
|
||||
pgh -= dsv.cosio * ph
|
||||
dsv.omgadf = dsv.omgadf + pgh
|
||||
dsv.xnode = dsv.xnode + ph
|
||||
dsv.xll = dsv.xll + pl
|
||||
} else {
|
||||
applyPeriodics()
|
||||
// This is a patch to Lyddane modification suggested by Rob Matson
|
||||
if (abs(xnoh - dsv.xnode) > Math.PI) {
|
||||
if (dsv.xnode < xnoh) dsv.xnode += twoPi else dsv.xnode -= twoPi
|
||||
}
|
||||
dsv.xll = dsv.xll + pl
|
||||
dsv.omgadf = xls - dsv.xll - cos(dsv.xinc) * dsv.xnode
|
||||
}
|
||||
}
|
||||
|
||||
// Entrance for deep space secular effects
|
||||
fun dpsec(params: OrbitalData) {
|
||||
dsv.xll = dsv.xll + ssl * dsv.t
|
||||
dsv.omgadf = dsv.omgadf + ssg * dsv.t
|
||||
dsv.xnode = dsv.xnode + ssh * dsv.t
|
||||
dsv.em = params.eccn + sse * dsv.t
|
||||
dsv.xinc = params.xincl + ssi * dsv.t
|
||||
if (dsv.xinc < 0) {
|
||||
dsv.xinc = -dsv.xinc
|
||||
dsv.xnode = dsv.xnode + Math.PI
|
||||
dsv.omgadf = dsv.omgadf - Math.PI
|
||||
}
|
||||
if (!resonance) return
|
||||
do processEpochRestartLoop() while (doLoop && epochRestart)
|
||||
dsv.xn = xni + xndot * ft + xnddt * ft * ft * 0.5
|
||||
xl = xli + xldot * ft + xndot * ft * ft * 0.5
|
||||
temp = -dsv.xnode + thgr + dsv.t * tHdt
|
||||
if (synchronous) dsv.xll = xl - dsv.omgadf + temp else dsv.xll = xl + temp + temp
|
||||
}
|
||||
|
||||
private fun doSolarTerms() {
|
||||
savtsn = 1E20
|
||||
zcosg = 1.945905E-1
|
||||
zsing = zSings
|
||||
zcosi = 9.1744867E-1
|
||||
zsini = zSinis
|
||||
zcosh = cosq
|
||||
zsinh = sinq
|
||||
cc = c1ss
|
||||
zn = zNs
|
||||
ze = zEs
|
||||
xnoi = invert(xnq)
|
||||
calculateSolarTerms()
|
||||
calculateLunarTerms()
|
||||
calculateSolarTerms() // Solar terms done again after Lunar terms are done
|
||||
sse += se
|
||||
ssi += si
|
||||
ssl += sl
|
||||
ssg = ssg + sgh - dsv.cosio / dsv.sinio * sh
|
||||
ssh += sh / dsv.sinio
|
||||
}
|
||||
|
||||
private fun calculateLunarTerms() {
|
||||
sse = se
|
||||
ssi = si
|
||||
ssl = sl
|
||||
ssh = sh / dsv.sinio
|
||||
ssg = sgh - dsv.cosio * ssh
|
||||
se2 = ee2
|
||||
si2 = xi2
|
||||
sl2 = xl2
|
||||
sgh2 = xgh2
|
||||
sh2 = xh2
|
||||
se3 = e3
|
||||
si3 = xi3
|
||||
sl3 = xl3
|
||||
sgh3 = xgh3
|
||||
sh3 = xh3
|
||||
sl4 = xl4
|
||||
sgh4 = xgh4
|
||||
zcosg = zcosgl
|
||||
zsing = zsingl
|
||||
zcosi = zcosil
|
||||
zsini = zsinil
|
||||
zcosh = zcoshl * cosq + zsinhl * sinq
|
||||
zsinh = sinq * zcoshl - cosq * zsinhl
|
||||
zn = zNl
|
||||
cc = c1l
|
||||
ze = zEl
|
||||
}
|
||||
|
||||
private fun calculateSolarTerms() {
|
||||
a1 = zcosg * zcosh + zsing * zcosi * zsinh
|
||||
a3 = -zsing * zcosh + zcosg * zcosi * zsinh
|
||||
a7 = -zcosg * zsinh + zsing * zcosi * zcosh
|
||||
a8 = zsing * zsini
|
||||
a9 = zsing * zsinh + zcosg * zcosi * zcosh
|
||||
a10 = zcosg * zsini
|
||||
a2 = dsv.cosio * a7 + dsv.sinio * a8
|
||||
a4 = dsv.cosio * a9 + dsv.sinio * a10
|
||||
a5 = -dsv.sinio * a7 + dsv.cosio * a8
|
||||
a6 = -dsv.sinio * a9 + dsv.cosio * a10
|
||||
x1 = a1 * dsv.cosg + a2 * dsv.sing
|
||||
x2 = a3 * dsv.cosg + a4 * dsv.sing
|
||||
x3 = -a1 * dsv.sing + a2 * dsv.cosg
|
||||
x4 = -a3 * dsv.sing + a4 * dsv.cosg
|
||||
x5 = a5 * dsv.sing
|
||||
x6 = a6 * dsv.sing
|
||||
x7 = a5 * dsv.cosg
|
||||
x8 = a6 * dsv.cosg
|
||||
z31 = 12 * x1 * x1 - 3.0 * x3 * x3
|
||||
z32 = 24 * x1 * x2 - 6 * x3 * x4
|
||||
z33 = 12 * x2 * x2 - 3.0 * x4 * x4
|
||||
z1 = 3.0 * (a1 * a1 + a2 * a2) + z31 * dsv.eosq
|
||||
z2 = 6.0 * (a1 * a3 + a2 * a4) + z32 * dsv.eosq
|
||||
z3 = 3.0 * (a3 * a3 + a4 * a4) + z33 * dsv.eosq
|
||||
z11 = -6 * a1 * a5 + dsv.eosq * (-24 * x1 * x7 - 6 * x3 * x5)
|
||||
z12 =
|
||||
-6 * (a1 * a6 + a3 * a5) + dsv.eosq * (-24 * (x2 * x7 + x1 * x8) - 6 * (x3 * x6 + x4 * x5))
|
||||
z13 = -6 * a3 * a6 + dsv.eosq * (-24 * x2 * x8 - 6 * x4 * x6)
|
||||
z21 = 6.0 * a2 * a5 + dsv.eosq * (24 * x1 * x5 - 6 * x3 * x7)
|
||||
z22 =
|
||||
6.0 * (a4 * a5 + a2 * a6) + dsv.eosq * (24 * (x2 * x5 + x1 * x6) - 6 * (x4 * x7 + x3 * x8))
|
||||
z23 = 6.0 * a4 * a6 + dsv.eosq * (24 * x2 * x6 - 6 * x4 * x8)
|
||||
z1 += z1 + dsv.betao2 * z31
|
||||
z2 += z2 + dsv.betao2 * z32
|
||||
z3 += z3 + dsv.betao2 * z33
|
||||
s3 = cc * xnoi
|
||||
s2 = -0.5 * s3 / dsv.betao
|
||||
s4 = s3 * dsv.betao
|
||||
s1 = -15 * eq * s4
|
||||
s5 = x1 * x3 + x2 * x4
|
||||
s6 = x2 * x3 + x1 * x4
|
||||
s7 = x2 * x4 - x1 * x3
|
||||
se = s1 * zn * s5
|
||||
si = s2 * zn * (z11 + z13)
|
||||
sl = -zn * s3 * (z1 + z3 - 14 - 6 * dsv.eosq)
|
||||
sgh = s4 * zn * (z31 + z33 - 6)
|
||||
sh = -zn * s2 * (z21 + z23)
|
||||
if (xqncl < 5.2359877E-2) sh = 0.0
|
||||
ee2 = 2.0 * s1 * s6
|
||||
e3 = 2.0 * s1 * s7
|
||||
xi2 = 2.0 * s2 * z12
|
||||
xi3 = 2.0 * s2 * (z13 - z11)
|
||||
xl2 = -2 * s3 * z2
|
||||
xl3 = -2 * s3 * (z3 - z1)
|
||||
xl4 = -2 * s3 * (-21 - 9 * dsv.eosq) * ze
|
||||
xgh2 = 2.0 * s4 * z32
|
||||
xgh3 = 2.0 * s4 * (z33 - z31)
|
||||
xgh4 = -18 * s4 * ze
|
||||
xh2 = -2 * s2 * z22
|
||||
xh3 = -2 * s2 * (z23 - z21)
|
||||
}
|
||||
|
||||
private fun processEpochRestartLoop() {
|
||||
if (atime == 0.0 || dsv.t >= 0 && atime < 0 || dsv.t < 0 && atime >= 0) {
|
||||
calculateDelta()
|
||||
atime = 0.0
|
||||
xni = xnq
|
||||
xli = xlamo
|
||||
} else if (abs(dsv.t) >= abs(atime)) calculateDelta()
|
||||
processNotEpochRestartLoop()
|
||||
}
|
||||
|
||||
private fun calculateDelta() {
|
||||
delt = if (dsv.t < 0) stepn else stepp
|
||||
}
|
||||
|
||||
private fun processNotEpochRestartLoop() {
|
||||
do {
|
||||
if (abs(dsv.t - atime) >= stepp) {
|
||||
doLoop = true
|
||||
epochRestart = false
|
||||
} else {
|
||||
ft = dsv.t - atime
|
||||
doLoop = false
|
||||
}
|
||||
if (abs(dsv.t) < abs(atime)) {
|
||||
delt = if (dsv.t >= 0) stepn else stepp
|
||||
doLoop = doLoop or epochRestart
|
||||
}
|
||||
if (synchronous) {
|
||||
xndot = del1 * sin(xli - fasx2) + del2 * sin(2.0 * (xli - fasx4))
|
||||
+del3 * sin(3.0 * (xli - fasx6))
|
||||
xnddt = del1 * cos(xli - fasx2) + 2 * del2 * cos(2.0 * (xli - fasx4))
|
||||
+3.0 * del3 * cos(3.0 * (xli - fasx6))
|
||||
} else {
|
||||
xomi = omegaq + dsv.omgdot * atime
|
||||
x2omi = xomi + xomi
|
||||
x2li = xli + xli
|
||||
xndot =
|
||||
d2201 * sin(x2omi + xli - g22) + d2211 * sin(xli - g22) + (d3210
|
||||
* sin(xomi + xli - g32)) + d3222 * sin(-xomi + xli - g32) + (d4410
|
||||
* sin(x2omi + x2li - g44)) + d4422 * sin(x2li - g44) + (d5220
|
||||
* sin(xomi + xli - g52)) + d5232 * sin(-xomi + xli - g52) + (d5421
|
||||
* sin(xomi + x2li - g54)) + d5433 * sin(-xomi + x2li - g54)
|
||||
xnddt =
|
||||
d2201 * cos(x2omi + xli - g22) + d2211 * cos(xli - g22) + (d3210
|
||||
* cos(xomi + xli - g32)) + d3222 * cos(-xomi + xli - g32) + (d5220
|
||||
* cos(xomi + xli - g52)) + d5232 * cos(-xomi + xli - g52) + (2
|
||||
* (d4410 * cos(x2omi + x2li - g44) + d4422 * cos(x2li - g44) + (d5421
|
||||
* cos(xomi + x2li - g54)) + d5433 * cos(-xomi + x2li - g54)))
|
||||
}
|
||||
xldot = xni + xfact
|
||||
xnddt *= xldot
|
||||
if (doLoop) {
|
||||
xli += xldot * delt + xndot * step2
|
||||
xni += xndot * delt + xnddt * step2
|
||||
atime += delt
|
||||
}
|
||||
} while (doLoop && !epochRestart)
|
||||
}
|
||||
|
||||
// Apply periodics with Lyddane modification
|
||||
private fun applyPeriodics() {
|
||||
sinok = sin(dsv.xnode)
|
||||
cosok = cos(dsv.xnode)
|
||||
alfdp = sinis * sinok
|
||||
betdp = sinis * cosok
|
||||
dalf = ph * cosok + pinc * cosis * sinok
|
||||
dbet = -ph * sinok + pinc * cosis * cosok
|
||||
alfdp += dalf
|
||||
betdp += dbet
|
||||
dsv.xnode = mod2PI(dsv.xnode)
|
||||
xls = dsv.xll + dsv.omgadf + cosis * dsv.xnode
|
||||
dls = pl + pgh - pinc * dsv.xnode * sinis
|
||||
xls += dls
|
||||
xnoh = dsv.xnode
|
||||
dsv.xnode = atan2(alfdp, betdp)
|
||||
}
|
||||
|
||||
// Calculates the Greenwich Mean Sidereal Time for an epoch, valid 1957 through 2056
|
||||
private fun thetaG(epoch: Double): Double {
|
||||
var year = floor(epoch * 1E-3)
|
||||
var dayOfYear = (epoch * 1E-3 - year) * 1000.0
|
||||
year = if (year < 57) year + 2000 else year + 1900
|
||||
val dayFloor = floor(dayOfYear)
|
||||
val dayFraction = dayOfYear - dayFloor
|
||||
dayOfYear = dayFloor
|
||||
val jd = julianDateOfYear(year) + dayOfYear
|
||||
dsv.ds50 = jd - 2433281.5 + dayFraction
|
||||
return mod2PI(6.3003880987 * dsv.ds50 + 1.72944494)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 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.predict
|
||||
|
||||
data class GeoPos(val latitude: Double, val longitude: Double, val name: String? = null)
|
||||
@@ -1,221 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 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.predict
|
||||
|
||||
import kotlin.math.*
|
||||
|
||||
class NearEarthSat(data: OrbitalData) : Satellite(data) {
|
||||
|
||||
private val aodp: Double
|
||||
private val aycof: Double
|
||||
private val c1: Double
|
||||
private val c4: Double
|
||||
private val c5: Double
|
||||
private val cosio: Double
|
||||
private var d2 = 0.0
|
||||
private var d3 = 0.0
|
||||
private var d4 = 0.0
|
||||
private val delmo: Double
|
||||
private val omgcof: Double
|
||||
private val eta: Double
|
||||
private val omgdot: Double
|
||||
private val sinio: Double
|
||||
private val xnodp: Double
|
||||
private val sinmo: Double
|
||||
private val t2cof: Double
|
||||
private var t3cof = 0.0
|
||||
private var t4cof = 0.0
|
||||
private var t5cof = 0.0
|
||||
private val x1mth2: Double
|
||||
private val x3thm1: Double
|
||||
private val x7thm1: Double
|
||||
private val xmcof: Double
|
||||
private val xmdot: Double
|
||||
private val xnodcf: Double
|
||||
private val xnodot: Double
|
||||
private val xlcof: Double
|
||||
private val sgp4Simple: Boolean
|
||||
|
||||
init {
|
||||
// Recover original mean motion (xnodp) and semimajor axis (aodp) from input elements
|
||||
val a1 = (xke / super.data.xno).pow(twoThirds)
|
||||
cosio = cos(super.data.xincl)
|
||||
val theta2 = sqr(cosio)
|
||||
x3thm1 = 3.0 * theta2 - 1.0
|
||||
val eo = super.data.eccn
|
||||
val eosq = sqr(eo)
|
||||
val betao2 = 1.0 - eosq
|
||||
val betao = sqrt(betao2)
|
||||
val del1 = 1.5 * ck2 * x3thm1 / (sqr(a1) * betao * betao2)
|
||||
val ao = a1 * (1.0 - del1 * (0.5 * twoThirds + del1 * (1.0 + 134.0 / 81.0 * del1)))
|
||||
val delo = 1.5 * ck2 * x3thm1 / (sqr(ao) * betao * betao2)
|
||||
xnodp = super.data.xno / (1.0 + delo)
|
||||
aodp = ao / (1.0 - delo)
|
||||
|
||||
// For perigee less than 220 kilometers, the "simple" flag is set
|
||||
sgp4Simple = aodp * (1.0 - eo) < 220 / earthRadius + 1.0
|
||||
|
||||
// For perigees below 156 km, the values of S and QOMS2T are altered
|
||||
setPerigee((aodp * (1.0 - eo) - 1.0) * earthRadius)
|
||||
val pinvsq = invert(sqr(aodp) * sqr(betao2))
|
||||
val tsi = invert(aodp - s4)
|
||||
eta = aodp * eo * tsi
|
||||
val etasq = eta * eta
|
||||
val eeta = eo * eta
|
||||
val psisq = abs(1.0 - etasq)
|
||||
val coef = qoms24 * tsi.pow(4.0)
|
||||
val coef1 = coef / psisq.pow(3.5)
|
||||
val bstar = super.data.bstar
|
||||
val c2 = coef1 * xnodp * (aodp * (1.0 + 1.5 * etasq + eeta * (4.0 + etasq)) + 0.75
|
||||
* ck2 * tsi / psisq * x3thm1 * (8.0 + 3.0 * etasq * (8.0 + etasq)))
|
||||
c1 = bstar * c2
|
||||
sinio = sin(super.data.xincl)
|
||||
val a3ovk2 = -j3Harmonic / ck2
|
||||
val c3 = coef * tsi * a3ovk2 * xnodp * sinio / eo
|
||||
x1mth2 = 1.0 - theta2
|
||||
val omegao = super.data.omegao
|
||||
c4 = 2 * xnodp * coef1 * aodp * betao2 * (eta * (2.0 + 0.5 * etasq) + eo * (0.5 + 2 * etasq)
|
||||
- 2 * ck2 * tsi / (aodp * psisq) * (-3 * x3thm1 * (1.0 - 2 * eeta + etasq
|
||||
* (1.5 - 0.5 * eeta)) + 0.75 * x1mth2 * (2.0 * etasq - eeta * (1.0 + etasq))
|
||||
* cos(2.0 * omegao)))
|
||||
c5 = 2.0 * coef1 * aodp * betao2 * (1.0 + 2.75 * (etasq + eeta) + eeta * etasq)
|
||||
val theta4 = sqr(theta2)
|
||||
val temp1 = 3.0 * ck2 * pinvsq * xnodp
|
||||
val temp2 = temp1 * ck2 * pinvsq
|
||||
val temp3 = 1.25 * ck4 * pinvsq * pinvsq * xnodp
|
||||
xmdot =
|
||||
xnodp + 0.5 * temp1 * betao * x3thm1 + (0.0625 * temp2 * betao * (13.0 - 78.0 * theta2 + 137.0 * theta4))
|
||||
val x1m5th = 1.0 - 5.0 * theta2
|
||||
omgdot =
|
||||
-0.5 * temp1 * x1m5th + 0.0625 * temp2 * (7.0 - 114.0 * theta2 + 395.0 * theta4) + temp3 * (3.0 - 36.0 * theta2 + 49.0 * theta4)
|
||||
val xhdot1 = -temp1 * cosio
|
||||
xnodot =
|
||||
xhdot1 + (0.5 * temp2 * (4.0 - 19.0 * theta2) + 2.0 * temp3 * (3.0 - 7.0 * theta2)) * cosio
|
||||
omgcof = bstar * c3 * cos(omegao)
|
||||
xmcof = -twoThirds * coef * bstar / eeta
|
||||
xnodcf = 3.5 * betao2 * xhdot1 * c1
|
||||
t2cof = 1.5 * c1
|
||||
xlcof = 0.125 * a3ovk2 * sinio * (3.0 + 5 * cosio) / (1.0 + cosio)
|
||||
aycof = 0.25 * a3ovk2 * sinio
|
||||
val xmo = super.data.xmo
|
||||
delmo = (1.0 + eta * cos(xmo)).pow(3.0)
|
||||
sinmo = sin(xmo)
|
||||
x7thm1 = 7.0 * theta2 - 1
|
||||
if (!sgp4Simple) {
|
||||
val c1sq = sqr(c1)
|
||||
d2 = 4.0 * aodp * tsi * c1sq
|
||||
val temp = d2 * tsi * c1 / 3.0
|
||||
d3 = (17 * aodp + s4) * temp
|
||||
d4 = 0.5 * temp * aodp * tsi * (221 * aodp + 31 * s4) * c1
|
||||
t3cof = d2 + 2 * c1sq
|
||||
t4cof = 0.25 * (3.0 * d3 + c1 * (12 * d2 + 10 * c1sq))
|
||||
t5cof = 0.2 * (3.0 * d4 + 12 * c1 * d3 + 6 * d2 * d2 + 15 * c1sq * (2.0 * d2 + c1sq))
|
||||
} else {
|
||||
d2 = 0.0
|
||||
d3 = 0.0
|
||||
d4 = 0.0
|
||||
t3cof = 0.0
|
||||
t4cof = 0.0
|
||||
t5cof = 0.0
|
||||
}
|
||||
}
|
||||
|
||||
internal fun calculateSGP4(tSince: Double) {
|
||||
synchronized(this) {
|
||||
val temp = DoubleArray(9)
|
||||
val xmdf = data.xmo + xmdot * tSince
|
||||
val omgadf = data.omegao + omgdot * tSince
|
||||
val xnoddf = data.xnodeo + xnodot * tSince
|
||||
var omega = omgadf
|
||||
var xmp = xmdf
|
||||
val tsq = sqr(tSince)
|
||||
val xnode = xnoddf + xnodcf * tsq
|
||||
val bstar = data.bstar
|
||||
var tempa = 1.0 - c1 * tSince
|
||||
var tempe = bstar * c4 * tSince
|
||||
var templ = t2cof * tsq
|
||||
if (!sgp4Simple) {
|
||||
val delomg = omgcof * tSince
|
||||
val delm = xmcof * ((1.0 + eta * cos(xmdf)).pow(3.0) - delmo)
|
||||
temp[0] = delomg + delm
|
||||
xmp = xmdf + temp[0]
|
||||
omega = omgadf - temp[0]
|
||||
val tcube = tsq * tSince
|
||||
val tfour = tSince * tcube
|
||||
tempa = tempa - d2 * tsq - d3 * tcube - d4 * tfour
|
||||
tempe += bstar * c5 * (sin(xmp) - sinmo)
|
||||
templ += t3cof * tcube + tfour * (t4cof + tSince * t5cof)
|
||||
}
|
||||
val a = aodp * tempa.pow(2.0)
|
||||
val eo = data.eccn
|
||||
val e = eo - tempe
|
||||
val xl = xmp + omega + xnode + xnodp * templ
|
||||
val beta = sqrt(1.0 - e * e)
|
||||
val xn = xke / a.pow(1.5)
|
||||
|
||||
// Long period periodics
|
||||
val axn = e * cos(omega)
|
||||
temp[0] = invert(a * sqr(beta))
|
||||
val xll = temp[0] * xlcof * axn
|
||||
val aynl = temp[0] * aycof
|
||||
val xlt = xl + xll
|
||||
val ayn = e * sin(omega) + aynl
|
||||
|
||||
// Solve Kepler's equation
|
||||
val capu = mod2PI(xlt - xnode)
|
||||
temp[2] = capu
|
||||
converge(temp, axn, ayn, capu)
|
||||
calculatePosAndVel(temp, xnode, a, xn, axn, ayn)
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculatePosAndVel(
|
||||
temp: DoubleArray, xnode: Double, a: Double,
|
||||
xn: Double, axn: Double, ayn: Double
|
||||
) {
|
||||
val ecose = temp[5] + temp[6]
|
||||
val esine = temp[3] - temp[4]
|
||||
val elsq = sqr(axn) + sqr(ayn)
|
||||
temp[0] = 1.0 - elsq
|
||||
val pl = a * temp[0]
|
||||
val r = a * (1.0 - ecose)
|
||||
temp[1] = invert(r)
|
||||
val rdot = xke * sqrt(a) * esine * temp[1]
|
||||
val rfdot = xke * sqrt(pl) * temp[1]
|
||||
temp[2] = a * temp[1]
|
||||
val betal = sqrt(temp[0])
|
||||
temp[3] = invert(1.0 + betal)
|
||||
val cosu = temp[2] * (temp[8] - axn + ayn * esine * temp[3])
|
||||
val sinu = temp[2] * (temp[7] - ayn - axn * esine * temp[3])
|
||||
val u = atan2(sinu, cosu)
|
||||
val sin2u = 2.0 * sinu * cosu
|
||||
val cos2u = 2.0 * cosu * cosu - 1
|
||||
temp[0] = invert(pl)
|
||||
temp[1] = ck2 * temp[0]
|
||||
temp[2] = temp[1] * temp[0]
|
||||
|
||||
// Update for short periodics
|
||||
val rk = r * (1.0 - 1.5 * temp[2] * betal * x3thm1) + 0.5 * temp[1] * x1mth2 * cos2u
|
||||
val uk = u - 0.25 * temp[2] * x7thm1 * sin2u
|
||||
val xnodek = xnode + 1.5 * temp[2] * cosio * sin2u
|
||||
val xinck = data.xincl + 1.5 * temp[2] * cosio * sinio * cos2u
|
||||
val rdotk = rdot - xn * temp[1] * x1mth2 * sin2u
|
||||
val rfdotk = rfdot + xn * temp[1] * (x1mth2 * cos2u + 1.5 * x3thm1)
|
||||
super.calculatePosAndVel(rk, uk, xnodek, xinck, rdotk, rfdotk)
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 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.predict
|
||||
|
||||
data class OrbitalData(
|
||||
val name: String,
|
||||
val epoch: Double,
|
||||
val meanmo: Double,
|
||||
val eccn: Double,
|
||||
val incl: Double,
|
||||
val raan: Double,
|
||||
val argper: Double,
|
||||
val meanan: Double,
|
||||
val catnum: Int,
|
||||
val bstar: Double,
|
||||
val xincl: Double = Math.toRadians(incl),
|
||||
val xnodeo: Double = Math.toRadians(raan),
|
||||
val omegao: Double = Math.toRadians(argper),
|
||||
val xmo: Double = Math.toRadians(meanan),
|
||||
val xno: Double = meanmo * Math.PI * 2.0 / 1440,
|
||||
val isDeepspace: Boolean = meanmo < 6.4
|
||||
) {
|
||||
|
||||
fun createSat(): Satellite {
|
||||
return when {
|
||||
this.isDeepspace -> DeepSpaceSat(this)
|
||||
else -> NearEarthSat(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 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.predict
|
||||
|
||||
import com.rtbishop.look4sat.domain.model.SatRadio
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class Predictor(private val predictorDispatcher: CoroutineDispatcher) {
|
||||
|
||||
private val _calculatedPasses = MutableSharedFlow<List<SatPass>>(replay = 1)
|
||||
val calculatedPasses: SharedFlow<List<SatPass>> = _calculatedPasses
|
||||
|
||||
suspend fun getSatPos(sat: Satellite, pos: GeoPos, time: Long): SatPos {
|
||||
return withContext(predictorDispatcher) { sat.getPosition(pos, time) }
|
||||
}
|
||||
|
||||
suspend fun getSatTrack(sat: Satellite, pos: GeoPos, start: Long, end: Long): List<SatPos> {
|
||||
return withContext(predictorDispatcher) {
|
||||
val positions = mutableListOf<SatPos>()
|
||||
var currentTime = start
|
||||
while (currentTime < end) {
|
||||
positions.add(sat.getPosition(pos, currentTime))
|
||||
currentTime += 15000
|
||||
}
|
||||
positions
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun processRadios(sat: Satellite, pos: GeoPos, radios: List<SatRadio>, time: Long): List<SatRadio> {
|
||||
return withContext(predictorDispatcher) {
|
||||
val satPos = sat.getPosition(pos, time)
|
||||
val copiedList = radios.map { it.copy() }
|
||||
copiedList.forEach { transmitter ->
|
||||
transmitter.downlink?.let {
|
||||
transmitter.downlink = satPos.getDownlinkFreq(it)
|
||||
}
|
||||
transmitter.uplink?.let {
|
||||
transmitter.uplink = satPos.getUplinkFreq(it)
|
||||
}
|
||||
}
|
||||
copiedList.map { it.copy() }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun processPasses(passList: List<SatPass>, time: Long): List<SatPass> {
|
||||
return withContext(predictorDispatcher) {
|
||||
passList.forEach { pass ->
|
||||
if (!pass.isDeepSpace) {
|
||||
val timeStart = pass.aosTime
|
||||
if (time > timeStart) {
|
||||
val deltaNow = time.minus(timeStart).toFloat()
|
||||
val deltaTotal = pass.losTime.minus(timeStart).toFloat()
|
||||
pass.progress = ((deltaNow / deltaTotal) * 100).toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
passList.filter { pass -> pass.progress < 100 }.map { it.copy() }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun forceCalculation(
|
||||
satList: List<Satellite>,
|
||||
pos: GeoPos,
|
||||
time: Long,
|
||||
hoursAhead: Int = 8,
|
||||
minElevation: Double = 16.0
|
||||
) {
|
||||
if (satList.isEmpty()) {
|
||||
_calculatedPasses.emit(emptyList())
|
||||
} else {
|
||||
withContext(predictorDispatcher) {
|
||||
val allPasses = mutableListOf<SatPass>()
|
||||
satList.forEach { satellite ->
|
||||
allPasses.addAll(satellite.getPasses(pos, time, hoursAhead))
|
||||
}
|
||||
_calculatedPasses.emit(allPasses.filter(time, hoursAhead, minElevation))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Satellite.getPasses(pos: GeoPos, time: Long, hours: Int): List<SatPass> {
|
||||
val passes = mutableListOf<SatPass>()
|
||||
val endDate = time + hours * 60L * 60L * 1000L
|
||||
val quarterOrbitMin = (this.orbitalPeriod / 4.0).toInt()
|
||||
var startDate = time
|
||||
var shouldRewind = true
|
||||
var lastAosDate: Long
|
||||
var count = 0
|
||||
if (this.willBeSeen(pos)) {
|
||||
if (this.data.isDeepspace) {
|
||||
passes.add(getGeoPass(this, pos, time))
|
||||
} else {
|
||||
do {
|
||||
if (count > 0) shouldRewind = false
|
||||
val pass = getLeoPass(this, pos, startDate, shouldRewind)
|
||||
lastAosDate = pass.aosTime
|
||||
passes.add(pass)
|
||||
startDate = pass.losTime + (quarterOrbitMin * 3) * 60L * 1000L
|
||||
count++
|
||||
} while (lastAosDate < endDate)
|
||||
}
|
||||
}
|
||||
return passes
|
||||
}
|
||||
|
||||
private fun List<SatPass>.filter(time: Long, hoursAhead: Int, minElev: Double): List<SatPass> {
|
||||
val timeFuture = time + (hoursAhead * 60L * 60L * 1000L)
|
||||
return this.filter { it.losTime > time }
|
||||
.filter { it.aosTime < timeFuture }
|
||||
.filter { it.maxElevation > minElev }
|
||||
.sortedBy { it.aosTime }
|
||||
}
|
||||
|
||||
private fun getGeoPass(sat: Satellite, pos: GeoPos, time: Long): SatPass {
|
||||
val satPos = sat.getPosition(pos, time)
|
||||
val aos = time - 24 * 60L * 60L * 1000L
|
||||
val los = time + 24 * 60L * 60L * 1000L
|
||||
val tca = (aos + los) / 2
|
||||
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: GeoPos, time: Long, rewind: Boolean): SatPass {
|
||||
val quarterOrbitMin = (sat.orbitalPeriod / 4.0).toInt()
|
||||
var calendarTimeMillis = time
|
||||
var elevation: Double
|
||||
var maxElevation = 0.0
|
||||
var alt = 0.0
|
||||
var tcaAz = 0.0
|
||||
// rewind 1/4 of an orbit
|
||||
if (rewind) calendarTimeMillis += -quarterOrbitMin * 60L * 1000L
|
||||
|
||||
var satPos = sat.getPosition(pos, calendarTimeMillis)
|
||||
if (satPos.elevation > 0.0) {
|
||||
// move forward in 30 second intervals until the sat goes below the horizon
|
||||
do {
|
||||
calendarTimeMillis += 30 * 1000L
|
||||
satPos = sat.getPosition(pos, calendarTimeMillis)
|
||||
} while (satPos.elevation > 0.0)
|
||||
// move forward 3/4 of an orbit
|
||||
calendarTimeMillis += quarterOrbitMin * 3 * 60L * 1000L
|
||||
}
|
||||
|
||||
// find the next time sat comes above the horizon
|
||||
do {
|
||||
calendarTimeMillis += 60L * 1000L
|
||||
satPos = sat.getPosition(pos, calendarTimeMillis)
|
||||
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
|
||||
calendarTimeMillis += -60L * 1000L
|
||||
do {
|
||||
calendarTimeMillis += 3L * 1000L
|
||||
satPos = sat.getPosition(pos, calendarTimeMillis)
|
||||
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 {
|
||||
calendarTimeMillis += 30L * 1000L
|
||||
satPos = sat.getPosition(pos, calendarTimeMillis)
|
||||
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
|
||||
calendarTimeMillis += -30L * 1000L
|
||||
do {
|
||||
calendarTimeMillis += 3L * 1000L
|
||||
satPos = sat.getPosition(pos, calendarTimeMillis)
|
||||
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 = (aos + los) / 2
|
||||
val elev = Math.toDegrees(maxElevation)
|
||||
return SatPass(aos, aosAz, los, losAz, tca, tcaAz, alt, elev, sat)
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 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.predict
|
||||
|
||||
data class SatPass(
|
||||
val aosTime: Long,
|
||||
val aosAzimuth: Double,
|
||||
val losTime: Long,
|
||||
val losAzimuth: Double,
|
||||
val tcaTime: Long,
|
||||
val tcaAzimuth: Double,
|
||||
val altitude: Double,
|
||||
val maxElevation: Double,
|
||||
val satellite: Satellite,
|
||||
var progress: Int = 0
|
||||
) {
|
||||
val catNum: Int = satellite.data.catnum
|
||||
val name: String = satellite.data.name
|
||||
val isDeepSpace: Boolean = satellite.data.isDeepspace
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 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.predict
|
||||
|
||||
import kotlin.math.*
|
||||
|
||||
data class SatPos(
|
||||
var azimuth: Double = 0.0,
|
||||
var elevation: Double = 0.0,
|
||||
var latitude: Double = 0.0,
|
||||
var longitude: Double = 0.0,
|
||||
var altitude: Double = 0.0,
|
||||
var distance: Double = 0.0,
|
||||
var distanceRate: Double = 0.0,
|
||||
var theta: Double = 0.0,
|
||||
var time: Long = 0L
|
||||
) {
|
||||
private val earthRadiusKm = 6378.16
|
||||
private val speedOfLight = 2.99792458E8
|
||||
|
||||
fun getDownlinkFreq(freq: Long): Long {
|
||||
return (freq.toDouble() * (speedOfLight - distanceRate * 1000.0) / speedOfLight).toLong()
|
||||
}
|
||||
|
||||
fun getUplinkFreq(freq: Long): Long {
|
||||
return (freq.toDouble() * (speedOfLight + distanceRate * 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> {
|
||||
val positions = mutableListOf<GeoPos>()
|
||||
val beta = acos(earthRadiusKm / (earthRadiusKm + altitude))
|
||||
var tempAzimuth = 0
|
||||
while (tempAzimuth < 360) {
|
||||
val azimuth = tempAzimuth / 360.0 * 2.0 * Math.PI
|
||||
var lat = asin(sin(latitude) * cos(beta) + cos(azimuth) * sin(beta) * cos(latitude))
|
||||
val num = (cos(beta) - (sin(latitude) * sin(lat)))
|
||||
val den = cos(latitude) * cos(lat)
|
||||
var lon = if (tempAzimuth == 0 && (beta > ((Math.PI / 2.0) - latitude))) {
|
||||
longitude + Math.PI
|
||||
} else if (tempAzimuth == 180 && (beta > ((Math.PI / 2.0) - latitude))) {
|
||||
longitude + Math.PI
|
||||
} else if (abs(num / den) > 1.0) {
|
||||
longitude
|
||||
} else {
|
||||
if ((180 - tempAzimuth) >= 0) {
|
||||
longitude - acos(num / den)
|
||||
} else {
|
||||
longitude + acos(num / den)
|
||||
}
|
||||
}
|
||||
while (lon < 0.0) lon += Math.PI * 2.0
|
||||
while (lon > Math.PI * 2.0) lon -= Math.PI * 2.0
|
||||
lat = Math.toDegrees(lat)
|
||||
lon = Math.toDegrees(lon)
|
||||
positions.add(GeoPos(lat, lon))
|
||||
tempAzimuth += 1
|
||||
}
|
||||
return positions
|
||||
}
|
||||
|
||||
fun getRangeCircleRadiusKm(): Double {
|
||||
return earthRadiusKm * acos(earthRadiusKm / (earthRadiusKm + altitude))
|
||||
}
|
||||
}
|
||||
@@ -1,338 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 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.predict
|
||||
|
||||
import kotlin.math.*
|
||||
|
||||
abstract class Satellite(val data: OrbitalData) {
|
||||
|
||||
private val flatFactor = 3.35281066474748E-3
|
||||
private val deg2Rad = 1.745329251994330E-2
|
||||
private val secPerDay = 8.6400E4
|
||||
private val minPerDay = 1.44E3
|
||||
private val epsilon = 1.0E-12
|
||||
private val position = Vector4()
|
||||
private val velocity = Vector4()
|
||||
private var gsPosTheta = 0.0
|
||||
private var perigee = 0.0
|
||||
val orbitalPeriod = 24 * 60 / data.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
|
||||
|
||||
internal fun willBeSeen(pos: GeoPos): Boolean {
|
||||
return if (data.meanmo < 1e-8) false
|
||||
else {
|
||||
val sma = 331.25 * exp(ln(1440.0 / data.meanmo) * (2.0 / 3.0))
|
||||
val apogee = sma * (1.0 + data.eccn) - earthRadius
|
||||
var lin = data.incl
|
||||
if (lin >= 90.0) lin = 180.0 - lin
|
||||
acos(earthRadius / (apogee + earthRadius)) + lin * deg2Rad > abs(pos.latitude * deg2Rad)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun getPosition(pos: GeoPos, time: Long): SatPos {
|
||||
val satPos = SatPos()
|
||||
// Date/time at which the position and velocity were calculated
|
||||
val julUTC = calcCurrentDaynum(time) + 2444238.5
|
||||
// Convert satellite's epoch time to Julian and calculate time since epoch in minutes
|
||||
val julEpoch = juliandDateOfEpoch(data.epoch)
|
||||
val tsince = (julUTC - julEpoch) * minPerDay
|
||||
calculateSDP4orSGP4(tsince)
|
||||
// Scale position and velocity vectors to km and km/sec
|
||||
convertSatState(position, velocity)
|
||||
// Calculate velocity of satellite
|
||||
magnitude(velocity)
|
||||
val squintVector = Vector4()
|
||||
// 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)
|
||||
return satPos.apply { this.time = time }
|
||||
}
|
||||
|
||||
// Read the system clock and return the number of days since 31Dec79 00:00:00 UTC (daynum 0)
|
||||
private fun calcCurrentDaynum(now: Long): Double {
|
||||
val then = 315446400000 // time in millis on 31Dec79 00:00:00 UTC (daynum 0)
|
||||
val millis = now - then
|
||||
return millis / 1000.0 / 60.0 / 60.0 / 24.0
|
||||
}
|
||||
|
||||
private fun juliandDateOfEpoch(epoch: Double): Double {
|
||||
var year = floor(epoch * 1E-3)
|
||||
val day = (epoch * 1E-3 - year) * 1000.0
|
||||
year = if (year < 57) year + 2000 else year + 1900
|
||||
return julianDateOfYear(year) + day
|
||||
}
|
||||
|
||||
internal fun julianDateOfYear(theYear: Double): Double {
|
||||
val aYear = theYear - 1
|
||||
var i = floor(aYear / 100).toLong()
|
||||
val a = i
|
||||
i = a / 4
|
||||
val b = 2 - a + i
|
||||
i = floor(365.25 * aYear).toLong()
|
||||
i += (30.6001 * 14).toLong()
|
||||
return i + 1720994.5 + b
|
||||
}
|
||||
|
||||
private fun calculateSDP4orSGP4(tsince: Double) {
|
||||
if (data.isDeepspace) (this as DeepSpaceSat).calculateSDP4(tsince)
|
||||
else (this as NearEarthSat).calculateSGP4(tsince)
|
||||
}
|
||||
|
||||
// Converts the sat position and velocity vectors to km and km/sec
|
||||
private fun convertSatState(pos: Vector4, vel: Vector4) {
|
||||
scaleVector(earthRadius, pos)
|
||||
scaleVector(earthRadius * minPerDay / secPerDay, vel)
|
||||
}
|
||||
|
||||
// Calculates the topocentric coordinates of the object with ECI pos and vel at time
|
||||
private fun calculateObs(
|
||||
julianUTC: Double,
|
||||
positionVector: Vector4,
|
||||
velocityVector: Vector4,
|
||||
gsPos: GeoPos,
|
||||
squintVector: Vector4,
|
||||
satPos: SatPos
|
||||
) {
|
||||
val obsPos = Vector4()
|
||||
val obsVel = Vector4()
|
||||
val range = Vector4()
|
||||
val rgvel = Vector4()
|
||||
calculateUserPosVel(julianUTC, gsPos, obsPos, obsVel)
|
||||
range.setXYZ(
|
||||
positionVector.x - obsPos.x,
|
||||
positionVector.y - obsPos.y,
|
||||
positionVector.z - obsPos.z
|
||||
)
|
||||
// Save these values globally for calculating squint angles later
|
||||
squintVector.setXYZ(range.x, range.y, range.z)
|
||||
rgvel.setXYZ(
|
||||
velocityVector.x - obsVel.x,
|
||||
velocityVector.y - obsVel.y,
|
||||
velocityVector.z - obsVel.z
|
||||
)
|
||||
magnitude(range)
|
||||
val sinLat = sin(deg2Rad * gsPos.latitude)
|
||||
val cosLat = cos(deg2Rad * gsPos.latitude)
|
||||
val sinTheta = sin(gsPosTheta)
|
||||
val cosTheta = cos(gsPosTheta)
|
||||
val topS = sinLat * cosTheta * range.x + sinLat * sinTheta * range.y - cosLat * range.z
|
||||
val topE = -sinTheta * range.x + cosTheta * range.y
|
||||
val topZ = cosLat * cosTheta * range.x + cosLat * sinTheta * range.y + sinLat * range.z
|
||||
var azim = atan(-topE / topS)
|
||||
if (topS > 0.0) azim += Math.PI
|
||||
if (azim < 0.0) azim += twoPi
|
||||
satPos.azimuth = azim
|
||||
satPos.elevation = asin(topZ / range.w)
|
||||
satPos.distance = range.w
|
||||
satPos.distanceRate = dot(range, rgvel) / range.w
|
||||
}
|
||||
|
||||
// Returns the ECI position and velocity of the observer
|
||||
private fun calculateUserPosVel(
|
||||
time: Double,
|
||||
gsPos: GeoPos,
|
||||
obsPos: Vector4,
|
||||
obsVel: Vector4
|
||||
) {
|
||||
val mFactor = 7.292115E-5
|
||||
gsPosTheta = mod2PI(thetaGJD(time) + deg2Rad * gsPos.longitude)
|
||||
val c =
|
||||
invert(sqrt(1.0 + flatFactor * (flatFactor - 2) * sqr(sin(deg2Rad * gsPos.latitude))))
|
||||
val sq = sqr(1.0 - flatFactor) * c
|
||||
val achcp = (earthRadius * c) * cos(deg2Rad * gsPos.latitude)
|
||||
obsPos.setXYZ(
|
||||
achcp * cos(gsPosTheta), achcp * sin(gsPosTheta),
|
||||
(earthRadius * sq) * sin(deg2Rad * gsPos.latitude)
|
||||
)
|
||||
obsVel.setXYZ(-mFactor * obsPos.y, mFactor * obsPos.x, 0.0)
|
||||
magnitude(obsPos)
|
||||
magnitude(obsVel)
|
||||
}
|
||||
|
||||
// Calculate the geodetic position of an object given its ECI pos and time
|
||||
private fun calculateLatLonAlt(
|
||||
time: Double,
|
||||
satPos: SatPos,
|
||||
position: Vector4 = this.position
|
||||
) {
|
||||
satPos.theta = atan2(position.y, position.x)
|
||||
satPos.longitude = mod2PI(satPos.theta - thetaGJD(time))
|
||||
val r = sqrt(sqr(position.x) + sqr(position.y))
|
||||
val e2 = flatFactor * (2.0 - flatFactor)
|
||||
satPos.latitude = atan2(position.z, r)
|
||||
var phi: Double
|
||||
var c: Double
|
||||
var i = 0
|
||||
var converged: Boolean
|
||||
do {
|
||||
phi = satPos.latitude
|
||||
c = invert(sqrt(1.0 - e2 * sqr(sin(phi))))
|
||||
satPos.latitude = atan2(position.z + earthRadius * c * e2 * sin(phi), r)
|
||||
converged = abs(satPos.latitude - phi) < epsilon
|
||||
} while (i++ < 10 && !converged)
|
||||
satPos.altitude = r / cos(satPos.latitude) - earthRadius * c
|
||||
var temp = satPos.latitude
|
||||
if (temp > Math.PI / 2.0) {
|
||||
temp -= twoPi
|
||||
satPos.latitude = temp
|
||||
}
|
||||
}
|
||||
|
||||
internal fun calculatePosAndVel(
|
||||
rk: Double, uk: Double, xnodek: Double,
|
||||
xinck: Double, rdotk: Double, rfdotk: Double
|
||||
) {
|
||||
// Orientation vectors
|
||||
val sinuk = sin(uk)
|
||||
val cosuk = cos(uk)
|
||||
val sinik = sin(xinck)
|
||||
val cosik = cos(xinck)
|
||||
val sinnok = sin(xnodek)
|
||||
val cosnok = cos(xnodek)
|
||||
val xmx = -sinnok * cosik
|
||||
val xmy = cosnok * cosik
|
||||
val ux = xmx * sinuk + cosnok * cosuk
|
||||
val uy = xmy * sinuk + sinnok * cosuk
|
||||
val uz = sinik * sinuk
|
||||
val vx = xmx * cosuk - cosnok * sinuk
|
||||
val vy = xmy * cosuk - sinnok * sinuk
|
||||
val vz = sinik * cosuk
|
||||
// Position and velocity
|
||||
position.setXYZ(ux, uy, uz)
|
||||
position.multiply(rk)
|
||||
velocity.x = rdotk * ux + rfdotk * vx
|
||||
velocity.y = rdotk * uy + rfdotk * vy
|
||||
velocity.z = rdotk * uz + rfdotk * vz
|
||||
}
|
||||
|
||||
internal class Vector4 {
|
||||
var w = 0.0
|
||||
var x = 0.0
|
||||
var y = 0.0
|
||||
var z = 0.0
|
||||
|
||||
fun multiply(multiplier: Double) {
|
||||
x *= multiplier
|
||||
y *= multiplier
|
||||
z *= multiplier
|
||||
}
|
||||
|
||||
fun setXYZ(xValue: Double, yValue: Double, zValue: Double) {
|
||||
x = xValue
|
||||
y = yValue
|
||||
z = zValue
|
||||
}
|
||||
}
|
||||
|
||||
internal fun sqr(arg: Double): Double {
|
||||
return arg * arg
|
||||
}
|
||||
|
||||
internal fun invert(value: Double): Double {
|
||||
return 1.0 / value
|
||||
}
|
||||
|
||||
// Calculates the modulus of 2 * PI
|
||||
internal fun mod2PI(value: Double): Double {
|
||||
var retVal = value
|
||||
val i = (retVal / twoPi).toInt()
|
||||
retVal -= i * twoPi
|
||||
if (retVal < 0.0) retVal += twoPi
|
||||
return retVal
|
||||
}
|
||||
|
||||
// Solves Keplers' Equation
|
||||
internal fun converge(temp: DoubleArray, axn: Double, ayn: Double, capu: Double) {
|
||||
var converged = false
|
||||
var i = 0
|
||||
do {
|
||||
temp[7] = sin(temp[2])
|
||||
temp[8] = cos(temp[2])
|
||||
temp[3] = axn * temp[7]
|
||||
temp[4] = ayn * temp[8]
|
||||
temp[5] = axn * temp[8]
|
||||
temp[6] = ayn * temp[7]
|
||||
val epw = (capu - temp[4] + temp[3] - temp[2]) / (1.0 - temp[5] - temp[6]) + temp[2]
|
||||
if (abs(epw - temp[2]) <= epsilon) converged = true else temp[2] = epw
|
||||
} while (i++ < 10 && !converged)
|
||||
}
|
||||
|
||||
// Sets perigee and checks and adjusts the calculation if the perigee is less tan 156KM
|
||||
internal fun setPerigee(perigee: Double) {
|
||||
this.perigee = perigee
|
||||
checkPerigee()
|
||||
}
|
||||
|
||||
// Checks and adjusts the calculation if the perigee is less tan 156KM
|
||||
private fun checkPerigee() {
|
||||
s4 = 1.012229
|
||||
qoms24 = 1.880279E-09
|
||||
if (perigee < 156.0) {
|
||||
s4 = if (perigee <= 98.0) 20.0 else perigee - 78.0
|
||||
qoms24 = ((120 - s4) / earthRadius).pow(4.0)
|
||||
s4 = s4 / earthRadius + 1.0
|
||||
}
|
||||
}
|
||||
|
||||
// Calculates the dot product of two vectors
|
||||
private fun dot(v1: Vector4, v2: Vector4): Double {
|
||||
return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z
|
||||
}
|
||||
|
||||
// Returns fractional part of double argument
|
||||
private fun fraction(arg: Double): Double {
|
||||
return arg - floor(arg)
|
||||
}
|
||||
|
||||
// Calculates scalar magnitude of a vector4 argument
|
||||
private fun magnitude(v: Vector4) {
|
||||
v.w = sqrt(sqr(v.x) + sqr(v.y) + sqr(v.z))
|
||||
}
|
||||
|
||||
private fun modulus(arg1: Double): Double {
|
||||
var returnValue = arg1
|
||||
val i = floor(returnValue / secPerDay).toInt()
|
||||
returnValue -= i * secPerDay
|
||||
if (returnValue < 0.0) returnValue += secPerDay
|
||||
return returnValue
|
||||
}
|
||||
|
||||
// Multiplies the vector v1 by the scalar k
|
||||
private fun scaleVector(k: Double, v: Vector4) {
|
||||
v.multiply(k)
|
||||
magnitude(v)
|
||||
}
|
||||
|
||||
private fun thetaGJD(theJD: Double): Double {
|
||||
val earthRotPerSidDay = 1.00273790934
|
||||
val ut = fraction(theJD + 0.5)
|
||||
val aJD = theJD - ut
|
||||
val tu = (aJD - 2451545.0) / 36525.0
|
||||
var gmst = 24110.54841 + tu * (8640184.812866 + tu * (0.093104 - tu * 6.2E-6))
|
||||
gmst = modulus(gmst + secPerDay * earthRotPerSidDay * ut)
|
||||
return twoPi * gmst / secPerDay
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 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
|
||||
|
||||
import com.rtbishop.look4sat.data.DataParser
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
class DataParserTest {
|
||||
|
||||
private val testDispatcher = StandardTestDispatcher()
|
||||
private val dataParser = DataParser(testDispatcher)
|
||||
private val validCSVStream = """
|
||||
OBJECT_NAME,OBJECT_ID,EPOCH,MEAN_MOTION,ECCENTRICITY,INCLINATION,RA_OF_ASC_NODE,ARG_OF_PERICENTER,MEAN_ANOMALY,EPHEMERIS_TYPE,CLASSIFICATION_TYPE,NORAD_CAT_ID,ELEMENT_SET_NO,REV_AT_EPOCH,BSTAR,MEAN_MOTION_DOT,MEAN_MOTION_DDOT
|
||||
ISS (ZARYA),1998-067A,2021-11-16T12:28:09.322176,15.48582035,.0004694,51.6447,309.4881,203.6966,299.8876,0,U,25544,999,31220,.31985E-4,.1288E-4,0
|
||||
FLTSATCOM 8 (USA 46),1989-077A,2022-01-07T11:37:38.074080,1.00273350,.0001114,12.9044,1.3272,91.5769,260.4200,0,U,20253,999,24434,0,-.85E-6,0
|
||||
""".trimIndent().byteInputStream()
|
||||
private val invalidCSVStream = """
|
||||
ISS (ZARYA),1998-067A,2021-11-16T12:28:09.322176,15.48582035,.0004694,51.6447,309.4881,203.6966,299.8876,0,U,25544,999,31220,.31985E-4,.1288E-4,0
|
||||
OBJECT_NAME,OBJECT_ID,EPOCH,MEAN_MOTION,ECCENTRICITY,INCLINATION,RA_OF_ASC_NODE,ARG_OF_PERICENTER,MEAN_ANOMALY,EPHEMERIS_TYPE,CLASSIFICATION_TYPE,NORAD_CAT_ID,ELEMENT_SET_NO,REV_AT_EPOCH,BSTAR,MEAN_MOTION_DOT,MEAN_MOTION_DDOT
|
||||
""".trimIndent().byteInputStream()
|
||||
private val validTLEStream = """
|
||||
ISS (ZARYA)
|
||||
1 25544U 98067A 21320.51955234 .00001288 00000+0 31985-4 0 9990
|
||||
2 25544 51.6447 309.4881 0004694 203.6966 299.8876 15.48582035312205
|
||||
FLTSATCOM 8 (USA 46)
|
||||
1 20253U 89077A 22007.48446845 -.00000085 00000+0 00000+0 0 9999
|
||||
2 20253 12.9044 1.3272 0001114 91.5769 260.4200 1.00273350244345
|
||||
""".trimIndent().byteInputStream()
|
||||
private val invalidTLEStream = """
|
||||
1 25544U 98067A 21320.51955234 .00001288 00000+0 31985-4 0 9990
|
||||
2 25544 51.6447 309.4881 0004694 203.6966 299.8876 15.48582035312205
|
||||
""".trimIndent().byteInputStream()
|
||||
private val validJSONStream = """
|
||||
[{"uuid":"UzPz4gcsNBPKPKAFPmer7g","description":"Upper side band (drifting)","alive":true,"type":"Transmitter","uplink_low":null,"uplink_high":null,"uplink_drift":null,"downlink_low":136658500,"downlink_high":null,"downlink_drift":null,"mode":"USB","mode_id":9,"uplink_mode":null,"invert":false,"baud":null,"sat_id":"SCHX-0895-2361-9925-0309","norad_cat_id":965,"status":"active","updated":"2019-04-18T05:39:53.343316Z","citation":"CITATION NEEDED - https://xkcd.com/285/","service":"Unknown","coordination":"","coordination_url":""}]
|
||||
""".trimIndent().byteInputStream()
|
||||
private val invalidJSONStream = """
|
||||
[{"description":"Upper side band (drifting)","alive":true,"type":"Transmitter","uplink_low":null,"uplink_high":null,"uplink_drift":null,"downlink_low":136658500,"downlink_high":null,"downlink_drift":null,"mode":"USB","mode_id":9,"uplink_mode":null,"invert":false,"baud":null,"sat_id":"SCHX-0895-2361-9925-0309","norad_cat_id":965,"status":"active","updated":"2019-04-18T05:39:53.343316Z","citation":"CITATION NEEDED - https://xkcd.com/285/","service":"Unknown","coordination":"","coordination_url":""}]
|
||||
""".trimIndent().byteInputStream()
|
||||
|
||||
@Test
|
||||
fun `Given valid CSV stream returns valid data`() = runTest(testDispatcher) {
|
||||
val parsedList = dataParser.parseCSVStream(validCSVStream)
|
||||
assert(parsedList[0].epoch == 21320.51955234)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given invalid CSV stream returns empty list`() = runTest(testDispatcher) {
|
||||
val parsedList = dataParser.parseCSVStream(invalidCSVStream)
|
||||
assert(parsedList.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given valid TLE stream returns valid data`() = runTest(testDispatcher) {
|
||||
val parsedList = dataParser.parseTLEStream(validTLEStream)
|
||||
assert(parsedList[0].epoch == 21320.51955234)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given invalid TLE stream returns empty list`() = runTest(testDispatcher) {
|
||||
val parsedList = dataParser.parseTLEStream(invalidTLEStream)
|
||||
assert(parsedList.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given valid data streams parsed results match`() = runTest(testDispatcher) {
|
||||
val csvResult = dataParser.parseCSVStream(validCSVStream)
|
||||
val tleResult = dataParser.parseTLEStream(validTLEStream)
|
||||
assert(csvResult == tleResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given valid JSON stream returns valid data`() = runTest(testDispatcher) {
|
||||
val parsedList = dataParser.parseJSONStream(validJSONStream)
|
||||
assert(parsedList[0].downlink == 136658500L)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given invalid JSON stream returns empty list`() = runTest(testDispatcher) {
|
||||
val parsedList = dataParser.parseJSONStream(invalidJSONStream)
|
||||
assert(parsedList.isEmpty())
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2022 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
|
||||
|
||||
import com.rtbishop.look4sat.domain.QthConverter
|
||||
import org.junit.Test
|
||||
|
||||
class QthConverterTest {
|
||||
|
||||
@Test
|
||||
fun `Given valid QTH returns correct POS`() {
|
||||
var result = QthConverter.qthToPosition("io91VL39FX")
|
||||
assert(result?.latitude == 51.4792 && result.longitude == -0.2083)
|
||||
result = QthConverter.qthToPosition("JN58TD")
|
||||
assert(result?.latitude == 48.1458 && result.longitude == 11.6250)
|
||||
result = QthConverter.qthToPosition("gf15vc")
|
||||
assert(result?.latitude == -34.8958 && result.longitude == -56.2083)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given invalid QTH returns null`() {
|
||||
assert(QthConverter.qthToPosition("ZZ00zz") == null)
|
||||
assert(QthConverter.qthToPosition("JN58") == 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")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given invalid POS returns null`() {
|
||||
assert(QthConverter.positionToQth(91.0542, -170.1142) == null)
|
||||
assert(QthConverter.positionToQth(89.0542, -240.1142) == null)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user