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:
@@ -1,8 +1,11 @@
|
||||
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 { 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 { Tgu2DMapScene } from "../tgu-map-2d/model/mapTypes";
|
||||
import type { MapPolygon, Tgu2DMapScene } from "../tgu-map-2d/model/mapTypes";
|
||||
import { saveEditedPlan } from "./editorApi";
|
||||
import {
|
||||
addDownlink,
|
||||
@@ -41,7 +44,8 @@ const EDITOR_MAP_LAYERS: Tgu2DMapLayersState = {
|
||||
swath: false,
|
||||
planWorks: false,
|
||||
stations: true,
|
||||
spacecraftMarkers: false
|
||||
spacecraftMarkers: false,
|
||||
requests: true
|
||||
};
|
||||
|
||||
export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange, platforms }: TguEditorTabProps) {
|
||||
@@ -67,6 +71,7 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
const [notice, setNotice] = useState<string>();
|
||||
const [applyError, setApplyError] = useState<string>();
|
||||
const [contextSpacecraftIds, setContextSpacecraftIds] = useState<Set<string>>(new Set());
|
||||
const [requestItems, setRequestItems] = useState<RequestMapItem[]>([]);
|
||||
const draft = history.draft;
|
||||
const stations = useMemo(() => buildStations(selectedPlan), [selectedPlan]);
|
||||
const modes: EditorMode[] = DEFAULT_EDITOR_MODES;
|
||||
@@ -87,8 +92,8 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
[selectedSpacecraftId, selectedWork, shootPasses, stations, target, window]
|
||||
);
|
||||
const mapScene = useMemo(
|
||||
() => buildEditorMapScene(selectedSpacecraftId, window, stations),
|
||||
[selectedSpacecraftId, stations, window]
|
||||
() => buildEditorMapScene(selectedSpacecraftId, window, stations, requestItems),
|
||||
[selectedSpacecraftId, stations, window, requestItems]
|
||||
);
|
||||
const contextLanes = useMemo(
|
||||
() =>
|
||||
@@ -115,6 +120,21 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
setApplyError(undefined);
|
||||
}, [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"]) => {
|
||||
setHistory((current) => mutate(current, transform));
|
||||
setServiceConflicts({});
|
||||
@@ -506,10 +526,9 @@ const MAP_TRACK_POINTS = 720; // 2-minute step over 24h → ~45 points per LEO o
|
||||
function buildEditorMapScene(
|
||||
spacecraftId: string | undefined,
|
||||
window: EditorTimelineWindow,
|
||||
stations: EditorGroundStation[]
|
||||
stations: EditorGroundStation[],
|
||||
requestItems: RequestMapItem[]
|
||||
): 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 mapRange = { fromMs: center - MAP_TRACK_WINDOW_MS / 2, toMs: center + MAP_TRACK_WINDOW_MS / 2 };
|
||||
|
||||
@@ -522,6 +541,7 @@ function buildEditorMapScene(
|
||||
zIndex: index
|
||||
}))
|
||||
: [];
|
||||
|
||||
const markers = stations.flatMap((station) => {
|
||||
if (station.lat === undefined || station.lon === undefined) {
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user