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:
@@ -6,7 +6,7 @@ import { Tgu2DMapView } from "../tgu-map-2d/Tgu2DMapView";
|
|||||||
import { parseWktPolygon } from "../tgu-map-2d/geometry/wktParser";
|
import { parseWktPolygon } from "../tgu-map-2d/geometry/wktParser";
|
||||||
import type { Tgu2DMapLayersState } from "../tgu-map-2d/model/mapLayerTypes";
|
import type { Tgu2DMapLayersState } from "../tgu-map-2d/model/mapLayerTypes";
|
||||||
import type { MapPolygon, Tgu2DMapScene } from "../tgu-map-2d/model/mapTypes";
|
import type { MapPolygon, Tgu2DMapScene } from "../tgu-map-2d/model/mapTypes";
|
||||||
import { saveEditedPlan } from "./editorApi";
|
import { fetchPlanModes, saveEditedPlan, type PlanMode } from "./editorApi";
|
||||||
import {
|
import {
|
||||||
addDownlink,
|
addDownlink,
|
||||||
addShoot,
|
addShoot,
|
||||||
@@ -28,6 +28,7 @@ import type {
|
|||||||
DraftHistory,
|
DraftHistory,
|
||||||
EditorGroundStation,
|
EditorGroundStation,
|
||||||
EditorMode,
|
EditorMode,
|
||||||
|
EditorShootWork,
|
||||||
EditorTarget,
|
EditorTarget,
|
||||||
EditorTimelineWindow,
|
EditorTimelineWindow,
|
||||||
EditorVisibilityTrack,
|
EditorVisibilityTrack,
|
||||||
@@ -42,7 +43,7 @@ const HOUR_MS = 60 * 60 * 1000;
|
|||||||
const EDITOR_MAP_LAYERS: Tgu2DMapLayersState = {
|
const EDITOR_MAP_LAYERS: Tgu2DMapLayersState = {
|
||||||
tracks: true,
|
tracks: true,
|
||||||
swath: false,
|
swath: false,
|
||||||
planWorks: false,
|
planWorks: true,
|
||||||
stations: true,
|
stations: true,
|
||||||
spacecraftMarkers: false,
|
spacecraftMarkers: false,
|
||||||
requests: true
|
requests: true
|
||||||
@@ -72,6 +73,7 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
|||||||
const [applyError, setApplyError] = useState<string>();
|
const [applyError, setApplyError] = useState<string>();
|
||||||
const [contextSpacecraftIds, setContextSpacecraftIds] = useState<Set<string>>(new Set());
|
const [contextSpacecraftIds, setContextSpacecraftIds] = useState<Set<string>>(new Set());
|
||||||
const [requestItems, setRequestItems] = useState<RequestMapItem[]>([]);
|
const [requestItems, setRequestItems] = useState<RequestMapItem[]>([]);
|
||||||
|
const [planModes, setPlanModes] = useState<PlanMode[]>([]);
|
||||||
const draft = history.draft;
|
const draft = history.draft;
|
||||||
const stations = useMemo(() => buildStations(selectedPlan), [selectedPlan]);
|
const stations = useMemo(() => buildStations(selectedPlan), [selectedPlan]);
|
||||||
const modes: EditorMode[] = DEFAULT_EDITOR_MODES;
|
const modes: EditorMode[] = DEFAULT_EDITOR_MODES;
|
||||||
@@ -92,8 +94,15 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
|||||||
[selectedSpacecraftId, selectedWork, shootPasses, stations, target, window]
|
[selectedSpacecraftId, selectedWork, shootPasses, stations, target, window]
|
||||||
);
|
);
|
||||||
const mapScene = useMemo(
|
const mapScene = useMemo(
|
||||||
() => buildEditorMapScene(selectedSpacecraftId, window, stations, requestItems),
|
() => buildEditorMapScene(selectedSpacecraftId, window, stations, requestItems, planModes),
|
||||||
[selectedSpacecraftId, stations, window, requestItems]
|
[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(
|
const contextLanes = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -118,6 +127,7 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
|||||||
setServiceConflicts({});
|
setServiceConflicts({});
|
||||||
setNotice(undefined);
|
setNotice(undefined);
|
||||||
setApplyError(undefined);
|
setApplyError(undefined);
|
||||||
|
setPlanModes([]);
|
||||||
}, [appliedRange, selectedPlan, selectedSpacecraftId]);
|
}, [appliedRange, selectedPlan, selectedSpacecraftId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -135,6 +145,18 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
|||||||
return () => controller.abort();
|
return () => controller.abort();
|
||||||
}, [window.fromMs, window.toMs]);
|
}, [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"]) => {
|
const changeDraft = (transform: (draft: DraftHistory["draft"]) => DraftHistory["draft"]) => {
|
||||||
setHistory((current) => mutate(current, transform));
|
setHistory((current) => mutate(current, transform));
|
||||||
setServiceConflicts({});
|
setServiceConflicts({});
|
||||||
@@ -330,7 +352,14 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
|||||||
layers={EDITOR_MAP_LAYERS}
|
layers={EDITOR_MAP_LAYERS}
|
||||||
redrawToken={0}
|
redrawToken={0}
|
||||||
pickMode={rightMode === "shoot"}
|
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) => {
|
onPickPoint={(point) => {
|
||||||
targetCounterRef.current += 1;
|
targetCounterRef.current += 1;
|
||||||
setTarget({
|
setTarget({
|
||||||
@@ -527,7 +556,8 @@ function buildEditorMapScene(
|
|||||||
spacecraftId: string | undefined,
|
spacecraftId: string | undefined,
|
||||||
window: EditorTimelineWindow,
|
window: EditorTimelineWindow,
|
||||||
stations: EditorGroundStation[],
|
stations: EditorGroundStation[],
|
||||||
requestItems: RequestMapItem[]
|
requestItems: RequestMapItem[],
|
||||||
|
planModes: PlanMode[]
|
||||||
): Tgu2DMapScene {
|
): Tgu2DMapScene {
|
||||||
const center = (window.fromMs + window.toMs) / 2;
|
const center = (window.fromMs + window.toMs) / 2;
|
||||||
const mapRange = { fromMs: center - MAP_TRACK_WINDOW_MS / 2, toMs: center + MAP_TRACK_WINDOW_MS / 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[] = [];
|
const polygons: MapPolygon[] = [];
|
||||||
let zIndex = 0;
|
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) {
|
for (const item of requestItems) {
|
||||||
const rings = parseWktPolygon(item.geometry);
|
const rings = parseWktPolygon(item.geometry);
|
||||||
for (const ring of rings) {
|
for (const ring of rings) {
|
||||||
|
|||||||
@@ -17,3 +17,40 @@ export async function saveEditedPlan(_request: SaveEditedPlanRequest): Promise<S
|
|||||||
message: "Сохранение правок плана во внешний сервис ещё не реализовано в backend."
|
message: "Сохранение правок плана во внешний сервис ещё не реализовано в backend."
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PlanSurveyMode = {
|
||||||
|
id: number;
|
||||||
|
planId: number;
|
||||||
|
timeStart: string | null;
|
||||||
|
revolution: number;
|
||||||
|
type: "SURVEY";
|
||||||
|
status: string;
|
||||||
|
lat: number;
|
||||||
|
longitude: number;
|
||||||
|
duration: number;
|
||||||
|
contourWkt: string | null;
|
||||||
|
roll: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PlanDropMode = {
|
||||||
|
id: number;
|
||||||
|
planId: number;
|
||||||
|
timeStart: string | null;
|
||||||
|
revolution: number;
|
||||||
|
type: "DROP";
|
||||||
|
station: string | null;
|
||||||
|
duration: number;
|
||||||
|
surveys: number[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PlanMode = PlanSurveyMode | PlanDropMode;
|
||||||
|
|
||||||
|
export async function fetchPlanModes(planId: string): Promise<PlanMode[]> {
|
||||||
|
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<PlanMode[]>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ type Tgu2DMapViewProps = {
|
|||||||
pickMode?: boolean;
|
pickMode?: boolean;
|
||||||
onPickPoint?: (point: { lat: number; lon: number }) => void;
|
onPickPoint?: (point: { lat: number; lon: number }) => void;
|
||||||
targets?: Tgu2DMapTarget[];
|
targets?: Tgu2DMapTarget[];
|
||||||
|
pickTarget?: Tgu2DMapTarget;
|
||||||
|
selectedTargetId?: string;
|
||||||
|
onTargetSelect?: (id: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Tgu2DMapTarget = {
|
export type Tgu2DMapTarget = {
|
||||||
@@ -58,7 +61,10 @@ export function Tgu2DMapView({
|
|||||||
onObjectSelect,
|
onObjectSelect,
|
||||||
pickMode = false,
|
pickMode = false,
|
||||||
onPickPoint,
|
onPickPoint,
|
||||||
targets = []
|
targets = [],
|
||||||
|
pickTarget,
|
||||||
|
selectedTargetId,
|
||||||
|
onTargetSelect
|
||||||
}: Tgu2DMapViewProps) {
|
}: Tgu2DMapViewProps) {
|
||||||
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
||||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||||
@@ -299,17 +305,35 @@ export function Tgu2DMapView({
|
|||||||
className={polygon.id === selectedPolygon?.id ? "is-selected" : "is-hovered"}
|
className={polygon.id === selectedPolygon?.id ? "is-selected" : "is-hovered"}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{targets.map((target) => {
|
{targets.map((t) => {
|
||||||
const screen = projection.project(target);
|
const screen = projection.project(t);
|
||||||
|
const isSelected = t.id === selectedTargetId;
|
||||||
return (
|
return (
|
||||||
<g className="tgu-2d-map__target" key={target.id} transform={`translate(${screen.x} ${screen.y})`}>
|
<g
|
||||||
|
className={`tgu-2d-map__target tgu-2d-map__target--confirmed${isSelected ? " is-selected" : ""}`}
|
||||||
|
key={t.id}
|
||||||
|
transform={`translate(${screen.x} ${screen.y})`}
|
||||||
|
onClick={(e) => { e.stopPropagation(); onTargetSelect?.(t.id); }}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
<circle r="6" />
|
<circle r="6" />
|
||||||
<line x1="-10" x2="10" y1="0" y2="0" />
|
<line x1="-10" x2="10" y1="0" y2="0" />
|
||||||
<line x1="0" x2="0" y1="-10" y2="10" />
|
<line x1="0" x2="0" y1="-10" y2="10" />
|
||||||
<text x="10" y="-8">{target.name}</text>
|
<text x="10" y="-8">{t.name}</text>
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{pickTarget && (() => {
|
||||||
|
const screen = projection.project(pickTarget);
|
||||||
|
return (
|
||||||
|
<g className="tgu-2d-map__target tgu-2d-map__target--pending" key="pick" transform={`translate(${screen.x} ${screen.y})`}>
|
||||||
|
<circle r="6" />
|
||||||
|
<line x1="-10" x2="10" y1="0" y2="0" />
|
||||||
|
<line x1="0" x2="0" y1="-10" y2="10" />
|
||||||
|
<text x="10" y="-8">{pickTarget.name}</text>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</svg>
|
</svg>
|
||||||
{tooltip && (
|
{tooltip && (
|
||||||
<div className="tgu-2d-map__tooltip" style={{ left: tooltip.x, top: tooltip.y }}>
|
<div className="tgu-2d-map__tooltip" style={{ left: tooltip.x, top: tooltip.y }}>
|
||||||
|
|||||||
@@ -291,6 +291,26 @@
|
|||||||
stroke-width: 3px;
|
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 {
|
.tgu-editor-timeline {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user