diff --git a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/AngularMotionCalculationRequestDTO.kt b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/AngularMotionCalculationRequestDTO.kt new file mode 100644 index 0000000..7b2af05 --- /dev/null +++ b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/AngularMotionCalculationRequestDTO.kt @@ -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, +} diff --git a/libs/pcp-types-lib/src/test/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/AngularMotionCalculationRequestDTOJacksonTest.kt b/libs/pcp-types-lib/src/test/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/AngularMotionCalculationRequestDTOJacksonTest.kt new file mode 100644 index 0000000..86b3df4 --- /dev/null +++ b/libs/pcp-types-lib/src/test/kotlin/space/nstart/pcp/pcp_types_lib/dto/ballistics/AngularMotionCalculationRequestDTOJacksonTest.kt @@ -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( + """ + { + "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) + } +} diff --git a/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/angular/AngularMotionController.kt b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/angular/AngularMotionController.kt index 7e38397..5deeddd 100644 --- a/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/angular/AngularMotionController.kt +++ b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/angular/AngularMotionController.kt @@ -7,8 +7,8 @@ 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 space.nstart.pcp.angularmotion.AngularMotionMode 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.nio.charset.StandardCharsets import java.time.LocalDateTime @@ -33,8 +33,8 @@ class AngularMotionController( ) @PostMapping("/calculate") - fun calculate(@RequestBody request: Map): AngularMotionCalculationResultDTO = - angularMotionService.calculate(AngularMotionRequestMapper.calculationRequest(request)) + fun calculate(@RequestBody request: AngularMotionCalculationRequestDTO): AngularMotionCalculationResultDTO = + angularMotionService.calculate(request) private fun parseTime(value: String?, required: Boolean): LocalDateTime? { val raw = value?.trim().orEmpty() @@ -52,80 +52,3 @@ class AngularMotionController( } } } - -internal object AngularMotionRequestMapper { - fun calculationRequest(payload: Map): 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.requiredString(field: String): String { - val value = this[field]?.toString()?.trim().orEmpty() - if (value.isEmpty()) { - throw CustomValidationException("Не задан параметр $field") - } - return value - } - - private fun Map.requiredLong(field: String): Long = - optionalLong(field) ?: throw CustomValidationException("Не задан параметр $field") - - private fun Map.requiredDouble(field: String): Double = - optionalDouble(field) ?: throw CustomValidationException("Не задан параметр $field") - - private fun Map.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.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.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.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", - ) - } - } -} diff --git a/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/angular/AngularMotionDto.kt b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/angular/AngularMotionDto.kt index 7b914a6..f364f9c 100644 --- a/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/angular/AngularMotionDto.kt +++ b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/angular/AngularMotionDto.kt @@ -3,24 +3,6 @@ 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, diff --git a/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/angular/AngularMotionService.kt b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/angular/AngularMotionService.kt index bdd5929..9521362 100644 --- a/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/angular/AngularMotionService.kt +++ b/services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/angular/AngularMotionService.kt @@ -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.service.SatelliteCatalogClient 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.OrbPointDTO import kotlin.math.PI @@ -81,7 +83,8 @@ class AngularMotionService( ) val sourcePoints = loadOrbitalPoints(request, config) 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 result = try { calculator.calculate(surveyId) @@ -125,8 +128,9 @@ class AngularMotionService( 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) + val sdi = request.sdi + if ((request.mode == AngularMotionModeDTO.AZIMUTH || request.mode == AngularMotionModeDTO.SMOOTH_SDI) && + (sdi == null || abs(sdi) < 1.0e-4) ) { throw CustomValidationException("Для азимутального режима и Smooth SDI требуется ненулевая СДИ") } @@ -180,6 +184,8 @@ class AngularMotionService( private fun Double.toDegrees(): Double = this * 180.0 / PI + private fun AngularMotionModeDTO.toCalculationMode(): AngularMotionMode = AngularMotionMode.valueOf(name) + private companion object { const val MAP_INTERVAL_MINUTES = 5L } diff --git a/services/pcp-ballistics-service/src/test/kotlin/space/nstart/pcp/pcp_request_service/angular/AngularMotionRequestMapperTest.kt b/services/pcp-ballistics-service/src/test/kotlin/space/nstart/pcp/pcp_request_service/angular/AngularMotionRequestMapperTest.kt deleted file mode 100644 index 0cd6423..0000000 --- a/services/pcp-ballistics-service/src/test/kotlin/space/nstart/pcp/pcp_request_service/angular/AngularMotionRequestMapperTest.kt +++ /dev/null @@ -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 { - 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, - ) - ) - } - } -} diff --git a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/DynamicPlanController.kt b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/DynamicPlanController.kt index afdb7c3..c32b6fc 100644 --- a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/DynamicPlanController.kt +++ b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/DynamicPlanController.kt @@ -9,6 +9,7 @@ import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.ResponseBody 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.service.DynamicPlanService import java.util.UUID @@ -94,7 +95,6 @@ class AngularMotionRestController( @PostMapping("/calculate") @ResponseBody - fun calculate(@RequestBody request: Map) = + fun calculate(@RequestBody request: AngularMotionCalculationRequestDTO) = dynamicPlanService.calculateAngularMotion(request) } - diff --git a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/DynamicPlanService.kt b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/DynamicPlanService.kt index e737cca..46acd25 100644 --- a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/DynamicPlanService.kt +++ b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/DynamicPlanService.kt @@ -5,6 +5,7 @@ import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Service import org.springframework.web.reactive.function.client.WebClient 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.dto.dynamicplan.DynamicPlanCalculationRequestDTO import tools.jackson.databind.JsonNode @@ -143,7 +144,7 @@ class DynamicPlanService(webClientBuilderProvider: ObjectProvider