routes: объём маршрута (volumeMb) — расчёт, хранение в БД и отображение
Объём режима = скорость × длительность, Мбайт: съёмка 120 Мбайт/с (12 ГБ / 100 с), сброс — канал 1200 Мбит/с ÷ 8. Единый источник — ModeDataRates в pcp-types-lib. Хранится колонкой volume_mb (Flyway V10 mission-planing, V9 complex-mission, бэкфилл из duration), заполняется на всех путях построения через @PrePersist/@PreUpdate. DTO отдают вычисляемое read-only поле (та же формула). Фронт pcp-tgu-ops-ui показывает объём в редакторе (инспектор, построенный маршрут) и в сводке комплексного плана.
This commit is contained in:
+14
-1
@@ -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<SatelliteModeCellEntity> = 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)
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -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;
|
||||
+14
-1
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+14
-1
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+8
@@ -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;
|
||||
@@ -99,6 +99,8 @@ export type ComplexPlanMode = {
|
||||
cellNum: number;
|
||||
bookedSlotIds: number[];
|
||||
snapshotId: number | null;
|
||||
/** Объём режима, МБ (скорость накопления/сброса × длительность). Считает бэк (pcp-types-lib). */
|
||||
volumeMb?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 (
|
||||
<main className="tgu-map-workspace">
|
||||
@@ -253,7 +256,7 @@ export function TguComplexPlanTab({ range, platforms }: TguComplexPlanTabProps)
|
||||
<div className="tgu-map-toolbar__spacer" style={{ flex: 1 }} />
|
||||
{displayRun && (
|
||||
<div className="tgu-map-toolbar__subtitle">
|
||||
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}` : ""}
|
||||
</div>
|
||||
|
||||
@@ -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({
|
||||
<dd className="mono">{formatDateTimeSeconds(work.endMs)}</dd>
|
||||
<dt>Длительность</dt>
|
||||
<dd className="mono">{formatDuration(work.endMs - work.startMs)}</dd>
|
||||
<dt>Объём</dt>
|
||||
<dd className="mono">{formatVolume(work.volumeMb)}</dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
@@ -427,7 +429,7 @@ export function ShootPanel({
|
||||
<div className="tgu-alert tgu-alert--danger tgu-alert--compact">{routeError}</div>
|
||||
) : route ? (
|
||||
<div className="tgu-editor-panel__hint">
|
||||
Маршрут построен · виток {route.revolution} · крен {route.roll.toFixed(1)}° · захват {route.captureAngle.toFixed(1)}°
|
||||
Маршрут построен · виток {route.revolution} · крен {route.roll.toFixed(1)}° · захват {route.captureAngle.toFixed(1)}° · объём {formatVolume(route.volumeMb)}
|
||||
<br />
|
||||
центр {route.lat.toFixed(2)}, {route.longitude.toFixed(2)} · контур показан на карте
|
||||
</div>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { EditorWorkKind } from "./model/editorTypes";
|
||||
|
||||
// Объём данных — общий форматтер (используется и в комплексном плане).
|
||||
export { formatVolume } from "../../utils/dataVolume";
|
||||
|
||||
export const EDITOR_MONTHS = ["янв", "фев", "мар", "апр", "май", "июн", "июл", "авг", "сен", "окт", "ноя", "дек"];
|
||||
|
||||
export const DEFAULT_EDITOR_MODES = [
|
||||
|
||||
@@ -12,6 +12,8 @@ export type EditorWorkBase = {
|
||||
endMs: number;
|
||||
origin: EditorWorkOrigin;
|
||||
isNew?: boolean;
|
||||
/** Объём включения, МБ (считает бэк: скорость × длительность). Есть у включений плана и сохранённых правок. */
|
||||
volumeMb?: number;
|
||||
};
|
||||
|
||||
export type EditorShootWork = EditorWorkBase & {
|
||||
|
||||
@@ -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)} МБ`;
|
||||
}
|
||||
Reference in New Issue
Block a user