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:
Дмитрий Соловьев
2026-05-30 23:28:58 +03:00
parent bb84844674
commit 42708450bb
16 changed files with 647 additions and 22 deletions
@@ -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
};
}