pcp-tgu-ops-ui: вкладка «Комплексный план» + слой карты
Новая вкладка планирования «Комплексный план» (TguComplexPlanTab) и одноимённый слой на 2D-карте, выключенный по умолчанию. - TguComplexPlanTab + complexPlanApi (через прокси /api/pcp-complex, добавлен в vite.config.ts) — данные из pcp-complex-mission-service; - слой complexPlan: тип в mapLayerTypes/mapTypes/mapSelectionTypes, отрисовка drawComplexPlan, тоггл и подписи в Tgu2DMapTab/Tgu2DMapView; - вкладка complex в TguActiveTab, кнопка в TguToolbar, рендер в TguPlanningPage; complexPlan: false в дефолтах слоёв и в редакторе/ заявках/тесте интервала.
This commit is contained in:
@@ -0,0 +1,139 @@
|
|||||||
|
import { TguApiError } from "../model/tguTypes";
|
||||||
|
import { toLocalIso } from "../utils/localTime";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Расчёт комплексного плана (этап 4) напрямую в pcp-complex-mission-service через Vite-прокси
|
||||||
|
* (`/api/pcp-complex` → :7002), как и остальные сервисы — без BFF. Это тот самый snapshot,
|
||||||
|
* который затем потребляет редактор (stage 5, `calculateSurveys`).
|
||||||
|
*/
|
||||||
|
const BASE = "/api/pcp-complex";
|
||||||
|
|
||||||
|
/** Статус запуска расчёта комплексного плана (ComplexPlanRunStatus бэка). */
|
||||||
|
export type ComplexPlanRunStatus = "CREATED" | "RUNNING" | "COMPLETED" | "FAILED";
|
||||||
|
|
||||||
|
export type RunComplexPlanParams = {
|
||||||
|
/** Начало интервала расчёта (эпоха-мс, локальное). */
|
||||||
|
fromMs: number;
|
||||||
|
/** Конец интервала расчёта (эпоха-мс, локальное). */
|
||||||
|
toMs: number;
|
||||||
|
/** NORAD-id КА, по которым считаем (все эксплуатируемые КА вкладки «Планы»). */
|
||||||
|
satelliteIds: number[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ответ синхронного расчёта комплексного плана (ComplexPlanRunResponseDTO).
|
||||||
|
* Расчёт уже завершён к моменту ответа: пишет snapshot’ы этапа 4 и возвращает их id + статистику.
|
||||||
|
*/
|
||||||
|
export type ComplexPlanRunResult = {
|
||||||
|
runId: number;
|
||||||
|
status: ComplexPlanRunStatus;
|
||||||
|
intervalStart: string;
|
||||||
|
intervalEnd: string;
|
||||||
|
satelliteIds: number[];
|
||||||
|
durationMs: number | null;
|
||||||
|
errorMessage: string | null;
|
||||||
|
calculatedModesCount: number;
|
||||||
|
bookedModesCount: number;
|
||||||
|
complanModesCount: number;
|
||||||
|
mixedModesCount: number;
|
||||||
|
affectedSatellitesCount: number;
|
||||||
|
processedCellsCount: number;
|
||||||
|
bookedSlotLinksCount: number;
|
||||||
|
snapshotIds: number[];
|
||||||
|
totalRequestsArea: number;
|
||||||
|
coveredArea: number;
|
||||||
|
coveredAreaPercent: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Режим съёмки/сброса из результата расчёта (SatelliteModeResponseDTO). Времена — локальные. */
|
||||||
|
export type ComplexPlanMode = {
|
||||||
|
id: number;
|
||||||
|
satelliteId: number;
|
||||||
|
source: string;
|
||||||
|
type: "SURVEY" | "DROP" | string;
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
revolution: number;
|
||||||
|
lat: number;
|
||||||
|
longitude: number;
|
||||||
|
duration: number;
|
||||||
|
/** Контур полосы съёмки, WKT POLYGON (lon lat, ...). Для сбросов обычно отсутствует. */
|
||||||
|
contourWkt: string | null;
|
||||||
|
roll: number;
|
||||||
|
cellNum: number;
|
||||||
|
bookedSlotIds: number[];
|
||||||
|
snapshotId: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запуск расчёта комплексного плана на интервал по списку КА
|
||||||
|
* (pcp-complex-mission-service, POST /api/com-plan/process). Вызов синхронный: возвращает
|
||||||
|
* завершённый run со статистикой и id записанных snapshot’ов. Бэк отклоняет запуск, если по
|
||||||
|
* одному из КА уже идёт пересекающийся расчёт.
|
||||||
|
*/
|
||||||
|
export async function runComplexPlan(params: RunComplexPlanParams): Promise<ComplexPlanRunResult> {
|
||||||
|
const body = {
|
||||||
|
intervalStart: toLocalIso(params.fromMs),
|
||||||
|
intervalEnd: toLocalIso(params.toMs),
|
||||||
|
satelliteIds: params.satelliteIds
|
||||||
|
};
|
||||||
|
|
||||||
|
const url = `${BASE}/api/com-plan/process`;
|
||||||
|
let response: Response;
|
||||||
|
try {
|
||||||
|
response = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { Accept: "application/json", "Content-Type": "application/json", "X-Requested-By": "pcp-tgu-ops-ui" },
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
throw new TguApiError("Не удалось подключиться к сервису комплексного плана.", "network");
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await response.text();
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new TguApiError(extractErrorText(text) || "Не удалось рассчитать комплексный план.", "http", response.status);
|
||||||
|
}
|
||||||
|
if (!text.trim()) {
|
||||||
|
throw new TguApiError("Сервис комплексного плана вернул пустой ответ.", "http", response.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.parse(text) as ComplexPlanRunResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Режимы (съёмки/сбросы) рассчитанного run’а для отрисовки на карте
|
||||||
|
* (pcp-complex-mission-service, GET /api/com-plan/runs/{runId}/modes).
|
||||||
|
*/
|
||||||
|
export async function fetchComplexPlanRunModes(runId: number): Promise<ComplexPlanMode[]> {
|
||||||
|
const url = `${BASE}/api/com-plan/runs/${encodeURIComponent(runId)}/modes`;
|
||||||
|
let response: Response;
|
||||||
|
try {
|
||||||
|
response = await fetch(url, { method: "GET", headers: { Accept: "application/json" } });
|
||||||
|
} catch {
|
||||||
|
throw new TguApiError("Не удалось подключиться к сервису комплексного плана.", "network");
|
||||||
|
}
|
||||||
|
const text = await response.text();
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new TguApiError(extractErrorText(text) || "Не удалось загрузить результат расчёта.", "http", response.status);
|
||||||
|
}
|
||||||
|
if (!text.trim()) return [];
|
||||||
|
return JSON.parse(text) as ComplexPlanMode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractErrorText(body: string): string {
|
||||||
|
const trimmed = body.trim();
|
||||||
|
if (!trimmed) return "";
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(trimmed) as Record<string, unknown>;
|
||||||
|
for (const field of ["message", "error", "reason", "detail"]) {
|
||||||
|
const value = parsed[field];
|
||||||
|
if (typeof value === "string" && value.trim()) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import {
|
||||||
|
fetchComplexPlanRunModes,
|
||||||
|
runComplexPlan,
|
||||||
|
type ComplexPlanMode,
|
||||||
|
type ComplexPlanRunResult
|
||||||
|
} from "../../api/complexPlanApi";
|
||||||
|
import type { TguPlatformUi, TimelineRange } from "../../model/timelineTypes";
|
||||||
|
import { DateTime24Field } from "../../components/timeFields";
|
||||||
|
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 { Tgu2DMapView } from "../tgu-map-2d/Tgu2DMapView";
|
||||||
|
import { parseLocal, toLocalInputValue } from "../../utils/localTime";
|
||||||
|
|
||||||
|
type TguComplexPlanTabProps = {
|
||||||
|
range: TimelineRange;
|
||||||
|
/** Все эксплуатируемые КА вкладки «Планы» — по ним считаем комплексный план. */
|
||||||
|
platforms: TguPlatformUi[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Включён только выделенный слой «Комплексный план» (свой цвет, своя отрисовка с фиксом
|
||||||
|
// антимеридиана) — контуры не путаются с полосой обзора/работами плана других вкладок.
|
||||||
|
const COMPLEX_PLAN_LAYERS: Tgu2DMapLayersState = {
|
||||||
|
tracks: false,
|
||||||
|
swath: false,
|
||||||
|
planWorks: false,
|
||||||
|
complexPlan: true,
|
||||||
|
stations: false,
|
||||||
|
spacecraftMarkers: false,
|
||||||
|
requests: false
|
||||||
|
};
|
||||||
|
|
||||||
|
export function TguComplexPlanTab({ range, platforms }: TguComplexPlanTabProps) {
|
||||||
|
const [fromValue, setFromValue] = useState(() => toLocalInputValue(range.fromMs));
|
||||||
|
const [toValue, setToValue] = useState(() => toLocalInputValue(range.toMs));
|
||||||
|
const [running, setRunning] = useState(false);
|
||||||
|
const [result, setResult] = useState<ComplexPlanRunResult>();
|
||||||
|
const [modes, setModes] = useState<ComplexPlanMode[]>([]);
|
||||||
|
const [error, setError] = useState<string>();
|
||||||
|
const [notice, setNotice] = useState<string>();
|
||||||
|
|
||||||
|
// NORAD-id всех эксплуатируемых КА «Планов» (spacecraftId == norad-id, см. tguTimelineMapper).
|
||||||
|
const satelliteIds = useMemo(
|
||||||
|
() =>
|
||||||
|
Array.from(
|
||||||
|
new Set(
|
||||||
|
platforms
|
||||||
|
.map((platform) => Number(platform.spacecraftId))
|
||||||
|
.filter((id) => Number.isFinite(id))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
[platforms]
|
||||||
|
);
|
||||||
|
|
||||||
|
const parsedRange = useMemo(
|
||||||
|
() => ({ fromMs: parseLocal(fromValue), toMs: parseLocal(toValue) }),
|
||||||
|
[fromValue, toValue]
|
||||||
|
);
|
||||||
|
const rangeInvalid =
|
||||||
|
!Number.isFinite(parsedRange.fromMs) ||
|
||||||
|
!Number.isFinite(parsedRange.toMs) ||
|
||||||
|
parsedRange.fromMs >= parsedRange.toMs;
|
||||||
|
|
||||||
|
const scene = useMemo<Tgu2DMapScene>(() => ({ polygons: buildModePolygons(modes), lines: [], markers: [] }), [modes]);
|
||||||
|
|
||||||
|
const runCalculation = async () => {
|
||||||
|
if (running || rangeInvalid || satelliteIds.length === 0) return;
|
||||||
|
setRunning(true);
|
||||||
|
setError(undefined);
|
||||||
|
setNotice(undefined);
|
||||||
|
try {
|
||||||
|
const runResult = await runComplexPlan({
|
||||||
|
fromMs: parsedRange.fromMs,
|
||||||
|
toMs: parsedRange.toMs,
|
||||||
|
satelliteIds
|
||||||
|
});
|
||||||
|
setResult(runResult);
|
||||||
|
const runModes = await fetchComplexPlanRunModes(runResult.runId);
|
||||||
|
setModes(runModes);
|
||||||
|
const contoursCount = runModes.filter((mode) => mode.contourWkt).length;
|
||||||
|
setNotice(
|
||||||
|
runResult.calculatedModesCount > 0
|
||||||
|
? `Расчёт завершён: режимов ${runResult.calculatedModesCount}, КА затронуто ${runResult.affectedSatellitesCount}, охват ${formatPercent(runResult.coveredAreaPercent)}. На карте — ${contoursCount} контуров.`
|
||||||
|
: "Расчёт завершён, но включений не построено: на интервал нет покрытия заявками."
|
||||||
|
);
|
||||||
|
} catch (caught) {
|
||||||
|
setError(caught instanceof Error ? caught.message : "Не удалось рассчитать комплексный план.");
|
||||||
|
} finally {
|
||||||
|
setRunning(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const canRun = !running && !rangeInvalid && satelliteIds.length > 0;
|
||||||
|
const overlayMessage = running
|
||||||
|
? "Идёт расчёт…"
|
||||||
|
: rangeInvalid
|
||||||
|
? "Некорректный интервал: «С» должно быть раньше «По»."
|
||||||
|
: modes.length === 0
|
||||||
|
? "Задайте интервал и нажмите «Рассчитать», чтобы построить комплексный план."
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="tgu-map-workspace">
|
||||||
|
<div className="tgu-map-toolbar">
|
||||||
|
<div>
|
||||||
|
<div className="tgu-map-toolbar__title">Комплексный план</div>
|
||||||
|
<div className="tgu-map-toolbar__subtitle">
|
||||||
|
Расчёт по {satelliteIds.length} КА вкладки «Планы»
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label className="tgu-field">
|
||||||
|
<span>С</span>
|
||||||
|
<DateTime24Field value={fromValue} onChange={setFromValue} />
|
||||||
|
</label>
|
||||||
|
<label className="tgu-field">
|
||||||
|
<span>По</span>
|
||||||
|
<DateTime24Field value={toValue} onChange={setToValue} />
|
||||||
|
</label>
|
||||||
|
<button className="tgu-button tgu-button--primary" type="button" onClick={runCalculation} disabled={!canRun}>
|
||||||
|
{running ? "Расчёт…" : "Рассчитать"}
|
||||||
|
</button>
|
||||||
|
<div className="tgu-map-toolbar__spacer" style={{ flex: 1 }} />
|
||||||
|
{result && (
|
||||||
|
<div className="tgu-map-toolbar__subtitle">
|
||||||
|
run #{result.runId} · режимов {result.calculatedModesCount} · охват {formatPercent(result.coveredAreaPercent)}
|
||||||
|
{result.snapshotIds.length > 0 ? ` · snapshot ${result.snapshotIds.join(", ")}` : ""}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section className="tgu-map-stage">
|
||||||
|
{overlayMessage && <div className="tgu-map-overlay">{overlayMessage}</div>}
|
||||||
|
<Tgu2DMapView
|
||||||
|
scene={scene}
|
||||||
|
layers={COMPLEX_PLAN_LAYERS}
|
||||||
|
redrawToken={modes.length}
|
||||||
|
onObjectSelect={() => undefined}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div className="tgu-map-errors">
|
||||||
|
{satelliteIds.length === 0 && (
|
||||||
|
<div className="tgu-alert tgu-alert--warning">
|
||||||
|
Нет эксплуатируемых КА на вкладке «Планы» — расчёт недоступен.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{notice && <div className="tgu-alert">{notice}</div>}
|
||||||
|
{error && <div className="tgu-alert tgu-alert--danger">{error}</div>}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Контуры режимов (survey) → полигоны слоя комплексного плана. Сбросы без контура пропускаем. */
|
||||||
|
function buildModePolygons(modes: ComplexPlanMode[]): MapPolygon[] {
|
||||||
|
const polygons: MapPolygon[] = [];
|
||||||
|
let zIndex = 0;
|
||||||
|
for (const mode of modes) {
|
||||||
|
if (!mode.contourWkt) continue;
|
||||||
|
const rings = parseWktPolygon(mode.contourWkt);
|
||||||
|
for (const ring of rings) {
|
||||||
|
if (ring.length < 3) continue;
|
||||||
|
polygons.push({
|
||||||
|
id: `complex-mode:${mode.id}:${zIndex}`,
|
||||||
|
name: `КА ${mode.satelliteId} · виток ${mode.revolution}`,
|
||||||
|
kind: "complexPlan",
|
||||||
|
layer: "complexPlan",
|
||||||
|
points: ring,
|
||||||
|
spacecraftId: String(mode.satelliteId),
|
||||||
|
modeId: mode.id,
|
||||||
|
zIndex: zIndex++
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return polygons;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPercent(value: number | null | undefined): string {
|
||||||
|
if (value == null || !Number.isFinite(value)) return "—";
|
||||||
|
return `${value.toFixed(1)}%`;
|
||||||
|
}
|
||||||
@@ -85,6 +85,7 @@ const EDITOR_MAP_LAYERS: Tgu2DMapLayersState = {
|
|||||||
tracks: true,
|
tracks: true,
|
||||||
swath: true,
|
swath: true,
|
||||||
planWorks: true,
|
planWorks: true,
|
||||||
|
complexPlan: false,
|
||||||
stations: true,
|
stations: true,
|
||||||
spacecraftMarkers: false,
|
spacecraftMarkers: false,
|
||||||
requests: true
|
requests: true
|
||||||
|
|||||||
@@ -312,6 +312,8 @@ function layerLabel(layer: Tgu2DMapLayerKey): string {
|
|||||||
return "Полоса обзора";
|
return "Полоса обзора";
|
||||||
case "planWorks":
|
case "planWorks":
|
||||||
return "Работы плана";
|
return "Работы плана";
|
||||||
|
case "complexPlan":
|
||||||
|
return "Комплексный план";
|
||||||
case "stations":
|
case "stations":
|
||||||
return "Станции";
|
return "Станции";
|
||||||
case "spacecraftMarkers":
|
case "spacecraftMarkers":
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { drawBaseMap } from "./canvas/drawBaseMap";
|
import { drawBaseMap } from "./canvas/drawBaseMap";
|
||||||
|
import { drawComplexPlan } from "./canvas/drawComplexPlan";
|
||||||
import { drawPlanWorks } from "./canvas/drawPlanWorks";
|
import { drawPlanWorks } from "./canvas/drawPlanWorks";
|
||||||
import { drawRequests } from "./canvas/drawRequests";
|
import { drawRequests } from "./canvas/drawRequests";
|
||||||
import { drawStations } from "./canvas/drawStations";
|
import { drawStations } from "./canvas/drawStations";
|
||||||
@@ -198,6 +199,9 @@ export function Tgu2DMapView({
|
|||||||
if (layers.swath) {
|
if (layers.swath) {
|
||||||
drawSwath(context, visiblePolygons.filter((polygon) => polygon.layer === "swath"), projection, size);
|
drawSwath(context, visiblePolygons.filter((polygon) => polygon.layer === "swath"), projection, size);
|
||||||
}
|
}
|
||||||
|
if (layers.complexPlan) {
|
||||||
|
drawComplexPlan(context, visiblePolygons.filter((polygon) => polygon.layer === "complexPlan"), projection, size);
|
||||||
|
}
|
||||||
if (layers.planWorks) {
|
if (layers.planWorks) {
|
||||||
drawPlanWorks(context, visiblePolygons.filter((polygon) => polygon.layer === "planWorks"), projection, size);
|
drawPlanWorks(context, visiblePolygons.filter((polygon) => polygon.layer === "planWorks"), projection, size);
|
||||||
}
|
}
|
||||||
@@ -597,6 +601,8 @@ function layerTitle(layer: MapPolygon["layer"]): string {
|
|||||||
return "Работы плана";
|
return "Работы плана";
|
||||||
case "swath":
|
case "swath":
|
||||||
return "Полоса обзора";
|
return "Полоса обзора";
|
||||||
|
case "complexPlan":
|
||||||
|
return "Комплексный план";
|
||||||
case "tracks":
|
case "tracks":
|
||||||
return "Трасса КА";
|
return "Трасса КА";
|
||||||
case "requests":
|
case "requests":
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import type { MapProjection } from "../geometry/mapProjection";
|
||||||
|
import type { MapPolygon } from "../model/mapTypes";
|
||||||
|
|
||||||
|
type Pt = { x: number; y: number };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* «Разворачивает» X-координаты так, чтобы соседние точки не расходились больше чем на
|
||||||
|
* полмира — иначе полигон, пересекающий антимеридиан (180°), рисуется через всю карту.
|
||||||
|
* Та же логика, что в drawSwath/drawPlanWorks — фикс антимеридиана.
|
||||||
|
*/
|
||||||
|
function unwrapX(points: Pt[], worldSize: number): Pt[] {
|
||||||
|
if (points.length === 0) return [];
|
||||||
|
const out: Pt[] = [points[0]];
|
||||||
|
let prevX = points[0].x;
|
||||||
|
for (let i = 1; i < points.length; i++) {
|
||||||
|
let x = points[i].x;
|
||||||
|
while (x - prevX > worldSize / 2) x -= worldSize;
|
||||||
|
while (prevX - x > worldSize / 2) x += worldSize;
|
||||||
|
out.push({ x, y: points[i].y });
|
||||||
|
prevX = x;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Контуры комплексного плана (этап 4). Отдельный слой со своим (янтарным) цветом, чтобы
|
||||||
|
* визуально не путать с «полосой обзора» (swath) и «работами плана» (planWorks). Рисуется
|
||||||
|
* в трёх копиях мира с отсечением по видимой области — как остальные полигональные слои.
|
||||||
|
*/
|
||||||
|
export function drawComplexPlan(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
polygons: MapPolygon[],
|
||||||
|
projection: MapProjection,
|
||||||
|
viewport: { width: number; height: number }
|
||||||
|
) {
|
||||||
|
ctx.save();
|
||||||
|
ctx.fillStyle = "rgba(240, 170, 70, 0.14)";
|
||||||
|
ctx.strokeStyle = "rgba(240, 170, 70, 0.62)";
|
||||||
|
ctx.lineWidth = 1.2;
|
||||||
|
for (const polygon of polygons) {
|
||||||
|
if (polygon.points.length < 3) continue;
|
||||||
|
const raw = polygon.points.map((point) => projection.project(point));
|
||||||
|
const pts = unwrapX(raw, projection.worldSize);
|
||||||
|
for (const dx of [0, projection.worldSize, -projection.worldSize]) {
|
||||||
|
const visible = pts.some((point) => {
|
||||||
|
const x = point.x + dx;
|
||||||
|
return x >= -64 && x <= viewport.width + 64 && point.y >= -64 && point.y <= viewport.height + 64;
|
||||||
|
});
|
||||||
|
if (!visible) continue;
|
||||||
|
ctx.beginPath();
|
||||||
|
for (let i = 0; i < pts.length; i++) {
|
||||||
|
if (i === 0) ctx.moveTo(pts[i].x + dx, pts[i].y);
|
||||||
|
else ctx.lineTo(pts[i].x + dx, pts[i].y);
|
||||||
|
}
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
@@ -1,4 +1,11 @@
|
|||||||
export type Tgu2DMapLayerKey = "tracks" | "swath" | "planWorks" | "stations" | "spacecraftMarkers" | "requests";
|
export type Tgu2DMapLayerKey =
|
||||||
|
| "tracks"
|
||||||
|
| "swath"
|
||||||
|
| "planWorks"
|
||||||
|
| "complexPlan"
|
||||||
|
| "stations"
|
||||||
|
| "spacecraftMarkers"
|
||||||
|
| "requests";
|
||||||
|
|
||||||
export type Tgu2DMapLayersState = Record<Tgu2DMapLayerKey, boolean>;
|
export type Tgu2DMapLayersState = Record<Tgu2DMapLayerKey, boolean>;
|
||||||
|
|
||||||
@@ -30,6 +37,7 @@ export const DEFAULT_TGU_2D_MAP_LAYERS: Tgu2DMapLayersState = {
|
|||||||
tracks: false,
|
tracks: false,
|
||||||
swath: false,
|
swath: false,
|
||||||
planWorks: true,
|
planWorks: true,
|
||||||
|
complexPlan: false,
|
||||||
stations: true,
|
stations: true,
|
||||||
spacecraftMarkers: false,
|
spacecraftMarkers: false,
|
||||||
requests: true
|
requests: true
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export type Tgu2DMapSelection =
|
|||||||
type: "polygon";
|
type: "polygon";
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
layer: "planWorks" | "swath" | "tracks" | "requests";
|
layer: "planWorks" | "swath" | "tracks" | "requests" | "complexPlan";
|
||||||
planId?: string;
|
planId?: string;
|
||||||
spacecraftId?: string;
|
spacecraftId?: string;
|
||||||
requestId?: string;
|
requestId?: string;
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ export type MapViewState = {
|
|||||||
export type MapPolygon = {
|
export type MapPolygon = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
kind: "planWork" | "swath" | "trackZone" | "request";
|
kind: "planWork" | "swath" | "trackZone" | "request" | "complexPlan";
|
||||||
layer: "planWorks" | "swath" | "tracks" | "requests";
|
layer: "planWorks" | "swath" | "tracks" | "requests" | "complexPlan";
|
||||||
points: GeoPoint[];
|
points: GeoPoint[];
|
||||||
status?: TguPlanStatus;
|
status?: TguPlanStatus;
|
||||||
planId?: string;
|
planId?: string;
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ describe("tgu2DMapInterval", () => {
|
|||||||
tracks: false,
|
tracks: false,
|
||||||
swath: false,
|
swath: false,
|
||||||
planWorks: true,
|
planWorks: true,
|
||||||
|
complexPlan: false,
|
||||||
stations: true,
|
stations: true,
|
||||||
spacecraftMarkers: false,
|
spacecraftMarkers: false,
|
||||||
requests: true
|
requests: true
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} from "../../api/tguApi";
|
} from "../../api/tguApi";
|
||||||
import type { TguPlan, TguPlanDecision, TguPlatform } from "../../model/tguTypes";
|
import type { TguPlan, TguPlanDecision, TguPlatform } from "../../model/tguTypes";
|
||||||
import type { TguActiveTab, TguPlanUi, TimelineRange } from "../../model/timelineTypes";
|
import type { TguActiveTab, TguPlanUi, TimelineRange } from "../../model/timelineTypes";
|
||||||
|
import { TguComplexPlanTab } from "../tgu-complex-plan/TguComplexPlanTab";
|
||||||
import { TguEditorTab } from "../tgu-editor/TguEditorTab";
|
import { TguEditorTab } from "../tgu-editor/TguEditorTab";
|
||||||
import { Tgu2DMapTab } from "../tgu-map-2d/Tgu2DMapTab";
|
import { Tgu2DMapTab } from "../tgu-map-2d/Tgu2DMapTab";
|
||||||
import { TguRequestsTab } from "../tgu-requests/TguRequestsTab";
|
import { TguRequestsTab } from "../tgu-requests/TguRequestsTab";
|
||||||
@@ -564,6 +565,15 @@ export function TguPlanningPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{visitedTabs.has("complex") && (
|
||||||
|
<div style={tabStyle("complex")}>
|
||||||
|
<TguComplexPlanTab
|
||||||
|
range={appliedRange}
|
||||||
|
platforms={platforms}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{visitedTabs.has("requests") && (
|
{visitedTabs.has("requests") && (
|
||||||
<div style={tabStyle("requests")}>
|
<div style={tabStyle("requests")}>
|
||||||
<TguRequestsTab
|
<TguRequestsTab
|
||||||
|
|||||||
@@ -56,6 +56,15 @@ export function TguToolbar({
|
|||||||
>
|
>
|
||||||
Карта
|
Карта
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className={activeTab === "complex" ? "is-active" : ""}
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
aria-selected={activeTab === "complex"}
|
||||||
|
onClick={() => onTabChange("complex")}
|
||||||
|
>
|
||||||
|
Комплексный план
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
className={activeTab === "editor" ? "is-active" : ""}
|
className={activeTab === "editor" ? "is-active" : ""}
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ export function TguRequestsTab({ range, invalidRange, platforms }: TguRequestsTa
|
|||||||
);
|
);
|
||||||
|
|
||||||
const layers = useMemo(
|
const layers = useMemo(
|
||||||
() => ({ tracks: false, swath: false, planWorks: false, stations: showStations, spacecraftMarkers: false, requests: true }),
|
() => ({ tracks: false, swath: false, planWorks: false, complexPlan: false, stations: showStations, spacecraftMarkers: false, requests: true }),
|
||||||
[showStations]
|
[showStations]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export type TimelineRange = {
|
|||||||
toMs: number;
|
toMs: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TguActiveTab = "timeline" | "map" | "editor" | "requests";
|
export type TguActiveTab = "timeline" | "map" | "complex" | "editor" | "requests";
|
||||||
|
|
||||||
export type TimelineRow = {
|
export type TimelineRow = {
|
||||||
spacecraftId: string;
|
spacecraftId: string;
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ export default defineConfig({
|
|||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path: string) => path.replace(/^\/api\/pcp-tgu/, "")
|
rewrite: (path: string) => path.replace(/^\/api\/pcp-tgu/, "")
|
||||||
},
|
},
|
||||||
|
"/api/pcp-complex": {
|
||||||
|
target: "http://localhost:7002",
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path: string) => path.replace(/^\/api\/pcp-complex/, "")
|
||||||
|
},
|
||||||
"/api/stations": {
|
"/api/stations": {
|
||||||
target: "http://localhost:7009",
|
target: "http://localhost:7009",
|
||||||
changeOrigin: true
|
changeOrigin: true
|
||||||
|
|||||||
Reference in New Issue
Block a user