Прототип страницы расчета ПУУД
This commit is contained in:
+3
-80
@@ -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<String, Any?>): 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<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",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-18
@@ -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,
|
||||
|
||||
+9
-3
@@ -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
|
||||
}
|
||||
|
||||
-70
@@ -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,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user