Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
+46
@@ -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
|
||||
)
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package space.nstart.pcp.pcp_types_lib.dto.satellite.mission
|
||||
|
||||
/**
|
||||
* Скорости накопления/сброса данных для оценки объёма режима.
|
||||
*
|
||||
* Объём (`volumeMb`) считается единообразно во всех сервисах в **мегабайтах (Мбайт)** как
|
||||
* `скорость × длительность`, поэтому константы живут в общей библиотеке типов — один источник
|
||||
* правды для текущего плана (pcp-ui-service), редактора и комплексного плана (pcp-tgu-ops-ui).
|
||||
*
|
||||
* Внимание на единицы: съёмка задана в Мбайт/с, а скорость канала сброса — в Мбит/с,
|
||||
* поэтому при расчёте объёма сброса скорость приводится к Мбайт делением на 8.
|
||||
*/
|
||||
object ModeDataRates {
|
||||
/**
|
||||
* Накопление при съёмке — единое для всех КА, **Мбайт/с**.
|
||||
* Из расчёта 100 с маршрута = 12 ГБ (1 ГБ = 1000 Мбайт): 12000 Мбайт / 100 с = 120 Мбайт/с.
|
||||
*/
|
||||
const val SURVEY_MBYTE_PER_SEC: Double = 120.0
|
||||
|
||||
/** Сброс — постоянная скорость канала, **Мбит/с**. */
|
||||
const val DROP_MBIT_PER_SEC: Double = 1200.0
|
||||
|
||||
/** Бит в байте — приведение скорости канала (Мбит/с) к объёму памяти (Мбайт). */
|
||||
private const val BITS_PER_BYTE: Double = 8.0
|
||||
|
||||
/** Объём маршрута съёмки, Мбайт, по его длительности (секунды). */
|
||||
fun surveyVolumeMb(durationSeconds: Double): Double = durationSeconds * SURVEY_MBYTE_PER_SEC
|
||||
|
||||
/** Объём сброса, Мбайт, по длительности сеанса (секунды): скорость канала Мбит/с → Мбайт. */
|
||||
fun dropVolumeMb(durationSeconds: Double): Double = durationSeconds * DROP_MBIT_PER_SEC / BITS_PER_BYTE
|
||||
}
|
||||
+17
-6
@@ -1,9 +1,10 @@
|
||||
package space.nstart.pcp.pcp_types_lib.dto.satellite.mission
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Schema(
|
||||
description = "Базовая модель режима",
|
||||
@@ -28,20 +29,24 @@ sealed interface ModeResponseDTO {
|
||||
val planId: Long
|
||||
|
||||
@get:Schema(description = "Время начала режима")
|
||||
val timeStart: OffsetDateTime?
|
||||
val timeStart: LocalDateTime?
|
||||
|
||||
@get:Schema(description = "Номер витка")
|
||||
val revolution: Long
|
||||
|
||||
@get:Schema(description = "Тип режима", allowableValues = ["SURVEY", "DROP"])
|
||||
val type: String
|
||||
|
||||
@get:Schema(description = "Объём режима, Мбайт (накопление съёмки 120 Мбайт/с, сброс — канал 1200 Мбит/с)")
|
||||
@get:JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||
val volumeMb: Double
|
||||
}
|
||||
|
||||
@Schema(description = "Режим съемки")
|
||||
data class SurveyModeResponseDTO(
|
||||
override val id: Long = 0,
|
||||
override val planId: Long = 0,
|
||||
override val timeStart: OffsetDateTime? = null,
|
||||
override val timeStart: LocalDateTime? = null,
|
||||
override val revolution: Long = 0,
|
||||
override val type: String = "SURVEY",
|
||||
val status: String = "",
|
||||
@@ -54,16 +59,22 @@ data class SurveyModeResponseDTO(
|
||||
val roll: Double = 0.0,
|
||||
@get:Schema(description = "Идентификаторы забронированных слотов")
|
||||
val bookedSlotIds: List<Long> = emptyList()
|
||||
) : ModeResponseDTO
|
||||
) : ModeResponseDTO {
|
||||
@get:JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||
override val volumeMb: Double get() = ModeDataRates.surveyVolumeMb(duration)
|
||||
}
|
||||
|
||||
@Schema(description = "Режим сброса")
|
||||
data class DropModeResponseDTO(
|
||||
override val id: Long = 0,
|
||||
override val planId: Long = 0,
|
||||
override val timeStart: OffsetDateTime? = null,
|
||||
override val timeStart: LocalDateTime? = null,
|
||||
override val revolution: Long = 0,
|
||||
override val type: String = "DROP",
|
||||
val station: String? = "",
|
||||
val duration: Double = 0.0,
|
||||
val surveys: List<Long> = emptyList()
|
||||
) : ModeResponseDTO
|
||||
) : ModeResponseDTO {
|
||||
@get:JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||
override val volumeMb: Double get() = ModeDataRates.dropVolumeMb(duration)
|
||||
}
|
||||
|
||||
+7
-1
@@ -1,5 +1,6 @@
|
||||
package space.nstart.pcp.pcp_types_lib.dto.satellite.mission
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import java.time.LocalDateTime
|
||||
|
||||
data class SatelliteModeResponseDTO(
|
||||
@@ -19,4 +20,9 @@ data class SatelliteModeResponseDTO(
|
||||
val cellNums: List<Long> = emptyList(),
|
||||
val bookedSlotIds: List<Long> = emptyList(),
|
||||
val snapshotId: Long? = null
|
||||
)
|
||||
) {
|
||||
/** Объём режима, Мбайт: сброс — канал 1200 Мбит/с (÷8), иначе — накопление съёмки 120 Мбайт/с. */
|
||||
@get:JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||
val volumeMb: Double
|
||||
get() = if (type == "DROP") ModeDataRates.dropVolumeMb(duration) else ModeDataRates.surveyVolumeMb(duration)
|
||||
}
|
||||
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package space.nstart.pcp.pcp_types_lib.dto.satellite.mission
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
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
|
||||
) {
|
||||
@get:Schema(description = "Объём маршрута, Мбайт (накопление съёмки 120 Мбайт/с × длительность)")
|
||||
@get:JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||
val volumeMb: Double get() = ModeDataRates.surveyVolumeMb(duration)
|
||||
}
|
||||
Executable → Regular
+22
-22
@@ -1,22 +1,22 @@
|
||||
package space.nstart.pcp.pcp_types_lib.utils.json
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.databind.JsonSerializer
|
||||
import com.fasterxml.jackson.databind.SerializerProvider
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
class LocalDateTimeTimeZoneSerializer: JsonSerializer<LocalDateTime?>() {
|
||||
|
||||
companion object {
|
||||
val fmt: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSxxx")
|
||||
}
|
||||
|
||||
override fun serialize(p0: LocalDateTime?, p1: JsonGenerator, p2: SerializerProvider) {
|
||||
|
||||
//todo: проверить корректность зоны по нормальным правилам
|
||||
// p1.writeString(p0?.atZone(ZoneId.of("Europe/Moscow"))?.format(fmt))
|
||||
p1.writeString(p0?.atZone(ZoneId.systemDefault())?.format(fmt))
|
||||
}
|
||||
}
|
||||
package space.nstart.pcp.pcp_types_lib.utils.json
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.databind.JsonSerializer
|
||||
import com.fasterxml.jackson.databind.SerializerProvider
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
class LocalDateTimeTimeZoneSerializer: JsonSerializer<LocalDateTime?>() {
|
||||
|
||||
companion object {
|
||||
val fmt: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSxxx")
|
||||
}
|
||||
|
||||
override fun serialize(p0: LocalDateTime?, p1: JsonGenerator, p2: SerializerProvider) {
|
||||
|
||||
//todo: проверить корректность зоны по нормальным правилам
|
||||
// p1.writeString(p0?.atZone(ZoneId.of("Europe/Moscow"))?.format(fmt))
|
||||
p1.writeString(p0?.atZone(ZoneId.systemDefault())?.format(fmt))
|
||||
}
|
||||
}
|
||||
|
||||
+23
@@ -6,6 +6,7 @@ import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertInstanceOf
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class ModeResponseDTOJacksonTest {
|
||||
|
||||
@@ -50,4 +51,26 @@ class ModeResponseDTOJacksonTest {
|
||||
assertEquals("SURVEY", result[0].type)
|
||||
assertEquals("DROP", result[1].type)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `serializes computed volume and tolerates it on read-back`() {
|
||||
// Сервис-источник теперь сериализует volumeMb; потребитель (pcp-ui-service)
|
||||
// десериализует тот же JSON обратно — read-only поле не должно ломать чтение.
|
||||
val modes: List<ModeResponseDTO> = listOf(
|
||||
SurveyModeResponseDTO(id = 1, planId = 10, duration = 100.0),
|
||||
DropModeResponseDTO(id = 2, planId = 10, duration = 5.0)
|
||||
)
|
||||
|
||||
val json = mapper.writeValueAsString(modes)
|
||||
val tree = mapper.readTree(json)
|
||||
|
||||
// Съёмка: 100 с × 120 Мбайт/с = 12000 Мбайт. Сброс: 5 с × 1200 Мбит/с ÷ 8 = 750 Мбайт.
|
||||
assertEquals(12000.0, tree[0]["volumeMb"].asDouble())
|
||||
assertEquals(750.0, tree[1]["volumeMb"].asDouble())
|
||||
|
||||
val roundTrip: List<ModeResponseDTO> = mapper.readValue(json)
|
||||
assertEquals(12000.0, roundTrip[0].volumeMb)
|
||||
assertEquals(750.0, roundTrip[1].volumeMb)
|
||||
assertTrue(roundTrip[0] is SurveyModeResponseDTO)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user