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 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) {
|
||||
|
||||
@@ -17,3 +17,40 @@ export async function saveEditedPlan(_request: SaveEditedPlanRequest): Promise<S
|
||||
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;
|
||||
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<HTMLDivElement | null>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(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 (
|
||||
<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" />
|
||||
<line x1="-10" x2="10" y1="0" y2="0" />
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
{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>
|
||||
{tooltip && (
|
||||
<div className="tgu-2d-map__tooltip" style={{ left: tooltip.x, top: tooltip.y }}>
|
||||
|
||||
Reference in New Issue
Block a user