pcp-tgu-ops-ui: вкладка Редактор — план, включения, нижняя панель

- Слева выводим даты начала/конца плана и корректные счётчики
  съёмки/сброса (из plan modes, а не из заглушки-черновика).
- На таймлайне показываем реальные включения плана (read-only): убран
  фейковый «сброс на весь интервал»; вместо линии «сейчас» — маркеры
  начала и конца плана; клик по включению открывает его инфо справа.
- Включения съёмки и сброса разнесены на два ряда (верх/низ) и различаются
  цветом, слева — подписи рядов.
- Нижняя панель: активная дорожка резиновая — примыкает к низу и не
  пустует, при подъёме панели ряды расширяются; высота бара ограничена
  потолком и центрируется в ряду, чтобы узкие включения не вытягивались.
- Время в редакторе парсится/форматируется через utils/utcTime.
This commit is contained in:
Дмитрий Соловьев
2026-06-02 16:54:01 +03:00
parent fb0332a030
commit ac2220d063
7 changed files with 340 additions and 181 deletions
@@ -6,6 +6,7 @@ import type { RequestMapItem } from "../../model/requestTypes";
import { stationPasses, type OrbitPassInfo, type PassRange } from "../tgu-map-2d/geometry/passes";
import { fetchPointVisibility, fetchSatelliteRva, type PointVisibilityParam, type RvaZone } from "../../api/ballisticsApi";
import { buildSurveyRoute, saveDropRoute, saveSurveyRoute, type SurveyRoute, type SurveyRouteAnchor } from "../../api/missionPlanningApi";
import { parseUtc } from "../../utils/utcTime";
import { Tgu2DMapView } from "../tgu-map-2d/Tgu2DMapView";
import { parseWktPolygon } from "../tgu-map-2d/geometry/wktParser";
import { buildStationMarkers } from "../tgu-map-2d/Tgu2DMapLayers";
@@ -19,6 +20,7 @@ import {
deleteWork,
duplicateWork,
mutate,
planModesToWorks,
redo,
seedDraft,
undo,
@@ -40,6 +42,7 @@ import type {
EditorTimelineWindow,
EditorVisibilityTrack,
EditorVisibilityWindow,
EditorWork,
EditorWorkPatch,
TguEditorTabProps
} from "./model/editorTypes";
@@ -68,7 +71,9 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
const idCounterRef = useRef(0);
const targetCounterRef = useRef(0);
const centerRef = useRef<HTMLElement>(null);
const [mapRatio, setMapRatio] = useState(0.72);
// Доля высоты под карту: по умолчанию карта крупная, а нижняя панель компактно
// примыкает к низу. Подъём резайзера вверх уменьшает долю и расширяет панель.
const [mapRatio, setMapRatio] = useState(0.8);
const [history, setHistory] = useState<DraftHistory>(() => ({
draft: seedDraft(selectedPlan, selectedSpacecraftId),
past: [],
@@ -124,14 +129,17 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
.map(([revolution, pts], idx) => ({
index: idx + 1,
revolution,
startMs: Date.parse(pts[0].time),
endMs: Date.parse(pts[pts.length - 1].time),
startMs: parseUtc(pts[0].time),
endMs: parseUtc(pts[pts.length - 1].time),
}));
}, [flightLineData]);
const modes: EditorMode[] = DEFAULT_EDITOR_MODES;
// Существующие включения плана (read-only) + правки из черновика — то, что видно на таймлайне.
const planWorks = useMemo<EditorWork[]>(() => planModesToWorks(planModes), [planModes]);
const displayWorks = useMemo<EditorWork[]>(() => [...planWorks, ...draft.works], [planWorks, draft.works]);
const liveConflicts = useMemo(() => overlaps(draft.works), [draft.works]);
const conflicts = useMemo(() => mergeConflictMaps(liveConflicts, serviceConflicts), [liveConflicts, serviceConflicts]);
const selectedWork = draft.works.find((work) => work.id === selectedWorkId);
const selectedWork = displayWorks.find((work) => work.id === selectedWorkId);
const activeLabel = platformLabel(platforms, selectedSpacecraftId);
const [shootPasses, setShootPasses] = useState<EditorVisibilityWindow[]>([]);
const [shootPassesLoading, setShootPassesLoading] = useState(false);
@@ -187,6 +195,10 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
const dirty = history.past.length > 0 || draft.works.some((work) => work.isNew);
const addedCount = draft.works.filter((work) => work.isNew).length;
// Реальный состав плана берём из включений (plan modes), а не из черновика:
// черновик сидируется заглушкой, когда у плана нет встроенных works.
const planSurveyCount = useMemo(() => planModes.filter((mode) => mode.type === "SURVEY").length, [planModes]);
const planDownlinkCount = useMemo(() => planModes.filter((mode) => mode.type === "DROP").length, [planModes]);
useEffect(() => {
setHistory({ draft: seedDraft(selectedPlan, selectedSpacecraftId), past: [], future: [] });
@@ -559,7 +571,7 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
const el = centerRef.current;
if (!el) return;
const dy = ev.clientY - startY;
setMapRatio(Math.max(0.15, Math.min(0.80, startRatio + dy / el.clientHeight)));
setMapRatio(Math.max(0.15, Math.min(0.86, startRatio + dy / el.clientHeight)));
};
const onUp = () => {
@@ -618,7 +630,8 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
selectedSpacecraftId={selectedSpacecraftId}
selectedPlan={selectedPlan}
platforms={platforms}
draft={draft}
surveyCount={planSurveyCount}
downlinkCount={planDownlinkCount}
conflictCount={conflictCount(conflicts)}
contextSpacecraftIds={contextSpacecraftIds}
onToggleContext={(spacecraftId) => {
@@ -681,10 +694,11 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
</div>
<EditorTimeline
activeLabel={activeLabel}
draft={draft.works}
works={displayWorks}
selectedWorkId={selectedWorkId}
window={window}
nowMs={Date.now()}
planStartMs={selectedPlan?.startMs}
planEndMs={selectedPlan?.endMs}
conflicts={conflicts}
contextLanes={contextLanes}
visibilityTracks={visibilityTracks}
@@ -699,7 +713,6 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
setRightMode("none");
}
}}
onChangeWork={onChangeWork}
/>
</section>
@@ -711,6 +724,7 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
modes={modes}
stations={stations}
conflicts={conflicts[selectedWork.id] ?? []}
readOnly={selectedWork.origin === "plan"}
onUpdate={onChangeWork}
onDelete={(workId) => {
changeDraft((current) => deleteWork(current, workId));
@@ -810,7 +824,7 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
}
function visibilityWindowFromParam(param: PointVisibilityParam): EditorVisibilityWindow {
const timeMs = Date.parse(param.time);
const timeMs = parseUtc(param.time);
const half = SHOOT_WINDOW_MS / 2;
return {
startMs: timeMs - half,
@@ -851,8 +865,7 @@ function buildStations(selectedPlan: TguEditorTabProps["selectedPlan"]): EditorG
* зоны ЗРВ приходят без таймзоны, а времена съёмок — с offset; приводим к одной шкале.
*/
function localDateTimeMs(s: string): number {
const hasZone = /[zZ]$|[+-]\d{2}:?\d{2}$/.test(s);
return Date.parse(hasZone ? s : `${s}Z`);
return parseUtc(s);
}
function buildVisibilityTracks(