Расчет витков по TLE

This commit is contained in:
emelianov
2026-06-17 15:28:04 +03:00
parent 6938700218
commit d38822db3e
6 changed files with 158 additions and 4 deletions
@@ -0,0 +1,6 @@
package space.nstart.pcp.pcp_types_lib.dto.ballistics
class TleRevolutionsRequestDTO(
val tle: TLEDTO = TLEDTO(),
val days: Double = 1.0
)
@@ -8,6 +8,7 @@ import org.springframework.web.bind.annotation.RestController
import space.nstart.pcp.pcp_request_service.service.TLEService import space.nstart.pcp.pcp_request_service.service.TLEService
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TlePointRequestDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.TlePointRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TleRevolutionsRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TleRvaRequestDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.TleRvaRequestDTO
@RestController @RestController
@@ -28,4 +29,7 @@ class TLEController {
@PostMapping("/rva") @PostMapping("/rva")
fun rva(@RequestBody req : TleRvaRequestDTO) = tleService.rva(req) fun rva(@RequestBody req : TleRvaRequestDTO) = tleService.rva(req)
@PostMapping("/revolutions")
fun revolutions(@RequestBody req: TleRevolutionsRequestDTO) = tleService.revolutions(req)
} }
@@ -4,7 +4,9 @@ import ballistics.Ballistics
import ballistics.orbitalPoints.timeStepper.TLEStepper import ballistics.orbitalPoints.timeStepper.TLEStepper
import ballistics.types.BallisticsError import ballistics.types.BallisticsError
import ballistics.types.EarthType import ballistics.types.EarthType
import ballistics.types.KeplerParams
import ballistics.types.PPI import ballistics.types.PPI
import ballistics.types.RevolutionParameter
import ballistics.types.TLE import ballistics.types.TLE
import ballistics.types.TLEParams import ballistics.types.TLEParams
import ballistics.types.VisibilityParametersZRV import ballistics.types.VisibilityParametersZRV
@@ -14,17 +16,25 @@ import ballistics.utils.toDateTime
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import space.nstart.pcp.pcp_request_service.configuration.CustomErrorException import space.nstart.pcp.pcp_request_service.configuration.CustomErrorException
import space.nstart.pcp.pcp_request_service.configuration.CustomValidationException import space.nstart.pcp.pcp_request_service.configuration.CustomValidationException
import space.nstart.pcp.pcp_types_lib.dto.ballistics.AscNodeDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.KeplersDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RadioVisibilityAreaDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.RadioVisibilityAreaDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TlePointRequestDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.TlePointRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TleRevolutionsRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TargetPositionDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.TargetPositionDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TleRvaRequestDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.TleRvaRequestDTO
import java.time.LocalDateTime
import kotlin.math.PI import kotlin.math.PI
@Service @Service
class TLEService() { class TLEService() {
private companion object {
const val SECONDS_IN_DAY = 86_400.0
const val PZ90_EQUATORIAL_RADIUS = 6_378_136.0
}
fun mkTP(p : VisibilityParametersZRV) = TargetPositionDTO( fun mkTP(p : VisibilityParametersZRV) = TargetPositionDTO(
toDateTime(p.t), toDateTime(p.t),
@@ -117,4 +127,55 @@ class TLEService() {
return bal.zrv.map { mkRVA(it, tleParams.noradId) } return bal.zrv.map { mkRVA(it, tleParams.noradId) }
} }
fun revolutions(req: TleRevolutionsRequestDTO): List<AscNodeDTO> {
if (!req.days.isFinite() || req.days <= 0.0) {
throw CustomValidationException("Интервал расчета должен быть больше 0 суток")
}
val bal = Ballistics()
val tle = TLE(req.tle.first, req.tle.second)
try {
bal.getTLEParams(tle, req.tle.header ?: "empty")
} catch (ex: Exception) {
throw CustomValidationException("Ошибка формата TLE : ${ex.message}")
}
val result = bal.calculateOrbPoints(tle, req.days * SECONDS_IN_DAY)
if (result != BallisticsError.OK) {
throw CustomErrorException("Ошибка расчета витков по TLE : $result")
}
return bal.revolutions.map { mkAscNode(it, bal.calculateKeplerParams(it.vuz)) }
}
private fun mkAscNode(revolution: RevolutionParameter, keps: KeplerParams) = AscNodeDTO(
time = toDateTime(revolution.vuz.t),
revolution = revolution.vuz.vit,
long = revolution.lVuz * 180 / PI,
height = revolution.hVuz,
x = revolution.vuz.r.x,
y = revolution.vuz.r.y,
z = revolution.vuz.r.z,
vx = revolution.vuz.v.x,
vy = revolution.vuz.v.y,
vz = revolution.vuz.v.z,
keps = KeplersDTO(
(keps.ael - PZ90_EQUATORIAL_RADIUS) / 1000.0,
keps.e,
keps.nakl * 180 / PI,
keps.omegab * 180 / PI,
keps.omegam * 180 / PI
),
localMeanTime = tmest(toDateTime(revolution.vuz.t), revolution.lVuz)
)
private fun tmest(t: LocalDateTime, longitudeRad: Double): LocalDateTime {
var longitude = longitudeRad
if (longitude > PI) {
longitude -= 2 * PI
}
return t.plusSeconds(-60 * 60 * 3)
.plusNanos((longitude / 2.0 / PI * SECONDS_IN_DAY * 1e9).toLong())
}
} }
@@ -0,0 +1,53 @@
package space.nstart.pcp.pcp_request_service.service
import org.junit.jupiter.api.Test
import space.nstart.pcp.pcp_request_service.configuration.CustomValidationException
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TleRevolutionsRequestDTO
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
class TLEServiceTest {
@Test
fun `revolutions calculates ascending nodes from tle epoch`() {
val service = TLEService()
val revolutions = service.revolutions(
TleRevolutionsRequestDTO(
tle = issTle,
days = 1.0
)
)
val epoch = service.parseTLE(issTle).time
assertTrue(revolutions.isNotEmpty())
assertTrue(revolutions.zipWithNext().all { (previous, next) -> previous.time < next.time })
assertTrue(revolutions.zipWithNext().all { (previous, next) -> previous.revolution < next.revolution })
assertTrue(revolutions.all { it.time >= epoch })
assertTrue(revolutions.all { it.localMeanTime != null })
assertTrue(revolutions.all { it.keps != null })
}
@Test
fun `revolutions rejects non-positive duration`() {
val service = TLEService()
assertFailsWith<CustomValidationException> {
service.revolutions(
TleRevolutionsRequestDTO(
tle = issTle,
days = 0.0
)
)
}
}
private companion object {
val issTle = TLEDTO(
header = "ISS",
first = "1 25994U 99068A 26167.13049375 .00000315 00000-0 73126-4 0 9997",
second = "2 25994 97.9457 216.7186 0001501 193.3628 244.8059 14.61102329409455"
)
}
}
@@ -13,6 +13,7 @@ import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteOrbitAvailabilityDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteOrbitAvailabilityDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TlePointRequestDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.TlePointRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TleRevolutionsRequestDTO
import space.nstart.pcp.slots_service.dto.tlecomparison.TleParsedDTO import space.nstart.pcp.slots_service.dto.tlecomparison.TleParsedDTO
import space.nstart.pcp.slots_service.configuration.CustomErrorException import space.nstart.pcp.slots_service.configuration.CustomErrorException
import java.net.URI import java.net.URI
@@ -115,6 +116,17 @@ class BallisticsService (webClientBuilderProvider: ObjectProvider<WebClient.Buil
.block() .block()
?: throw CustomErrorException("Не удалось рассчитать положение по TLE на время $time") ?: throw CustomErrorException("Не удалось рассчитать положение по TLE на время $time")
fun tleRevolutions(tle: TLEDTO, days: Double): List<AscNodeDTO> =
webClientBuilder.build()
.post()
.uri("$url/api/tle/revolutions")
.bodyValue(TleRevolutionsRequestDTO(tle = tle, days = days))
.retrieve()
.bodyToFlux(AscNodeDTO::class.java)
.collectList()
.block()
?: emptyList()
private fun withTimeInterval( private fun withTimeInterval(
path: String, path: String,
timeStart: LocalDateTime?, timeStart: LocalDateTime?,
@@ -11,6 +11,7 @@ import space.nstart.pcp.pcp_types_lib.dto.ballistics.InitialConditionsDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.MovementModel import space.nstart.pcp.pcp_types_lib.dto.ballistics.MovementModel
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.URI import java.net.URI
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -60,6 +61,13 @@ class BallisticsServiceTest {
) )
) )
val currentInitialConditions = service.currentInitialConditions(101L) val currentInitialConditions = service.currentInitialConditions(101L)
val tleRevolutions = service.tleRevolutions(
TLEDTO(
first = "1 25994U 99068A 26167.13049375 .00000315 00000-0 73126-4 0 9997",
second = "2 25994 97.9457 216.7186 0001501 193.3628 244.8059 14.61102329409455"
),
days = 1.5
)
assertEquals( assertEquals(
listOf( listOf(
@@ -69,7 +77,8 @@ class BallisticsServiceTest {
"/api/satellites/orbit/availability", "/api/satellites/orbit/availability",
"/api/satellites/101/extract-time", "/api/satellites/101/extract-time",
"/api/satellites/receive-rv", "/api/satellites/receive-rv",
"/api/satellites/101/initial-conditions/current" "/api/satellites/101/initial-conditions/current",
"/api/tle/revolutions"
), ),
requests.map { it.path } requests.map { it.path }
) )
@@ -81,6 +90,7 @@ class BallisticsServiceTest {
null, null,
null, null,
null, null,
null,
null null
), ),
requests.map { it.query } requests.map { it.query }
@@ -89,9 +99,12 @@ class BallisticsServiceTest {
assertEquals(101L, availability.single().satelliteId) assertEquals(101L, availability.single().satelliteId)
assertNotNull(exactPoint) assertNotNull(exactPoint)
assertEquals(timeStart, exactPoint.time) assertEquals(timeStart, exactPoint.time)
assertEquals(true, requestBodies.single().contains(""""movementModel":"KONDOR""""))
assertNotNull(currentInitialConditions) assertNotNull(currentInitialConditions)
assertEquals(101L, currentInitialConditions.satelliteId) assertEquals(101L, currentInitialConditions.satelliteId)
assertEquals(emptyList(), tleRevolutions)
assertEquals(2, requestBodies.size)
assertEquals(true, requestBodies.any { it.contains(""""movementModel":"KONDOR"""") })
assertEquals(true, requestBodies.any { it.contains(""""days":1.5""") })
} }
private fun createService(serverUrl: String): BallisticsService { private fun createService(serverUrl: String): BallisticsService {
@@ -164,6 +177,11 @@ class BallisticsServiceTest {
""".trimIndent() """.trimIndent()
) )
} }
startedServer.createContext("/api/tle/revolutions") { exchange ->
requests.add(exchange.requestURI)
requestBodies.add(exchange.requestBody.bufferedReader().use { it.readText() })
respond(exchange, "[]")
}
startedServer.start() startedServer.start()
server = startedServer server = startedServer
return "http://localhost:${startedServer.address.port}" return "http://localhost:${startedServer.address.port}"