Added basic convention plugins setup for modularization

This commit is contained in:
Arty Bishop committed 2026-03-01 20:02:30 +00:00
1 parent de99e09f5e
commit 4d88214c1d
5675 files changed
+10339 -9969

No files matched your search

@@ -0,0 +1,20 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* 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.core.domain.model
data class SatItem(val catnum: Int, val name: String, val isSelected: Boolean = false)
@@ -0,0 +1,32 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* 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.core.domain.model
data class SatRadio(
val uuid: String,
val info: String,
val isAlive: Boolean,
var downlinkLow: Long?,
var downlinkHigh: Long?,
val downlinkMode: String?,
var uplinkLow: Long?,
var uplinkHigh: Long?,
val uplinkMode: String?,
val isInverted: Boolean,
val catnum: Int?
)
@@ -0,0 +1,65 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* 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.core.domain.model
data class DatabaseState(
val numberOfRadios: Int,
val numberOfSatellites: Int,
val updateTimestamp: Long
)
data class PassesSettings(
val hoursAhead: Int,
val minElevation: Double,
val selectedModes: List<String>
)
data class RCSettings(
val rotatorState: Boolean,
val rotatorAddress: String,
val rotatorPort: String,
val rotatorFormat: String,
val frequencyState: Boolean,
val frequencyAddress: String,
val frequencyPort: String,
val frequencyFormat: String,
val bluetoothRotatorState: Boolean,
val bluetoothRotatorFormat: String,
val bluetoothRotatorName: String,
val bluetoothRotatorAddress: String,
val bluetoothFrequencyState: Boolean,
val bluetoothFrequencyFormat: String,
val bluetoothFrequencyAddress: String
)
data class OtherSettings(
val stateOfAutoUpdate: Boolean,
val stateOfSensors: Boolean,
val stateOfSweep: Boolean,
val stateOfUtc: Boolean,
val stateOfLightTheme: Boolean,
val shouldSeeWarning: Boolean,
val shouldSeeWhatsNew: Boolean
)
data class DataSourcesSettings(
val useCustomTLE: Boolean,
val useCustomTransceivers: Boolean,
val tleUrl: String,
val transceiversUrl: String
)
@@ -0,0 +1,37 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* 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.core.domain.predict
const val ASTRONOMICAL_UNIT = 1.49597870691E8
const val DEG2RAD = 0.017453292519943295
const val RAD2DEG = 57.29577951308232
const val EARTH_RADIUS = 6378.137
const val EPSILON = 1.0E-12
const val FLAT_FACT = 3.35281066474748E-3
const val J3_HARMONIC = -2.53881E-6
const val MIN_PER_DAY = 1.44E3
const val SEC_PER_DAY = 8.6400E4
const val SOLAR_RADIUS = 6.96000E5
const val SPEED_OF_LIGHT = 2.99792458E8
const val PI = 3.141592653589793
const val PI_2 = PI / 2.0
const val TWO_PI = PI * 2.0
const val TWO_THIRDS = 2.0 / 3.0
const val CK2 = 5.413079E-4
const val CK4 = 6.209887E-7
const val XKE = 7.43669161E-2
@@ -0,0 +1,853 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* 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.core.domain.predict
import kotlin.math.abs
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.floor
import kotlin.math.pow
import kotlin.math.sin
import kotlin.math.sqrt
class DeepSpaceObject(data: OrbitalData) : OrbitalObject(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 / data.xno).pow(TWO_THIRDS)
dsv.cosio = cos(data.xincl)
dsv.theta2 = dsv.cosio * dsv.cosio
x3thm1 = 3.0 * dsv.theta2 - 1
dsv.eosq = data.eccn * 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 * TWO_THIRDS + del1 * (1.0 + 134.0 / 81.0 * del1)))
val delo = 1.5 * CK2 * x3thm1 / (ao * ao * dsv.betao * dsv.betao2)
dsv.xnodp = 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 - data.eccn) - 1.0) * EARTH_RADIUS)
val pinvsq = invert(dsv.aodp * dsv.aodp * dsv.betao2 * dsv.betao2)
dsv.sing = sin(data.omegao)
dsv.cosg = cos(data.omegao)
val tsi = invert(dsv.aodp - s4)
val eta = dsv.aodp * data.eccn * tsi
val etasq = eta * eta
val eeta = 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 = data.bstar * c2
dsv.sinio = sin(data.xincl)
val a3ovk2 = -J3_HARMONIC / CK2
x1mth2 = 1.0 - dsv.theta2
c4 =
2 * dsv.xnodp * coef1 * dsv.aodp * dsv.betao2 * (eta * (2.0 + 0.5 * etasq) + 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 * 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(TWO_THIRDS) * tempa * tempa
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)
calculatePhase(xlt, dsv.xnode, dsv.omgadf)
}
}
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)
}
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 !in 0.00826..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 += pinc
dsv.em += pe
if (xqncl >= 0.2) {
/* Apply periodics directly */
ph /= dsv.sinio
pgh -= dsv.cosio * ph
dsv.omgadf += pgh
dsv.xnode += ph
dsv.xll += pl
} else {
applyPeriodics()
// This is a patch to Lyddane modification suggested by Rob Matson
if (abs(xnoh - dsv.xnode) > PI) {
if (dsv.xnode < xnoh) dsv.xnode += TWO_PI else dsv.xnode -= TWO_PI
}
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 += ssl * dsv.t
dsv.omgadf += ssg * dsv.t
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 += PI
dsv.omgadf -= 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)
}
}
}
@@ -0,0 +1,26 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* 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.core.domain.predict
data class GeoPos(
val latitude: Double,
val longitude: Double,
val altitude: Double = 0.0,
val qthLocator: String = "null",
val timestamp: Long = 0L
)
@@ -0,0 +1,226 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* 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.core.domain.predict
import kotlin.math.abs
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.sin
import kotlin.math.sqrt
class NearEarthObject(data: OrbitalData) : OrbitalObject(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 / data.xno).pow(TWO_THIRDS)
cosio = cos(data.xincl)
val theta2 = sqr(cosio)
x3thm1 = 3.0 * theta2 - 1.0
val eo = 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 * TWO_THIRDS + del1 * (1.0 + 134.0 / 81.0 * del1)))
val delo = 1.5 * CK2 * x3thm1 / (sqr(ao) * betao * betao2)
xnodp = 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 / EARTH_RADIUS + 1.0
// For perigees below 156 km, the values of S and QOMS2T are altered
setPerigee((aodp * (1.0 - eo) - 1.0) * EARTH_RADIUS)
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 = 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(data.xincl)
val a3ovk2 = -J3_HARMONIC / CK2
val c3 = coef * tsi * a3ovk2 * xnodp * sinio / eo
x1mth2 = 1.0 - theta2
val omegao = 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 = -TWO_THIRDS * 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 = 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)
calculatePhase(xlt, xnode, omgadf)
}
}
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)
}
}
@@ -0,0 +1,40 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* 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.core.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 = incl * DEG2RAD
val xnodeo: Double = raan * DEG2RAD
val omegao: Double = argper * DEG2RAD
val xmo: Double = meanan * DEG2RAD
val xno: Double = meanmo * TWO_PI / MIN_PER_DAY
val orbitalPeriod: Double = MIN_PER_DAY / meanmo
val isDeepSpace: Boolean = orbitalPeriod >= 225.0 // NearEarth (period < 225 min) or DeepSpace (period >= 225 min)
fun getObject(): OrbitalObject = if (isDeepSpace) DeepSpaceObject(this) else NearEarthObject(this)
}
@@ -0,0 +1,423 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* 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.core.domain.predict
import kotlin.math.abs
import kotlin.math.acos
import kotlin.math.asin
import kotlin.math.atan
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.exp
import kotlin.math.floor
import kotlin.math.ln
import kotlin.math.pow
import kotlin.math.sin
import kotlin.math.sqrt
abstract class OrbitalObject(val data: OrbitalData) {
private val position = Vector4()
private val velocity = Vector4()
private var orbitalPos = OrbitalPos()
private var eclipseDepth = 0.0
private var gsPosTheta = 0.0
private var julUTC = 0.0
private var perigee = 0.0
var qoms24 = 0.0
var s4 = 0.0
fun willBeSeen(pos: GeoPos): Boolean {
return if (data.meanmo < 1e-8) false
else {
val sma = 331.25 * exp(ln(MIN_PER_DAY / data.meanmo) * (2.0 / 3.0))
val apogee = sma * (1.0 + data.eccn) - EARTH_RADIUS
var lin = data.incl
if (lin >= 90.0) lin = 180.0 - lin
acos(EARTH_RADIUS / (apogee + EARTH_RADIUS)) + lin * DEG2RAD > abs(pos.latitude * DEG2RAD)
}
}
fun getPosition(pos: GeoPos, time: Long): OrbitalPos {
orbitalPos = OrbitalPos()
// Date/time at which the position and velocity were calculated
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) * MIN_PER_DAY
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)
calculateLatLonAlt(julUTC)
orbitalPos.time = time
orbitalPos.eclipsed = isEclipsed()
orbitalPos.eclipseDepth = eclipseDepth
return orbitalPos
}
private fun calcCurrentDaynum(now: Long): Double {
val then = 315446400000 // time in millis on 31Dec79 00:00:00 UTC (daynum 0)
return (now - then) / 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 DeepSpaceObject).calculateSDP4(tsince)
else (this as NearEarthObject).calculateSGP4(tsince)
}
// Converts the sat position and velocity vectors to km and km/sec
private fun convertSatState(pos: Vector4, vel: Vector4) {
scaleVector(EARTH_RADIUS, pos)
scaleVector(EARTH_RADIUS * MIN_PER_DAY / SEC_PER_DAY, 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
) {
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 += PI
if (azim < 0.0) azim += TWO_PI
orbitalPos.azimuth = azim
orbitalPos.elevation = asin(topZ / range.w)
orbitalPos.distance = range.w
orbitalPos.distanceRate = dot(range, rgvel) / range.w
var elevation = orbitalPos.elevation / TWO_PI * 360.0
if (elevation > 90) elevation = 180 - elevation
orbitalPos.aboveHorizon = elevation - 0 > EPSILON
}
// 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 + FLAT_FACT * (FLAT_FACT - 2) * sqr(sin(DEG2RAD * gsPos.latitude))))
val sq = sqr(1.0 - FLAT_FACT) * c
val achcp = (EARTH_RADIUS * c + gsPos.altitude / 1000.0) * cos(DEG2RAD * gsPos.latitude)
obsPos.setXYZ(
achcp * cos(gsPosTheta), achcp * sin(gsPosTheta),
(EARTH_RADIUS * sq + gsPos.altitude / 1000.0) * 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) {
orbitalPos.theta = atan2(position.y, position.x)
orbitalPos.longitude = mod2PI(orbitalPos.theta - thetaGJD(time))
val r = sqrt(sqr(position.x) + sqr(position.y))
val e2 = FLAT_FACT * (2.0 - FLAT_FACT)
orbitalPos.latitude = atan2(position.z, r)
var phi: Double
var c: Double
var i = 0
var converged: Boolean
do {
phi = orbitalPos.latitude
c = invert(sqrt(1.0 - e2 * sqr(sin(phi))))
orbitalPos.latitude = atan2(position.z + EARTH_RADIUS * c * e2 * sin(phi), r)
converged = abs(orbitalPos.latitude - phi) < EPSILON
} while (i++ < 10 && !converged)
orbitalPos.altitude = r / cos(orbitalPos.latitude) - EARTH_RADIUS * c
var temp = orbitalPos.latitude
if (temp > PI_2) {
temp -= TWO_PI
orbitalPos.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: Double = 0.0,
var x: Double = 0.0,
var y: Double = 0.0,
var z: Double = 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 / TWO_PI).toInt()
retVal -= i * TWO_PI
if (retVal < 0.0) retVal += TWO_PI
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)
}
internal fun calculatePhase(xlt: Double, xnode: Double, omgadf: Double) {
var phaseValue = xlt - xnode - omgadf + TWO_PI
if (phaseValue < 0.0) phaseValue += TWO_PI
orbitalPos.phase = mod2PI(phaseValue)
}
// 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) / EARTH_RADIUS).pow(4.0)
s4 = s4 / EARTH_RADIUS + 1.0
}
}
// Checks if the satellite is in sunlight
private fun isEclipsed(): Boolean {
val sunVector = calculateSunVector()
val sdEarth = asin(EARTH_RADIUS / position.w)
val rho = subtract(sunVector, position)
val sdSun = asin(SOLAR_RADIUS / rho.w)
val earth = scalarNegMultiply(position)
val delta = angle(sunVector, earth)
eclipseDepth = sdEarth - sdSun - delta
return if (sdEarth < sdSun) false else eclipseDepth >= 0
}
private fun calculateSunVector(): Vector4 {
val mjd = julUTC - 2415020.0
val year = 1900 + mjd / 365.25
val solTime = (mjd + deltaEt(year) / SEC_PER_DAY) / 36525.0
val mTemp = modulus(35999.04975 * solTime, 360.0)
val m = radians(
modulus(358.47583 + mTemp - (0.000150 + 0.0000033 * solTime) * sqr(solTime), 360.0)
)
val lTemp = modulus(36000.76892 * solTime, 360.0)
val l = radians(
modulus(279.69668 + lTemp + 0.0003025 * sqr(solTime), 360.0)
)
val e = 0.01675104 - (0.0000418 + 0.000000126 * solTime) * solTime
val c = radians(
((1.919460 - (0.004789 + 0.000014 * solTime) * solTime) * sin(m))
+ ((0.020094 - 0.000100 * solTime) * sin(2 * m)) + 0.000293 * sin(3 * m)
)
val o = radians(modulus(259.18 - 1934.142 * solTime, 360.0))
val lsa = modulus(l + c - radians(0.00569 - 0.00479 * sin(o)), TWO_PI)
val nu = modulus(m + c, TWO_PI)
var r = (1.0000002 * (1.0 - sqr(e)) / (1.0 + e * cos(nu)))
val eps = radians(
23.452294 - (0.0130125 + (0.00000164 - 0.000000503 * solTime) * solTime)
* solTime + 0.00256 * cos(o)
)
r *= ASTRONOMICAL_UNIT
return Vector4(r, r * cos(lsa), r * sin(lsa) * cos(eps), r * sin(lsa) * sin(eps))
}
private fun subtract(v1: Vector4, v2: Vector4): Vector4 {
val v3 = Vector4()
v3.x = v1.x - v2.x
v3.y = v1.y - v2.y
v3.z = v1.z - v2.z
magnitude(v3)
return v3
}
private fun scalarNegMultiply(vector: Vector4): Vector4 {
val neg = -1.0
return Vector4(vector.w * abs(neg), vector.x * neg, vector.y * neg, vector.z * neg)
}
private fun angle(v1: Vector4, v2: Vector4): Double {
magnitude(v1)
magnitude(v2)
return acos(dot(v1, v2) / (v1.w * v2.w))
}
/**
* The function Delta_ET has been added to allow calculations on the
* position of the sun. It provides the difference between UT (approximately
* the same as UTC) and ET (now referred to as TDT) This function is based
* on a least squares fit of data from 1950 to 1991 and will need to be
* updated periodically.
*
* Values determined using data from 1950-1991 in the 1990 Astronomical
* Almanac. See DELTA_ET.WQ1 for details.
*/
private fun deltaEt(year: Double): Double {
return 26.465 + 0.747622 * (year - 1950) + (1.886913 * sin(TWO_PI * (year - 1975) / 33))
}
private fun radians(degrees: Double): Double {
return degrees * DEG2RAD
}
// 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, arg2: Double = SEC_PER_DAY): Double {
var returnValue = arg1
val i = floor(returnValue / arg2).toInt()
returnValue -= i * arg2
if (returnValue < 0.0) returnValue += arg2
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 + SEC_PER_DAY * earthRotPerSidDay * ut)
return TWO_PI * gmst / SEC_PER_DAY
}
}
@@ -0,0 +1,33 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* 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.core.domain.predict
data class OrbitalPass(
val aosTime: Long = 0L,
val aosAzimuth: Double = 90.0,
val losTime: Long = 0L,
val losAzimuth: Double = 270.0,
val altitude: Int = 1000,
val maxElevation: Double = 75.0,
val orbitalObject: OrbitalObject,
var progress: Float = 0.0f
) {
val catNum: Int = orbitalObject.data.catnum
val name: String = orbitalObject.data.name
val isDeepSpace: Boolean = orbitalObject.data.isDeepSpace
}
@@ -0,0 +1,72 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* 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.core.domain.predict
import kotlin.math.acos
import kotlin.math.asin
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.sin
import kotlin.math.sqrt
data class OrbitalPos(
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,
var phase: Double = 0.0,
var eclipseDepth: Double = 0.0,
var eclipsed: Boolean = false,
var aboveHorizon: Boolean = false
) {
fun getDownlinkFreq(freq: Long): Long {
return (freq.toDouble() * (SPEED_OF_LIGHT - distanceRate * 1000.0) / SPEED_OF_LIGHT).toLong()
}
fun getUplinkFreq(freq: Long): Long {
return (freq.toDouble() * (SPEED_OF_LIGHT + distanceRate * 1000.0) / SPEED_OF_LIGHT).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 rangeCirclePoints = mutableListOf<GeoPos>()
val beta = acos(EARTH_RADIUS / (EARTH_RADIUS + altitude)) // * EARTH_RADIUS = radiusKm
for (azimuth in 0..720) {
val rads = azimuth * DEG2RAD
val lat = asin(sin(latitude) * cos(beta) + (cos(latitude) * sin(beta) * cos(rads)))
val lon = (longitude + atan2(
sin(rads) * sin(beta) * cos(latitude), cos(beta) - sin(latitude) * sin(lat)
))
rangeCirclePoints.add(GeoPos(lat * RAD2DEG, lon * RAD2DEG))
}
return rangeCirclePoints
}
}
@@ -0,0 +1,25 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* 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.core.domain.repository
interface IDatabaseRepo {
suspend fun updateTLEFromFile(uri: String)
suspend fun updateTransceiversFromFile(uri: String)
suspend fun updateFromRemote()
suspend fun clearAllData()
}
@@ -0,0 +1,22 @@
package com.rtbishop.look4sat.core.domain.repository
import com.rtbishop.look4sat.core.domain.usecase.IAddToCalendar
import com.rtbishop.look4sat.core.domain.usecase.IShowToast
import kotlinx.coroutines.CoroutineScope
interface IMainContainer {
val appScope: CoroutineScope
val settingsRepo: ISettingsRepo
val selectionRepo: ISelectionRepo
val satelliteRepo: ISatelliteRepo
val databaseRepo: IDatabaseRepo
fun provideAddToCalendar(): IAddToCalendar
fun provideShowToast(): IShowToast
fun provideBluetoothReporter(): IReporterRepo<WithoutExtParams>
fun provideNetworkReporter(): IReporterRepo<ExtendedParams>
fun provideSensorsRepo(): ISensorsRepo
}
interface IContainerProvider {
fun getMainContainer(): IMainContainer
}
@@ -0,0 +1,34 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* 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.core.domain.repository
interface IReporterParams
interface IReporterRepo<T : IReporterParams> {
fun isConnected(service: BtService): Boolean
fun isConnecting(service: BtService): Boolean
fun connect(service: BtService, deviceId: String)
fun reportRotation(format: String, azimuth: Double, elevation: Double, params: T)
fun reportFrequency(format: String, frequency: Long, params: T)
}
data class ExtendedParams(val server: String, val port: Int) : IReporterParams
data class WithoutExtParams(val dummy: Int) : IReporterParams
enum class BtService { ROTATOR, FREQUENCY }
@@ -0,0 +1,37 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* 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.core.domain.repository
import com.rtbishop.look4sat.core.domain.model.SatRadio
import com.rtbishop.look4sat.core.domain.predict.GeoPos
import com.rtbishop.look4sat.core.domain.predict.OrbitalObject
import com.rtbishop.look4sat.core.domain.predict.OrbitalPass
import com.rtbishop.look4sat.core.domain.predict.OrbitalPos
import kotlinx.coroutines.flow.StateFlow
interface ISatelliteRepo {
val passes: StateFlow<List<OrbitalPass>>
val satellites: StateFlow<List<OrbitalObject>>
suspend fun getRadiosWithId(id: Int): List<SatRadio>
suspend fun initRepository()
suspend fun getPosition(sat: OrbitalObject, pos: GeoPos, time: Long): OrbitalPos
suspend fun getTrack(sat: OrbitalObject, pos: GeoPos, start: Long, end: Long): List<OrbitalPos>
suspend fun getRadios(sat: OrbitalObject, pos: GeoPos, radios: List<SatRadio>, time: Long): List<SatRadio>
suspend fun processPasses(passList: List<OrbitalPass>, time: Long): List<OrbitalPass>
suspend fun calculatePasses(time: Long, hoursAhead: Int, minElevation: Double, modes: List<String>)
}
@@ -0,0 +1,32 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* 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.core.domain.repository
import com.rtbishop.look4sat.core.domain.model.SatItem
import kotlinx.coroutines.flow.Flow
interface ISelectionRepo {
fun getCurrentTypes(): List<String>
fun getTypesList(): List<String>
suspend fun getEntriesFlow(): Flow<List<SatItem>>
suspend fun setTypes(types: List<String>)
suspend fun setQuery(query: String)
suspend fun setSelection(selectAll: Boolean)
suspend fun setSelection(ids: List<Int>, isTicked: Boolean)
suspend fun saveSelection()
}
@@ -0,0 +1,28 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* 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.core.domain.repository
import com.rtbishop.look4sat.core.domain.predict.GeoPos
import kotlinx.coroutines.flow.StateFlow
interface ISensorsRepo {
val orientation: StateFlow<Pair<Float, Float>>
fun getMagDeclination(geoPos: GeoPos, time: Long = System.currentTimeMillis()): Float
fun enableSensor()
fun disableSensor()
}
@@ -0,0 +1,95 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* 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.core.domain.repository
import com.rtbishop.look4sat.core.domain.model.DataSourcesSettings
import com.rtbishop.look4sat.core.domain.model.DatabaseState
import com.rtbishop.look4sat.core.domain.model.OtherSettings
import com.rtbishop.look4sat.core.domain.model.PassesSettings
import com.rtbishop.look4sat.core.domain.model.RCSettings
import com.rtbishop.look4sat.core.domain.predict.GeoPos
import kotlinx.coroutines.flow.StateFlow
interface ISettingsRepo {
val appVersionName: String
//region # Satellites selection settings
val selectedIds: StateFlow<List<Int>>
val selectedTypes: StateFlow<List<String>>
fun setSelectedIds(ids: List<Int>)
fun setSelectedTypes(types: List<String>)
//endregion
//region # Passes filter settings
val passesSettings: StateFlow<PassesSettings>
fun setPassesSettings(settings: PassesSettings)
//endregion
//region # Station position settings
val stationPosition: StateFlow<GeoPos>
fun setStationPosition(latitude: Double, longitude: Double, altitude: Double): Boolean
fun setStationPosition(): Boolean
fun setStationPosition(locator: String): Boolean
//endregion
//region # Database update settings
val databaseState: StateFlow<DatabaseState>
fun getSatelliteTypesIds(types: List<String>): List<Int>
fun setSatelliteTypeIds(type: String, ids: List<Int>)
fun updateDatabaseState(state: DatabaseState)
//endregion
//region # RC settings
val rcSettings: StateFlow<RCSettings>
fun setBluetoothRotatorAddress(value: String)
fun setBluetoothRotatorFormat(value: String)
fun setBluetoothRotatorName(value: String)
fun setBluetoothRotatorState(value: Boolean)
fun setBluetoothFrequencyAddress(value: String)
fun setBluetoothFrequencyFormat(value: String)
fun setBluetoothFrequencyState(value: Boolean)
fun setRotatorAddress(value: String)
fun setRotatorPort(value: String)
fun setRotatorState(value: Boolean)
fun setRotatorFormat(value: String)
fun setFrequencyAddress(value: String)
fun setFrequencyPort(value: String)
fun setFrequencyState(value: Boolean)
fun setFrequencyFormat(value: String)
//endregion
//region # Other settings
val otherSettings: StateFlow<OtherSettings>
fun setStateOfAutoUpdate(value: Boolean)
fun setStateOfSensors(value: Boolean)
fun setStateOfSweep(value: Boolean)
fun setStateOfUtc(value: Boolean)
fun setStateOfLightTheme(value: Boolean)
fun setWarningDismissed()
fun setWhatsNewDismissed()
//endregion
//region # Transceivers settings
val dataSourcesSettings: StateFlow<DataSourcesSettings>
fun setUseCustomTle(value: Boolean)
fun setUseCustomTransceivers(value: Boolean)
fun setTleUrl(value: String)
fun setTransceiversUrl(value: String)
//endregion
}
@@ -0,0 +1,36 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* 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.core.domain.source
import com.rtbishop.look4sat.core.domain.model.SatItem
import com.rtbishop.look4sat.core.domain.model.SatRadio
import com.rtbishop.look4sat.core.domain.predict.OrbitalData
import com.rtbishop.look4sat.core.domain.predict.OrbitalObject
interface ILocalSource {
suspend fun getEntriesTotal(): Int
suspend fun getEntriesList(): List<SatItem>
suspend fun getEntriesWithIds(ids: List<Int>): List<OrbitalObject>
suspend fun insertEntries(entries: List<OrbitalData>)
suspend fun deleteEntries()
suspend fun getIdsWithModes(modes: List<String>): List<Int>
suspend fun getRadiosTotal(): Int
suspend fun getRadiosWithId(id: Int): List<SatRadio>
suspend fun insertRadios(radios: List<SatRadio>)
suspend fun deleteRadios()
}
@@ -0,0 +1,25 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* 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.core.domain.source
import java.io.InputStream
interface IRemoteSource {
suspend fun getFileStream(uri: String): InputStream?
suspend fun getNetworkStream(url: String): InputStream?
}
@@ -0,0 +1,52 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* 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.core.domain.source
object Sources {
const val RADIO_DATA_URL = "https://db.satnogs.org/api/transmitters/?format=json&status=active"
val satelliteDataUrls = mapOf(
"All" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=active&FORMAT=csv",
"Amateur" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=amateur&FORMAT=csv",
"Brightest" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=visual&FORMAT=csv",
"Cubesat" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=cubesat&FORMAT=csv",
"Education" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=education&FORMAT=csv",
"Engineer" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=engineering&FORMAT=csv",
"Geostationary" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=geo&FORMAT=csv",
"Globalstar" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=globalstar&FORMAT=csv",
"GNSS" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=gnss&FORMAT=csv",
"Intelsat" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=intelsat&FORMAT=csv",
"Iridium" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=iridium-NEXT&FORMAT=csv",
"Military" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=military&FORMAT=csv",
"New" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=last-30-days&FORMAT=csv",
"OneWeb" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=oneweb&FORMAT=csv",
"Orbcomm" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=orbcomm&FORMAT=csv",
"Resource" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=resource&FORMAT=csv",
"SatNOGS" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=satnogs&FORMAT=csv",
"Science" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=science&FORMAT=csv",
"Spire" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=spire&FORMAT=csv",
"Starlink" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=starlink&FORMAT=csv",
"Swarm" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=swarm&FORMAT=csv",
"Weather" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=weather&FORMAT=csv",
"X-Comm" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=x-comm&FORMAT=csv",
"Amsat" to "https://amsat.org/tle/current/nasabare.txt",
"Classified" to "https://www.mmccants.org/tles/classfd.zip",
"McCants" to "https://www.mmccants.org/tles/inttles.zip",
"R4UAB" to "https://r4uab.ru/satonline.txt",
"Other" to "" // key for sats filter
)
}
@@ -0,0 +1,22 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* 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.core.domain.usecase
interface IAddToCalendar {
operator fun invoke(name: String, aosTime: Long, losTime: Long)
}
@@ -0,0 +1,22 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* 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.core.domain.usecase
interface IShowToast {
operator fun invoke(message: String)
}
@@ -0,0 +1,154 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* 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.core.domain.utility
import com.rtbishop.look4sat.core.domain.model.SatRadio
import com.rtbishop.look4sat.core.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 dispatcher: CoroutineDispatcher) {
suspend fun parseCSVStream(stream: InputStream): List<OrbitalData> = withContext(dispatcher) {
val parsedItems = mutableListOf<OrbitalData>()
stream.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(stream: InputStream): List<OrbitalData> = withContext(dispatcher) {
val tleStrings = mutableListOf(String(), String(), String())
val parsedItems = mutableListOf<OrbitalData>()
var lineIndex = 0
stream.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(stream: InputStream): List<SatRadio> = withContext(dispatcher) {
val parsedItems = mutableListOf<SatRadio>()
try {
val jsonArray = JSONArray(stream.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) {
return@withContext parsedItems
}
}
fun isLeapYear(year: Int): Boolean {
return ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)
}
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()
OrbitalData(name, epoch, meanmo, eccn, incl, raan, argper, meanan, catnum, bstar)
} catch (exception: Exception) {
println("CSV parsing exception: $exception")
null
}
private fun parseTLE(tle: List<String>): OrbitalData? = 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())
OrbitalData(name, epoch, meanmo, eccn, incl, raan, argper, meanan, catnum, bstar)
} catch (exception: Exception) {
println("TLE parsing exception: $exception")
null
}
private fun parseJSON(json: JSONObject): SatRadio? = try {
val uuid = json.getString("uuid")
val info = json.getString("description")
val alive = json.getBoolean("alive")
val dlinkLow = if (json.isNull("downlink_low")) null else json.getLong("downlink_low")
val dlinkHigh = if (json.isNull("downlink_high")) null else json.getLong("downlink_high")
val dlinkMode = if (json.isNull("mode")) null else json.getString("mode")
val ulinkLow = if (json.isNull("uplink_low")) null else json.getLong("uplink_low")
val ulinkHigh = if (json.isNull("uplink_high")) null else json.getLong("uplink_high")
val ulinkMode = if (json.isNull("uplink_mode")) null else json.getString("uplink_mode")
val inverted = json.getBoolean("invert")
val catnum = if (json.isNull("norad_cat_id")) null else json.getInt("norad_cat_id")
SatRadio(uuid, info, alive, dlinkLow, dlinkHigh, dlinkMode, ulinkLow, ulinkHigh, ulinkMode, inverted, catnum)
} catch (exception: Exception) {
println("JSON parsing exception: $exception")
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 (isLeapYear(year)) daysArray[1]++
for (i in 0 until month - 1) {
dayOfYear += daysArray[i]
}
return dayOfYear
}
}
@@ -0,0 +1,69 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* 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.core.domain.utility
import java.util.Locale
import java.util.concurrent.TimeUnit
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(Locale.ENGLISH, format, hours, minutes, seconds)
}
fun Float.round(decimals: Int): Float {
var multiplier = 1.0f
repeat(decimals) { multiplier *= 10 }
return kotlin.math.round(this * multiplier) / multiplier
}
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 = "^((\\d|[1-9]\\d|1\\d\\d|2[0-4]\\d|25[0-5])(\\.(?!\$)|\$)){4}\$"
// return this.matches(ip4.toRegex())
//}
//fun String.isValidPort(): Boolean {
// val port = "([1-9]|[1-9]\\d{1,3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])"
// return this.matches(port.toRegex()) && this.toInt() in 1024..65535
//}
@@ -0,0 +1,64 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* 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.core.domain.utility
import com.rtbishop.look4sat.core.domain.predict.DEG2RAD
import com.rtbishop.look4sat.core.domain.predict.RAD2DEG
import kotlin.math.max
import kotlin.math.min
private const val MIN_LATITUDE = -85.05112877980658
private const val MAX_LATITUDE = 85.05112877980658
private const val MIN_LONGITUDE = -180.0
private const val MAX_LONGITUDE = 180.0
fun Double.toDegrees(): Double = this * RAD2DEG
fun Double.toRadians(): Double = this * DEG2RAD
//fun Double.latToY01(): Double {
// val sinus = sin(clipLat(this) * PI / MAX_LONGITUDE)
// return 0.5 - ln((1 + sinus) / (1 - sinus)) / (4 * PI)
//}
//fun Double.lonToX01(): Double {
// return (clipLon(this) - MIN_LONGITUDE) / (MAX_LONGITUDE - MIN_LONGITUDE)
//}
//fun Double.y01ToLat(): Double {
// return 90 - 360 * atan(exp((this - 0.5) * 2 * PI)) / PI
//}
//fun Double.x01ToLon(): Double {
// return MIN_LONGITUDE + (MAX_LONGITUDE - MIN_LONGITUDE) * this
//}
fun clipLat(latitude: Double): Double {
return clip(latitude, MIN_LATITUDE, MAX_LATITUDE)
}
fun clipLon(longitude: Double): Double {
var result = longitude
while (result < MIN_LONGITUDE) result += 360.0
while (result > MAX_LONGITUDE) result -= 360.0
return clip(result, MIN_LONGITUDE, MAX_LONGITUDE)
}
private fun clip(currentValue: Double, minValue: Double, maxValue: Double): Double {
return min(max(currentValue, minValue), maxValue)
}
@@ -0,0 +1,55 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* 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.core.domain.utility
import com.rtbishop.look4sat.core.domain.predict.GeoPos
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(latitude: Double, longitude: Double): String? {
if (!isValidPosition(latitude, longitude)) return null
val newLongitude = if (longitude > 180.0) longitude else longitude + 180
val newLatitude = latitude + 90
val lonFirst = (65 + (newLongitude / 20)).toInt().toChar()
val latFirst = (65 + (newLatitude / 10)).toInt().toChar()
val lonSecond = ((newLongitude / 2) % 10).toInt()
val latSecond = (newLatitude % 10).toInt()
val lonThird = (65 + (newLongitude % 2) * 12).toInt().toChar().lowercaseChar()
val latThird = (65 + (newLatitude % 1) * 24).toInt().toChar().lowercaseChar()
return "$lonFirst$latFirst$lonSecond$latSecond$lonThird$latThird"
}
private fun isValidPosition(lat: Double, lon: Double): Boolean {
return (lat >= -90.0 && lat <= 90.0) && (lon >= -180.0 && lon <= 360.0)
}
private fun isValidLocator(locator: String): Boolean {
return locator.matches("[a-xA-X][a-xA-X]\\d\\d[a-xA-X][a-xA-X]".toRegex())
}
@@ -0,0 +1,105 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* 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.core.domain
import com.rtbishop.look4sat.core.domain.utility.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
ISS (ZARYA),1998-067A,2024-03-09T05:45:04.737024,15.49756209,.0005741,51.6418,90.7424,343.9724,92.8274,0,U,25544,999,44305,.25016E-3,.1373E-3,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
ISS (ZARYA)
1 25544U 98067A 24069.23963816 .00013730 00000+0 25016-3 0 9999
2 25544 51.6418 90.7424 0005741 343.9724 92.8274 15.49756209443058
""".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)
assert(parsedList[1].epoch == 24069.23963816)
}
@Test
fun `Given invalid CSV stream returns empty list`() = runTest(testDispatcher) {
assert(dataParser.parseCSVStream(invalidCSVStream).isEmpty())
}
@Test
fun `Given valid TLE stream returns valid data`() = runTest(testDispatcher) {
val parsedList = dataParser.parseTLEStream(validTLEStream)
assert(parsedList[0].epoch == 21320.51955234)
assert(parsedList[1].epoch == 24069.23963816)
}
@Test
fun `Given invalid TLE stream returns empty list`() = runTest(testDispatcher) {
assert(dataParser.parseTLEStream(invalidTLEStream).isEmpty())
}
@Test
fun `Given valid JSON stream returns valid data`() = runTest(testDispatcher) {
assert(dataParser.parseJSONStream(validJSONStream)[0].downlinkLow == 136658500L)
}
@Test
fun `Given invalid JSON stream returns empty list`() = runTest(testDispatcher) {
assert(dataParser.parseJSONStream(invalidJSONStream).isEmpty())
}
@Test
fun `Given valid data streams parsed results match`() = runTest(testDispatcher) {
assert(dataParser.parseCSVStream(validCSVStream) == dataParser.parseTLEStream(validTLEStream))
}
@Test
fun `Function isLeapYear returns correct data`() = runTest(testDispatcher) {
val years = listOf(1900, 1984, 1994, 2016, 2022, 2024, 2042, 2048)
val answers = listOf(false, true, false, true, false, true, false, true)
val results = years.map { dataParser.isLeapYear(it) }
assert(results == answers)
}
}
@@ -0,0 +1,51 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* 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.core.domain
import com.rtbishop.look4sat.core.domain.utility.positionToQth
import com.rtbishop.look4sat.core.domain.utility.qthToPosition
import org.junit.Test
class QthConverterTest {
@Test
fun `Given valid QTH returns correct POS`() {
var result = qthToPosition("io91VL39FX")
assert(result?.latitude == 51.4792 && result.longitude == -0.2083)
result = qthToPosition("gf15vc")
assert(result?.latitude == -34.8958 && result.longitude == -56.2083)
}
@Test
fun `Given invalid QTH returns null`() {
assert(qthToPosition("ZZ00zz") == null)
assert(qthToPosition("JN58") == null)
}
@Test
fun `Given valid POS returns correct QTH`() {
assert(positionToQth(51.4878, -0.2146) == "IO91vl")
assert(positionToQth(48.1466, 11.6083) == "JN58td")
}
@Test
fun `Given invalid POS returns null`() {
assert(positionToQth(91.0542, -170.1142) == null)
assert(positionToQth(89.0542, -240.1142) == null)
}
}