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,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 { platformLabel } from "../tgu-planning/tguTimelineMapper";
|
||||
import { DEFAULT_TGU_2D_MAP_LAYERS, type Tgu2DMapLayerKey, type Tgu2DMapLayersState } from "./model/mapLayerTypes";
|
||||
@@ -17,6 +19,31 @@ type Tgu2DMapTabProps = {
|
||||
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({
|
||||
range,
|
||||
invalidRange,
|
||||
@@ -28,10 +55,68 @@ export function Tgu2DMapTab({
|
||||
const [redrawToken, setRedrawToken] = useState(0);
|
||||
const [selectedObject, setSelectedObject] = useState<Tgu2DMapSelection>();
|
||||
const [hiddenIds, setHiddenIds] = useState<ReadonlySet<string>>(new Set());
|
||||
const [requestsState, setRequestsState] = useState<RequestsState>(() => buildInitialRequestsState(range));
|
||||
|
||||
const mapInterval = getTgu2DMapIntervalState(range, selectedPlan);
|
||||
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) => {
|
||||
setHiddenIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
@@ -56,8 +141,14 @@ export function Tgu2DMapTab({
|
||||
const intervalLabel = `${formatDate(mapInterval.range.fromMs)} - ${formatDate(mapInterval.range.toMs)}`;
|
||||
|
||||
const scene = useMemo(
|
||||
() => buildTgu2DMapScene({ range: mapInterval.range, selectedPlan, platforms, hiddenSpacecraftIds: hiddenIds }),
|
||||
[mapInterval.range, platforms, selectedPlan, hiddenIds]
|
||||
() => buildTgu2DMapScene({
|
||||
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]);
|
||||
|
||||
@@ -89,10 +180,15 @@ export function Tgu2DMapTab({
|
||||
layers={layers}
|
||||
search={search}
|
||||
layerAvailability={layerAvailability}
|
||||
requestsState={requestsState}
|
||||
onSearchChange={setSearch}
|
||||
onToggleSpacecraft={toggleSpacecraft}
|
||||
onShowAll={showAll}
|
||||
onToggleLayer={toggleLayer}
|
||||
onRequestsFromChange={setRequestsFrom}
|
||||
onRequestsToChange={setRequestsTo}
|
||||
onRequestsApply={applyRequestsRange}
|
||||
onRequestsQuickRange={setRequestsQuickRange}
|
||||
/>
|
||||
<section className="tgu-map-stage">
|
||||
{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 {
|
||||
switch (layer) {
|
||||
case "tracks":
|
||||
@@ -141,5 +247,7 @@ function layerLabel(layer: Tgu2DMapLayerKey): string {
|
||||
return "Станции";
|
||||
case "spacecraftMarkers":
|
||||
return "Маркеры КА";
|
||||
case "requests":
|
||||
return "Заявки на съёмку";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user