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:
Дмитрий Соловьев
2026-06-06 18:34:29 +03:00
parent 316e0ee552
commit 2499aebf2d
20 changed files with 190 additions and 13 deletions
@@ -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)} МБ`;
}