Прототип страницы расчета ПУУД

This commit is contained in:
emelianov
2026-06-15 10:13:27 +03:00
parent bf2e88431e
commit ce32a03072
13 changed files with 1017 additions and 0 deletions
@@ -31,6 +31,7 @@ configurations {
dependencies {
implementation(project(":libs:pcp-types-lib"))
implementation(project(":libs:ballistics-lib"))
implementation(project(":libs:angular-motion-lib"))
// implementation("org.nstart.dep265:ballistics:2.4.0")
implementation("${property("dep.spring.actuator")}")
@@ -0,0 +1,36 @@
package space.nstart.pcp.pcp_request_service.angular
import org.springframework.format.annotation.DateTimeFormat
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import java.time.LocalDateTime
@RestController
@RequestMapping("/api/angular-motion")
class AngularMotionController(
private val angularMotionService: AngularMotionService,
) {
@GetMapping("/satellites")
fun availableSatellites(
@RequestParam(required = false)
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
time: LocalDateTime?,
): List<AngularMotionSatelliteDTO> = angularMotionService.availableSatellites(time)
@GetMapping("/satellites/{satelliteId}/flight-line")
fun flightLine(
@PathVariable satelliteId: Long,
@RequestParam
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
time: LocalDateTime,
): List<AngularMotionFlightLinePointDTO> = angularMotionService.flightLine(satelliteId, time)
@PostMapping("/calculate")
fun calculate(@RequestBody request: AngularMotionCalculationRequestDTO): AngularMotionCalculationResultDTO =
angularMotionService.calculate(request)
}
@@ -0,0 +1,67 @@
package space.nstart.pcp.pcp_request_service.angular
import space.nstart.pcp.angularmotion.AngularMotionMode
import java.time.LocalDateTime
/** Запрос на ручной расчет программы управления угловым движением. */
data class AngularMotionCalculationRequestDTO(
val satelliteId: Long,
val approximateTime: LocalDateTime,
val mode: AngularMotionMode = AngularMotionMode.CONST_ORIENT,
val latitudeDeg: Double,
val longitudeDeg: Double,
val heightM: Double = 0.0,
val durationSec: Double,
val leadAngleDeg: Double = 0.0,
val azimuthDeg: Double = 0.0,
val sdi: Double? = null,
val pointInCenter: Boolean = false,
val focusMm: Double? = null,
val stepPuudSec: Double? = null,
val stepSdiSec: Double? = null,
)
data class AngularMotionSatelliteDTO(
val satelliteId: Long,
val code: String,
val name: String,
val typeCode: String,
val availabilityStart: LocalDateTime,
val availabilityStop: LocalDateTime,
)
data class AngularMotionFlightLinePointDTO(
val time: LocalDateTime,
val revolution: Long,
val latitudeDeg: Double,
val longitudeDeg: Double,
val leftLatitudeDeg: Double,
val leftLongitudeDeg: Double,
val rightLatitudeDeg: Double,
val rightLongitudeDeg: Double,
)
data class AngularMotionCalculationResultDTO(
val mode: AngularMotionMode,
val satelliteId: Long,
val approximateTime: LocalDateTime,
val startTime: LocalDateTime,
val points: List<AngularMotionPointDTO>,
)
data class AngularMotionPointDTO(
val time: LocalDateTime,
val revolution: Long,
val tangDeg: Double,
val krenDeg: Double,
val riskDeg: Double,
val groundLatitudeDeg: Double?,
val groundLongitudeDeg: Double?,
val omegaX: Double,
val omegaY: Double,
val omegaZ: Double,
val wdX: Double,
val wdY: Double,
val wdZ: Double,
val sdi: Double,
)
@@ -0,0 +1,182 @@
package space.nstart.pcp.pcp_request_service.angular
import ballistics.orbitalPoints.timeStepper.RungeStepper
import ballistics.types.EarthType
import ballistics.types.OrbitalPoint
import ballistics.utils.fromDateTime
import ballistics.utils.toDateTime
import ballistics.utils.math.Vector3D
import org.springframework.stereotype.Service
import space.nstart.pcp.angularmotion.AngularMotionCalculatorFactory
import space.nstart.pcp.angularmotion.AngularMotionConfig
import space.nstart.pcp.angularmotion.AngularMotionMode
import space.nstart.pcp.angularmotion.SurveyId
import space.nstart.pcp.pcp_request_service.configuration.CustomValidationException
import space.nstart.pcp.pcp_request_service.service.SatelliteCatalogClient
import space.nstart.pcp.pcp_request_service.service.SatelliteService
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ExactTimePositionRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.roundToLong
@Service
class AngularMotionService(
private val satelliteService: SatelliteService,
private val satelliteCatalogClient: SatelliteCatalogClient,
) {
fun availableSatellites(time: java.time.LocalDateTime?): List<AngularMotionSatelliteDTO> {
val satellitesByBallisticsId = satelliteCatalogClient.getSatellites()
.flatMap { satellite ->
listOfNotNull(
satellite.id to satellite,
satellite.noradId?.let { it to satellite },
)
}
.toMap()
return satelliteService.getOrbitAvailability(time)
.map { availability ->
val satellite = satellitesByBallisticsId[availability.satelliteId]
AngularMotionSatelliteDTO(
satelliteId = availability.satelliteId,
code = satellite?.code ?: availability.satelliteId.toString(),
name = satellite?.name ?: "КА ${availability.satelliteId}",
typeCode = satellite?.typeCode.orEmpty(),
availabilityStart = availability.timeStart,
availabilityStop = availability.timeStop,
)
}
.sortedWith(compareBy<AngularMotionSatelliteDTO> { it.code }.thenBy { it.satelliteId })
}
fun flightLine(satelliteId: Long, time: java.time.LocalDateTime): List<AngularMotionFlightLinePointDTO> {
val start = time.minusMinutes(MAP_INTERVAL_MINUTES)
val stop = time.plusMinutes(MAP_INTERVAL_MINUTES)
return satelliteService.getFL(satelliteId, start, stop, 60.0).map { point ->
AngularMotionFlightLinePointDTO(
time = point.time,
revolution = point.revolution,
latitudeDeg = point.lat,
longitudeDeg = point.long,
leftLatitudeDeg = point.latLeft,
leftLongitudeDeg = point.longLeft,
rightLatitudeDeg = point.latRight,
rightLongitudeDeg = point.longRight,
)
}
}
fun calculate(request: AngularMotionCalculationRequestDTO): AngularMotionCalculationResultDTO {
validate(request)
val defaultConfig = AngularMotionConfig()
val config = AngularMotionConfig(
focus = request.focusMm ?: defaultConfig.focus,
stepPuud = request.stepPuudSec ?: defaultConfig.stepPuud,
stepSdi = request.stepSdiSec ?: defaultConfig.stepSdi,
)
val sourcePoints = loadOrbitalPoints(request, config)
val stepper = RungeStepper(sourcePoints.toMutableList(), EarthType.PZ90d02)
val calculator = AngularMotionCalculatorFactory.create(request.mode, stepper, EarthType.PZ90d02, config)
val surveyId = request.toSurveyId()
val result = try {
calculator.calculate(surveyId)
} catch (error: RuntimeException) {
throw CustomValidationException(error.message ?: "Ошибка расчета ПУУД")
}
return AngularMotionCalculationResultDTO(
mode = result.mode,
satelliteId = request.satelliteId,
approximateTime = request.approximateTime,
startTime = toDateTime(result.startTime),
points = result.points.map { point ->
AngularMotionPointDTO(
time = toDateTime(point.t),
revolution = point.orbitalPoint.vit.toLong(),
tangDeg = point.orientation.tang.toDegrees(),
krenDeg = point.orientation.kren.toDegrees(),
riskDeg = point.orientation.risk.toDegrees(),
groundLatitudeDeg = point.groundPoint?.lat?.toDegrees(),
groundLongitudeDeg = point.groundPoint?.long?.toDegrees(),
omegaX = point.omega.x,
omegaY = point.omega.y,
omegaZ = point.omega.z,
wdX = point.wd.x,
wdY = point.wd.y,
wdZ = point.wd.z,
sdi = point.sdi,
)
},
)
}
private fun validate(request: AngularMotionCalculationRequestDTO) {
if (request.durationSec <= 0.0) {
throw CustomValidationException("durationSec must be positive")
}
if (request.latitudeDeg !in -90.0..90.0) {
throw CustomValidationException("latitudeDeg must be in [-90; 90]")
}
if (request.longitudeDeg !in -180.0..180.0) {
throw CustomValidationException("longitudeDeg must be in [-180; 180]")
}
if ((request.mode == AngularMotionMode.AZIMUTH || request.mode == AngularMotionMode.SMOOTH_SDI) &&
(request.sdi == null || abs(request.sdi) < 1.0e-4)
) {
throw CustomValidationException("Для азимутального режима и Smooth SDI требуется ненулевая СДИ")
}
}
private fun loadOrbitalPoints(
request: AngularMotionCalculationRequestDTO,
config: AngularMotionConfig,
): List<OrbitalPoint> {
val reserveBeforeSec = 240L
val reserveAfterSec = max(240L, request.durationSec.roundToLong() + 240L)
val stepMs = ((config.stepPuud * 1000.0).roundToLong()).coerceAtLeast(1L)
val response = satelliteService.exactTime(
request.satelliteId,
ExactTimePositionRequestDTO(
time = request.approximateTime.minusSeconds(reserveBeforeSec),
timeStop = request.approximateTime.plusSeconds(reserveAfterSec),
stepMs = stepMs,
),
).collectList().block().orEmpty()
if (response.isEmpty()) {
throw CustomValidationException(
"Баллистика не вернула точки орбиты для КА ${request.satelliteId} на интервале расчета",
)
}
return response.map { it.toOrbitalPoint() }
}
private fun AngularMotionCalculationRequestDTO.toSurveyId(): SurveyId = SurveyId(
oep = listOf(true, false, false, false),
t = fromDateTime(approximateTime),
b = latitudeDeg.toRadians(),
l = longitudeDeg.toRadians(),
h = heightM,
duration = durationSec,
sdi = sdi?.let { listOf(it) } ?: emptyList(),
azimuth = azimuthDeg.toRadians(),
uprAngle = leadAngleDeg.toRadians(),
pointInCenter = pointInCenter,
)
private fun OrbPointDTO.toOrbitalPoint(): OrbitalPoint = OrbitalPoint(
t = fromDateTime(time),
vit = revolution.toInt(),
r = Vector3D(x, y, z),
v = Vector3D(vx, vy, vz),
)
private fun Double.toRadians(): Double = this * PI / 180.0
private fun Double.toDegrees(): Double = this * 180.0 / PI
private companion object {
const val MAP_INTERVAL_MINUTES = 5L
}
}
@@ -0,0 +1,34 @@
package space.nstart.pcp.pcp_request_service.service
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.ObjectProvider
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import org.springframework.web.reactive.function.client.WebClient
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteSummaryDTO
/** Клиент к pcp-satellite-catalog-service для отображения человекочитаемых данных КА. */
@Service
class SatelliteCatalogClient(webClientBuilderProvider: ObjectProvider<WebClient.Builder>) {
private val webClientBuilder: WebClient.Builder = webClientBuilderProvider.ifAvailable ?: WebClient.builder()
private val logger: Logger = LoggerFactory.getLogger(this::class.java)
@Value("\${settings.satellite-catalog-service:pcp-satellite-catalog-service}")
val url = ""
fun getSatellites(): List<SatelliteSummaryDTO> =
runCatching {
webClientBuilder.build()
.get()
.uri("$url/api/satellites")
.retrieve()
.bodyToFlux(SatelliteSummaryDTO::class.java)
.collectList()
.block()
?: emptyList()
}.getOrElse { e ->
logger.error("Не удалось получить список КА из $url/api/satellites: ${e.message}")
emptyList()
}
}