Added tweaks to positions and passes calculation

This commit is contained in:
Arty Bishop committed 2026-03-29 13:09:03 +01:00
1 parent 279594c425
commit 0bef2ffddc
5 files changed
+257 -171

No files matched your search

@@ -28,6 +28,9 @@ import com.rtbishop.look4sat.core.domain.source.ILocalSource
import com.rtbishop.look4sat.core.domain.utility.round
import com.rtbishop.look4sat.core.domain.utility.toDegrees
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
@@ -62,7 +65,8 @@ class SatelliteRepo(
override suspend fun getTrack(sat: OrbitalObject, pos: GeoPos, start: Long, end: Long): List<OrbitalPos> {
return withContext(dispatcher) {
val positions = mutableListOf<OrbitalPos>()
val estimatedSize = ((end - start) / 15000).toInt() + 1
val positions = ArrayList<OrbitalPos>(estimatedSize)
var currentTime = start
while (currentTime < end) {
positions.add(sat.getPosition(pos, currentTime))
@@ -80,20 +84,21 @@ class SatelliteRepo(
): List<SatRadio> {
return withContext(dispatcher) {
val satPos = sat.getPosition(pos, time)
val copiedList = radios.map { it.copy() }
copiedList.forEach { transmitter ->
transmitter.downlinkLow?.let { transmitter.downlinkLow = satPos.getDownlinkFreq(it) }
transmitter.downlinkHigh?.let { transmitter.downlinkHigh = satPos.getDownlinkFreq(it) }
transmitter.uplinkLow?.let { transmitter.uplinkLow = satPos.getUplinkFreq(it) }
transmitter.uplinkHigh?.let { transmitter.uplinkHigh = satPos.getUplinkFreq(it) }
radios.map { transmitter ->
transmitter.copy(
downlinkLow = transmitter.downlinkLow?.let { satPos.getDownlinkFreq(it) },
downlinkHigh = transmitter.downlinkHigh?.let { satPos.getDownlinkFreq(it) },
uplinkLow = transmitter.uplinkLow?.let { satPos.getUplinkFreq(it) },
uplinkHigh = transmitter.uplinkHigh?.let { satPos.getUplinkFreq(it) }
)
}
copiedList.map { it.copy() }
}
}
override suspend fun processPasses(passList: List<OrbitalPass>, time: Long): List<OrbitalPass> {
return withContext(dispatcher) {
passList.forEach { pass ->
val result = ArrayList<OrbitalPass>(passList.size)
for (pass in passList) {
if (!pass.isDeepSpace) {
val timeStart = pass.aosTime
if (time > timeStart) {
@@ -102,22 +107,45 @@ class SatelliteRepo(
pass.progress = (deltaNow / deltaTotal).round(2)
}
}
if (pass.progress < 1.0f) {
result.add(pass.copy())
}
}
passList.filter { pass -> pass.progress < 1.0 }.map { it.copy() }
result
}
}
override suspend fun calculatePasses(time: Long, hoursAhead: Int, minElevation: Double, modes: List<String>) {
if (_satellites.value.isNotEmpty()) {
val currentSatellites = _satellites.value
if (currentSatellites.isNotEmpty()) {
withContext(dispatcher) {
val newPasses = mutableListOf<OrbitalPass>()
val idsWithModes = localStorage.getIdsWithModes(modes)
_satellites.value.forEach { satellite ->
if (idsWithModes.isEmpty() || satellite.data.catnum in idsWithModes) {
newPasses.addAll(satellite.getPasses(settingsRepo.stationPosition.value, time, hoursAhead))
val stationPos = settingsRepo.stationPosition.value
val filteredSatellites = if (idsWithModes.isEmpty()) {
currentSatellites
} else {
currentSatellites.filter { it.data.catnum in idsWithModes }
}
// Compute passes for each satellite in parallel
val passLists = coroutineScope {
filteredSatellites.map { satellite ->
async {
satellite.getPasses(stationPos, time, hoursAhead)
}
}.awaitAll()
}
// Flatten and filter in a single pass
val timeFuture = time + (hoursAhead * 60L * 60L * 1000L)
val newPasses = ArrayList<OrbitalPass>()
for (list in passLists) {
for (pass in list) {
if (pass.losTime > time && pass.aosTime < timeFuture && pass.maxElevation > minElevation) {
newPasses.add(pass)
}
}
}
_passes.update { newPasses.filter(time, hoursAhead, minElevation) }
newPasses.sortBy { it.aosTime }
_passes.update { newPasses }
}
} else {
_passes.update { emptyList() }
@@ -149,11 +177,6 @@ class SatelliteRepo(
return passes
}
private fun List<OrbitalPass>.filter(time: Long, hoursAhead: Int, minElev: Double): List<OrbitalPass> {
val timeFuture = time + (hoursAhead * 60L * 60L * 1000L)
return this.filter { it.losTime > time }.filter { it.aosTime < timeFuture }
.filter { it.maxElevation > minElev }.sortedBy { it.aosTime }
}
private fun getGeoPass(sat: OrbitalObject, pos: GeoPos, time: Long): OrbitalPass {
val satPos = sat.getPosition(pos, time)
@@ -170,72 +193,72 @@ class SatelliteRepo(
var calendarTimeMillis = time
var elevation: Double
var maxElevation = 0.0
var alt = 0.0 // var tcaAz = 0.0
// rewind 1/4 of an orbit
if (rewind) calendarTimeMillis += -quarterOrbitMin * 60L * 1000L
if (rewind) calendarTimeMillis -= quarterOrbitMin * 60L * 1000L
var satPos = sat.getPosition(pos, calendarTimeMillis)
if (satPos.elevation > 0.0) {
// Use lightweight elevation check for coarse searching
if (sat.getElevation(pos, calendarTimeMillis) > 0.0) {
// move forward in 30 second intervals until the sat goes below the horizon
do {
calendarTimeMillis += 30 * 1000L
satPos = sat.getPosition(pos, calendarTimeMillis)
} while (satPos.elevation > 0.0)
} while (sat.getElevation(pos, calendarTimeMillis) > 0.0)
// move forward 3/4 of an orbit
calendarTimeMillis += quarterOrbitMin * 3 * 60L * 1000L
}
// find the next time sat comes above the horizon
// find the next time sat comes above the horizon (coarse: 60s steps)
do {
calendarTimeMillis += 60L * 1000L
satPos = sat.getPosition(pos, calendarTimeMillis)
elevation = satPos.elevation
elevation = sat.getElevation(pos, calendarTimeMillis)
if (elevation > maxElevation) {
maxElevation = elevation
alt = satPos.altitude // tcaAz = satPos.azimuth.toDegrees()
}
} while (satPos.elevation < 0.0)
} while (elevation < 0.0)
// refine to 1 second
calendarTimeMillis += -60L * 1000L
// refine AOS to ~500ms precision
calendarTimeMillis -= 60L * 1000L
do {
calendarTimeMillis += 1L * 500L
satPos = sat.getPosition(pos, calendarTimeMillis)
elevation = satPos.elevation
calendarTimeMillis += 500L
elevation = sat.getElevation(pos, calendarTimeMillis)
if (elevation > maxElevation) {
maxElevation = elevation
alt = satPos.altitude // tcaAz = satPos.azimuth.toDegrees()
}
} while (satPos.elevation < 0.0)
} while (elevation < 0.0)
val aos = 1000 * ((satPos.time + 500) / 1000)
val aosAz = satPos.azimuth.toDegrees().round(1)
// Get full position for AOS data (azimuth, altitude)
val aosPos = sat.getFullPosition(pos, calendarTimeMillis)
val aos = 1000 * ((aosPos.time + 500) / 1000)
val aosAz = aosPos.azimuth.toDegrees().round(1)
// find when sat goes below
// find when sat goes below (coarse: 30s steps)
do {
calendarTimeMillis += 30L * 1000L
satPos = sat.getPosition(pos, calendarTimeMillis)
elevation = satPos.elevation
elevation = sat.getElevation(pos, calendarTimeMillis)
if (elevation > maxElevation) {
maxElevation = elevation
alt = satPos.altitude // tcaAz = satPos.azimuth.toDegrees()
}
} while (satPos.elevation > 0.0)
} while (elevation > 0.0)
// refine to 1 second
calendarTimeMillis += -30L * 1000L
// refine LOS to ~500ms precision
calendarTimeMillis -= 30L * 1000L
do {
calendarTimeMillis += 1L * 500L
satPos = sat.getPosition(pos, calendarTimeMillis)
elevation = satPos.elevation
calendarTimeMillis += 500L
elevation = sat.getElevation(pos, calendarTimeMillis)
if (elevation > maxElevation) {
maxElevation = elevation
alt = satPos.altitude // tcaAz = satPos.azimuth.toDegrees()
}
} while (satPos.elevation > 0.0)
} while (elevation > 0.0)
// Get full position for LOS data (azimuth, altitude)
val losPos = sat.getFullPosition(pos, calendarTimeMillis)
val los = 1000 * ((losPos.time + 500) / 1000)
val losAz = losPos.azimuth.toDegrees().round(1)
// Get altitude at approximate TCA (max elevation)
val tcaTime = (aos + los) / 2
val tcaPos = sat.getFullPosition(pos, tcaTime)
val alt = tcaPos.altitude
val los = 1000 * ((satPos.time + 500) / 1000) // val tca = (aos + los) / 2
val losAz = satPos.azimuth.toDegrees().round(1)
val elev = maxElevation.toDegrees().round(1)
return OrbitalPass(aos, aosAz, los, losAz, alt.toInt(), elev, sat)
}
@@ -97,40 +97,38 @@ class DeepSpaceObject(data: OrbitalData) : OrbitalObject(data) {
}
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)
}
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) {
@@ -141,53 +141,51 @@ class NearEarthObject(data: OrbitalData) : OrbitalObject(data) {
}
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)
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 = tSince * 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 * tempa
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(
@@ -42,6 +42,27 @@ abstract class OrbitalObject(val data: OrbitalData) {
var qoms24 = 0.0
var s4 = 0.0
// Pre-allocated reusable vectors to avoid GC pressure in hot loops
private val obsPos = Vector4()
private val obsVel = Vector4()
private val rangeVector = Vector4()
private val rgvelVector = Vector4()
private val squintVector = Vector4()
// Cached observer position data to avoid recalculation when observer hasn't moved
private var cachedGsLat = Double.NaN
private var cachedGsLon = Double.NaN
private var cachedGsAlt = Double.NaN
private var cachedSinLat = 0.0
private var cachedCosLat = 0.0
private var cachedObsC = 0.0
private var cachedObsSq = 0.0
private var cachedObsAchFactor = 0.0
private var cachedObsZFactor = 0.0
// Cache for julian epoch to avoid recomputing every call
private val julEpoch: Double = juliandDateOfEpoch(data.epoch)
fun willBeSeen(pos: GeoPos): Boolean {
return if (data.meanmo < 1e-8) false
else {
@@ -57,15 +78,13 @@ abstract class OrbitalObject(val data: OrbitalData) {
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)
// Calculate time since epoch in minutes
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)
@@ -75,6 +94,38 @@ abstract class OrbitalObject(val data: OrbitalData) {
return orbitalPos
}
/**
* Lightweight elevation-only check for pass finding. Avoids full lat/lon/alt,
* eclipse, and squint calculations that aren't needed when just searching for
* horizon crossings.
*/
fun getElevation(pos: GeoPos, time: Long): Double {
julUTC = calcCurrentDaynum(time) + 2444238.5
val tsince = (julUTC - julEpoch) * MIN_PER_DAY
calculateSDP4orSGP4(tsince)
convertSatState(position, velocity)
magnitude(velocity)
calculateObs(julUTC, position, velocity, pos, squintVector)
return orbitalPos.elevation
}
/**
* Full position calculation that also populates azimuth, altitude, etc.
* Used when we need all fields (AOS/LOS refinement, track computation).
*/
fun getFullPosition(pos: GeoPos, time: Long): OrbitalPos {
orbitalPos = OrbitalPos()
julUTC = calcCurrentDaynum(time) + 2444238.5
val tsince = (julUTC - julEpoch) * MIN_PER_DAY
calculateSDP4orSGP4(tsince)
convertSatState(position, velocity)
magnitude(velocity)
calculateObs(julUTC, position, velocity, pos, squintVector)
calculateLatLonAlt(julUTC)
orbitalPos.time = time
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
@@ -117,38 +168,34 @@ abstract class OrbitalObject(val data: OrbitalData) {
gsPos: GeoPos,
squintVector: Vector4
) {
val obsPos = Vector4()
val obsVel = Vector4()
val range = Vector4()
val rgvel = Vector4()
calculateUserPosVel(julianUTC, gsPos, obsPos, obsVel)
range.setXYZ(
rangeVector.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(
squintVector.setXYZ(rangeVector.x, rangeVector.y, rangeVector.z)
rgvelVector.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)
magnitude(rangeVector)
val sinLat = cachedSinLat
val cosLat = cachedCosLat
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
val topS = sinLat * cosTheta * rangeVector.x + sinLat * sinTheta * rangeVector.y - cosLat * rangeVector.z
val topE = -sinTheta * rangeVector.x + cosTheta * rangeVector.y
val topZ = cosLat * cosTheta * rangeVector.x + cosLat * sinTheta * rangeVector.y + sinLat * rangeVector.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
orbitalPos.elevation = asin(topZ / rangeVector.w)
orbitalPos.distance = rangeVector.w
orbitalPos.distanceRate = dot(rangeVector, rgvelVector) / rangeVector.w
var elevation = orbitalPos.elevation / TWO_PI * 360.0
if (elevation > 90) elevation = 180 - elevation
orbitalPos.aboveHorizon = elevation - 0 > EPSILON
@@ -163,12 +210,24 @@ abstract class OrbitalObject(val data: OrbitalData) {
) {
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)
// Cache trig and position factors when observer position changes
if (gsPos.latitude != cachedGsLat || gsPos.longitude != cachedGsLon || gsPos.altitude != cachedGsAlt) {
cachedGsLat = gsPos.latitude
cachedGsLon = gsPos.longitude
cachedGsAlt = gsPos.altitude
cachedSinLat = sin(DEG2RAD * gsPos.latitude)
cachedCosLat = cos(DEG2RAD * gsPos.latitude)
cachedObsC = invert(sqrt(1.0 + FLAT_FACT * (FLAT_FACT - 2) * sqr(cachedSinLat)))
cachedObsSq = sqr(1.0 - FLAT_FACT) * cachedObsC
cachedObsAchFactor = (EARTH_RADIUS * cachedObsC + gsPos.altitude / 1000.0) * cachedCosLat
cachedObsZFactor = (EARTH_RADIUS * cachedObsSq + gsPos.altitude / 1000.0) * cachedSinLat
}
obsPos.setXYZ(
achcp * cos(gsPosTheta), achcp * sin(gsPosTheta),
(EARTH_RADIUS * sq + gsPos.altitude / 1000.0) * sin(DEG2RAD * gsPos.latitude)
cachedObsAchFactor * cos(gsPosTheta),
cachedObsAchFactor * sin(gsPosTheta),
cachedObsZFactor
)
obsVel.setXYZ(-mFactor * obsPos.y, mFactor * obsPos.x, 0.0)
magnitude(obsPos)
@@ -368,7 +427,7 @@ abstract class OrbitalObject(val data: OrbitalData) {
* 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
* on the 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
@@ -21,7 +21,6 @@ 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
@@ -50,23 +49,32 @@ data class OrbitalPos(
}
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
val radius = EARTH_RADIUS_M + altitude * 1000.0
return sqrt(GM_EARTH / radius) / 1000.0
}
fun getRangeCircle(): List<GeoPos> {
val rangeCirclePoints = mutableListOf<GeoPos>()
val beta = acos(EARTH_RADIUS / (EARTH_RADIUS + altitude)) // * EARTH_RADIUS = radiusKm
val pointCount = 721
val rangeCirclePoints = ArrayList<GeoPos>(pointCount)
val beta = acos(EARTH_RADIUS / (EARTH_RADIUS + altitude))
val sinLat = sin(latitude)
val cosLat = cos(latitude)
val cosBeta = cos(beta)
val sinBeta = sin(beta)
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)
))
val sinRads = sin(rads)
val cosRads = cos(rads)
val lat = asin(sinLat * cosBeta + cosLat * sinBeta * cosRads)
val lon = longitude + atan2(sinRads * sinBeta * cosLat, cosBeta - sinLat * sin(lat))
rangeCirclePoints.add(GeoPos(lat * RAD2DEG, lon * RAD2DEG))
}
return rangeCirclePoints
}
companion object {
// Pre-computed constants for orbital velocity calculation
private const val GM_EARTH = 3.986004418E14 // m^3/s^2
private const val EARTH_RADIUS_M = 6.37E6 // meters
}
}