Расчет витков по 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
@@ -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)
}
@PostMapping("/revolutions")
fun revolutions(@RequestBody req: TleRevolutionsRequestDTO) = tleService.revolutions(req)
}
@@ -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) }
}
}
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.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<WebClient.Buil
.block()
?: 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(
path: String,
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.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}"