feat(tgu-ops-ui): display request polygons on 2D map
Добавлен слой заявок (КНС) на 2D карту ТГУ: - requestApi.ts — загрузка заявок постранично через GET /v1/requests/map с прогресс-коллбэком; автоматически собирает все страницы - wktParser.ts — парсинг WKT POLYGON/MULTIPOLYGON в массив координат - drawRequests.ts — Canvas-рендеринг полигонов заявок - Tgu2DMapLayers.ts — buildTgu2DMapScene строит MapPolygon[] из RequestMapItem[] - Tgu2DMapTab.tsx — состояние requestsState, кнопка «Загрузить», фильтры по дате - Tgu2DMapSidebar.tsx — панель слоя заявок: счётчик, ошибки, truncated-предупреждение - requestTypes.ts — RequestMapItem, RequestMapPage модели - Поддержка выбора заявки на карте (mapSelectionTypes, mapTypes) - nginx.conf, vite.config.ts — проксирование /v1 на pcp-request-service в dev Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,15 @@ server {
|
|||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|
||||||
|
location /api/pcp-request/ {
|
||||||
|
proxy_pass http://host.docker.internal:7005/;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
location /api/ {
|
location /api/ {
|
||||||
proxy_pass http://host.docker.internal:7008/api/;
|
proxy_pass http://host.docker.internal:7008/api/;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import type { RequestFilter, RequestMapItem } from "../model/requestTypes";
|
||||||
|
|
||||||
|
const BASE = "/api/pcp-request/v1";
|
||||||
|
const PAGE_SIZE = 500;
|
||||||
|
const MAX_PAGES = 10; // up to 5000 requests
|
||||||
|
|
||||||
|
type WithGeometryPage = {
|
||||||
|
items: RequestMapItem[];
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
totalItems: number;
|
||||||
|
totalPages: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function fetchWithGeometryPage(filter: RequestFilter, page: number): Promise<WithGeometryPage> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filter.beginFrom) params.set("beginFrom", filter.beginFrom);
|
||||||
|
if (filter.beginTo) params.set("beginTo", filter.beginTo);
|
||||||
|
if (filter.endFrom) params.set("endFrom", filter.endFrom);
|
||||||
|
if (filter.endTo) params.set("endTo", filter.endTo);
|
||||||
|
if (filter.status) params.set("status", filter.status);
|
||||||
|
if (filter.surveyType) params.set("surveyType", filter.surveyType);
|
||||||
|
params.set("size", String(PAGE_SIZE));
|
||||||
|
params.set("page", String(page));
|
||||||
|
params.set("sort", "beginDateTime,asc");
|
||||||
|
|
||||||
|
const response = await fetch(`${BASE}/requests/map?${params}`, {
|
||||||
|
headers: { Accept: "application/json" }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Ошибка загрузки заявок: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json() as Promise<WithGeometryPage>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchRequestsForMap(
|
||||||
|
filter: RequestFilter,
|
||||||
|
onProgress?: (loaded: number, total: number) => void
|
||||||
|
): Promise<{ items: RequestMapItem[]; totalItems: number; truncated: boolean }> {
|
||||||
|
const first = await fetchWithGeometryPage(filter, 0);
|
||||||
|
onProgress?.(first.items.length, first.totalItems);
|
||||||
|
|
||||||
|
if (first.totalPages <= 1) {
|
||||||
|
return { items: first.items, totalItems: first.totalItems, truncated: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const pagesToLoad = Math.min(first.totalPages, MAX_PAGES) - 1;
|
||||||
|
const remaining = await Promise.all(
|
||||||
|
Array.from({ length: pagesToLoad }, (_, i) => fetchWithGeometryPage(filter, i + 1))
|
||||||
|
);
|
||||||
|
|
||||||
|
const allItems = [...first.items, ...remaining.flatMap((page) => page.items)];
|
||||||
|
onProgress?.(allItems.length, first.totalItems);
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: allItems,
|
||||||
|
totalItems: first.totalItems,
|
||||||
|
truncated: first.totalPages > MAX_PAGES
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
import { useEffect, useMemo, useRef, useState, useCallback, type MutableRefObject } from "react";
|
import { useEffect, useMemo, useRef, useState, useCallback, type MutableRefObject } from "react";
|
||||||
|
import { fetchRequestsForMap } from "../../api/requestApi";
|
||||||
|
import type { RequestMapItem } from "../../model/requestTypes";
|
||||||
import { groundTrack, stationPasses, targetPasses } from "../tgu-map-2d/geometry/passes";
|
import { groundTrack, stationPasses, targetPasses } from "../tgu-map-2d/geometry/passes";
|
||||||
import { Tgu2DMapView } from "../tgu-map-2d/Tgu2DMapView";
|
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 { Tgu2DMapLayersState } from "../tgu-map-2d/model/mapLayerTypes";
|
||||||
import type { Tgu2DMapScene } from "../tgu-map-2d/model/mapTypes";
|
import type { MapPolygon, Tgu2DMapScene } from "../tgu-map-2d/model/mapTypes";
|
||||||
import { saveEditedPlan } from "./editorApi";
|
import { saveEditedPlan } from "./editorApi";
|
||||||
import {
|
import {
|
||||||
addDownlink,
|
addDownlink,
|
||||||
@@ -41,7 +44,8 @@ const EDITOR_MAP_LAYERS: Tgu2DMapLayersState = {
|
|||||||
swath: false,
|
swath: false,
|
||||||
planWorks: false,
|
planWorks: false,
|
||||||
stations: true,
|
stations: true,
|
||||||
spacecraftMarkers: false
|
spacecraftMarkers: false,
|
||||||
|
requests: true
|
||||||
};
|
};
|
||||||
|
|
||||||
export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange, platforms }: TguEditorTabProps) {
|
export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange, platforms }: TguEditorTabProps) {
|
||||||
@@ -67,6 +71,7 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
|||||||
const [notice, setNotice] = useState<string>();
|
const [notice, setNotice] = useState<string>();
|
||||||
const [applyError, setApplyError] = useState<string>();
|
const [applyError, setApplyError] = useState<string>();
|
||||||
const [contextSpacecraftIds, setContextSpacecraftIds] = useState<Set<string>>(new Set());
|
const [contextSpacecraftIds, setContextSpacecraftIds] = useState<Set<string>>(new Set());
|
||||||
|
const [requestItems, setRequestItems] = useState<RequestMapItem[]>([]);
|
||||||
const draft = history.draft;
|
const draft = history.draft;
|
||||||
const stations = useMemo(() => buildStations(selectedPlan), [selectedPlan]);
|
const stations = useMemo(() => buildStations(selectedPlan), [selectedPlan]);
|
||||||
const modes: EditorMode[] = DEFAULT_EDITOR_MODES;
|
const modes: EditorMode[] = DEFAULT_EDITOR_MODES;
|
||||||
@@ -87,8 +92,8 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
|||||||
[selectedSpacecraftId, selectedWork, shootPasses, stations, target, window]
|
[selectedSpacecraftId, selectedWork, shootPasses, stations, target, window]
|
||||||
);
|
);
|
||||||
const mapScene = useMemo(
|
const mapScene = useMemo(
|
||||||
() => buildEditorMapScene(selectedSpacecraftId, window, stations),
|
() => buildEditorMapScene(selectedSpacecraftId, window, stations, requestItems),
|
||||||
[selectedSpacecraftId, stations, window]
|
[selectedSpacecraftId, stations, window, requestItems]
|
||||||
);
|
);
|
||||||
const contextLanes = useMemo(
|
const contextLanes = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -115,6 +120,21 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
|||||||
setApplyError(undefined);
|
setApplyError(undefined);
|
||||||
}, [appliedRange, selectedPlan, selectedSpacecraftId]);
|
}, [appliedRange, selectedPlan, selectedSpacecraftId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
void fetchRequestsForMap({
|
||||||
|
endFrom: new Date(window.fromMs).toISOString(),
|
||||||
|
beginTo: new Date(window.toMs).toISOString()
|
||||||
|
}).then((result) => {
|
||||||
|
if (!controller.signal.aborted) {
|
||||||
|
setRequestItems(result.items);
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
// silently ignore — requests are supplementary
|
||||||
|
});
|
||||||
|
return () => controller.abort();
|
||||||
|
}, [window.fromMs, window.toMs]);
|
||||||
|
|
||||||
const changeDraft = (transform: (draft: DraftHistory["draft"]) => DraftHistory["draft"]) => {
|
const changeDraft = (transform: (draft: DraftHistory["draft"]) => DraftHistory["draft"]) => {
|
||||||
setHistory((current) => mutate(current, transform));
|
setHistory((current) => mutate(current, transform));
|
||||||
setServiceConflicts({});
|
setServiceConflicts({});
|
||||||
@@ -506,10 +526,9 @@ const MAP_TRACK_POINTS = 720; // 2-minute step over 24h → ~45 points per LEO o
|
|||||||
function buildEditorMapScene(
|
function buildEditorMapScene(
|
||||||
spacecraftId: string | undefined,
|
spacecraftId: string | undefined,
|
||||||
window: EditorTimelineWindow,
|
window: EditorTimelineWindow,
|
||||||
stations: EditorGroundStation[]
|
stations: EditorGroundStation[],
|
||||||
|
requestItems: RequestMapItem[]
|
||||||
): Tgu2DMapScene {
|
): Tgu2DMapScene {
|
||||||
// Always show 24h of ground track centred on the timeline window,
|
|
||||||
// regardless of how wide the timeline is zoomed out.
|
|
||||||
const center = (window.fromMs + window.toMs) / 2;
|
const center = (window.fromMs + window.toMs) / 2;
|
||||||
const mapRange = { fromMs: center - MAP_TRACK_WINDOW_MS / 2, toMs: center + MAP_TRACK_WINDOW_MS / 2 };
|
const mapRange = { fromMs: center - MAP_TRACK_WINDOW_MS / 2, toMs: center + MAP_TRACK_WINDOW_MS / 2 };
|
||||||
|
|
||||||
@@ -522,6 +541,7 @@ function buildEditorMapScene(
|
|||||||
zIndex: index
|
zIndex: index
|
||||||
}))
|
}))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const markers = stations.flatMap((station) => {
|
const markers = stations.flatMap((station) => {
|
||||||
if (station.lat === undefined || station.lon === undefined) {
|
if (station.lat === undefined || station.lon === undefined) {
|
||||||
return [];
|
return [];
|
||||||
@@ -535,7 +555,26 @@ function buildEditorMapScene(
|
|||||||
}];
|
}];
|
||||||
});
|
});
|
||||||
|
|
||||||
return { polygons: [], lines, markers };
|
const polygons: MapPolygon[] = [];
|
||||||
|
let zIndex = 0;
|
||||||
|
for (const item of requestItems) {
|
||||||
|
const rings = parseWktPolygon(item.geometry);
|
||||||
|
for (const ring of rings) {
|
||||||
|
if (ring.length < 3) continue;
|
||||||
|
polygons.push({
|
||||||
|
id: `request:${item.id}:${zIndex}`,
|
||||||
|
name: item.name,
|
||||||
|
kind: "request",
|
||||||
|
layer: "requests",
|
||||||
|
points: ring,
|
||||||
|
requestId: item.id,
|
||||||
|
surveyType: item.surveyType,
|
||||||
|
zIndex: zIndex++
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { polygons, lines, markers };
|
||||||
}
|
}
|
||||||
|
|
||||||
function platformLabel(platforms: TguEditorTabProps["platforms"], spacecraftId?: string): string {
|
function platformLabel(platforms: TguEditorTabProps["platforms"], spacecraftId?: string): string {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import type { RequestMapItem } from "../../model/requestTypes";
|
||||||
import type { TguPlanUi, TguPlatformUi, TimelineRange } from "../../model/timelineTypes";
|
import type { TguPlanUi, TguPlatformUi, TimelineRange } from "../../model/timelineTypes";
|
||||||
|
import { parseWktPolygon } from "./geometry/wktParser";
|
||||||
import type { Tgu2DMapLayersState } from "./model/mapLayerTypes";
|
import type { Tgu2DMapLayersState } from "./model/mapLayerTypes";
|
||||||
import type { Tgu2DMapScene } from "./model/mapTypes";
|
import type { MapPolygon, Tgu2DMapScene } from "./model/mapTypes";
|
||||||
|
|
||||||
export type Tgu2DMapLayerAvailability = Partial<Record<keyof Tgu2DMapLayersState, string>>;
|
export type Tgu2DMapLayerAvailability = Partial<Record<keyof Tgu2DMapLayersState, string>>;
|
||||||
|
|
||||||
@@ -9,18 +11,45 @@ export type Tgu2DMapSceneInput = {
|
|||||||
selectedPlan?: TguPlanUi;
|
selectedPlan?: TguPlanUi;
|
||||||
platforms: TguPlatformUi[];
|
platforms: TguPlatformUi[];
|
||||||
hiddenSpacecraftIds?: ReadonlySet<string>;
|
hiddenSpacecraftIds?: ReadonlySet<string>;
|
||||||
|
requestItems?: RequestMapItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const UNAVAILABLE = "Данные слоя недоступны в текущем API.";
|
const UNAVAILABLE = "Данные слоя недоступны в текущем API.";
|
||||||
|
|
||||||
export function buildTgu2DMapScene(_input: Tgu2DMapSceneInput): Tgu2DMapScene {
|
export function buildTgu2DMapScene(input: Tgu2DMapSceneInput): Tgu2DMapScene {
|
||||||
|
const requestPolygons = buildRequestPolygons(input.requestItems ?? []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
polygons: [],
|
polygons: requestPolygons,
|
||||||
lines: [],
|
lines: [],
|
||||||
markers: []
|
markers: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildRequestPolygons(items: RequestMapItem[]): MapPolygon[] {
|
||||||
|
const polygons: MapPolygon[] = [];
|
||||||
|
let zIndex = 0;
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const rings = parseWktPolygon(item.geometry);
|
||||||
|
for (const ring of rings) {
|
||||||
|
if (ring.length < 3) continue;
|
||||||
|
polygons.push({
|
||||||
|
id: `request:${item.id}:${zIndex}`,
|
||||||
|
name: item.name,
|
||||||
|
kind: "request",
|
||||||
|
layer: "requests",
|
||||||
|
points: ring,
|
||||||
|
requestId: item.id,
|
||||||
|
surveyType: item.surveyType,
|
||||||
|
zIndex: zIndex++
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return polygons;
|
||||||
|
}
|
||||||
|
|
||||||
export function getLayerAvailability(layers: Tgu2DMapLayersState, scene: Tgu2DMapScene): Tgu2DMapLayerAvailability {
|
export function getLayerAvailability(layers: Tgu2DMapLayersState, scene: Tgu2DMapScene): Tgu2DMapLayerAvailability {
|
||||||
const availability: Tgu2DMapLayerAvailability = {};
|
const availability: Tgu2DMapLayerAvailability = {};
|
||||||
if (layers.tracks && scene.lines.filter((line) => line.layer === "tracks").length === 0) {
|
if (layers.tracks && scene.lines.filter((line) => line.layer === "tracks").length === 0) {
|
||||||
@@ -40,4 +69,3 @@ export function getLayerAvailability(layers: Tgu2DMapLayersState, scene: Tgu2DMa
|
|||||||
}
|
}
|
||||||
return availability;
|
return availability;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import type { TguPlatformUi } from "../../model/timelineTypes";
|
|||||||
import { filterPlatforms, platformLabel } from "../tgu-planning/tguTimelineMapper";
|
import { filterPlatforms, platformLabel } from "../tgu-planning/tguTimelineMapper";
|
||||||
import { TGU_2D_MAP_LAYERS, type Tgu2DMapLayerKey, type Tgu2DMapLayersState } from "./model/mapLayerTypes";
|
import { TGU_2D_MAP_LAYERS, type Tgu2DMapLayerKey, type Tgu2DMapLayersState } from "./model/mapLayerTypes";
|
||||||
import type { Tgu2DMapLayerAvailability } from "./Tgu2DMapLayers";
|
import type { Tgu2DMapLayerAvailability } from "./Tgu2DMapLayers";
|
||||||
|
import type { RequestsState } from "./Tgu2DMapTab";
|
||||||
|
|
||||||
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
type Tgu2DMapSidebarProps = {
|
type Tgu2DMapSidebarProps = {
|
||||||
platforms: TguPlatformUi[];
|
platforms: TguPlatformUi[];
|
||||||
@@ -10,10 +13,15 @@ type Tgu2DMapSidebarProps = {
|
|||||||
layers: Tgu2DMapLayersState;
|
layers: Tgu2DMapLayersState;
|
||||||
search: string;
|
search: string;
|
||||||
layerAvailability: Tgu2DMapLayerAvailability;
|
layerAvailability: Tgu2DMapLayerAvailability;
|
||||||
|
requestsState: RequestsState;
|
||||||
onSearchChange: (value: string) => void;
|
onSearchChange: (value: string) => void;
|
||||||
onToggleSpacecraft: (id: string) => void;
|
onToggleSpacecraft: (id: string) => void;
|
||||||
onShowAll: () => void;
|
onShowAll: () => void;
|
||||||
onToggleLayer: (key: Tgu2DMapLayerKey) => void;
|
onToggleLayer: (key: Tgu2DMapLayerKey) => void;
|
||||||
|
onRequestsFromChange: (value: string) => void;
|
||||||
|
onRequestsToChange: (value: string) => void;
|
||||||
|
onRequestsApply: () => void;
|
||||||
|
onRequestsQuickRange: (fromMs: number, toMs: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Tgu2DMapSidebar({
|
export function Tgu2DMapSidebar({
|
||||||
@@ -23,14 +31,42 @@ export function Tgu2DMapSidebar({
|
|||||||
layers,
|
layers,
|
||||||
search,
|
search,
|
||||||
layerAvailability,
|
layerAvailability,
|
||||||
|
requestsState,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
onToggleSpacecraft,
|
onToggleSpacecraft,
|
||||||
onShowAll,
|
onShowAll,
|
||||||
onToggleLayer
|
onToggleLayer,
|
||||||
|
onRequestsFromChange,
|
||||||
|
onRequestsToChange,
|
||||||
|
onRequestsApply,
|
||||||
|
onRequestsQuickRange
|
||||||
}: Tgu2DMapSidebarProps) {
|
}: Tgu2DMapSidebarProps) {
|
||||||
const filteredPlatforms = filterPlatforms(platforms, search);
|
const filteredPlatforms = filterPlatforms(platforms, search);
|
||||||
const hiddenCount = hiddenIds.size;
|
const hiddenCount = hiddenIds.size;
|
||||||
|
|
||||||
|
const quickToday = () => {
|
||||||
|
const now = new Date();
|
||||||
|
const fromMs = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
||||||
|
onRequestsQuickRange(fromMs, fromMs + DAY_MS);
|
||||||
|
};
|
||||||
|
|
||||||
|
const quick7days = () => {
|
||||||
|
const fromMs = Date.now();
|
||||||
|
onRequestsQuickRange(fromMs, fromMs + 7 * DAY_MS);
|
||||||
|
};
|
||||||
|
|
||||||
|
const quick30days = () => {
|
||||||
|
const fromMs = Date.now();
|
||||||
|
onRequestsQuickRange(fromMs, fromMs + 30 * DAY_MS);
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestsFromMs = new Date(requestsState.fromValue).getTime();
|
||||||
|
const requestsToMs = new Date(requestsState.toValue).getTime();
|
||||||
|
const requestsRangeInvalid =
|
||||||
|
!Number.isFinite(requestsFromMs) ||
|
||||||
|
!Number.isFinite(requestsToMs) ||
|
||||||
|
requestsFromMs >= requestsToMs;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="tgu-map-panel">
|
<aside className="tgu-map-panel">
|
||||||
<div className="tgu-panel-heading">
|
<div className="tgu-panel-heading">
|
||||||
@@ -62,6 +98,73 @@ export function Tgu2DMapSidebar({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="tgu-map-panel__section">
|
||||||
|
<div className="tgu-map-panel__label">Заявки на съёмку</div>
|
||||||
|
<div className="tgu-map-requests-filter">
|
||||||
|
<label className="tgu-field">
|
||||||
|
<span>С</span>
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
value={requestsState.fromValue}
|
||||||
|
onChange={(e) => onRequestsFromChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="tgu-field">
|
||||||
|
<span>По</span>
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
value={requestsState.toValue}
|
||||||
|
onChange={(e) => onRequestsToChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div className="tgu-map-requests-filter__actions">
|
||||||
|
<button
|
||||||
|
className="tgu-button tgu-button--primary tgu-button--sm"
|
||||||
|
type="button"
|
||||||
|
onClick={onRequestsApply}
|
||||||
|
disabled={requestsState.loading || requestsRangeInvalid}
|
||||||
|
>
|
||||||
|
{requestsState.loading ? "Загрузка..." : "Загрузить"}
|
||||||
|
</button>
|
||||||
|
<button className="tgu-button tgu-button--sm" type="button" onClick={quickToday} disabled={requestsState.loading}>
|
||||||
|
Сегодня
|
||||||
|
</button>
|
||||||
|
<button className="tgu-button tgu-button--sm" type="button" onClick={quick7days} disabled={requestsState.loading}>
|
||||||
|
7 дней
|
||||||
|
</button>
|
||||||
|
<button className="tgu-button tgu-button--sm" type="button" onClick={quick30days} disabled={requestsState.loading}>
|
||||||
|
30 дней
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{requestsState.error && (
|
||||||
|
<div className="tgu-alert tgu-alert--danger tgu-alert--compact">{requestsState.error}</div>
|
||||||
|
)}
|
||||||
|
{requestsState.loading && requestsState.totalItems > 0 && (
|
||||||
|
<div className="tgu-map-requests-filter__status">
|
||||||
|
Загрузка: {requestsState.loadedCount} / {requestsState.totalItems}...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{requestsState.loaded && !requestsState.error && (
|
||||||
|
<div className="tgu-map-requests-filter__status">
|
||||||
|
{requestsState.items.length === 0
|
||||||
|
? "Заявок не найдено."
|
||||||
|
: `Загружено: ${requestsState.items.length} из ${requestsState.totalItems}`}
|
||||||
|
{requestsState.truncated && (
|
||||||
|
<span className="tgu-map-requests-filter__truncated"> (показаны первые 5000 — уточните интервал)</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{requestsState.loaded && requestsState.items.length > 0 && (
|
||||||
|
<div className="tgu-map-requests-legend">
|
||||||
|
<span><i className="req-optics" />OPTICS</span>
|
||||||
|
<span><i className="req-rsa" />RSA</span>
|
||||||
|
<span><i className="req-combined" />COMBINED</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="tgu-map-panel__section">
|
<div className="tgu-map-panel__section">
|
||||||
<div className="tgu-map-panel__label">Космические аппараты</div>
|
<div className="tgu-map-panel__label">Космические аппараты</div>
|
||||||
<div className="tgu-search">
|
<div className="tgu-search">
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { fetchRequestsForMap } from "../../api/requestApi";
|
||||||
|
import type { RequestMapItem } from "../../model/requestTypes";
|
||||||
import type { TguPlanUi, TguPlatformUi, TimelineRange } from "../../model/timelineTypes";
|
import type { TguPlanUi, TguPlatformUi, TimelineRange } from "../../model/timelineTypes";
|
||||||
import { platformLabel } from "../tgu-planning/tguTimelineMapper";
|
import { platformLabel } from "../tgu-planning/tguTimelineMapper";
|
||||||
import { DEFAULT_TGU_2D_MAP_LAYERS, type Tgu2DMapLayerKey, type Tgu2DMapLayersState } from "./model/mapLayerTypes";
|
import { DEFAULT_TGU_2D_MAP_LAYERS, type Tgu2DMapLayerKey, type Tgu2DMapLayersState } from "./model/mapLayerTypes";
|
||||||
@@ -17,6 +19,31 @@ type Tgu2DMapTabProps = {
|
|||||||
platforms: TguPlatformUi[];
|
platforms: TguPlatformUi[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type RequestsState = {
|
||||||
|
fromValue: string;
|
||||||
|
toValue: string;
|
||||||
|
items: RequestMapItem[];
|
||||||
|
loading: boolean;
|
||||||
|
loadedCount: number;
|
||||||
|
error?: string;
|
||||||
|
totalItems: number;
|
||||||
|
truncated: boolean;
|
||||||
|
loaded: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function buildInitialRequestsState(range: TimelineRange): RequestsState {
|
||||||
|
return {
|
||||||
|
fromValue: formatDateTimeLocal(range.fromMs),
|
||||||
|
toValue: formatDateTimeLocal(range.toMs),
|
||||||
|
items: [],
|
||||||
|
loading: false,
|
||||||
|
loadedCount: 0,
|
||||||
|
totalItems: 0,
|
||||||
|
truncated: false,
|
||||||
|
loaded: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function Tgu2DMapTab({
|
export function Tgu2DMapTab({
|
||||||
range,
|
range,
|
||||||
invalidRange,
|
invalidRange,
|
||||||
@@ -28,10 +55,68 @@ export function Tgu2DMapTab({
|
|||||||
const [redrawToken, setRedrawToken] = useState(0);
|
const [redrawToken, setRedrawToken] = useState(0);
|
||||||
const [selectedObject, setSelectedObject] = useState<Tgu2DMapSelection>();
|
const [selectedObject, setSelectedObject] = useState<Tgu2DMapSelection>();
|
||||||
const [hiddenIds, setHiddenIds] = useState<ReadonlySet<string>>(new Set());
|
const [hiddenIds, setHiddenIds] = useState<ReadonlySet<string>>(new Set());
|
||||||
|
const [requestsState, setRequestsState] = useState<RequestsState>(() => buildInitialRequestsState(range));
|
||||||
|
|
||||||
const mapInterval = getTgu2DMapIntervalState(range, selectedPlan);
|
const mapInterval = getTgu2DMapIntervalState(range, selectedPlan);
|
||||||
const mapInvalidRange = invalidRange || mapInterval.tooLarge;
|
const mapInvalidRange = invalidRange || mapInterval.tooLarge;
|
||||||
|
|
||||||
|
const loadRequests = useCallback(async (fromValue: string, toValue: string) => {
|
||||||
|
const fromMs = new Date(fromValue).getTime();
|
||||||
|
const toMs = new Date(toValue).getTime();
|
||||||
|
if (!Number.isFinite(fromMs) || !Number.isFinite(toMs) || fromMs >= toMs) return;
|
||||||
|
|
||||||
|
setRequestsState((current) => ({ ...current, loading: true, error: undefined, loadedCount: 0 }));
|
||||||
|
try {
|
||||||
|
const result = await fetchRequestsForMap(
|
||||||
|
{
|
||||||
|
endFrom: new Date(fromMs).toISOString(),
|
||||||
|
beginTo: new Date(toMs).toISOString()
|
||||||
|
},
|
||||||
|
(loaded, total) => {
|
||||||
|
setRequestsState((current) => ({ ...current, loadedCount: loaded, totalItems: total }));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
setRequestsState((current) => ({
|
||||||
|
...current,
|
||||||
|
items: result.items,
|
||||||
|
loadedCount: result.items.length,
|
||||||
|
totalItems: result.totalItems,
|
||||||
|
truncated: result.truncated,
|
||||||
|
loading: false,
|
||||||
|
loaded: true
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
setRequestsState((current) => ({
|
||||||
|
...current,
|
||||||
|
loading: false,
|
||||||
|
error: error instanceof Error ? error.message : "Не удалось загрузить заявки."
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const applyRequestsRange = useCallback(() => {
|
||||||
|
void loadRequests(requestsState.fromValue, requestsState.toValue);
|
||||||
|
}, [loadRequests, requestsState.fromValue, requestsState.toValue]);
|
||||||
|
|
||||||
|
const setRequestsFrom = useCallback((fromValue: string) => {
|
||||||
|
setRequestsState((current) => ({ ...current, fromValue }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setRequestsTo = useCallback((toValue: string) => {
|
||||||
|
setRequestsState((current) => ({ ...current, toValue }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setRequestsQuickRange = useCallback((fromMs: number, toMs: number) => {
|
||||||
|
const fromValue = formatDateTimeLocal(fromMs);
|
||||||
|
const toValue = formatDateTimeLocal(toMs);
|
||||||
|
setRequestsState((current) => ({ ...current, fromValue, toValue }));
|
||||||
|
void loadRequests(fromValue, toValue);
|
||||||
|
}, [loadRequests]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setRequestsState(buildInitialRequestsState(range));
|
||||||
|
}, [range]);
|
||||||
|
|
||||||
const toggleSpacecraft = useCallback((id: string) => {
|
const toggleSpacecraft = useCallback((id: string) => {
|
||||||
setHiddenIds((prev) => {
|
setHiddenIds((prev) => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
@@ -56,8 +141,14 @@ export function Tgu2DMapTab({
|
|||||||
const intervalLabel = `${formatDate(mapInterval.range.fromMs)} - ${formatDate(mapInterval.range.toMs)}`;
|
const intervalLabel = `${formatDate(mapInterval.range.fromMs)} - ${formatDate(mapInterval.range.toMs)}`;
|
||||||
|
|
||||||
const scene = useMemo(
|
const scene = useMemo(
|
||||||
() => buildTgu2DMapScene({ range: mapInterval.range, selectedPlan, platforms, hiddenSpacecraftIds: hiddenIds }),
|
() => buildTgu2DMapScene({
|
||||||
[mapInterval.range, platforms, selectedPlan, hiddenIds]
|
range: mapInterval.range,
|
||||||
|
selectedPlan,
|
||||||
|
platforms,
|
||||||
|
hiddenSpacecraftIds: hiddenIds,
|
||||||
|
requestItems: layers.requests ? requestsState.items : []
|
||||||
|
}),
|
||||||
|
[mapInterval.range, platforms, selectedPlan, hiddenIds, requestsState.items, layers.requests]
|
||||||
);
|
);
|
||||||
const layerAvailability = useMemo(() => getLayerAvailability(layers, scene), [layers, scene]);
|
const layerAvailability = useMemo(() => getLayerAvailability(layers, scene), [layers, scene]);
|
||||||
|
|
||||||
@@ -89,10 +180,15 @@ export function Tgu2DMapTab({
|
|||||||
layers={layers}
|
layers={layers}
|
||||||
search={search}
|
search={search}
|
||||||
layerAvailability={layerAvailability}
|
layerAvailability={layerAvailability}
|
||||||
|
requestsState={requestsState}
|
||||||
onSearchChange={setSearch}
|
onSearchChange={setSearch}
|
||||||
onToggleSpacecraft={toggleSpacecraft}
|
onToggleSpacecraft={toggleSpacecraft}
|
||||||
onShowAll={showAll}
|
onShowAll={showAll}
|
||||||
onToggleLayer={toggleLayer}
|
onToggleLayer={toggleLayer}
|
||||||
|
onRequestsFromChange={setRequestsFrom}
|
||||||
|
onRequestsToChange={setRequestsTo}
|
||||||
|
onRequestsApply={applyRequestsRange}
|
||||||
|
onRequestsQuickRange={setRequestsQuickRange}
|
||||||
/>
|
/>
|
||||||
<section className="tgu-map-stage">
|
<section className="tgu-map-stage">
|
||||||
{overlayMessage && <div className="tgu-map-overlay">{overlayMessage}</div>}
|
{overlayMessage && <div className="tgu-map-overlay">{overlayMessage}</div>}
|
||||||
@@ -129,6 +225,16 @@ function formatDate(ms: number): string {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDateTimeLocal(ms: number): string {
|
||||||
|
const date = new Date(ms);
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(date.getDate()).padStart(2, "0");
|
||||||
|
const hours = String(date.getHours()).padStart(2, "0");
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, "0");
|
||||||
|
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||||||
|
}
|
||||||
|
|
||||||
function layerLabel(layer: Tgu2DMapLayerKey): string {
|
function layerLabel(layer: Tgu2DMapLayerKey): string {
|
||||||
switch (layer) {
|
switch (layer) {
|
||||||
case "tracks":
|
case "tracks":
|
||||||
@@ -141,5 +247,7 @@ function layerLabel(layer: Tgu2DMapLayerKey): string {
|
|||||||
return "Станции";
|
return "Станции";
|
||||||
case "spacecraftMarkers":
|
case "spacecraftMarkers":
|
||||||
return "Маркеры КА";
|
return "Маркеры КА";
|
||||||
|
case "requests":
|
||||||
|
return "Заявки на съёмку";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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 { drawPlanWorks } from "./canvas/drawPlanWorks";
|
import { drawPlanWorks } from "./canvas/drawPlanWorks";
|
||||||
|
import { drawRequests } from "./canvas/drawRequests";
|
||||||
import { drawStations } from "./canvas/drawStations";
|
import { drawStations } from "./canvas/drawStations";
|
||||||
import { drawSwath } from "./canvas/drawSwath";
|
import { drawSwath } from "./canvas/drawSwath";
|
||||||
import { drawTiles } from "./canvas/drawTiles";
|
import { drawTiles } from "./canvas/drawTiles";
|
||||||
@@ -131,6 +132,9 @@ export function Tgu2DMapView({
|
|||||||
drawTiles(context, view, projection, size, handleTileLoad);
|
drawTiles(context, view, projection, size, handleTileLoad);
|
||||||
drawBaseMap(context, projection);
|
drawBaseMap(context, projection);
|
||||||
|
|
||||||
|
if (layers.requests) {
|
||||||
|
drawRequests(context, visiblePolygons.filter((polygon) => polygon.layer === "requests"), projection, size);
|
||||||
|
}
|
||||||
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);
|
||||||
}
|
}
|
||||||
@@ -242,6 +246,8 @@ export function Tgu2DMapView({
|
|||||||
layer: polygon.layer,
|
layer: polygon.layer,
|
||||||
planId: polygon.planId,
|
planId: polygon.planId,
|
||||||
spacecraftId: polygon.spacecraftId,
|
spacecraftId: polygon.spacecraftId,
|
||||||
|
requestId: polygon.requestId,
|
||||||
|
surveyType: polygon.surveyType,
|
||||||
status: polygon.status
|
status: polygon.status
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -329,6 +335,8 @@ function layerTitle(layer: MapPolygon["layer"]): string {
|
|||||||
return "Полоса обзора";
|
return "Полоса обзора";
|
||||||
case "tracks":
|
case "tracks":
|
||||||
return "Трасса КА";
|
return "Трасса КА";
|
||||||
|
case "requests":
|
||||||
|
return "Заявка на съёмку";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import type { MapProjection } from "../geometry/mapProjection";
|
||||||
|
import type { MapPolygon } from "../model/mapTypes";
|
||||||
|
|
||||||
|
const SURVEY_COLORS: Record<string, string> = {
|
||||||
|
OPTICS: "#FFA040",
|
||||||
|
RSA: "#40AFFF",
|
||||||
|
COMBINED: "#C080FF"
|
||||||
|
};
|
||||||
|
const DEFAULT_COLOR = "#FFA040";
|
||||||
|
|
||||||
|
export function drawRequests(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
polygons: MapPolygon[],
|
||||||
|
projection: MapProjection,
|
||||||
|
viewport: { width: number; height: number }
|
||||||
|
) {
|
||||||
|
ctx.save();
|
||||||
|
for (const polygon of polygons) {
|
||||||
|
if (polygon.points.length < 3) continue;
|
||||||
|
const screenPoints = polygon.points.map((point) => projection.project(point));
|
||||||
|
if (!screenPoints.some((point) => point.x >= -32 && point.x <= viewport.width + 32 && point.y >= -32 && point.y <= viewport.height + 32)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const color = SURVEY_COLORS[polygon.surveyType ?? ""] ?? DEFAULT_COLOR;
|
||||||
|
ctx.beginPath();
|
||||||
|
screenPoints.forEach((point, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
ctx.moveTo(point.x, point.y);
|
||||||
|
} else {
|
||||||
|
ctx.lineTo(point.x, point.y);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fillStyle = hexToRgba(color, 0.18);
|
||||||
|
ctx.strokeStyle = hexToRgba(color, 0.85);
|
||||||
|
ctx.lineWidth = 1.5;
|
||||||
|
ctx.setLineDash([4, 3]);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hexToRgba(hex: string, alpha: number): string {
|
||||||
|
const value = hex.replace("#", "");
|
||||||
|
const red = Number.parseInt(value.slice(0, 2), 16);
|
||||||
|
const green = Number.parseInt(value.slice(2, 4), 16);
|
||||||
|
const blue = Number.parseInt(value.slice(4, 6), 16);
|
||||||
|
return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import type { GeoPoint } from "../model/mapTypes";
|
||||||
|
|
||||||
|
export function parseWktPolygon(wkt: string): GeoPoint[][] {
|
||||||
|
const upper = wkt.trim().toUpperCase();
|
||||||
|
|
||||||
|
if (upper.startsWith("MULTIPOLYGON")) {
|
||||||
|
return parseMultiPolygon(wkt);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (upper.startsWith("POLYGON")) {
|
||||||
|
const ring = parseSinglePolygon(wkt);
|
||||||
|
return ring ? [ring] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSinglePolygon(wkt: string): GeoPoint[] | null {
|
||||||
|
const match = wkt.match(/POLYGON\s*\(\s*\(([^)]+)\)/i);
|
||||||
|
if (!match) return null;
|
||||||
|
return parseRing(match[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMultiPolygon(wkt: string): GeoPoint[][] {
|
||||||
|
const rings: GeoPoint[][] = [];
|
||||||
|
const inner = wkt.replace(/MULTIPOLYGON\s*\(/i, "").replace(/\)\s*$/, "");
|
||||||
|
const polygonStrings = inner.match(/\(\s*\([^)]+\)\s*\)/g);
|
||||||
|
if (!polygonStrings) return rings;
|
||||||
|
|
||||||
|
for (const polygonStr of polygonStrings) {
|
||||||
|
const ringMatch = polygonStr.match(/\(\s*\(([^)]+)\)/);
|
||||||
|
if (ringMatch) {
|
||||||
|
const ring = parseRing(ringMatch[1]);
|
||||||
|
if (ring.length >= 3) {
|
||||||
|
rings.push(ring);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rings;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseRing(coordStr: string): GeoPoint[] {
|
||||||
|
return coordStr
|
||||||
|
.split(",")
|
||||||
|
.map((pair) => {
|
||||||
|
const parts = pair.trim().split(/\s+/);
|
||||||
|
const lon = parseFloat(parts[0]);
|
||||||
|
const lat = parseFloat(parts[1]);
|
||||||
|
if (!Number.isFinite(lon) || !Number.isFinite(lat)) return null;
|
||||||
|
return { lon, lat };
|
||||||
|
})
|
||||||
|
.filter((point): point is GeoPoint => point !== null);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export type Tgu2DMapLayerKey = "tracks" | "swath" | "planWorks" | "stations" | "spacecraftMarkers";
|
export type Tgu2DMapLayerKey = "tracks" | "swath" | "planWorks" | "stations" | "spacecraftMarkers" | "requests";
|
||||||
|
|
||||||
export type Tgu2DMapLayersState = Record<Tgu2DMapLayerKey, boolean>;
|
export type Tgu2DMapLayersState = Record<Tgu2DMapLayerKey, boolean>;
|
||||||
|
|
||||||
@@ -33,6 +33,11 @@ export const TGU_2D_MAP_LAYERS: Tgu2DMapLayerDefinition[] = [
|
|||||||
key: "spacecraftMarkers",
|
key: "spacecraftMarkers",
|
||||||
label: "Маркеры КА",
|
label: "Маркеры КА",
|
||||||
description: "Текущие или расчётные позиции КА."
|
description: "Текущие или расчётные позиции КА."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "requests",
|
||||||
|
label: "Заявки на съёмку",
|
||||||
|
description: "Полигоны заявок из pcp-request-service."
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -41,6 +46,7 @@ export const DEFAULT_TGU_2D_MAP_LAYERS: Tgu2DMapLayersState = {
|
|||||||
swath: true,
|
swath: true,
|
||||||
planWorks: true,
|
planWorks: true,
|
||||||
stations: true,
|
stations: true,
|
||||||
spacecraftMarkers: true
|
spacecraftMarkers: true,
|
||||||
|
requests: true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ export type Tgu2DMapSelection =
|
|||||||
type: "polygon";
|
type: "polygon";
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
layer: "planWorks" | "swath" | "tracks";
|
layer: "planWorks" | "swath" | "tracks" | "requests";
|
||||||
planId?: string;
|
planId?: string;
|
||||||
spacecraftId?: string;
|
spacecraftId?: string;
|
||||||
|
requestId?: string;
|
||||||
|
surveyType?: string;
|
||||||
status?: TguPlanStatus;
|
status?: TguPlanStatus;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
|
|||||||
@@ -24,12 +24,14 @@ export type MapViewState = {
|
|||||||
export type MapPolygon = {
|
export type MapPolygon = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
kind: "planWork" | "swath" | "trackZone";
|
kind: "planWork" | "swath" | "trackZone" | "request";
|
||||||
layer: "planWorks" | "swath" | "tracks";
|
layer: "planWorks" | "swath" | "tracks" | "requests";
|
||||||
points: GeoPoint[];
|
points: GeoPoint[];
|
||||||
status?: TguPlanStatus;
|
status?: TguPlanStatus;
|
||||||
planId?: string;
|
planId?: string;
|
||||||
spacecraftId?: string;
|
spacecraftId?: string;
|
||||||
|
requestId?: string;
|
||||||
|
surveyType?: string;
|
||||||
zIndex: number;
|
zIndex: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,8 @@ describe("tgu2DMapInterval", () => {
|
|||||||
swath: true,
|
swath: true,
|
||||||
planWorks: true,
|
planWorks: true,
|
||||||
stations: true,
|
stations: true,
|
||||||
spacecraftMarkers: true
|
spacecraftMarkers: true,
|
||||||
|
requests: true
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
export type RequestStatus = "ACCEPTED" | "ACTIVE" | "COMPLETED" | "EXPIRED" | "DELETED";
|
||||||
|
export type SurveyType = "OPTICS" | "RSA" | "COMBINED";
|
||||||
|
|
||||||
|
export type RequestSummary = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
status: RequestStatus;
|
||||||
|
surveyType: SurveyType;
|
||||||
|
importance: number;
|
||||||
|
beginDateTime: string;
|
||||||
|
endDateTime: string;
|
||||||
|
kpp?: number[];
|
||||||
|
highPriorityTransmit: boolean;
|
||||||
|
coveragePercent: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RequestDetail = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
status: RequestStatus;
|
||||||
|
surveyType: SurveyType;
|
||||||
|
geometry: string;
|
||||||
|
importance: number;
|
||||||
|
beginDateTime: string;
|
||||||
|
endDateTime: string;
|
||||||
|
kpp?: number[];
|
||||||
|
highPriorityTransmit: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RequestListResponse = {
|
||||||
|
items: RequestSummary[];
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
totalItems: number;
|
||||||
|
totalPages: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RequestFilter = {
|
||||||
|
beginFrom?: string;
|
||||||
|
beginTo?: string;
|
||||||
|
endFrom?: string;
|
||||||
|
endTo?: string;
|
||||||
|
status?: RequestStatus;
|
||||||
|
surveyType?: SurveyType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RequestMapItem = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
status: RequestStatus;
|
||||||
|
surveyType: SurveyType;
|
||||||
|
geometry: string;
|
||||||
|
importance: number;
|
||||||
|
beginDateTime: string;
|
||||||
|
endDateTime: string;
|
||||||
|
kpp?: number[];
|
||||||
|
highPriorityTransmit: boolean;
|
||||||
|
coveragePercent: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
@@ -978,3 +978,84 @@
|
|||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Requests filter panel ─────────────────────────────── */
|
||||||
|
|
||||||
|
.tgu-button--sm {
|
||||||
|
min-height: 1.75rem;
|
||||||
|
padding: 0.28rem 0.55rem;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tgu-alert--compact {
|
||||||
|
padding: 0.42rem 0.58rem;
|
||||||
|
font-size: 0.76rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tgu-map-requests-filter {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tgu-map-requests-filter .tgu-field {
|
||||||
|
grid-template-columns: 2rem minmax(0, 1fr);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tgu-map-requests-filter .tgu-field input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tgu-map-requests-filter__actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tgu-map-requests-filter__status {
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tgu-map-requests-filter__truncated {
|
||||||
|
color: #ffe0a3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tgu-map-requests-legend {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.45rem 0.8rem;
|
||||||
|
padding-top: 0.2rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 0.74rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tgu-map-requests-legend span {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.38rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tgu-map-requests-legend i {
|
||||||
|
display: inline-block;
|
||||||
|
width: 1.1rem;
|
||||||
|
height: 0.42rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1.5px dashed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.req-optics {
|
||||||
|
background: rgba(255, 160, 64, 0.25);
|
||||||
|
border-color: rgba(255, 160, 64, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.req-rsa {
|
||||||
|
background: rgba(64, 175, 255, 0.25);
|
||||||
|
border-color: rgba(64, 175, 255, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.req-combined {
|
||||||
|
background: rgba(192, 128, 255, 0.25);
|
||||||
|
border-color: rgba(192, 128, 255, 0.85);
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
port: 5174,
|
port: 5174,
|
||||||
proxy: {
|
proxy: {
|
||||||
|
"/api/pcp-request": {
|
||||||
|
target: "http://localhost:7005",
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path: string) => path.replace(/^\/api\/pcp-request/, "")
|
||||||
|
},
|
||||||
"/api": {
|
"/api": {
|
||||||
target: "http://localhost:7008",
|
target: "http://localhost:7008",
|
||||||
changeOrigin: true
|
changeOrigin: true
|
||||||
|
|||||||
Reference in New Issue
Block a user