diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SatelliteRepo.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SatelliteRepo.kt index 81ee6440..1de7451b 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SatelliteRepo.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SatelliteRepo.kt @@ -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 { return withContext(dispatcher) { - val positions = mutableListOf() + val estimatedSize = ((end - start) / 15000).toInt() + 1 + val positions = ArrayList(estimatedSize) var currentTime = start while (currentTime < end) { positions.add(sat.getPosition(pos, currentTime)) @@ -80,20 +84,21 @@ class SatelliteRepo( ): List { 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, time: Long): List { return withContext(dispatcher) { - passList.forEach { pass -> + val result = ArrayList(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) { - if (_satellites.value.isNotEmpty()) { + val currentSatellites = _satellites.value + if (currentSatellites.isNotEmpty()) { withContext(dispatcher) { - val newPasses = mutableListOf() 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() + 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.filter(time: Long, hoursAhead: Int, minElev: Double): List { - 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) } diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/predict/DeepSpaceObject.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/predict/DeepSpaceObject.kt index 5bfd9e7a..3b86a15f 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/predict/DeepSpaceObject.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/predict/DeepSpaceObject.kt @@ -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) { diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/predict/NearEarthObject.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/predict/NearEarthObject.kt index 7309315b..9a495192 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/predict/NearEarthObject.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/predict/NearEarthObject.kt @@ -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( diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/predict/OrbitalObject.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/predict/OrbitalObject.kt index 3b07f7ef..c386e4a3 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/predict/OrbitalObject.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/predict/OrbitalObject.kt @@ -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 diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/predict/OrbitalPos.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/predict/OrbitalPos.kt index 2445ba26..eeea96dc 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/predict/OrbitalPos.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/predict/OrbitalPos.kt @@ -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 { - val rangeCirclePoints = mutableListOf() - val beta = acos(EARTH_RADIUS / (EARTH_RADIUS + altitude)) // * EARTH_RADIUS = radiusKm + val pointCount = 721 + val rangeCirclePoints = ArrayList(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 + } }