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,
|
||||
swath: true,
|
||||
planWorks: true,
|
||||
complexPlan: false,
|
||||
stations: true,
|
||||
spacecraftMarkers: false,
|
||||
requests: true
|
||||
|
||||
@@ -312,6 +312,8 @@ function layerLabel(layer: Tgu2DMapLayerKey): string {
|
||||
return "Полоса обзора";
|
||||
case "planWorks":
|
||||
return "Работы плана";
|
||||
case "complexPlan":
|
||||
return "Комплексный план";
|
||||
case "stations":
|
||||
return "Станции";
|
||||
case "spacecraftMarkers":
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { drawBaseMap } from "./canvas/drawBaseMap";
|
||||
import { drawComplexPlan } from "./canvas/drawComplexPlan";
|
||||
import { drawPlanWorks } from "./canvas/drawPlanWorks";
|
||||
import { drawRequests } from "./canvas/drawRequests";
|
||||
import { drawStations } from "./canvas/drawStations";
|
||||
@@ -198,6 +199,9 @@ export function Tgu2DMapView({
|
||||
if (layers.swath) {
|
||||
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) {
|
||||
drawPlanWorks(context, visiblePolygons.filter((polygon) => polygon.layer === "planWorks"), projection, size);
|
||||
}
|
||||
@@ -597,6 +601,8 @@ function layerTitle(layer: MapPolygon["layer"]): string {
|
||||
return "Работы плана";
|
||||
case "swath":
|
||||
return "Полоса обзора";
|
||||
case "complexPlan":
|
||||
return "Комплексный план";
|
||||
case "tracks":
|
||||
return "Трасса КА";
|
||||
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>;
|
||||
|
||||
@@ -30,6 +37,7 @@ export const DEFAULT_TGU_2D_MAP_LAYERS: Tgu2DMapLayersState = {
|
||||
tracks: false,
|
||||
swath: false,
|
||||
planWorks: true,
|
||||
complexPlan: false,
|
||||
stations: true,
|
||||
spacecraftMarkers: false,
|
||||
requests: true
|
||||
|
||||
@@ -5,7 +5,7 @@ export type Tgu2DMapSelection =
|
||||
type: "polygon";
|
||||
id: string;
|
||||
name: string;
|
||||
layer: "planWorks" | "swath" | "tracks" | "requests";
|
||||
layer: "planWorks" | "swath" | "tracks" | "requests" | "complexPlan";
|
||||
planId?: string;
|
||||
spacecraftId?: string;
|
||||
requestId?: string;
|
||||
|
||||
@@ -24,8 +24,8 @@ export type MapViewState = {
|
||||
export type MapPolygon = {
|
||||
id: string;
|
||||
name: string;
|
||||
kind: "planWork" | "swath" | "trackZone" | "request";
|
||||
layer: "planWorks" | "swath" | "tracks" | "requests";
|
||||
kind: "planWork" | "swath" | "trackZone" | "request" | "complexPlan";
|
||||
layer: "planWorks" | "swath" | "tracks" | "requests" | "complexPlan";
|
||||
points: GeoPoint[];
|
||||
status?: TguPlanStatus;
|
||||
planId?: string;
|
||||
|
||||
@@ -51,6 +51,7 @@ describe("tgu2DMapInterval", () => {
|
||||
tracks: false,
|
||||
swath: false,
|
||||
planWorks: true,
|
||||
complexPlan: false,
|
||||
stations: true,
|
||||
spacecraftMarkers: false,
|
||||
requests: true
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "../../api/tguApi";
|
||||
import type { TguPlan, TguPlanDecision, TguPlatform } from "../../model/tguTypes";
|
||||
import type { TguActiveTab, TguPlanUi, TimelineRange } from "../../model/timelineTypes";
|
||||
import { TguComplexPlanTab } from "../tgu-complex-plan/TguComplexPlanTab";
|
||||
import { TguEditorTab } from "../tgu-editor/TguEditorTab";
|
||||
import { Tgu2DMapTab } from "../tgu-map-2d/Tgu2DMapTab";
|
||||
import { TguRequestsTab } from "../tgu-requests/TguRequestsTab";
|
||||
@@ -564,6 +565,15 @@ export function TguPlanningPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{visitedTabs.has("complex") && (
|
||||
<div style={tabStyle("complex")}>
|
||||
<TguComplexPlanTab
|
||||
range={appliedRange}
|
||||
platforms={platforms}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{visitedTabs.has("requests") && (
|
||||
<div style={tabStyle("requests")}>
|
||||
<TguRequestsTab
|
||||
|
||||
@@ -56,6 +56,15 @@ export function TguToolbar({
|
||||
>
|
||||
Карта
|
||||
</button>
|
||||
<button
|
||||
className={activeTab === "complex" ? "is-active" : ""}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={activeTab === "complex"}
|
||||
onClick={() => onTabChange("complex")}
|
||||
>
|
||||
Комплексный план
|
||||
</button>
|
||||
<button
|
||||
className={activeTab === "editor" ? "is-active" : ""}
|
||||
type="button"
|
||||
|
||||
@@ -123,7 +123,7 @@ export function TguRequestsTab({ range, invalidRange, platforms }: TguRequestsTa
|
||||
);
|
||||
|
||||
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]
|
||||
);
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ export type TimelineRange = {
|
||||
toMs: number;
|
||||
};
|
||||
|
||||
export type TguActiveTab = "timeline" | "map" | "editor" | "requests";
|
||||
export type TguActiveTab = "timeline" | "map" | "complex" | "editor" | "requests";
|
||||
|
||||
export type TimelineRow = {
|
||||
spacecraftId: string;
|
||||
|
||||
@@ -27,6 +27,11 @@ export default defineConfig({
|
||||
changeOrigin: true,
|
||||
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": {
|
||||
target: "http://localhost:7009",
|
||||
changeOrigin: true
|
||||
|
||||
Reference in New Issue
Block a user