Ручное сохранение маршрутов съёмки и сбросов в плане

Эндпоинты по id выбранного плана (= mission_id):
- POST /api/missions/{plan_id}/survey-route — строит контур и сохраняет
  SurveyModeEntity (source=MANUAL, status=PLANNED);
- POST /api/missions/{plan_id}/drop-route — ручной сброс по выбранной
  зоне радиовидимости: валидирует, что съёмки принадлежат плану и
  заканчиваются строго до начала зоны, а сумма длительностей не
  превышает длительность зоны; пишет DropModeEntity + связи
  DropModeSurveyEntity.

Миссия/план (planNumber=1) создаются на лету, если их ещё нет, с
интервалом из planStart/planEnd запроса. loadSlots чистит только
пересчитываемые маршруты (deleteRecalculableSurveyModesByPlanId),
поэтому ручные маршруты съёмки переживают пересчёт. Новые DTO —
DropRouteSaveRequestDTO и SurveyRouteBuildDTOs в pcp-types-lib.
This commit is contained in:
Дмитрий Соловьев
2026-06-01 22:17:20 +03:00
parent fafaeed418
commit ef97ebbcf7
5 changed files with 496 additions and 1 deletions
@@ -0,0 +1,46 @@
package space.nstart.pcp.pcp_types_lib.dto.satellite.mission
import io.swagger.v3.oas.annotations.media.Schema
import java.time.LocalDateTime
/**
* Запрос на ручное добавление сброса в план по выбранной зоне радиовидимости (ЗРВ).
*
* Оператор выбирает ЗРВ станции и набор уже запланированных маршрутов съёмки, время конца
* которых строго меньше начала зоны (т.е. съёмка успевает завершиться до сеанса сброса).
* Сброс сохраняется как [space.nstart.pcp.pcp_mission_planing_service]-режим DROP под основным
* планом миссии (planNumber = 1) с теми же связями съёмка↔сброс, что и при штатном расчёте.
*/
@Schema(description = "Запрос на ручное добавление сброса по выбранной зоне радиовидимости")
data class DropRouteSaveRequestDTO(
@field:Schema(description = "Идентификатор КА (NORAD)")
val satelliteId: Long = 0,
@field:Schema(description = "Идентификатор/номер станции приёма (из ЗРВ)")
val station: String = "",
@field:Schema(description = "Номер витка зоны радиовидимости")
val revolution: Long = 0,
@field:Schema(description = "Начало зоны радиовидимости (локальное время без зоны)")
val zoneStart: LocalDateTime = LocalDateTime.now(),
@field:Schema(description = "Конец зоны радиовидимости (локальное время без зоны)")
val zoneStop: LocalDateTime = LocalDateTime.now(),
@field:Schema(description = "Идентификаторы режимов съёмки (survey_modes), которые сбрасываются в этой зоне")
val surveyModeIds: List<Long> = emptyList(),
@field:Schema(
description = "Начало интервала плана (локальное время без зоны). Используется только при " +
"сохранении, если миссии/плана с таким id ещё нет — чтобы создать их с корректным окном.",
nullable = true
)
val planStart: LocalDateTime? = null,
@field:Schema(
description = "Конец интервала плана (локальное время без зоны). См. planStart.",
nullable = true
)
val planEnd: LocalDateTime? = null
)
@@ -0,0 +1,77 @@
package space.nstart.pcp.pcp_types_lib.dto.satellite.mission
import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.DecimalMin
import java.time.LocalDateTime
/**
* Способ построения временного окна маршрута относительно момента видимости.
*/
enum class SurveyRouteAnchor {
/** Окно центрируется на моменте видимости: [time - duration/2, time + duration/2]. */
CENTERED,
/** Момент видимости — начало съёмки: [time, time + duration]. */
FORWARD
}
@Schema(description = "Запрос на построение одного маршрута съёмки по параметрам видимости точки")
data class SurveyRouteBuildRequestDTO(
@field:Schema(description = "Идентификатор КА (тот же, что используется в obj-view/mpl-point — NORAD)")
val satelliteId: Long = 0,
@field:Schema(description = "Момент видимости (локальное время без зоны)")
val time: LocalDateTime = LocalDateTime.now(),
@field:DecimalMin(value = "1", message = "Длительность съёмки должна быть не меньше 1 секунды")
@field:Schema(description = "Длительность съёмки, секунды")
val durationSeconds: Double = 60.0,
@field:Schema(description = "Крен (gamma), градусы")
val roll: Double = 0.0,
@field:Schema(description = "Угол захвата (полуширина полосы), градусы")
val captureAngle: Double = 1.5,
@field:Schema(description = "Номер витка (0 — определить по орбитальным точкам)")
val revolution: Long = 0,
@field:Schema(description = "Способ построения окна относительно момента видимости")
val anchor: SurveyRouteAnchor = SurveyRouteAnchor.CENTERED,
@field:Schema(
description = "Начало интервала плана (локальное время без зоны). Используется только при " +
"сохранении маршрута, если миссии/плана с таким id ещё нет — чтобы создать их с " +
"корректным окном (для расчёта пересечений и наложений). Игнорируется в /build.",
nullable = true
)
val planStart: LocalDateTime? = null,
@field:Schema(
description = "Конец интервала плана (локальное время без зоны). См. planStart.",
nullable = true
)
val planEnd: LocalDateTime? = null
)
@Schema(description = "Построенный маршрут съёмки")
data class SurveyRouteBuildResponseDTO(
val satelliteId: Long,
@field:Schema(description = "Начало съёмки (локальное время без зоны)")
val timeStart: LocalDateTime,
@field:Schema(description = "Длительность съёмки, секунды")
val duration: Double,
val revolution: Long,
@field:Schema(description = "Крен (gamma), градусы")
val roll: Double,
@field:Schema(description = "Угол захвата, градусы")
val captureAngle: Double,
@field:Schema(description = "Широта центра контура, градусы")
val lat: Double,
@field:Schema(description = "Долгота центра контура, градусы")
val longitude: Double,
@field:Schema(description = "Контур полосы съёмки, WKT POLYGON (lon lat, ...)")
val contourWkt: String,
@field:Schema(description = "Идентификатор сохранённого режима съёмки в БД (null — маршрут не сохранялся)")
val surveyModeId: Long? = null
)