diff --git a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/mission/ModeDataRates.kt b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/mission/ModeDataRates.kt new file mode 100644 index 0000000..784ece3 --- /dev/null +++ b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/mission/ModeDataRates.kt @@ -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 +} diff --git a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/mission/ModeResponseDTO.kt b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/mission/ModeResponseDTO.kt index 9ca3e7f..87f749c 100644 --- a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/mission/ModeResponseDTO.kt +++ b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/mission/ModeResponseDTO.kt @@ -1,5 +1,6 @@ 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 @@ -35,6 +36,10 @@ sealed interface ModeResponseDTO { @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 = "Режим съемки") @@ -54,7 +59,10 @@ data class SurveyModeResponseDTO( val roll: Double = 0.0, @get:Schema(description = "Идентификаторы забронированных слотов") val bookedSlotIds: List = emptyList() -) : ModeResponseDTO +) : ModeResponseDTO { + @get:JsonProperty(access = JsonProperty.Access.READ_ONLY) + override val volumeMb: Double get() = ModeDataRates.surveyVolumeMb(duration) +} @Schema(description = "Режим сброса") data class DropModeResponseDTO( @@ -66,4 +74,7 @@ data class DropModeResponseDTO( val station: String? = "", val duration: Double = 0.0, val surveys: List = emptyList() -) : ModeResponseDTO +) : ModeResponseDTO { + @get:JsonProperty(access = JsonProperty.Access.READ_ONLY) + override val volumeMb: Double get() = ModeDataRates.dropVolumeMb(duration) +} diff --git a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/mission/SatelliteModeResponseDTO.kt b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/mission/SatelliteModeResponseDTO.kt index ef23741..d79d04d 100644 --- a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/mission/SatelliteModeResponseDTO.kt +++ b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/mission/SatelliteModeResponseDTO.kt @@ -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 = emptyList(), val bookedSlotIds: List = 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) +} diff --git a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/mission/SurveyRouteBuildDTOs.kt b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/mission/SurveyRouteBuildDTOs.kt index 29afdfd..cbde248 100644 --- a/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/mission/SurveyRouteBuildDTOs.kt +++ b/libs/pcp-types-lib/src/main/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/mission/SurveyRouteBuildDTOs.kt @@ -1,5 +1,6 @@ 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 @@ -74,4 +75,8 @@ data class SurveyRouteBuildResponseDTO( 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) +} diff --git a/libs/pcp-types-lib/src/test/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/mission/ModeResponseDTOJacksonTest.kt b/libs/pcp-types-lib/src/test/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/mission/ModeResponseDTOJacksonTest.kt index 7fd1aad..7a0f836 100644 --- a/libs/pcp-types-lib/src/test/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/mission/ModeResponseDTOJacksonTest.kt +++ b/libs/pcp-types-lib/src/test/kotlin/space/nstart/pcp/pcp_types_lib/dto/satellite/mission/ModeResponseDTOJacksonTest.kt @@ -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 = 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 = mapper.readValue(json) + assertEquals(12000.0, roundTrip[0].volumeMb) + assertEquals(750.0, roundTrip[1].volumeMb) + assertTrue(roundTrip[0] is SurveyModeResponseDTO) + } } diff --git a/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/entity/SatelliteModeEntity.kt b/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/entity/SatelliteModeEntity.kt index 1c519d5..a37b4d4 100644 --- a/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/entity/SatelliteModeEntity.kt +++ b/services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/entity/SatelliteModeEntity.kt @@ -12,8 +12,11 @@ import jakarta.persistence.Id import jakarta.persistence.OneToMany import jakarta.persistence.JoinColumn import jakarta.persistence.ManyToOne +import jakarta.persistence.PrePersist +import jakarta.persistence.PreUpdate import jakarta.persistence.Table import jakarta.persistence.Index +import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.ModeDataRates import java.time.LocalDateTime @Entity @@ -92,4 +95,14 @@ class SatelliteModeEntity( fetch = FetchType.LAZY ) var cells: MutableList = mutableListOf() -) +) { + /** Объём маршрута, Мбайт. Денормализация: считается из duration при построении (satellite_modes — только SURVEY). */ + @Column(name = "volume_mb", nullable = false) + var volumeMb: Double = ModeDataRates.surveyVolumeMb(duration) + + @PrePersist + @PreUpdate + fun computeVolumeMb() { + volumeMb = ModeDataRates.surveyVolumeMb(duration) + } +} diff --git a/services/pcp-complex-mission-service/src/main/resources/db/migration/V9__satellite_mode_volume.sql b/services/pcp-complex-mission-service/src/main/resources/db/migration/V9__satellite_mode_volume.sql new file mode 100644 index 0000000..54516ec --- /dev/null +++ b/services/pcp-complex-mission-service/src/main/resources/db/migration/V9__satellite_mode_volume.sql @@ -0,0 +1,6 @@ +-- Объём маршрута съёмки как атрибут, Мбайт. Считается при построении из длительности +-- (см. ModeDataRates в pcp-types-lib): накопление 120 Мбайт/с. satellite_modes — только SURVEY. +ALTER TABLE satellite_modes ADD COLUMN volume_mb DOUBLE PRECISION NOT NULL DEFAULT 0; + +-- Бэкфилл существующих маршрутов из длительности. +UPDATE satellite_modes SET volume_mb = duration * 120; diff --git a/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/entity/DropModeEntity.kt b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/entity/DropModeEntity.kt index 0b065db..0cc04b3 100644 --- a/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/entity/DropModeEntity.kt +++ b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/entity/DropModeEntity.kt @@ -3,7 +3,10 @@ package space.nstart.pcp.pcp_mission_planing_service.entity import jakarta.persistence.Column import jakarta.persistence.DiscriminatorValue import jakarta.persistence.Entity +import jakarta.persistence.PrePersist +import jakarta.persistence.PreUpdate import jakarta.persistence.Table +import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.ModeDataRates import java.time.LocalDateTime @Entity @@ -20,4 +23,14 @@ class DropModeEntity( @Column(name = "duration", nullable = false) var duration: Double = 0.0 -) : ModeEntity(id, plan, timeStart, revolution) +) : ModeEntity(id, plan, timeStart, revolution) { + /** Объём сброса, Мбайт. Денормализация: считается из duration (канал 1200 Мбит/с ÷ 8) при сохранении. */ + @Column(name = "volume_mb", nullable = false) + var volumeMb: Double = ModeDataRates.dropVolumeMb(duration) + + @PrePersist + @PreUpdate + fun computeVolumeMb() { + volumeMb = ModeDataRates.dropVolumeMb(duration) + } +} diff --git a/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/entity/SurveyModeEntity.kt b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/entity/SurveyModeEntity.kt index cdeaacc..ce6d8f6 100644 --- a/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/entity/SurveyModeEntity.kt +++ b/services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/entity/SurveyModeEntity.kt @@ -5,8 +5,11 @@ import jakarta.persistence.DiscriminatorValue import jakarta.persistence.Entity import jakarta.persistence.EnumType import jakarta.persistence.Enumerated +import jakarta.persistence.PrePersist +import jakarta.persistence.PreUpdate import jakarta.persistence.Table import space.nstart.pcp.pcp_types_lib.dto.routes.SurveyModeStatus +import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.ModeDataRates import java.time.LocalDateTime @Entity @@ -46,4 +49,14 @@ class SurveyModeEntity( @Enumerated(EnumType.STRING) @Column(name = "source", nullable = false, length = 20) var source: SurveyModeSource = SurveyModeSource.SLOT -) : ModeEntity(id, plan, timeStart, revolution) +) : ModeEntity(id, plan, timeStart, revolution) { + /** Объём маршрута съёмки, Мбайт. Денормализация: считается из duration при построении/сохранении. */ + @Column(name = "volume_mb", nullable = false) + var volumeMb: Double = ModeDataRates.surveyVolumeMb(duration) + + @PrePersist + @PreUpdate + fun computeVolumeMb() { + volumeMb = ModeDataRates.surveyVolumeMb(duration) + } +} diff --git a/services/pcp-mission-planing-service/src/main/resources/db/migration/V10__mode_volume.sql b/services/pcp-mission-planing-service/src/main/resources/db/migration/V10__mode_volume.sql new file mode 100644 index 0000000..a78fede --- /dev/null +++ b/services/pcp-mission-planing-service/src/main/resources/db/migration/V10__mode_volume.sql @@ -0,0 +1,8 @@ +-- Объём данных как атрибут маршрута, Мбайт. Считается при построении из длительности +-- (см. ModeDataRates в pcp-types-lib): съёмка 120 Мбайт/с; сброс — канал 1200 Мбит/с ÷ 8 = 150 Мбайт/с. +ALTER TABLE survey_modes ADD COLUMN volume_mb DOUBLE PRECISION NOT NULL DEFAULT 0; +ALTER TABLE drop_modes ADD COLUMN volume_mb DOUBLE PRECISION NOT NULL DEFAULT 0; + +-- Бэкфилл существующих маршрутов из длительности. +UPDATE survey_modes SET volume_mb = duration * 120; +UPDATE drop_modes SET volume_mb = duration * 1200.0 / 8; diff --git a/services/pcp-tgu-ops-ui/src/api/complexPlanApi.ts b/services/pcp-tgu-ops-ui/src/api/complexPlanApi.ts index b7dc1a9..2f379b6 100644 --- a/services/pcp-tgu-ops-ui/src/api/complexPlanApi.ts +++ b/services/pcp-tgu-ops-ui/src/api/complexPlanApi.ts @@ -99,6 +99,8 @@ export type ComplexPlanMode = { cellNum: number; bookedSlotIds: number[]; snapshotId: number | null; + /** Объём режима, МБ (скорость накопления/сброса × длительность). Считает бэк (pcp-types-lib). */ + volumeMb?: number; }; /** diff --git a/services/pcp-tgu-ops-ui/src/api/missionPlanningApi.ts b/services/pcp-tgu-ops-ui/src/api/missionPlanningApi.ts index acd72bc..368632c 100644 --- a/services/pcp-tgu-ops-ui/src/api/missionPlanningApi.ts +++ b/services/pcp-tgu-ops-ui/src/api/missionPlanningApi.ts @@ -46,6 +46,8 @@ export type SurveyRoute = { longitude: number; /** Контур полосы съёмки, WKT POLYGON (lon lat, ...). */ contourWkt: string; + /** Объём маршрута, МБ (скорость накопления × длительность). Считает бэк (pcp-types-lib). */ + volumeMb?: number; /** Идентификатор сохранённого режима съёмки в БД (есть только после сохранения). */ surveyModeId?: number; }; @@ -96,6 +98,8 @@ export type DropMode = { station: string | null; duration: number; surveys: number[]; + /** Объём сброса, Мбайт (канал 1200 Мбит/с ÷ 8 × длительность). Считает бэк (pcp-types-lib). */ + volumeMb?: number; }; /** diff --git a/services/pcp-tgu-ops-ui/src/features/tgu-complex-plan/TguComplexPlanTab.tsx b/services/pcp-tgu-ops-ui/src/features/tgu-complex-plan/TguComplexPlanTab.tsx index 095d1a0..0183bba 100644 --- a/services/pcp-tgu-ops-ui/src/features/tgu-complex-plan/TguComplexPlanTab.tsx +++ b/services/pcp-tgu-ops-ui/src/features/tgu-complex-plan/TguComplexPlanTab.tsx @@ -18,6 +18,7 @@ import type { Tgu2DMapLayersState } from "../tgu-map-2d/model/mapLayerTypes"; import type { MapPolygon, Tgu2DMapScene } from "../tgu-map-2d/model/mapTypes"; import { Tgu2DMapView } from "../tgu-map-2d/Tgu2DMapView"; import { formatLocalDateTime, parseLocal, toLocalInputValue, toLocalIso } from "../../utils/localTime"; +import { formatVolume } from "../../utils/dataVolume"; type TguComplexPlanTabProps = { range: TimelineRange; @@ -203,6 +204,8 @@ export function TguComplexPlanTab({ range, platforms }: TguComplexPlanTabProps) const displayRun = selectedRun; const contoursCount = modePolygons.length; + // Суммарный объём режимов run’а (МБ) — съёмка по скорости накопления, сброс по скорости канала. + const totalVolumeMb = modes.reduce((sum, mode) => sum + (mode.volumeMb ?? 0), 0); return (
@@ -253,7 +256,7 @@ export function TguComplexPlanTab({ range, platforms }: TguComplexPlanTabProps)
{displayRun && (
- run #{displayRun.runId} · режимов {displayRun.calculatedModesCount} · охват {formatPercent(displayRun.coveredAreaPercent)} + run #{displayRun.runId} · режимов {displayRun.calculatedModesCount} · охват {formatPercent(displayRun.coveredAreaPercent)} · объём {formatVolume(totalVolumeMb)} {displayRun.snapshotIds.length > 0 ? ` · snapshot ${displayRun.snapshotIds.join(", ")}` : ""} · на карте {contoursCount} контуров {showRequests ? ` · заявок ${requestItems.length}` : ""}
diff --git a/services/pcp-tgu-ops-ui/src/features/tgu-editor/EditorPanels.tsx b/services/pcp-tgu-ops-ui/src/features/tgu-editor/EditorPanels.tsx index abf4ed0..e0794dd 100644 --- a/services/pcp-tgu-ops-ui/src/features/tgu-editor/EditorPanels.tsx +++ b/services/pcp-tgu-ops-ui/src/features/tgu-editor/EditorPanels.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from "react"; -import { formatDate, formatDateTime, formatDateTimeSeconds, formatDuration, formatTime, kindColor, kindLabel } from "./editorPresentation"; +import { formatDate, formatDateTime, formatDateTimeSeconds, formatDuration, formatTime, formatVolume, kindColor, kindLabel } from "./editorPresentation"; import { statusStyle } from "../tgu-planning/tguStatus"; import { formatLocalDateTime, parseLocal, toLocalIso } from "../../utils/localTime"; import { Time24Field } from "../../components/timeFields"; @@ -172,6 +172,8 @@ export function WorkInspector({
{formatDateTimeSeconds(work.endMs)}
Длительность
{formatDuration(work.endMs - work.startMs)}
+
Объём
+
{formatVolume(work.volumeMb)}
@@ -427,7 +429,7 @@ export function ShootPanel({
{routeError}
) : route ? (
- Маршрут построен · виток {route.revolution} · крен {route.roll.toFixed(1)}° · захват {route.captureAngle.toFixed(1)}° + Маршрут построен · виток {route.revolution} · крен {route.roll.toFixed(1)}° · захват {route.captureAngle.toFixed(1)}° · объём {formatVolume(route.volumeMb)}
центр {route.lat.toFixed(2)}, {route.longitude.toFixed(2)} · контур показан на карте
diff --git a/services/pcp-tgu-ops-ui/src/features/tgu-editor/TguEditorTab.tsx b/services/pcp-tgu-ops-ui/src/features/tgu-editor/TguEditorTab.tsx index de9a1b2..f505ca2 100644 --- a/services/pcp-tgu-ops-ui/src/features/tgu-editor/TguEditorTab.tsx +++ b/services/pcp-tgu-ops-ui/src/features/tgu-editor/TguEditorTab.tsx @@ -741,7 +741,8 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange, rollDeg: savedRoute?.roll ?? routeRoll, captureAngleDeg: savedRoute?.captureAngle ?? DEFAULT_CAPTURE_ANGLE_DEG, centerLat: savedRoute?.lat, - centerLon: savedRoute?.longitude + centerLon: savedRoute?.longitude, + volumeMb: savedRoute?.volumeMb }) ); setEditingWorkId(undefined); @@ -788,6 +789,7 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange, setDropSaving(true); setDropError(undefined); let savedDropId: number | undefined; + let savedDropVolumeMb: number | undefined; try { // Ленивое создание черновика на первую правку; адресуем сброс в DRAFT, а не в живой план. const draftId = await ensureDraft(); @@ -806,6 +808,7 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange, planEnd: toLocalIso(selectedPlan.endMs) }); savedDropId = savedDrop.id; + savedDropVolumeMb = savedDrop.volumeMb; // Перечитываем включения DRAFT (источник истины) и ревалидируем. void refreshDraftModes(draftId); } catch (error) { @@ -824,7 +827,8 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange, endMs: zoneStopMsVal, label: `Сброс · ${stationNm}`, station: { id: String(selectedZone.stationId), name: stationNm }, - planModeId: savedDropId + planModeId: savedDropId, + volumeMb: savedDropVolumeMb }) ); setEditingWorkId(undefined); diff --git a/services/pcp-tgu-ops-ui/src/features/tgu-editor/editorApi.ts b/services/pcp-tgu-ops-ui/src/features/tgu-editor/editorApi.ts index 6a1cb94..ceab45e 100644 --- a/services/pcp-tgu-ops-ui/src/features/tgu-editor/editorApi.ts +++ b/services/pcp-tgu-ops-ui/src/features/tgu-editor/editorApi.ts @@ -10,6 +10,8 @@ export type PlanSurveyMode = { duration: number; contourWkt: string | null; roll: number; + /** Объём маршрута, МБ (скорость накопления × длительность). Считает бэк (pcp-types-lib). */ + volumeMb?: number; }; export type PlanDropMode = { @@ -21,6 +23,8 @@ export type PlanDropMode = { station: string | null; duration: number; surveys: number[]; + /** Объём сброса, Мбайт (канал 1200 Мбит/с ÷ 8 × длительность). Считает бэк (pcp-types-lib). */ + volumeMb?: number; }; export type PlanMode = PlanSurveyMode | PlanDropMode; diff --git a/services/pcp-tgu-ops-ui/src/features/tgu-editor/editorDraft.ts b/services/pcp-tgu-ops-ui/src/features/tgu-editor/editorDraft.ts index 280cc32..1665d11 100644 --- a/services/pcp-tgu-ops-ui/src/features/tgu-editor/editorDraft.ts +++ b/services/pcp-tgu-ops-ui/src/features/tgu-editor/editorDraft.ts @@ -29,6 +29,8 @@ export type AddShootInput = { captureAngleDeg?: number; centerLat?: number; centerLon?: number; + /** Объём маршрута, МБ (из ответа бэка). */ + volumeMb?: number; }; export type AddDownlinkInput = { @@ -39,6 +41,8 @@ export type AddDownlinkInput = { station: EditorGroundStation; /** id сохранённого режима сброса в БД черновика (если сброс персистнут). */ planModeId?: number; + /** Объём сброса, МБ (из ответа бэка). */ + volumeMb?: number; }; export function seedDraft(plan?: TguPlanUi, spacecraftId?: string): Draft { @@ -80,6 +84,7 @@ export function planModesToWorks(modes: PlanMode[]): EditorWork[] { rollDeg: mode.roll, centerLat: mode.lat, centerLon: mode.longitude, + volumeMb: mode.volumeMb, // Центр контура — он же опорная точка для проверки видимости. targetLat: mode.lat, targetLon: mode.longitude @@ -96,7 +101,8 @@ export function planModesToWorks(modes: PlanMode[]): EditorWork[] { planModeId: mode.id, revolution: mode.revolution, stationId: mode.station ?? undefined, - surveyIds: mode.surveys + surveyIds: mode.surveys, + volumeMb: mode.volumeMb }]; }); } @@ -160,6 +166,7 @@ export function addShoot(draft: Draft, input: AddShootInput): Draft { captureAngleDeg: input.captureAngleDeg, centerLat: input.centerLat, centerLon: input.centerLon, + volumeMb: input.volumeMb, origin: "added", isNew: true }; @@ -176,6 +183,7 @@ export function addDownlink(draft: Draft, input: AddDownlinkInput): Draft { endMs: input.endMs, stationId: input.station.id, planModeId: input.planModeId, + volumeMb: input.volumeMb, origin: "added", isNew: true }; diff --git a/services/pcp-tgu-ops-ui/src/features/tgu-editor/editorPresentation.ts b/services/pcp-tgu-ops-ui/src/features/tgu-editor/editorPresentation.ts index 99b3765..629e22d 100644 --- a/services/pcp-tgu-ops-ui/src/features/tgu-editor/editorPresentation.ts +++ b/services/pcp-tgu-ops-ui/src/features/tgu-editor/editorPresentation.ts @@ -1,5 +1,8 @@ import type { EditorWorkKind } from "./model/editorTypes"; +// Объём данных — общий форматтер (используется и в комплексном плане). +export { formatVolume } from "../../utils/dataVolume"; + export const EDITOR_MONTHS = ["янв", "фев", "мар", "апр", "май", "июн", "июл", "авг", "сен", "окт", "ноя", "дек"]; export const DEFAULT_EDITOR_MODES = [ diff --git a/services/pcp-tgu-ops-ui/src/features/tgu-editor/model/editorTypes.ts b/services/pcp-tgu-ops-ui/src/features/tgu-editor/model/editorTypes.ts index 2a69a9d..59fc57c 100644 --- a/services/pcp-tgu-ops-ui/src/features/tgu-editor/model/editorTypes.ts +++ b/services/pcp-tgu-ops-ui/src/features/tgu-editor/model/editorTypes.ts @@ -12,6 +12,8 @@ export type EditorWorkBase = { endMs: number; origin: EditorWorkOrigin; isNew?: boolean; + /** Объём включения, МБ (считает бэк: скорость × длительность). Есть у включений плана и сохранённых правок. */ + volumeMb?: number; }; export type EditorShootWork = EditorWorkBase & { diff --git a/services/pcp-tgu-ops-ui/src/utils/dataVolume.ts b/services/pcp-tgu-ops-ui/src/utils/dataVolume.ts new file mode 100644 index 0000000..dbd03be --- /dev/null +++ b/services/pcp-tgu-ops-ui/src/utils/dataVolume.ts @@ -0,0 +1,16 @@ +/** + * Форматирование объёма данных режима (съёмка/сброс). Бэк отдаёт объём в мегабайтах + * (см. ModeDataRates в pcp-types-lib: накопление съёмки 120 Мбайт/с, сброс — канал 1200 Мбит/с ÷ 8). + */ + +/** До 1000 — в МБ, дальше — в ГБ (1 ГБ = 1000 МБ). Тут МБ = мегабайт. «—», если значения нет. */ +export function formatVolume(mb?: number | null): string { + if (mb == null || !Number.isFinite(mb) || mb <= 0) { + return "—"; + } + if (mb >= 1000) { + const gb = mb / 1000; + return `${gb >= 100 ? Math.round(gb) : gb.toFixed(1)} ГБ`; + } + return `${Math.round(mb)} МБ`; +}