From 637a5e8848b0bb8585d68fc202f73605819a27c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9=20=D0=A1=D0=BE?= =?UTF-8?q?=D0=BB=D0=BE=D0=B2=D1=8C=D0=B5=D0=B2?= Date: Sun, 31 May 2026 09:18:53 +0300 Subject: [PATCH] feat(tgu-ops-ui): display plan works and new shoots on editor map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit На вкладке Редактор 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 --- .../src/features/tgu-editor/TguEditorTab.tsx | 60 +++++++++++++++++-- .../src/features/tgu-editor/editorApi.ts | 37 ++++++++++++ .../src/features/tgu-map-2d/Tgu2DMapView.tsx | 34 +++++++++-- .../pcp-tgu-ops-ui/src/styles/tgu-editor.css | 20 +++++++ 4 files changed, 140 insertions(+), 11 deletions(-) diff --git a/services/pcp-tgu-ops-ui/src/features/tgu-editor/TguEditorTab.tsx b/services/pcp-tgu-ops-ui/src/features/tgu-editor/TguEditorTab.tsx index 7ae397e..0b6cd55 100644 --- a/services/pcp-tgu-ops-ui/src/features/tgu-editor/TguEditorTab.tsx +++ b/services/pcp-tgu-ops-ui/src/features/tgu-editor/TguEditorTab.tsx @@ -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(); const [contextSpacecraftIds, setContextSpacecraftIds] = useState>(new Set()); const [requestItems, setRequestItems] = useState([]); + const [planModes, setPlanModes] = useState([]); 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) { diff --git a/services/pcp-tgu-ops-ui/src/features/tgu-editor/editorApi.ts b/services/pcp-tgu-ops-ui/src/features/tgu-editor/editorApi.ts index 670fadb..bf184f5 100644 --- a/services/pcp-tgu-ops-ui/src/features/tgu-editor/editorApi.ts +++ b/services/pcp-tgu-ops-ui/src/features/tgu-editor/editorApi.ts @@ -17,3 +17,40 @@ export async function saveEditedPlan(_request: SaveEditedPlanRequest): Promise { + const response = await fetch(`/api/current-plans/missions/${encodeURIComponent(planId)}/modes`, { + headers: { Accept: "application/json" } + }); + if (!response.ok) { + throw new Error(`Ошибка загрузки включений плана: ${response.status}`); + } + return response.json() as Promise; +} diff --git a/services/pcp-tgu-ops-ui/src/features/tgu-map-2d/Tgu2DMapView.tsx b/services/pcp-tgu-ops-ui/src/features/tgu-map-2d/Tgu2DMapView.tsx index 4cf3519..6515960 100644 --- a/services/pcp-tgu-ops-ui/src/features/tgu-map-2d/Tgu2DMapView.tsx +++ b/services/pcp-tgu-ops-ui/src/features/tgu-map-2d/Tgu2DMapView.tsx @@ -20,6 +20,9 @@ type Tgu2DMapViewProps = { pickMode?: boolean; onPickPoint?: (point: { lat: number; lon: number }) => void; targets?: Tgu2DMapTarget[]; + pickTarget?: Tgu2DMapTarget; + selectedTargetId?: string; + onTargetSelect?: (id: string) => void; }; export type Tgu2DMapTarget = { @@ -58,7 +61,10 @@ export function Tgu2DMapView({ onObjectSelect, pickMode = false, onPickPoint, - targets = [] + targets = [], + pickTarget, + selectedTargetId, + onTargetSelect }: Tgu2DMapViewProps) { const wrapperRef = useRef(null); const canvasRef = useRef(null); @@ -299,17 +305,35 @@ export function Tgu2DMapView({ className={polygon.id === selectedPolygon?.id ? "is-selected" : "is-hovered"} /> ))} - {targets.map((target) => { - const screen = projection.project(target); + {targets.map((t) => { + const screen = projection.project(t); + const isSelected = t.id === selectedTargetId; return ( - + { e.stopPropagation(); onTargetSelect?.(t.id); }} + style={{ cursor: "pointer" }} + > - {target.name} + {t.name} ); })} + {pickTarget && (() => { + const screen = projection.project(pickTarget); + return ( + + + + + {pickTarget.name} + + ); + })()} {tooltip && (
diff --git a/services/pcp-tgu-ops-ui/src/styles/tgu-editor.css b/services/pcp-tgu-ops-ui/src/styles/tgu-editor.css index fcd93b9..e70c928 100644 --- a/services/pcp-tgu-ops-ui/src/styles/tgu-editor.css +++ b/services/pcp-tgu-ops-ui/src/styles/tgu-editor.css @@ -291,6 +291,26 @@ stroke-width: 3px; } +.tgu-2d-map__target--confirmed circle { + fill: var(--t-optical-dim); + stroke: var(--t-optical); +} + +.tgu-2d-map__target--confirmed line { + stroke: var(--t-optical); +} + +.tgu-2d-map__target--confirmed.is-selected circle { + fill: rgba(104, 195, 189, 0.35); + stroke: var(--t-optical); + stroke-width: 2.5; + r: 8; +} + +.tgu-2d-map__target--confirmed.is-selected line { + stroke-width: 2; +} + .tgu-editor-timeline { min-width: 0; min-height: 0;