исправлен расчет витка для tle
This commit is contained in:
+41
-1
@@ -23,7 +23,7 @@ class SurveyContourCalculator(
|
|||||||
fun calculate(result: AngularMotionResult, y: Double,z: Double,focus: Double): String {
|
fun calculate(result: AngularMotionResult, y: Double,z: Double,focus: Double): String {
|
||||||
|
|
||||||
|
|
||||||
val sourcePoints = result.points
|
val sourcePoints = decimateContourSourcePoints(result.points)
|
||||||
require(sourcePoints.size >= MIN_CONTOUR_POINTS) {
|
require(sourcePoints.size >= MIN_CONTOUR_POINTS) {
|
||||||
"Для построения контура съемки требуется не менее $MIN_CONTOUR_POINTS точек ПУУД"
|
"Для построения контура съемки требуется не менее $MIN_CONTOUR_POINTS точек ПУУД"
|
||||||
}
|
}
|
||||||
@@ -62,6 +62,43 @@ class SurveyContourCalculator(
|
|||||||
return toWkt(shell)
|
return toWkt(shell)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun decimateContourSourcePoints(points: List<AngularMotionPoint>): List<AngularMotionPoint> {
|
||||||
|
if (points.size <= MIN_CONTOUR_POINTS) {
|
||||||
|
return points
|
||||||
|
}
|
||||||
|
|
||||||
|
val startTime = points.first().t
|
||||||
|
val stopTime = points.last().t
|
||||||
|
val duration = stopTime - startTime
|
||||||
|
if (duration <= 0.0) {
|
||||||
|
return listOf(points.first(), points.last()).distinct()
|
||||||
|
}
|
||||||
|
|
||||||
|
val step = maxOf(
|
||||||
|
DEFAULT_CONTOUR_SOURCE_STEP_SEC,
|
||||||
|
duration / (MAX_CONTOUR_BOUNDARY_POINTS - 1),
|
||||||
|
)
|
||||||
|
|
||||||
|
val selected = ArrayList<AngularMotionPoint>(MAX_CONTOUR_BOUNDARY_POINTS)
|
||||||
|
selected += points.first()
|
||||||
|
var nextTime = startTime + step
|
||||||
|
|
||||||
|
for (index in 1 until points.lastIndex) {
|
||||||
|
val point = points[index]
|
||||||
|
if (point.t + TIME_EPS >= nextTime) {
|
||||||
|
selected += point
|
||||||
|
nextTime += step
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selected.last() !== points.last()) {
|
||||||
|
selected += points.last()
|
||||||
|
}
|
||||||
|
|
||||||
|
return selected
|
||||||
|
}
|
||||||
|
|
||||||
private fun toWkt(points: List<SurveyContourPoint>): String {
|
private fun toWkt(points: List<SurveyContourPoint>): String {
|
||||||
val coordinates = points.joinToString(", ") { point ->
|
val coordinates = points.joinToString(", ") { point ->
|
||||||
String.format(Locale.US, "%.8f %.8f", point.longitudeDeg, point.latitudeDeg)
|
String.format(Locale.US, "%.8f %.8f", point.longitudeDeg, point.latitudeDeg)
|
||||||
@@ -85,5 +122,8 @@ class SurveyContourCalculator(
|
|||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val MIN_CONTOUR_POINTS = 2
|
const val MIN_CONTOUR_POINTS = 2
|
||||||
|
const val DEFAULT_CONTOUR_SOURCE_STEP_SEC = 2.0
|
||||||
|
const val MAX_CONTOUR_BOUNDARY_POINTS = 10
|
||||||
|
const val TIME_EPS = 1.0e-9
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+87
-1
@@ -10,15 +10,21 @@ import org.nstart.dep265.tletools.zeptomoby.orbit.Satellite
|
|||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
import java.util.GregorianCalendar
|
import java.util.GregorianCalendar
|
||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
|
import kotlin.math.floor
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
class TLEStepper(str1: String, str2: String, earthType: EarthType) : AbstractStepper {
|
class TLEStepper(str1: String, str2: String, earthType: EarthType) : AbstractStepper {
|
||||||
val astro = AstronomerJ2000(earthType)
|
val astro = AstronomerJ2000(earthType)
|
||||||
val tleParser: TLE = TLE("", str1, str2)
|
val tleParser: TLE = TLE("", str1, str2)
|
||||||
val satellite: Satellite = Satellite(tleParser)
|
val satellite: Satellite = Satellite(tleParser)
|
||||||
val baseEpoch: Double
|
val baseEpoch: Double
|
||||||
|
private val nextAscendingNodeAfterEpoch: Double
|
||||||
|
private val ascendingNodeCache = mutableMapOf<Int, Double>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
baseEpoch = extractUTCMillis(satellite) / 1000.0 + 10800
|
baseEpoch = extractUTCMillis(satellite) / 1000.0 + 10800
|
||||||
|
nextAscendingNodeAfterEpoch = findNextAscendingNodeAfterEpoch()
|
||||||
|
ascendingNodeCache[0] = nextAscendingNodeAfterEpoch
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun clear() {
|
override fun clear() {
|
||||||
@@ -68,7 +74,7 @@ class TLEStepper(str1: String, str2: String, earthType: EarthType) : AbstractSte
|
|||||||
val ask =
|
val ask =
|
||||||
OrbitalPoint(
|
OrbitalPoint(
|
||||||
t,
|
t,
|
||||||
satellite.orbit.orbitNum + (dt / satellite.orbit.period).toInt(),
|
calculateRevolution(t),
|
||||||
Vector3D(
|
Vector3D(
|
||||||
pos.position.x * 1000.0,
|
pos.position.x * 1000.0,
|
||||||
pos.position.y * 1000.0,
|
pos.position.y * 1000.0,
|
||||||
@@ -87,6 +93,86 @@ class TLEStepper(str1: String, str2: String, earthType: EarthType) : AbstractSte
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun calculateRevolution(t: Double): Int {
|
||||||
|
var nodeIndex = floor((t - nextAscendingNodeAfterEpoch) / satellite.orbit.period).toInt()
|
||||||
|
|
||||||
|
while (t < ascendingNode(nodeIndex)) {
|
||||||
|
--nodeIndex
|
||||||
|
}
|
||||||
|
while (t >= ascendingNode(nodeIndex + 1)) {
|
||||||
|
++nodeIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
return satellite.orbit.orbitNum + nodeIndex + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ascendingNode(index: Int): Double =
|
||||||
|
ascendingNodeCache.getOrPut(index) {
|
||||||
|
val estimatedNode = nextAscendingNodeAfterEpoch + index * satellite.orbit.period
|
||||||
|
findAscendingNodeNear(estimatedNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findNextAscendingNodeAfterEpoch(): Double {
|
||||||
|
val scanStep = min(60.0, satellite.orbit.period / 64.0)
|
||||||
|
var left = baseEpoch
|
||||||
|
var leftZ = eciZ(left)
|
||||||
|
var right = left + scanStep
|
||||||
|
val scanLimit = baseEpoch + satellite.orbit.period * 2.0
|
||||||
|
|
||||||
|
while (right <= scanLimit) {
|
||||||
|
val rightZ = eciZ(right)
|
||||||
|
if (leftZ < 0.0 && rightZ >= 0.0) {
|
||||||
|
return refineAscendingNode(left, right)
|
||||||
|
}
|
||||||
|
left = right
|
||||||
|
leftZ = rightZ
|
||||||
|
right += scanStep
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseEpoch + satellite.orbit.period
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findAscendingNodeNear(estimatedNode: Double): Double {
|
||||||
|
val halfPeriod = satellite.orbit.period / 2.0
|
||||||
|
val scanStep = min(60.0, satellite.orbit.period / 64.0)
|
||||||
|
var left = estimatedNode - halfPeriod
|
||||||
|
var leftZ = eciZ(left)
|
||||||
|
var right = left + scanStep
|
||||||
|
val scanLimit = estimatedNode + halfPeriod
|
||||||
|
|
||||||
|
while (right <= scanLimit) {
|
||||||
|
val rightZ = eciZ(right)
|
||||||
|
if (leftZ < 0.0 && rightZ >= 0.0) {
|
||||||
|
return refineAscendingNode(left, right)
|
||||||
|
}
|
||||||
|
left = right
|
||||||
|
leftZ = rightZ
|
||||||
|
right += scanStep
|
||||||
|
}
|
||||||
|
|
||||||
|
return estimatedNode
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refineAscendingNode(
|
||||||
|
intervalStart: Double,
|
||||||
|
intervalStop: Double,
|
||||||
|
): Double {
|
||||||
|
var left = intervalStart
|
||||||
|
var right = intervalStop
|
||||||
|
repeat(60) {
|
||||||
|
val middle = (left + right) / 2.0
|
||||||
|
if (eciZ(middle) < 0.0) {
|
||||||
|
left = middle
|
||||||
|
} else {
|
||||||
|
right = middle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (left + right) / 2.0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun eciZ(t: Double): Double =
|
||||||
|
satellite.positionEci((t - baseEpoch) / 60.0).position.z
|
||||||
|
|
||||||
override fun calculate(
|
override fun calculate(
|
||||||
t: Double,
|
t: Double,
|
||||||
p: OrbitalPoint,
|
p: OrbitalPoint,
|
||||||
|
|||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
package ballistics.orbitalPoints.timeStepper
|
||||||
|
|
||||||
|
import ballistics.types.EarthType
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
|
||||||
|
internal class TLEStepperTest {
|
||||||
|
@Test
|
||||||
|
fun `calculate keeps TLE revolution at epoch`() {
|
||||||
|
val stepper = TLEStepper(issTleFirst, issTleSecond, EarthType.PZ90d02)
|
||||||
|
|
||||||
|
val point = stepper.calculate(stepper.baseEpoch)
|
||||||
|
|
||||||
|
assertEquals(40945, point.vit)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `calculate increments revolution at ascending node instead of elapsed period from epoch`() {
|
||||||
|
val stepper = TLEStepper(issTleFirst, issTleSecond, EarthType.PZ90d02)
|
||||||
|
|
||||||
|
val firstPass = stepper.calculate(LocalDateTime.of(2026, 6, 16, 14, 19, 50).toEpochSecond(ZoneOffset.UTC).toDouble())
|
||||||
|
val secondPass = stepper.calculate(LocalDateTime.of(2026, 6, 16, 15, 56, 14).toEpochSecond(ZoneOffset.UTC).toDouble())
|
||||||
|
|
||||||
|
assertEquals(firstPass.vit + 1, secondPass.vit)
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val issTleFirst = "1 25994U 99068A 26167.13049375 .00000315 00000-0 73126-4 0 9997"
|
||||||
|
const val issTleSecond = "2 25994 97.9457 216.7186 0001501 193.3628 244.8059 14.61102329409455"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import ballistics.types.OrbitalPoint
|
|||||||
import ballistics.types.PPI
|
import ballistics.types.PPI
|
||||||
import ballistics.types.TLE
|
import ballistics.types.TLE
|
||||||
import ballistics.utils.math.Vector3D
|
import ballistics.utils.math.Vector3D
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.Assertions.assertTrue
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
@@ -17,6 +18,49 @@ import java.time.ZoneOffset
|
|||||||
import kotlin.math.PI
|
import kotlin.math.PI
|
||||||
|
|
||||||
internal class ZRVStepperCalculatorTest {
|
internal class ZRVStepperCalculatorTest {
|
||||||
|
@Test
|
||||||
|
fun `calculate keeps different revolutions for consecutive TLE visibility zones`() {
|
||||||
|
val ballistics = Ballistics()
|
||||||
|
val tle = TLE(
|
||||||
|
"1 25994U 99068A 26167.13049375 .00000315 00000-0 73126-4 0 9997",
|
||||||
|
"2 25994 97.9457 216.7186 0001501 193.3628 244.8059 14.61102329409455",
|
||||||
|
)
|
||||||
|
val timeStart = LocalDateTime.of(2026, 6, 16, 10, 41, 50, 189_000_000).toEpochSecond(ZoneOffset.UTC).toDouble()
|
||||||
|
val timeStop = LocalDateTime.of(2026, 6, 18, 0, 41, 50, 189_000_000).toEpochSecond(ZoneOffset.UTC).toDouble()
|
||||||
|
|
||||||
|
assertEquals(BallisticsError.OK, ballistics.calculateOrbPoints(tle, timeStart, timeStop))
|
||||||
|
assertEquals(
|
||||||
|
BallisticsError.OK,
|
||||||
|
ballistics.calculateZRV(
|
||||||
|
listOf(
|
||||||
|
PPI(
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
69.038863 * PI / 180.0,
|
||||||
|
32.861553 * PI / 180.0,
|
||||||
|
124.67,
|
||||||
|
5 * PI / 180.0,
|
||||||
|
90 * PI / 180.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
timeStart,
|
||||||
|
timeStop,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val zones = ballistics.zrv.toList()
|
||||||
|
val firstZone = zones.first {
|
||||||
|
it.zoneIn.t >= LocalDateTime.of(2026, 6, 16, 14, 0).toEpochSecond(ZoneOffset.UTC) &&
|
||||||
|
it.zoneIn.t < LocalDateTime.of(2026, 6, 16, 15, 0).toEpochSecond(ZoneOffset.UTC)
|
||||||
|
}
|
||||||
|
val secondZone = zones.first {
|
||||||
|
it.zoneIn.t >= LocalDateTime.of(2026, 6, 16, 15, 0).toEpochSecond(ZoneOffset.UTC) &&
|
||||||
|
it.zoneIn.t < LocalDateTime.of(2026, 6, 16, 17, 0).toEpochSecond(ZoneOffset.UTC)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(firstZone.vit + 1, secondZone.vit)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun calculate() {
|
fun calculate() {
|
||||||
val r = Ballistics()
|
val r = Ballistics()
|
||||||
|
|||||||
Reference in New Issue
Block a user