Редактор: ручное добавление маршрутов съёмки и сбросов
Режим «Сброс» теперь рабочий: загружает ЗРВ станций (ballisticsApi.fetchSatelliteRva), даёт оператору выбрать зону, затем показывает съёмки плана, заканчивающиеся строго до начала зоны, и сохраняет сброс в плане (missionPlanningApi.saveDropRoute → DropPanel). Старый фейковый DownlinkPanel на stationPasses убран из потока. Режим «Съёмка» сохраняет маршрут через saveSurveyRoute. Времена ЗРВ приходят как LocalDateTime без зоны и трактуются как UTC (helper localDateTimeMs), чтобы сравнивать с временами съёмок. В vite-прокси добавлены pcp-ballistics, pcp-mission и stations.
This commit is contained in:
@@ -1,13 +1,18 @@
|
||||
import { useEffect, useMemo, useRef, useState, useCallback, type MutableRefObject } from "react";
|
||||
import { fetchRequestsForMap } from "../../api/requestApi";
|
||||
import { fetchStations, type StationDto } from "../../api/stationsApi";
|
||||
import { fetchFlightLine, type FlightLinePoint } from "../../api/tguApi";
|
||||
import type { RequestMapItem } from "../../model/requestTypes";
|
||||
import { stationPasses, targetPasses, type OrbitPassInfo, type PassRange } from "../tgu-map-2d/geometry/passes";
|
||||
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 { Tgu2DMapView } from "../tgu-map-2d/Tgu2DMapView";
|
||||
import { parseWktPolygon } from "../tgu-map-2d/geometry/wktParser";
|
||||
import { buildStationMarkers } from "../tgu-map-2d/Tgu2DMapLayers";
|
||||
import type { Tgu2DMapLayersState } from "../tgu-map-2d/model/mapLayerTypes";
|
||||
import type { Tgu2DMapSelection } from "../tgu-map-2d/model/mapSelectionTypes";
|
||||
import type { MapLine, MapPolygon, Tgu2DMapScene } from "../tgu-map-2d/model/mapTypes";
|
||||
import { fetchPlanModes, saveEditedPlan, type PlanMode } from "./editorApi";
|
||||
import { fetchPlanModes, saveEditedPlan, type PlanMode, type PlanSurveyMode } from "./editorApi";
|
||||
import {
|
||||
addDownlink,
|
||||
addShoot,
|
||||
@@ -21,7 +26,7 @@ import {
|
||||
} from "./editorDraft";
|
||||
import { checkVisibility, conflictCount, mergeConflictMaps, overlaps, type EditorConflictMap } from "./editorConflicts";
|
||||
import { DEFAULT_EDITOR_MODES, formatDuration } from "./editorPresentation";
|
||||
import { DownlinkPanel, type DownlinkSelection, type DownlinkStationOption, ShootPanel, WorkInspector } from "./EditorPanels";
|
||||
import { DropPanel, type DropCandidateSurvey, MapInfoPanel, ShootPanel, WorkInspector } from "./EditorPanels";
|
||||
import { MapPassSelector } from "./MapPassSelector";
|
||||
import { EditorRail } from "./EditorRail";
|
||||
import { EditorTimeline } from "./EditorTimeline";
|
||||
@@ -39,9 +44,17 @@ import type {
|
||||
TguEditorTabProps
|
||||
} from "./model/editorTypes";
|
||||
|
||||
type EditorRightMode = "none" | "inspect" | "shoot" | "downlink";
|
||||
type EditorRightMode = "none" | "inspect" | "shoot" | "downlink" | "mapInfo";
|
||||
|
||||
const HOUR_MS = 60 * 60 * 1000;
|
||||
/** Фиксированная длительность окна съёмки, строится вокруг момента видимости. */
|
||||
const SHOOT_WINDOW_MS = 6 * 60 * 1000;
|
||||
/**
|
||||
* Угол захвата (полуширина полосы) по умолчанию, градусы.
|
||||
* Пока задаётся на фронте так же, как в текущем построении маршрутов; в дальнейшем
|
||||
* это значение будет определять бэкенд по профилю наблюдения КА.
|
||||
*/
|
||||
const DEFAULT_CAPTURE_ANGLE_DEG = 1.5;
|
||||
const EDITOR_MAP_LAYERS: Tgu2DMapLayersState = {
|
||||
tracks: true,
|
||||
swath: true,
|
||||
@@ -55,7 +68,7 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
const idCounterRef = useRef(0);
|
||||
const targetCounterRef = useRef(0);
|
||||
const centerRef = useRef<HTMLElement>(null);
|
||||
const [mapRatio, setMapRatio] = useState(0.5);
|
||||
const [mapRatio, setMapRatio] = useState(0.72);
|
||||
const [history, setHistory] = useState<DraftHistory>(() => ({
|
||||
draft: seedDraft(selectedPlan, selectedSpacecraftId),
|
||||
past: [],
|
||||
@@ -67,7 +80,15 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
const [target, setTarget] = useState<EditorTarget>();
|
||||
const [selectedShootPass, setSelectedShootPass] = useState<EditorVisibilityWindow>();
|
||||
const [modeId, setModeId] = useState(DEFAULT_EDITOR_MODES[0].id);
|
||||
const [selectedDownlink, setSelectedDownlink] = useState<DownlinkSelection>();
|
||||
const [rvaZones, setRvaZones] = useState<RvaZone[]>([]);
|
||||
const [rvaLoading, setRvaLoading] = useState(false);
|
||||
const [rvaError, setRvaError] = useState<string>();
|
||||
const [selectedZone, setSelectedZone] = useState<RvaZone>();
|
||||
const [selectedDropSurveyIds, setSelectedDropSurveyIds] = useState<Set<number>>(new Set());
|
||||
const [dropSaving, setDropSaving] = useState(false);
|
||||
const [dropError, setDropError] = useState<string>();
|
||||
const [mapInfoSelection, setMapInfoSelection] = useState<Tgu2DMapSelection>();
|
||||
const [stationDtos, setStationDtos] = useState<StationDto[]>([]);
|
||||
const [serviceConflicts, setServiceConflicts] = useState<EditorConflictMap>({});
|
||||
const [checking, setChecking] = useState(false);
|
||||
const [applying, setApplying] = useState(false);
|
||||
@@ -112,14 +133,13 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
const conflicts = useMemo(() => mergeConflictMaps(liveConflicts, serviceConflicts), [liveConflicts, serviceConflicts]);
|
||||
const selectedWork = draft.works.find((work) => work.id === selectedWorkId);
|
||||
const activeLabel = platformLabel(platforms, selectedSpacecraftId);
|
||||
const shootPasses = useMemo(
|
||||
() => target ? targetPasses({ spacecraftId: selectedSpacecraftId }, target, { fromMs: window.fromMs, toMs: window.toMs }) : [],
|
||||
[selectedSpacecraftId, target, window.fromMs, window.toMs]
|
||||
);
|
||||
const stationOptions = useMemo(
|
||||
() => buildStationOptions(stations, selectedSpacecraftId, window, selectedPlan),
|
||||
[selectedPlan, selectedSpacecraftId, stations, window]
|
||||
);
|
||||
const [shootPasses, setShootPasses] = useState<EditorVisibilityWindow[]>([]);
|
||||
const [shootPassesLoading, setShootPassesLoading] = useState(false);
|
||||
const [shootPassesError, setShootPassesError] = useState<string>();
|
||||
const [routeAnchor, setRouteAnchor] = useState<SurveyRouteAnchor>("CENTERED");
|
||||
const [surveyRoute, setSurveyRoute] = useState<SurveyRoute>();
|
||||
const [routeLoading, setRouteLoading] = useState(false);
|
||||
const [routeError, setRouteError] = useState<string>();
|
||||
const visibilityTracks = useMemo(
|
||||
() => buildVisibilityTracks(target, shootPasses, selectedWork, stations, selectedSpacecraftId, window),
|
||||
[selectedSpacecraftId, selectedWork, shootPasses, stations, target, window]
|
||||
@@ -127,8 +147,8 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
const effectiveFrom = Math.max(1, Math.min(passFrom, orbitPasses.length || 1));
|
||||
const effectiveTo = Math.max(effectiveFrom, Math.min(passTo, orbitPasses.length || 1));
|
||||
const mapScene = useMemo(
|
||||
() => buildEditorMapScene(selectedSpacecraftId, flightLineData, orbitPasses, stations, requestItems, planModes, effectiveFrom, effectiveTo),
|
||||
[selectedSpacecraftId, flightLineData, orbitPasses, stations, requestItems, planModes, effectiveFrom, effectiveTo]
|
||||
() => buildEditorMapScene(selectedSpacecraftId, flightLineData, orbitPasses, stationDtos, requestItems, planModes, effectiveFrom, effectiveTo, surveyRoute),
|
||||
[selectedSpacecraftId, flightLineData, orbitPasses, stationDtos, requestItems, planModes, effectiveFrom, effectiveTo, surveyRoute]
|
||||
);
|
||||
const shootTargets = useMemo(
|
||||
() =>
|
||||
@@ -146,6 +166,25 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
})),
|
||||
[contextSpacecraftIds, platforms]
|
||||
);
|
||||
// Кандидаты на сброс: маршруты съёмки плана, заканчивающиеся строго до начала выбранной зоны.
|
||||
const dropCandidates = useMemo<DropCandidateSurvey[]>(() => {
|
||||
if (!selectedZone) return [];
|
||||
const zoneStart = localDateTimeMs(selectedZone.onStart.time);
|
||||
return planModes
|
||||
.filter((m): m is PlanSurveyMode => m.type === "SURVEY" && m.timeStart != null)
|
||||
.map((m) => {
|
||||
const startMs = localDateTimeMs(m.timeStart as string);
|
||||
return {
|
||||
id: m.id,
|
||||
label: `Съёмка витка ${m.revolution}`,
|
||||
startMs,
|
||||
endMs: startMs + m.duration * 1000
|
||||
};
|
||||
})
|
||||
.filter((c) => c.endMs < zoneStart)
|
||||
.sort((a, b) => a.startMs - b.startMs);
|
||||
}, [planModes, selectedZone]);
|
||||
|
||||
const dirty = history.past.length > 0 || draft.works.some((work) => work.isNew);
|
||||
const addedCount = draft.works.filter((work) => work.isNew).length;
|
||||
|
||||
@@ -156,7 +195,13 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
setRightMode("none");
|
||||
setTarget(undefined);
|
||||
setSelectedShootPass(undefined);
|
||||
setSelectedDownlink(undefined);
|
||||
setSurveyRoute(undefined);
|
||||
setRouteError(undefined);
|
||||
setSelectedZone(undefined);
|
||||
setSelectedDropSurveyIds(new Set());
|
||||
setDropError(undefined);
|
||||
setRvaZones([]);
|
||||
setMapInfoSelection(undefined);
|
||||
setServiceConflicts({});
|
||||
setNotice(undefined);
|
||||
setApplyError(undefined);
|
||||
@@ -190,6 +235,125 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
return () => controller.abort();
|
||||
}, [window.fromMs, window.toMs]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void fetchStations()
|
||||
.then((items) => { if (!cancelled) setStationDtos(items); })
|
||||
.catch(() => { if (!cancelled) setStationDtos([]); });
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const numericNorad = noradId ? Number(noradId) : NaN;
|
||||
if (rightMode !== "shoot" || !target || !Number.isFinite(numericNorad)) {
|
||||
setShootPasses([]);
|
||||
setShootPassesError(noradId ? undefined : target ? "Не задан NORAD-идентификатор КА." : undefined);
|
||||
setShootPassesLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
setShootPassesLoading(true);
|
||||
setShootPassesError(undefined);
|
||||
void fetchPointVisibility(
|
||||
numericNorad,
|
||||
{ id: target.id, lat: target.lat, lon: target.lon },
|
||||
window.fromMs,
|
||||
window.toMs
|
||||
)
|
||||
.then((params) => {
|
||||
if (cancelled) return;
|
||||
setShootPasses(
|
||||
params.map(visibilityWindowFromParam).sort((a, b) => a.startMs - b.startMs)
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (cancelled) return;
|
||||
setShootPasses([]);
|
||||
setShootPassesError(error instanceof Error ? error.message : "Не удалось получить параметры видимости.");
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setShootPassesLoading(false);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [rightMode, target, noradId, window.fromMs, window.toMs]);
|
||||
|
||||
useEffect(() => {
|
||||
const numericNorad = noradId ? Number(noradId) : NaN;
|
||||
if (rightMode !== "shoot" || !target || !selectedShootPass || !Number.isFinite(numericNorad)) {
|
||||
setSurveyRoute(undefined);
|
||||
setRouteError(undefined);
|
||||
setRouteLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const momentMs = (selectedShootPass.startMs + selectedShootPass.endMs) / 2;
|
||||
const durationSeconds = Math.max(1, (selectedShootPass.endMs - selectedShootPass.startMs) / 1000);
|
||||
|
||||
let cancelled = false;
|
||||
setRouteLoading(true);
|
||||
setRouteError(undefined);
|
||||
void buildSurveyRoute({
|
||||
satelliteId: numericNorad,
|
||||
time: new Date(momentMs).toISOString().slice(0, 19),
|
||||
durationSeconds,
|
||||
roll: selectedShootPass.rollDeg ?? 0,
|
||||
captureAngle: DEFAULT_CAPTURE_ANGLE_DEG,
|
||||
revolution: selectedShootPass.revolution ?? 0,
|
||||
anchor: routeAnchor
|
||||
})
|
||||
.then((route) => {
|
||||
if (!cancelled) setSurveyRoute(route);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (cancelled) return;
|
||||
setSurveyRoute(undefined);
|
||||
setRouteError(error instanceof Error ? error.message : "Не удалось построить маршрут съёмки.");
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setRouteLoading(false);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [rightMode, target, selectedShootPass, noradId, routeAnchor]);
|
||||
|
||||
// Зоны радиовидимости (ЗРВ) для ручного сброса — грузим при входе в режим «Сброс».
|
||||
useEffect(() => {
|
||||
const numericNorad = noradId ? Number(noradId) : NaN;
|
||||
if (rightMode !== "downlink" || !Number.isFinite(numericNorad)) {
|
||||
setRvaZones([]);
|
||||
setRvaError(rightMode === "downlink" && !noradId ? "Не задан NORAD-идентификатор КА." : undefined);
|
||||
setRvaLoading(false);
|
||||
setSelectedZone(undefined);
|
||||
setSelectedDropSurveyIds(new Set());
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
setRvaLoading(true);
|
||||
setRvaError(undefined);
|
||||
setSelectedZone(undefined);
|
||||
setSelectedDropSurveyIds(new Set());
|
||||
void fetchSatelliteRva(numericNorad, window.fromMs, window.toMs)
|
||||
.then((zones) => {
|
||||
if (!cancelled) setRvaZones(zones);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (cancelled) return;
|
||||
setRvaZones([]);
|
||||
setRvaError(error instanceof Error ? error.message : "Не удалось получить зоны радиовидимости.");
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setRvaLoading(false);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [rightMode, noradId, window.fromMs, window.toMs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedPlan) return;
|
||||
const planId = selectedPlan.planId;
|
||||
@@ -213,9 +377,41 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
changeDraft((current) => updateWork(current, workId, patch));
|
||||
};
|
||||
|
||||
const addShootWork = () => {
|
||||
const addShootWork = async () => {
|
||||
if (!target || !selectedShootPass || !modeId) return;
|
||||
|
||||
// Персистенция построенного маршрута съёмки в pcp-mission-planing-service по id
|
||||
// выбранного плана (planId == mission_id). Сохраняем только если план выбран и маршрут
|
||||
// уже построен; иначе (открыт только КА без плана) добавляем включение без сохранения.
|
||||
const numericNorad = noradId ? Number(noradId) : NaN;
|
||||
if (selectedPlan && surveyRoute && Number.isFinite(numericNorad)) {
|
||||
const planId = selectedPlan.planId;
|
||||
const momentMs = (selectedShootPass.startMs + selectedShootPass.endMs) / 2;
|
||||
const routeDurationSeconds = Math.max(1, (selectedShootPass.endMs - selectedShootPass.startMs) / 1000);
|
||||
setRouteLoading(true);
|
||||
setRouteError(undefined);
|
||||
try {
|
||||
const saved = await saveSurveyRoute(planId, {
|
||||
satelliteId: numericNorad,
|
||||
time: new Date(momentMs).toISOString().slice(0, 19),
|
||||
durationSeconds: routeDurationSeconds,
|
||||
roll: selectedShootPass.rollDeg ?? 0,
|
||||
captureAngle: DEFAULT_CAPTURE_ANGLE_DEG,
|
||||
revolution: selectedShootPass.revolution ?? 0,
|
||||
anchor: routeAnchor,
|
||||
// Окно выбранного плана — чтобы при автосоздании миссии/плана задать корректный интервал.
|
||||
planStart: new Date(selectedPlan.startMs).toISOString().slice(0, 19),
|
||||
planEnd: new Date(selectedPlan.endMs).toISOString().slice(0, 19)
|
||||
});
|
||||
setSurveyRoute(saved);
|
||||
} catch (error) {
|
||||
setRouteError(error instanceof Error ? error.message : "Не удалось сохранить маршрут съёмки.");
|
||||
return;
|
||||
} finally {
|
||||
setRouteLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
const durationMs = Math.min(selectedShootPass.endMs - selectedShootPass.startMs, 30 * 60 * 1000);
|
||||
const startMs = selectedShootPass.startMs + Math.floor((selectedShootPass.endMs - selectedShootPass.startMs - durationMs) / 2);
|
||||
const mode = modes.find((item) => item.id === modeId);
|
||||
@@ -237,25 +433,63 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
setSelectedShootPass(undefined);
|
||||
};
|
||||
|
||||
const addDownlinkWork = () => {
|
||||
if (!selectedDownlink) return;
|
||||
const stationNameById = useCallback(
|
||||
(stationId: number) =>
|
||||
stationDtos.find((s) => s.number === stationId)?.name ?? `Станция ${stationId}`,
|
||||
[stationDtos]
|
||||
);
|
||||
|
||||
const durationMs = Math.min(selectedDownlink.pass.endMs - selectedDownlink.pass.startMs, 12 * 60 * 1000);
|
||||
const startMs = selectedDownlink.pass.startMs + Math.floor((selectedDownlink.pass.endMs - selectedDownlink.pass.startMs - durationMs) / 2);
|
||||
const addDropWork = async () => {
|
||||
if (!selectedPlan || !selectedZone || selectedDropSurveyIds.size === 0) return;
|
||||
const numericNorad = noradId ? Number(noradId) : NaN;
|
||||
if (!Number.isFinite(numericNorad)) {
|
||||
setDropError("Не задан NORAD-идентификатор КА.");
|
||||
return;
|
||||
}
|
||||
|
||||
const planId = selectedPlan.planId;
|
||||
const zoneStartMsVal = localDateTimeMs(selectedZone.onStart.time);
|
||||
const zoneStopMsVal = localDateTimeMs(selectedZone.onStop.time);
|
||||
|
||||
setDropSaving(true);
|
||||
setDropError(undefined);
|
||||
try {
|
||||
await saveDropRoute(planId, {
|
||||
satelliteId: numericNorad,
|
||||
station: String(selectedZone.stationId),
|
||||
revolution: selectedZone.revolution,
|
||||
// Зона — LocalDateTime без зоны (как вернула баллистика).
|
||||
zoneStart: selectedZone.onStart.time.slice(0, 19),
|
||||
zoneStop: selectedZone.onStop.time.slice(0, 19),
|
||||
surveyModeIds: [...selectedDropSurveyIds],
|
||||
// Окно выбранного плана — для автосоздания миссии/плана с корректным интервалом.
|
||||
planStart: new Date(selectedPlan.startMs).toISOString().slice(0, 19),
|
||||
planEnd: new Date(selectedPlan.endMs).toISOString().slice(0, 19)
|
||||
});
|
||||
} catch (error) {
|
||||
setDropError(error instanceof Error ? error.message : "Не удалось сохранить сброс.");
|
||||
return;
|
||||
} finally {
|
||||
setDropSaving(false);
|
||||
}
|
||||
|
||||
// Обновляем включения плана и добавляем работу-сброс в черновик для отображения на таймлайне.
|
||||
void fetchPlanModes(planId).then(setPlanModes).catch(() => {});
|
||||
const stationNm = stationNameById(selectedZone.stationId);
|
||||
const id = nextWorkId(idCounterRef);
|
||||
|
||||
changeDraft((current) =>
|
||||
addDownlink(current, {
|
||||
id,
|
||||
startMs,
|
||||
endMs: startMs + durationMs,
|
||||
label: "Сброс на НКПОР",
|
||||
station: selectedDownlink.station
|
||||
startMs: zoneStartMsVal,
|
||||
endMs: zoneStopMsVal,
|
||||
label: `Сброс · ${stationNm}`,
|
||||
station: { id: String(selectedZone.stationId), name: stationNm }
|
||||
})
|
||||
);
|
||||
setSelectedWorkId(id);
|
||||
setRightMode("inspect");
|
||||
setSelectedDownlink(undefined);
|
||||
setSelectedZone(undefined);
|
||||
setSelectedDropSurveyIds(new Set());
|
||||
};
|
||||
|
||||
const runCheck = () => {
|
||||
@@ -302,6 +536,20 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
});
|
||||
};
|
||||
|
||||
const handleMapObjectSelect = useCallback((selection?: Tgu2DMapSelection) => {
|
||||
if (rightMode === "shoot") return;
|
||||
if (!selection) {
|
||||
if (rightMode === "mapInfo") {
|
||||
setRightMode("none");
|
||||
setMapInfoSelection(undefined);
|
||||
}
|
||||
return;
|
||||
}
|
||||
setMapInfoSelection(selection);
|
||||
setRightMode("mapInfo");
|
||||
setSelectedWorkId(undefined);
|
||||
}, [rightMode]);
|
||||
|
||||
const handleResizerDown = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
const startY = e.clientY;
|
||||
@@ -349,6 +597,7 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
onSetRightMode={(mode) => {
|
||||
setRightMode(mode);
|
||||
setSelectedWorkId(undefined);
|
||||
setMapInfoSelection(undefined);
|
||||
setNotice(undefined);
|
||||
}}
|
||||
onUndo={() => setHistory((current) => undo(current))}
|
||||
@@ -426,7 +675,7 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
setModeId(DEFAULT_EDITOR_MODES[0].id);
|
||||
setNotice(undefined);
|
||||
}}
|
||||
onObjectSelect={() => undefined}
|
||||
onObjectSelect={handleMapObjectSelect}
|
||||
/>
|
||||
<div className="tgu-editor-map-resizer" onMouseDown={handleResizerDown} />
|
||||
</div>
|
||||
@@ -484,9 +733,16 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
<ShootPanel
|
||||
target={target}
|
||||
passes={shootPasses}
|
||||
loading={shootPassesLoading}
|
||||
error={shootPassesError}
|
||||
selectedPass={selectedShootPass}
|
||||
modeId={modeId}
|
||||
modes={modes}
|
||||
routeAnchor={routeAnchor}
|
||||
routeLoading={routeLoading}
|
||||
routeError={routeError}
|
||||
route={surveyRoute}
|
||||
onAnchorChange={setRouteAnchor}
|
||||
onSelectPass={setSelectedShootPass}
|
||||
onModeChange={setModeId}
|
||||
onAdd={addShootWork}
|
||||
@@ -502,14 +758,47 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
/>
|
||||
)}
|
||||
{rightMode === "downlink" && (
|
||||
<DownlinkPanel
|
||||
stationOptions={stationOptions}
|
||||
selected={selectedDownlink}
|
||||
onSelect={setSelectedDownlink}
|
||||
onAdd={addDownlinkWork}
|
||||
<DropPanel
|
||||
zones={rvaZones}
|
||||
loading={rvaLoading}
|
||||
error={rvaError}
|
||||
selectedZone={selectedZone}
|
||||
onSelectZone={(zone) => {
|
||||
setSelectedZone(zone);
|
||||
setSelectedDropSurveyIds(new Set());
|
||||
setDropError(undefined);
|
||||
}}
|
||||
stationName={stationNameById}
|
||||
zoneStartMs={(zone) => localDateTimeMs(zone.onStart.time)}
|
||||
zoneStopMs={(zone) => localDateTimeMs(zone.onStop.time)}
|
||||
candidates={dropCandidates}
|
||||
selectedSurveyIds={selectedDropSurveyIds}
|
||||
onToggleSurvey={(id) =>
|
||||
setSelectedDropSurveyIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(id)) next.delete(id);
|
||||
else next.add(id);
|
||||
return next;
|
||||
})
|
||||
}
|
||||
saving={dropSaving}
|
||||
saveError={dropError}
|
||||
onAdd={addDropWork}
|
||||
onClose={() => {
|
||||
setRightMode("none");
|
||||
setSelectedDownlink(undefined);
|
||||
setSelectedZone(undefined);
|
||||
setSelectedDropSurveyIds(new Set());
|
||||
setDropError(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{rightMode === "mapInfo" && mapInfoSelection && (
|
||||
<MapInfoPanel
|
||||
selection={mapInfoSelection}
|
||||
requestItems={requestItems}
|
||||
onClose={() => {
|
||||
setRightMode("none");
|
||||
setMapInfoSelection(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -520,6 +809,21 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
);
|
||||
}
|
||||
|
||||
function visibilityWindowFromParam(param: PointVisibilityParam): EditorVisibilityWindow {
|
||||
const timeMs = Date.parse(param.time);
|
||||
const half = SHOOT_WINDOW_MS / 2;
|
||||
return {
|
||||
startMs: timeMs - half,
|
||||
endMs: timeMs + half,
|
||||
quality: 0,
|
||||
revolution: param.revolution,
|
||||
rollDeg: param.gamma,
|
||||
sunAngleDeg: param.sunAngle,
|
||||
range: param.range,
|
||||
revSign: param.revSign
|
||||
};
|
||||
}
|
||||
|
||||
function defaultEditorWindow(
|
||||
selectedPlan: TguEditorTabProps["selectedPlan"],
|
||||
appliedRange: TguEditorTabProps["appliedRange"]
|
||||
@@ -542,30 +846,13 @@ function buildStations(selectedPlan: TguEditorTabProps["selectedPlan"]): EditorG
|
||||
return [{ id: selectedPlan.kppId, name: selectedPlan.kppId }];
|
||||
}
|
||||
|
||||
function buildStationOptions(
|
||||
stations: EditorGroundStation[],
|
||||
spacecraftId: string | undefined,
|
||||
window: EditorTimelineWindow,
|
||||
selectedPlan: TguEditorTabProps["selectedPlan"]
|
||||
): DownlinkStationOption[] {
|
||||
return stations.map((station) => {
|
||||
const computedPasses =
|
||||
station.lat !== undefined && station.lon !== undefined
|
||||
? stationPasses({ spacecraftId }, { lat: station.lat, lon: station.lon }, { fromMs: window.fromMs, toMs: window.toMs })
|
||||
: [];
|
||||
const fallbackPass = selectedPlan && selectedPlan.kppId === station.id
|
||||
? [{
|
||||
startMs: Math.max(window.fromMs, selectedPlan.startMs),
|
||||
endMs: Math.min(window.toMs, selectedPlan.endMs),
|
||||
quality: 0
|
||||
}]
|
||||
: [];
|
||||
|
||||
return {
|
||||
station,
|
||||
passes: computedPasses.length > 0 ? computedPasses : fallbackPass.filter((pass) => pass.endMs > pass.startMs)
|
||||
};
|
||||
});
|
||||
/**
|
||||
* LocalDateTime без зоны от бэка трактуем как UTC (как и поле `time` в ballisticsApi):
|
||||
* зоны ЗРВ приходят без таймзоны, а времена съёмок — с offset; приводим к одной шкале.
|
||||
*/
|
||||
function localDateTimeMs(s: string): number {
|
||||
const hasZone = /[zZ]$|[+-]\d{2}:?\d{2}$/.test(s);
|
||||
return Date.parse(hasZone ? s : `${s}Z`);
|
||||
}
|
||||
|
||||
function buildVisibilityTracks(
|
||||
@@ -609,11 +896,12 @@ function buildEditorMapScene(
|
||||
spacecraftId: string | undefined,
|
||||
flightLineData: FlightLinePoint[],
|
||||
orbitPasses: OrbitPassInfo[],
|
||||
stations: EditorGroundStation[],
|
||||
stationDtos: StationDto[],
|
||||
requestItems: RequestMapItem[],
|
||||
planModes: PlanMode[],
|
||||
fromIndex: number,
|
||||
toIndex: number
|
||||
toIndex: number,
|
||||
surveyRoute?: SurveyRoute
|
||||
): Tgu2DMapScene {
|
||||
const visibleRevolutions = new Set(
|
||||
orbitPasses.filter((p) => p.index >= fromIndex && p.index <= toIndex).map((p) => p.revolution!)
|
||||
@@ -689,16 +977,26 @@ function buildEditorMapScene(
|
||||
}
|
||||
}
|
||||
|
||||
const markers = stations.flatMap((station) => {
|
||||
if (station.lat === undefined || station.lon === undefined) return [];
|
||||
return [{
|
||||
id: station.id,
|
||||
layer: "stations" as const,
|
||||
point: { lat: station.lat, lon: station.lon },
|
||||
label: station.name,
|
||||
zIndex: 10
|
||||
}];
|
||||
});
|
||||
// Построенный (но ещё не сохранённый) маршрут съёмки — показываем поверх работ плана
|
||||
// отдельным цветом (статус ISSUING). Рендерится в слое planWorks, поэтому виден без
|
||||
// дополнительной настройки слоёв.
|
||||
if (surveyRoute?.contourWkt) {
|
||||
const rings = parseWktPolygon(surveyRoute.contourWkt);
|
||||
for (const ring of rings) {
|
||||
if (ring.length < 3) continue;
|
||||
polygons.push({
|
||||
id: `surveyRoute:${zIndex}`,
|
||||
name: `Маршрут съёмки (виток ${surveyRoute.revolution})`,
|
||||
kind: "planWork",
|
||||
layer: "planWorks",
|
||||
points: ring,
|
||||
status: "ISSUING",
|
||||
zIndex: zIndex++
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const markers = buildStationMarkers(stationDtos);
|
||||
|
||||
return { polygons, lines, markers };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user