Прототип страницы расчета ПУУД
This commit is contained in:
@@ -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")}")
|
||||
|
||||
+36
@@ -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)
|
||||
}
|
||||
+67
@@ -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,
|
||||
)
|
||||
+182
@@ -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
|
||||
}
|
||||
}
|
||||
+34
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user