From d38822db3efe2e217af1ac80f035df5621703e68 Mon Sep 17 00:00:00 2001 From: emelianov Date: Wed, 17 Jun 2026 15:28:04 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B0=D1=81=D1=87=D0=B5=D1=82=20=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D0=BA=D0=BE=D0=B2=20=D0=BF=D0=BE=20TLE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ballistics/TleRevolutionsRequestDTO.kt | 6 ++ .../controller/TLEController.kt | 6 +- .../pcp_request_service/service/TLEService.kt | 63 ++++++++++++++++++- .../service/TLEServiceTest.kt | 53 ++++++++++++++++ .../service/BallisticsService.kt | 12 ++++ .../service/BallisticsServiceTest.kt | 22 ++++++- 6 files changed, 158 insertions(+), 4 deletions(-) create mode 100644 libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/TleRevolutionsRequestDTO.kt create mode 100644 services/pcp-ballistics-service/src/test/kotlin/space/nstart/pcp/pcp_request_service/service/TLEServiceTest.kt diff --git a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/TleRevolutionsRequestDTO.kt b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/TleRevolutionsRequestDTO.kt new file mode 100644 index 0000000..c38c4f1 --- /dev/null +++ b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/TleRevolutionsRequestDTO.kt @@ -0,0 +1,6 @@ +package space.nstart.pcp.pcp_types_lib.dto.ballistics + +class TleRevolutionsRequestDTO( + val tle: TLEDTO = TLEDTO(), + val days: Double = 1.0 +) diff --git a/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/controller/TLEController.kt b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/controller/TLEController.kt index c7e06e5..bdeba17 100644 --- a/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/controller/TLEController.kt +++ b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/controller/TLEController.kt @@ -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_types_lib.dto.ballistics.TLEDTO 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 @RestController @@ -28,4 +29,7 @@ class TLEController { @PostMapping("/rva") fun rva(@RequestBody req : TleRvaRequestDTO) = tleService.rva(req) -} \ No newline at end of file + @PostMapping("/revolutions") + fun revolutions(@RequestBody req: TleRevolutionsRequestDTO) = tleService.revolutions(req) + +} diff --git a/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/service/TLEService.kt b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/service/TLEService.kt index 5bd8fc7..e90178e 100644 --- a/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/service/TLEService.kt +++ b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/service/TLEService.kt @@ -4,7 +4,9 @@ import ballistics.Ballistics import ballistics.orbitalPoints.timeStepper.TLEStepper import ballistics.types.BallisticsError import ballistics.types.EarthType +import ballistics.types.KeplerParams import ballistics.types.PPI +import ballistics.types.RevolutionParameter import ballistics.types.TLE import ballistics.types.TLEParams import ballistics.types.VisibilityParametersZRV @@ -14,17 +16,25 @@ import ballistics.utils.toDateTime import org.springframework.stereotype.Service import space.nstart.pcp.pcp_request_service.configuration.CustomErrorException 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.RadioVisibilityAreaDTO 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.TleRevolutionsRequestDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.TargetPositionDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.TleRvaRequestDTO +import java.time.LocalDateTime import kotlin.math.PI @Service 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( toDateTime(p.t), @@ -117,4 +127,55 @@ class TLEService() { return bal.zrv.map { mkRVA(it, tleParams.noradId) } } -} \ No newline at end of file + + fun revolutions(req: TleRevolutionsRequestDTO): List { + 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()) + } +} diff --git a/services/pcp-ballistics-service/src/test/kotlin/space/nstart/pcp/pcp_request_service/service/TLEServiceTest.kt b/services/pcp-ballistics-service/src/test/kotlin/space/nstart/pcp/pcp_request_service/service/TLEServiceTest.kt new file mode 100644 index 0000000..b3b82f5 --- /dev/null +++ b/services/pcp-ballistics-service/src/test/kotlin/space/nstart/pcp/pcp_request_service/service/TLEServiceTest.kt @@ -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 { + 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" + ) + } +} diff --git a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/BallisticsService.kt b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/BallisticsService.kt index 1812c3e..cc64fbd 100644 --- a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/BallisticsService.kt +++ b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/BallisticsService.kt @@ -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.TLEDTO 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.configuration.CustomErrorException import java.net.URI @@ -115,6 +116,17 @@ class BallisticsService (webClientBuilderProvider: ObjectProvider = + 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( path: String, timeStart: LocalDateTime?, diff --git a/services/pcp-ui-service/src/test/kotlin/space/nstart/pcp/slots_service/service/BallisticsServiceTest.kt b/services/pcp-ui-service/src/test/kotlin/space/nstart/pcp/slots_service/service/BallisticsServiceTest.kt index 6639838..f372f26 100644 --- a/services/pcp-ui-service/src/test/kotlin/space/nstart/pcp/slots_service/service/BallisticsServiceTest.kt +++ b/services/pcp-ui-service/src/test/kotlin/space/nstart/pcp/slots_service/service/BallisticsServiceTest.kt @@ -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.OrbPointDTO 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.URI import java.time.LocalDateTime @@ -60,6 +61,13 @@ class BallisticsServiceTest { ) ) 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( listOf( @@ -69,7 +77,8 @@ class BallisticsServiceTest { "/api/satellites/orbit/availability", "/api/satellites/101/extract-time", "/api/satellites/receive-rv", - "/api/satellites/101/initial-conditions/current" + "/api/satellites/101/initial-conditions/current", + "/api/tle/revolutions" ), requests.map { it.path } ) @@ -81,6 +90,7 @@ class BallisticsServiceTest { null, null, null, + null, null ), requests.map { it.query } @@ -89,9 +99,12 @@ class BallisticsServiceTest { assertEquals(101L, availability.single().satelliteId) assertNotNull(exactPoint) assertEquals(timeStart, exactPoint.time) - assertEquals(true, requestBodies.single().contains(""""movementModel":"KONDOR"""")) assertNotNull(currentInitialConditions) 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 { @@ -164,6 +177,11 @@ class BallisticsServiceTest { """.trimIndent() ) } + startedServer.createContext("/api/tle/revolutions") { exchange -> + requests.add(exchange.requestURI) + requestBodies.add(exchange.requestBody.bufferedReader().use { it.readText() }) + respond(exchange, "[]") + } startedServer.start() server = startedServer return "http://localhost:${startedServer.address.port}"