2499aebf2d
Объём режима = скорость × длительность, Мбайт: съёмка 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 показывает объём в редакторе (инспектор, построенный маршрут) и в сводке комплексного плана.
114 lines
4.3 KiB
TypeScript
114 lines
4.3 KiB
TypeScript
import type { EditorWorkKind } from "./model/editorTypes";
|
||
|
||
// Объём данных — общий форматтер (используется и в комплексном плане).
|
||
export { formatVolume } from "../../utils/dataVolume";
|
||
|
||
export const EDITOR_MONTHS = ["янв", "фев", "мар", "апр", "май", "июн", "июл", "авг", "сен", "окт", "ноя", "дек"];
|
||
|
||
export const DEFAULT_EDITOR_MODES = [
|
||
{ id: "pan", label: "Панхром." },
|
||
{ id: "ms", label: "Мультиспектр." },
|
||
{ id: "stereo", label: "Стерео" },
|
||
{ id: "scan", label: "Скан" }
|
||
];
|
||
|
||
export function kindLabel(kind: EditorWorkKind): string {
|
||
switch (kind) {
|
||
case "shoot":
|
||
return "Съёмка";
|
||
case "downlink":
|
||
return "Сброс";
|
||
}
|
||
}
|
||
|
||
export function kindColor(kind: EditorWorkKind): string {
|
||
return kind === "shoot" ? "var(--t-optical)" : "var(--accent)";
|
||
}
|
||
|
||
export function formatTime(ms: number): string {
|
||
const date = new Date(ms);
|
||
return `${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||
}
|
||
|
||
/**
|
||
* Подпись деления оси с учётом шага: при шаге меньше минуты показываем секунды
|
||
* (ЧЧ:ММ:СС), иначе — ЧЧ:ММ. Так при сильном зуме видно более мелкий масштаб времени.
|
||
*/
|
||
export function formatTick(ms: number, stepMs: number): string {
|
||
const date = new Date(ms);
|
||
if (stepMs < 60_000) {
|
||
return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
||
}
|
||
return `${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||
}
|
||
|
||
export function formatDate(ms: number): string {
|
||
const date = new Date(ms);
|
||
return `${pad(date.getDate())} ${EDITOR_MONTHS[date.getMonth()]}`;
|
||
}
|
||
|
||
export function formatDateTime(ms: number): string {
|
||
return `${formatDate(ms)} ${formatTime(ms)}`;
|
||
}
|
||
|
||
/** Время с секундами и одной десятой долей: ЧЧ:ММ:СС.с (локальное). */
|
||
export function formatTimeSeconds(ms: number): string {
|
||
const date = new Date(ms);
|
||
const tenths = Math.floor((((ms % 1000) + 1000) % 1000) / 100);
|
||
return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${tenths}`;
|
||
}
|
||
|
||
/** Дата и время с десятыми долями секунды: «дд мес ЧЧ:ММ:СС.с» (локальное). */
|
||
export function formatDateTimeSeconds(ms: number): string {
|
||
return `${formatDate(ms)} ${formatTimeSeconds(ms)}`;
|
||
}
|
||
|
||
export function formatDuration(ms: number): string {
|
||
const totalSeconds = Math.max(0, Math.round(ms / 1000));
|
||
if (totalSeconds < 60) {
|
||
return `${totalSeconds} с`;
|
||
}
|
||
if (totalSeconds < 3600) {
|
||
const minutes = Math.floor(totalSeconds / 60);
|
||
const seconds = totalSeconds % 60;
|
||
return seconds > 0 ? `${minutes} мин ${seconds} с` : `${minutes} мин`;
|
||
}
|
||
|
||
const hours = ms / 3_600_000;
|
||
if (hours < 24) {
|
||
return `${Number.isInteger(hours) ? hours : hours.toFixed(1)} ч`;
|
||
}
|
||
|
||
const days = Math.floor(hours / 24);
|
||
const restHours = Math.round(hours - days * 24);
|
||
return restHours > 0 ? `${days} сут ${restHours} ч` : `${days} сут`;
|
||
}
|
||
|
||
/** Деления оси времени: {@link ticks} — отметки, {@link step} — выбранный шаг (мс). */
|
||
export type TimelineTicks = { ticks: number[]; step: number };
|
||
|
||
export function niceTicks(fromMs: number, toMs: number, pxPerMs: number, minPx: number): TimelineTicks {
|
||
const minuteMs = 60_000;
|
||
const hourMs = 3_600_000;
|
||
const dayMs = 24 * hourMs;
|
||
// От секунд до недели — чем сильнее зум, тем мельче шаг, который влезает по minPx.
|
||
const steps = [
|
||
1_000, 2_000, 5_000, 10_000, 15_000, 30_000,
|
||
minuteMs, 2 * minuteMs, 5 * minuteMs, 10 * minuteMs, 15 * minuteMs, 30 * minuteMs,
|
||
hourMs, 2 * hourMs, 3 * hourMs, 6 * hourMs, 12 * hourMs, dayMs, 2 * dayMs, 7 * dayMs
|
||
];
|
||
const step = steps.find((item) => item * pxPerMs >= minPx) ?? steps[steps.length - 1];
|
||
const ticks: number[] = [];
|
||
const firstTick = Math.ceil(fromMs / step) * step;
|
||
|
||
for (let timeMs = firstTick; timeMs <= toMs; timeMs += step) {
|
||
ticks.push(timeMs);
|
||
}
|
||
|
||
return { ticks, step };
|
||
}
|
||
|
||
function pad(value: number): string {
|
||
return String(value).padStart(2, "0");
|
||
}
|