feat(tgu-ops-ui): display plan works and new shoots on editor map

На вкладке Редактор 2D-карта теперь показывает включения выбранного плана
и новые съёмки, добавленные в текущей сессии.

- editorApi: типы PlanSurveyMode/PlanDropMode/PlanMode и fetchPlanModes —
  вызывает GET /api/current-plans/missions/{planId}/modes на pcp-mission-planing-service,
  где planId TGU совпадает с missionId (выяснено по Camunda-процессу)
- TguEditorTab: загружает включения при выборе плана, строит MapPolygon[]
  из SURVEY-режимов через parseWktPolygon(contourWkt), включён слой planWorks
- Tgu2DMapView: разделены pending-маркер (оранжевый, выбор точки)
  и confirmed-маркеры (бирюзовые, подтверждённые съёмки из драфта);
  клик на маркер съёмки открывает WorkInspector

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Дмитрий Соловьев
2026-05-31 09:18:53 +03:00
parent 42708450bb
commit 637a5e8848
4 changed files with 140 additions and 11 deletions
@@ -6,7 +6,7 @@ import { Tgu2DMapView } from "../tgu-map-2d/Tgu2DMapView";
import { parseWktPolygon } from "../tgu-map-2d/geometry/wktParser";
import type { Tgu2DMapLayersState } from "../tgu-map-2d/model/mapLayerTypes";
import type { MapPolygon, Tgu2DMapScene } from "../tgu-map-2d/model/mapTypes";
import { saveEditedPlan } from "./editorApi";
import { fetchPlanModes, saveEditedPlan, type PlanMode } from "./editorApi";
import {
addDownlink,
addShoot,
@@ -28,6 +28,7 @@ import type {
DraftHistory,
EditorGroundStation,
EditorMode,
EditorShootWork,
EditorTarget,
EditorTimelineWindow,
EditorVisibilityTrack,
@@ -42,7 +43,7 @@ const HOUR_MS = 60 * 60 * 1000;
const EDITOR_MAP_LAYERS: Tgu2DMapLayersState = {
tracks: true,
swath: false,
planWorks: false,
planWorks: true,
stations: true,
spacecraftMarkers: false,
requests: true
@@ -72,6 +73,7 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
const [applyError, setApplyError] = useState<string>();
const [contextSpacecraftIds, setContextSpacecraftIds] = useState<Set<string>>(new Set());
const [requestItems, setRequestItems] = useState<RequestMapItem[]>([]);
const [planModes, setPlanModes] = useState<PlanMode[]>([]);
const draft = history.draft;
const stations = useMemo(() => buildStations(selectedPlan), [selectedPlan]);
const modes: EditorMode[] = DEFAULT_EDITOR_MODES;
@@ -92,8 +94,15 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
[selectedSpacecraftId, selectedWork, shootPasses, stations, target, window]
);
const mapScene = useMemo(
() => buildEditorMapScene(selectedSpacecraftId, window, stations, requestItems),
[selectedSpacecraftId, stations, window, requestItems]
() => buildEditorMapScene(selectedSpacecraftId, window, stations, requestItems, planModes),
[selectedSpacecraftId, stations, window, requestItems, planModes]
);
const shootTargets = useMemo(
() =>
draft.works
.filter((w): w is EditorShootWork => w.kind === "shoot" && w.targetLat !== undefined && w.targetLon !== undefined)
.map((w) => ({ id: w.id, name: w.label, lat: w.targetLat!, lon: w.targetLon! })),
[draft.works]
);
const contextLanes = useMemo(
() =>
@@ -118,6 +127,7 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
setServiceConflicts({});
setNotice(undefined);
setApplyError(undefined);
setPlanModes([]);
}, [appliedRange, selectedPlan, selectedSpacecraftId]);
useEffect(() => {
@@ -135,6 +145,18 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
return () => controller.abort();
}, [window.fromMs, window.toMs]);
useEffect(() => {
if (!selectedPlan) return;
const planId = selectedPlan.planId;
let cancelled = false;
void fetchPlanModes(planId).then((modes) => {
if (!cancelled) setPlanModes(modes);
}).catch(() => {
// silently ignore — plan modes are supplementary display data
});
return () => { cancelled = true; };
}, [selectedPlan]);
const changeDraft = (transform: (draft: DraftHistory["draft"]) => DraftHistory["draft"]) => {
setHistory((current) => mutate(current, transform));
setServiceConflicts({});
@@ -330,7 +352,14 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
layers={EDITOR_MAP_LAYERS}
redrawToken={0}
pickMode={rightMode === "shoot"}
targets={target ? [target] : []}
targets={shootTargets}
pickTarget={target ?? undefined}
selectedTargetId={selectedWork?.kind === "shoot" ? selectedWorkId : undefined}
onTargetSelect={(id) => {
setSelectedWorkId(id);
setRightMode("inspect");
setNotice(undefined);
}}
onPickPoint={(point) => {
targetCounterRef.current += 1;
setTarget({
@@ -527,7 +556,8 @@ function buildEditorMapScene(
spacecraftId: string | undefined,
window: EditorTimelineWindow,
stations: EditorGroundStation[],
requestItems: RequestMapItem[]
requestItems: RequestMapItem[],
planModes: PlanMode[]
): Tgu2DMapScene {
const center = (window.fromMs + window.toMs) / 2;
const mapRange = { fromMs: center - MAP_TRACK_WINDOW_MS / 2, toMs: center + MAP_TRACK_WINDOW_MS / 2 };
@@ -557,6 +587,24 @@ function buildEditorMapScene(
const polygons: MapPolygon[] = [];
let zIndex = 0;
for (const mode of planModes) {
if (mode.type !== "SURVEY" || !mode.contourWkt) continue;
const rings = parseWktPolygon(mode.contourWkt);
for (const ring of rings) {
if (ring.length < 3) continue;
polygons.push({
id: `planWork:${mode.id}:${zIndex}`,
name: `Съёмка витка ${mode.revolution}`,
kind: "planWork",
layer: "planWorks",
points: ring,
status: "PLANNED",
zIndex: zIndex++
});
}
}
for (const item of requestItems) {
const rings = parseWktPolygon(item.geometry);
for (const ring of rings) {