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

This commit is contained in:
emelianov
2026-06-15 12:19:53 +03:00
parent 07ba650e03
commit 7fdac8c71e
9 changed files with 88 additions and 174 deletions
@@ -0,0 +1,27 @@
package space.nstart.pcp.pcp_types_lib.dto.ballistics
import java.time.LocalDateTime
/** Запрос на ручной расчет программы управления угловым движением. */
data class AngularMotionCalculationRequestDTO(
val satelliteId: Long,
val approximateTime: LocalDateTime,
val mode: AngularMotionModeDTO = AngularMotionModeDTO.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,
)
enum class AngularMotionModeDTO {
CONST_ORIENT,
AZIMUTH,
SMOOTH_SDI,
}
@@ -0,0 +1,44 @@
package space.nstart.pcp.pcp_types_lib.dto.ballistics
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.time.LocalDateTime
import kotlin.test.assertFalse
class AngularMotionCalculationRequestDTOJacksonTest {
private val mapper = jacksonObjectMapper().registerModule(JavaTimeModule())
@Test
fun `deserializes calculation request contract`() {
val request = mapper.readValue<AngularMotionCalculationRequestDTO>(
"""
{
"satelliteId": 123,
"approximateTime": "2026-06-15T10:30:00",
"mode": "CONST_ORIENT",
"latitudeDeg": 55.75,
"longitudeDeg": 37.61,
"heightM": 0,
"durationSec": 60,
"leadAngleDeg": 0,
"azimuthDeg": 0,
"pointInCenter": false,
"focusMm": 5500,
"stepPuudSec": 0.125,
"stepSdiSec": 20
}
""".trimIndent(),
)
assertEquals(123L, request.satelliteId)
assertEquals(LocalDateTime.of(2026, 6, 15, 10, 30), request.approximateTime)
assertEquals(AngularMotionModeDTO.CONST_ORIENT, request.mode)
assertEquals(55.75, request.latitudeDeg)
assertEquals(37.61, request.longitudeDeg)
assertFalse(request.pointInCenter)
}
}
@@ -7,8 +7,8 @@ import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import space.nstart.pcp.angularmotion.AngularMotionMode
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.AngularMotionCalculationRequestDTO
import java.net.URLDecoder import java.net.URLDecoder
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -33,8 +33,8 @@ class AngularMotionController(
) )
@PostMapping("/calculate") @PostMapping("/calculate")
fun calculate(@RequestBody request: Map<String, Any?>): AngularMotionCalculationResultDTO = fun calculate(@RequestBody request: AngularMotionCalculationRequestDTO): AngularMotionCalculationResultDTO =
angularMotionService.calculate(AngularMotionRequestMapper.calculationRequest(request)) angularMotionService.calculate(request)
private fun parseTime(value: String?, required: Boolean): LocalDateTime? { private fun parseTime(value: String?, required: Boolean): LocalDateTime? {
val raw = value?.trim().orEmpty() val raw = value?.trim().orEmpty()
@@ -52,80 +52,3 @@ class AngularMotionController(
} }
} }
} }
internal object AngularMotionRequestMapper {
fun calculationRequest(payload: Map<String, Any?>): AngularMotionCalculationRequestDTO =
AngularMotionCalculationRequestDTO(
satelliteId = payload.requiredLong("satelliteId"),
approximateTime = parseLocalDateTime(payload.requiredString("approximateTime"), "approximateTime"),
mode = payload.optionalEnum("mode") ?: AngularMotionMode.CONST_ORIENT,
latitudeDeg = payload.requiredDouble("latitudeDeg"),
longitudeDeg = payload.requiredDouble("longitudeDeg"),
heightM = payload.optionalDouble("heightM") ?: 0.0,
durationSec = payload.requiredDouble("durationSec"),
leadAngleDeg = payload.optionalDouble("leadAngleDeg") ?: 0.0,
azimuthDeg = payload.optionalDouble("azimuthDeg") ?: 0.0,
sdi = payload.optionalDouble("sdi"),
pointInCenter = payload.optionalBoolean("pointInCenter") ?: false,
focusMm = payload.optionalDouble("focusMm"),
stepPuudSec = payload.optionalDouble("stepPuudSec"),
stepSdiSec = payload.optionalDouble("stepSdiSec"),
)
private fun Map<String, Any?>.requiredString(field: String): String {
val value = this[field]?.toString()?.trim().orEmpty()
if (value.isEmpty()) {
throw CustomValidationException("Не задан параметр $field")
}
return value
}
private fun Map<String, Any?>.requiredLong(field: String): Long =
optionalLong(field) ?: throw CustomValidationException("Не задан параметр $field")
private fun Map<String, Any?>.requiredDouble(field: String): Double =
optionalDouble(field) ?: throw CustomValidationException("Не задан параметр $field")
private fun Map<String, Any?>.optionalLong(field: String): Long? {
val value = this[field] ?: return null
return when (value) {
is Number -> value.toLong()
is String -> value.trim().takeIf { it.isNotEmpty() }?.toLongOrNull()
else -> null
} ?: throw CustomValidationException("Параметр $field должен быть целым числом")
}
private fun Map<String, Any?>.optionalDouble(field: String): Double? {
val value = this[field] ?: return null
return when (value) {
is Number -> value.toDouble()
is String -> value.trim().takeIf { it.isNotEmpty() }?.toDoubleOrNull()
else -> null
}?.takeIf { it.isFinite() } ?: throw CustomValidationException("Параметр $field должен быть числом")
}
private fun Map<String, Any?>.optionalBoolean(field: String): Boolean? {
val value = this[field] ?: return null
return when (value) {
is Boolean -> value
is String -> value.trim().takeIf { it.isNotEmpty() }?.toBooleanStrictOrNull()
else -> null
} ?: throw CustomValidationException("Параметр $field должен быть true или false")
}
private fun Map<String, Any?>.optionalEnum(field: String): AngularMotionMode? {
val value = this[field]?.toString()?.trim()?.takeIf { it.isNotEmpty() } ?: return null
return runCatching { AngularMotionMode.valueOf(value) }
.getOrElse { throw CustomValidationException("Некорректный режим ПУУД '$value'") }
}
private fun parseLocalDateTime(value: String, field: String): LocalDateTime {
val normalized = if (value.length == 16) "$value:00" else value
return runCatching { LocalDateTime.parse(normalized) }
.getOrElse {
throw CustomValidationException(
"Некорректный формат времени $field='$value'. Ожидается yyyy-MM-dd'T'HH:mm или yyyy-MM-dd'T'HH:mm:ss",
)
}
}
}
@@ -3,24 +3,6 @@ package space.nstart.pcp.pcp_request_service.angular
import space.nstart.pcp.angularmotion.AngularMotionMode import space.nstart.pcp.angularmotion.AngularMotionMode
import java.time.LocalDateTime 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( data class AngularMotionSatelliteDTO(
val satelliteId: Long, val satelliteId: Long,
val code: String, val code: String,
@@ -14,6 +14,8 @@ import space.nstart.pcp.angularmotion.SurveyId
import space.nstart.pcp.pcp_request_service.configuration.CustomValidationException 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.SatelliteCatalogClient
import space.nstart.pcp.pcp_request_service.service.SatelliteService import space.nstart.pcp.pcp_request_service.service.SatelliteService
import space.nstart.pcp.pcp_types_lib.dto.ballistics.AngularMotionCalculationRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.AngularMotionModeDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ExactTimePositionRequestDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.ExactTimePositionRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
import kotlin.math.PI import kotlin.math.PI
@@ -81,7 +83,8 @@ class AngularMotionService(
) )
val sourcePoints = loadOrbitalPoints(request, config) val sourcePoints = loadOrbitalPoints(request, config)
val stepper = RungeStepper(sourcePoints.toMutableList(), EarthType.PZ90d02) val stepper = RungeStepper(sourcePoints.toMutableList(), EarthType.PZ90d02)
val calculator = AngularMotionCalculatorFactory.create(request.mode, stepper, EarthType.PZ90d02, config) val mode = request.mode.toCalculationMode()
val calculator = AngularMotionCalculatorFactory.create(mode, stepper, EarthType.PZ90d02, config)
val surveyId = request.toSurveyId() val surveyId = request.toSurveyId()
val result = try { val result = try {
calculator.calculate(surveyId) calculator.calculate(surveyId)
@@ -125,8 +128,9 @@ class AngularMotionService(
if (request.longitudeDeg !in -180.0..180.0) { if (request.longitudeDeg !in -180.0..180.0) {
throw CustomValidationException("longitudeDeg must be in [-180; 180]") throw CustomValidationException("longitudeDeg must be in [-180; 180]")
} }
if ((request.mode == AngularMotionMode.AZIMUTH || request.mode == AngularMotionMode.SMOOTH_SDI) && val sdi = request.sdi
(request.sdi == null || abs(request.sdi) < 1.0e-4) if ((request.mode == AngularMotionModeDTO.AZIMUTH || request.mode == AngularMotionModeDTO.SMOOTH_SDI) &&
(sdi == null || abs(sdi) < 1.0e-4)
) { ) {
throw CustomValidationException("Для азимутального режима и Smooth SDI требуется ненулевая СДИ") throw CustomValidationException("Для азимутального режима и Smooth SDI требуется ненулевая СДИ")
} }
@@ -180,6 +184,8 @@ class AngularMotionService(
private fun Double.toDegrees(): Double = this * 180.0 / PI private fun Double.toDegrees(): Double = this * 180.0 / PI
private fun AngularMotionModeDTO.toCalculationMode(): AngularMotionMode = AngularMotionMode.valueOf(name)
private companion object { private companion object {
const val MAP_INTERVAL_MINUTES = 5L const val MAP_INTERVAL_MINUTES = 5L
} }
@@ -1,70 +0,0 @@
package space.nstart.pcp.pcp_request_service.angular
import org.junit.jupiter.api.assertThrows
import space.nstart.pcp.angularmotion.AngularMotionMode
import space.nstart.pcp.pcp_request_service.configuration.CustomValidationException
import java.time.LocalDateTime
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
class AngularMotionRequestMapperTest {
@Test
fun `maps calculation payload sent by UI`() {
val request = AngularMotionRequestMapper.calculationRequest(
mapOf(
"satelliteId" to 123L,
"approximateTime" to "2026-06-15T10:30:00",
"mode" to "CONST_ORIENT",
"latitudeDeg" to 55.75,
"longitudeDeg" to 37.61,
"heightM" to 0,
"durationSec" to 60,
"leadAngleDeg" to 0,
"azimuthDeg" to 0,
"focusMm" to 5500,
"stepPuudSec" to 0.125,
"stepSdiSec" to 20,
)
)
assertEquals(123L, request.satelliteId)
assertEquals(LocalDateTime.of(2026, 6, 15, 10, 30), request.approximateTime)
assertEquals(AngularMotionMode.CONST_ORIENT, request.mode)
assertEquals(55.75, request.latitudeDeg)
assertEquals(37.61, request.longitudeDeg)
assertFalse(request.pointInCenter)
}
@Test
fun `maps datetime without seconds`() {
val request = AngularMotionRequestMapper.calculationRequest(
mapOf(
"satelliteId" to 123L,
"approximateTime" to "2026-06-15T10:30",
"latitudeDeg" to 55.75,
"longitudeDeg" to 37.61,
"durationSec" to 60,
)
)
assertEquals(LocalDateTime.of(2026, 6, 15, 10, 30), request.approximateTime)
assertEquals(AngularMotionMode.CONST_ORIENT, request.mode)
}
@Test
fun `rejects unknown calculation mode with validation error`() {
assertThrows<CustomValidationException> {
AngularMotionRequestMapper.calculationRequest(
mapOf(
"satelliteId" to 123L,
"approximateTime" to "2026-06-15T10:30:00",
"mode" to "UNKNOWN",
"latitudeDeg" to 55.75,
"longitudeDeg" to 37.61,
"durationSec" to 60,
)
)
}
}
}
@@ -9,6 +9,7 @@ import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.ResponseBody import org.springframework.web.bind.annotation.ResponseBody
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import space.nstart.pcp.pcp_types_lib.dto.ballistics.AngularMotionCalculationRequestDTO
import space.nstart.pcp.slots_service.dto.dynamicplan.DynamicPlanCalculationRequestDTO import space.nstart.pcp.slots_service.dto.dynamicplan.DynamicPlanCalculationRequestDTO
import space.nstart.pcp.slots_service.service.DynamicPlanService import space.nstart.pcp.slots_service.service.DynamicPlanService
import java.util.UUID import java.util.UUID
@@ -94,7 +95,6 @@ class AngularMotionRestController(
@PostMapping("/calculate") @PostMapping("/calculate")
@ResponseBody @ResponseBody
fun calculate(@RequestBody request: Map<String, Any?>) = fun calculate(@RequestBody request: AngularMotionCalculationRequestDTO) =
dynamicPlanService.calculateAngularMotion(request) dynamicPlanService.calculateAngularMotion(request)
} }
@@ -5,6 +5,7 @@ import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.web.reactive.function.client.WebClient import org.springframework.web.reactive.function.client.WebClient
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import space.nstart.pcp.pcp_types_lib.dto.ballistics.AngularMotionCalculationRequestDTO
import space.nstart.pcp.slots_service.configuration.CustomErrorException import space.nstart.pcp.slots_service.configuration.CustomErrorException
import space.nstart.pcp.slots_service.dto.dynamicplan.DynamicPlanCalculationRequestDTO import space.nstart.pcp.slots_service.dto.dynamicplan.DynamicPlanCalculationRequestDTO
import tools.jackson.databind.JsonNode import tools.jackson.databind.JsonNode
@@ -143,7 +144,7 @@ class DynamicPlanService(webClientBuilderProvider: ObjectProvider<WebClient.Buil
.bodyToMono(JsonNode::class.java) .bodyToMono(JsonNode::class.java)
.block() ?: ObjectMapper().createArrayNode() .block() ?: ObjectMapper().createArrayNode()
fun calculateAngularMotion(request: Any): JsonNode = fun calculateAngularMotion(request: AngularMotionCalculationRequestDTO): JsonNode =
webClientBuilder.build() webClientBuilder.build()
.post() .post()
.uri("$ballisticsServiceUrl/api/angular-motion/calculate") .uri("$ballisticsServiceUrl/api/angular-motion/calculate")
@@ -379,6 +379,7 @@
durationSec: Number(el('angular-motion-duration').value), durationSec: Number(el('angular-motion-duration').value),
leadAngleDeg: Number(el('angular-motion-lead-angle').value || 0), leadAngleDeg: Number(el('angular-motion-lead-angle').value || 0),
azimuthDeg: Number(el('angular-motion-azimuth').value || 0), azimuthDeg: Number(el('angular-motion-azimuth').value || 0),
pointInCenter: false,
focusMm: Number(el('angular-motion-focus').value || 5500), focusMm: Number(el('angular-motion-focus').value || 5500),
stepPuudSec: Number(el('angular-motion-step').value || 0.125), stepPuudSec: Number(el('angular-motion-step').value || 0.125),
stepSdiSec: Number(el('angular-motion-step-sdi').value || 20), stepSdiSec: Number(el('angular-motion-step-sdi').value || 20),