pcp-tgu-ops-ui: вкладка Редактор — шаг времени маршрута и формат длительности
- RouteTimeStepper и доработки EditorPanels/TguEditorTab по вводу времени; - formatDuration: секунды/минуты для коротких интервалов; - стили tgu-editor.css.
This commit is contained in:
@@ -5,7 +5,7 @@ import { fetchFlightLine, type FlightLinePoint } from "../../api/tguApi";
|
||||
import type { RequestMapItem } from "../../model/requestTypes";
|
||||
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 { buildSurveyRoute, saveDropRoute, saveSurveyRoute, type SurveyRoute } from "../../api/missionPlanningApi";
|
||||
import { parseUtc } from "../../utils/utcTime";
|
||||
import { Tgu2DMapView } from "../tgu-map-2d/Tgu2DMapView";
|
||||
import { parseWktPolygon } from "../tgu-map-2d/geometry/wktParser";
|
||||
@@ -58,6 +58,12 @@ const SHOOT_WINDOW_MS = 6 * 60 * 1000;
|
||||
* это значение будет определять бэкенд по профилю наблюдения КА.
|
||||
*/
|
||||
const DEFAULT_CAPTURE_ANGLE_DEG = 1.5;
|
||||
/** Дефолтная длительность строящегося маршрута съёмки (мс). */
|
||||
const DEFAULT_ROUTE_DURATION_MS = 30_000;
|
||||
/** Предел крена (gamma) маршрута при ручной правке, градусы. */
|
||||
const MAX_ROUTE_ROLL_DEG = 89;
|
||||
/** Допустимый выход окна маршрута за пределы окна видимости, мс (±30 минут). */
|
||||
const ROUTE_TIME_MARGIN_MS = 30 * 60_000;
|
||||
const EDITOR_MAP_LAYERS: Tgu2DMapLayersState = {
|
||||
tracks: true,
|
||||
swath: true,
|
||||
@@ -144,7 +150,10 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
const [shootPasses, setShootPasses] = useState<EditorVisibilityWindow[]>([]);
|
||||
const [shootPassesLoading, setShootPassesLoading] = useState(false);
|
||||
const [shootPassesError, setShootPassesError] = useState<string>();
|
||||
const [routeAnchor, setRouteAnchor] = useState<SurveyRouteAnchor>("CENTERED");
|
||||
// Окно строящегося маршрута съёмки: задаётся вручную в правой панели (начало/конец, UTC).
|
||||
const [routeWindow, setRouteWindow] = useState<{ startMs: number; endMs: number }>();
|
||||
// Крен (gamma) строящегося маршрута, градусы. По умолчанию — из прохода, дальше правится вручную.
|
||||
const [routeRoll, setRouteRoll] = useState<number>();
|
||||
const [surveyRoute, setSurveyRoute] = useState<SurveyRoute>();
|
||||
const [routeLoading, setRouteLoading] = useState(false);
|
||||
const [routeError, setRouteError] = useState<string>();
|
||||
@@ -292,29 +301,44 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
};
|
||||
}, [rightMode, target, noradId, window.fromMs, window.toMs]);
|
||||
|
||||
// При выборе нового прохода задаём дефолтное окно маршрута: 30 c вокруг момента
|
||||
// видимости, обрезанное окном видимости прохода. Дальше пользователь правит вручную.
|
||||
useEffect(() => {
|
||||
if (rightMode !== "shoot" || !selectedShootPass) {
|
||||
setRouteWindow(undefined);
|
||||
setRouteRoll(undefined);
|
||||
return;
|
||||
}
|
||||
const passDurationMs = selectedShootPass.endMs - selectedShootPass.startMs;
|
||||
const half = Math.min(DEFAULT_ROUTE_DURATION_MS, passDurationMs) / 2;
|
||||
const mid = (selectedShootPass.startMs + selectedShootPass.endMs) / 2;
|
||||
setRouteWindow({ startMs: mid - half, endMs: mid + half });
|
||||
setRouteRoll(selectedShootPass.rollDeg ?? 0);
|
||||
}, [rightMode, selectedShootPass]);
|
||||
|
||||
useEffect(() => {
|
||||
const numericNorad = noradId ? Number(noradId) : NaN;
|
||||
if (rightMode !== "shoot" || !target || !selectedShootPass || !Number.isFinite(numericNorad)) {
|
||||
if (rightMode !== "shoot" || !target || !selectedShootPass || !routeWindow || routeRoll == null || !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);
|
||||
const durationSeconds = Math.max(1, (routeWindow.endMs - routeWindow.startMs) / 1000);
|
||||
|
||||
let cancelled = false;
|
||||
setRouteLoading(true);
|
||||
setRouteError(undefined);
|
||||
void buildSurveyRoute({
|
||||
satelliteId: numericNorad,
|
||||
time: new Date(momentMs).toISOString().slice(0, 19),
|
||||
// Окно задано явно (начало/конец) — строим вперёд от начала на выбранную длительность.
|
||||
time: new Date(routeWindow.startMs).toISOString().slice(0, 19),
|
||||
durationSeconds,
|
||||
roll: selectedShootPass.rollDeg ?? 0,
|
||||
roll: routeRoll,
|
||||
captureAngle: DEFAULT_CAPTURE_ANGLE_DEG,
|
||||
revolution: selectedShootPass.revolution ?? 0,
|
||||
anchor: routeAnchor
|
||||
anchor: "FORWARD"
|
||||
})
|
||||
.then((route) => {
|
||||
if (!cancelled) setSurveyRoute(route);
|
||||
@@ -330,7 +354,7 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [rightMode, target, selectedShootPass, noradId, routeAnchor]);
|
||||
}, [rightMode, target, selectedShootPass, noradId, routeWindow, routeRoll]);
|
||||
|
||||
// Зоны радиовидимости (ЗРВ) для ручного сброса — грузим при входе в режим «Сброс».
|
||||
useEffect(() => {
|
||||
@@ -396,21 +420,20 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
// выбранного плана (planId == mission_id). Сохраняем только если план выбран и маршрут
|
||||
// уже построен; иначе (открыт только КА без плана) добавляем включение без сохранения.
|
||||
const numericNorad = noradId ? Number(noradId) : NaN;
|
||||
if (selectedPlan && surveyRoute && Number.isFinite(numericNorad)) {
|
||||
if (selectedPlan && surveyRoute && routeWindow && routeRoll != null && Number.isFinite(numericNorad)) {
|
||||
const planId = selectedPlan.planId;
|
||||
const momentMs = (selectedShootPass.startMs + selectedShootPass.endMs) / 2;
|
||||
const routeDurationSeconds = Math.max(1, (selectedShootPass.endMs - selectedShootPass.startMs) / 1000);
|
||||
const routeDurationSeconds = Math.max(1, (routeWindow.endMs - routeWindow.startMs) / 1000);
|
||||
setRouteLoading(true);
|
||||
setRouteError(undefined);
|
||||
try {
|
||||
const saved = await saveSurveyRoute(planId, {
|
||||
satelliteId: numericNorad,
|
||||
time: new Date(momentMs).toISOString().slice(0, 19),
|
||||
time: new Date(routeWindow.startMs).toISOString().slice(0, 19),
|
||||
durationSeconds: routeDurationSeconds,
|
||||
roll: selectedShootPass.rollDeg ?? 0,
|
||||
roll: routeRoll,
|
||||
captureAngle: DEFAULT_CAPTURE_ANGLE_DEG,
|
||||
revolution: selectedShootPass.revolution ?? 0,
|
||||
anchor: routeAnchor,
|
||||
anchor: "FORWARD",
|
||||
// Окно выбранного плана — чтобы при автосоздании миссии/плана задать корректный интервал.
|
||||
planStart: new Date(selectedPlan.startMs).toISOString().slice(0, 19),
|
||||
planEnd: new Date(selectedPlan.endMs).toISOString().slice(0, 19)
|
||||
@@ -424,8 +447,9 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
}
|
||||
}
|
||||
|
||||
const durationMs = Math.min(selectedShootPass.endMs - selectedShootPass.startMs, 30 * 60 * 1000);
|
||||
const startMs = selectedShootPass.startMs + Math.floor((selectedShootPass.endMs - selectedShootPass.startMs - durationMs) / 2);
|
||||
// Окно включения в черновике — заданное вручную окно маршрута (фолбэк — окно прохода).
|
||||
const startMs = routeWindow?.startMs ?? selectedShootPass.startMs;
|
||||
const endMs = routeWindow?.endMs ?? selectedShootPass.endMs;
|
||||
const mode = modes.find((item) => item.id === modeId);
|
||||
const id = nextWorkId(idCounterRef);
|
||||
|
||||
@@ -433,7 +457,7 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
addShoot(current, {
|
||||
id,
|
||||
startMs,
|
||||
endMs: startMs + durationMs,
|
||||
endMs,
|
||||
label: mode?.label ?? "Съёмка",
|
||||
modeId,
|
||||
target
|
||||
@@ -752,11 +776,39 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
selectedPass={selectedShootPass}
|
||||
modeId={modeId}
|
||||
modes={modes}
|
||||
routeAnchor={routeAnchor}
|
||||
routeWindow={routeWindow}
|
||||
routeRoll={routeRoll}
|
||||
passStartMs={selectedShootPass?.startMs}
|
||||
passEndMs={selectedShootPass?.endMs}
|
||||
routeMinMs={selectedShootPass != null ? selectedShootPass.startMs - ROUTE_TIME_MARGIN_MS : undefined}
|
||||
routeMaxMs={selectedShootPass != null ? selectedShootPass.endMs + ROUTE_TIME_MARGIN_MS : undefined}
|
||||
routeLoading={routeLoading}
|
||||
routeError={routeError}
|
||||
route={surveyRoute}
|
||||
onAnchorChange={setRouteAnchor}
|
||||
onRouteStartChange={(startMs) => setRouteWindow((current) => (current ? { ...current, startMs } : current))}
|
||||
onRouteEndChange={(endMs) => setRouteWindow((current) => (current ? { ...current, endMs } : current))}
|
||||
onRouteShift={(deltaMs) =>
|
||||
setRouteWindow((current) => {
|
||||
if (!current || selectedShootPass == null) return current;
|
||||
// Двигаем окно целиком, сохраняя длительность; не выходим за ±30 мин от окна видимости.
|
||||
const minMs = selectedShootPass.startMs - ROUTE_TIME_MARGIN_MS;
|
||||
const maxMs = selectedShootPass.endMs + ROUTE_TIME_MARGIN_MS;
|
||||
let shift = deltaMs;
|
||||
if (current.endMs + shift > maxMs) shift = maxMs - current.endMs;
|
||||
if (current.startMs + shift < minMs) shift = minMs - current.startMs;
|
||||
return { startMs: current.startMs + shift, endMs: current.endMs + shift };
|
||||
})
|
||||
}
|
||||
onRouteRollChange={(deltaDeg) =>
|
||||
setRouteRoll((current) =>
|
||||
Math.max(-MAX_ROUTE_ROLL_DEG, Math.min(MAX_ROUTE_ROLL_DEG, (current ?? 0) + deltaDeg))
|
||||
)
|
||||
}
|
||||
onRouteRollSet={(deg) =>
|
||||
setRouteRoll((current) =>
|
||||
Number.isFinite(deg) ? Math.max(-MAX_ROUTE_ROLL_DEG, Math.min(MAX_ROUTE_ROLL_DEG, deg)) : current
|
||||
)
|
||||
}
|
||||
onSelectPass={setSelectedShootPass}
|
||||
onModeChange={setModeId}
|
||||
onAdd={addShootWork}
|
||||
|
||||
Reference in New Issue
Block a user