Files
dc-observatio/services/pcp-tgu-ops-ui/src/api/tguApi.ts
T
Дмитрий Соловьев 4ab765b0ee feat(tgu-ops-ui): display real ground track and swath from ballistics service
Replace synthetic orbit calculation (Kepler + spacecraft ID hash) with real
data from pcp-ballistics-service via a new proxy endpoint in pcp-ui-service.

Backend:
- TguPlanningController: add GET /api/tgu-planning/spacecraft/{noradId}/flight-line
  that proxies FlightLineDTO[] from BallisticsService for a given time interval

Frontend:
- Fetch FlightLineDTO[] (revolution, time, lat/long, swath boundaries) using
  the spacecraft's noradId for the current 24h map window
- Group points by real revolution number to build OrbitPassInfo[] with actual
  orbit numbers and UTC times instead of synthetic Kepler-based estimates
- Build ground track MapLines and swath MapPolygons from real coordinates;
  swath polygon = outer-left edge forward + outer-right edge reversed
- Enable swath layer in the editor map; draw with teal fill/stroke matching
  the track colour
- MapPassSelector now shows real revolution numbers in header and handle labels

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 16:05:41 +03:00

125 lines
3.4 KiB
TypeScript

import {
TguApiError,
type TguPlan,
type TguPlanDecision,
type TguPlanDecisionMessage,
type TguPlatform
} from "../model/tguTypes";
export type FlightLinePoint = {
time: string;
revolution: number;
lat: number;
long: number;
latLeft: number;
longLeft: number;
latInnerLeft: number;
longInnerLeft: number;
latInnerRight: number;
longInnerRight: number;
latRight: number;
longRight: number;
revSign: "ASC" | "DESC";
};
export async function fetchFlightLine(
noradId: string,
fromMs: number,
toMs: number
): Promise<FlightLinePoint[]> {
const toLocalDT = (ms: number) => new Date(ms).toISOString().slice(0, 19);
const query = new URLSearchParams({
time_start: toLocalDT(fromMs),
time_stop: toLocalDT(toMs)
});
return fetchJson<FlightLinePoint[]>(
`/api/tgu-planning/spacecraft/${encodeURIComponent(noradId)}/flight-line?${query}`
);
}
export async function fetchPlatforms(): Promise<TguPlatform[]> {
return fetchJson<TguPlatform[]>("/api/tgu-planning/platforms");
}
export async function fetchPlans(historyDays: number, spacecraftId?: string): Promise<TguPlan[]> {
const query = new URLSearchParams({ historyDays: String(historyDays) });
const path = spacecraftId
? `/api/tgu-planning/spacecraft/${encodeURIComponent(spacecraftId)}/plans?${query}`
: `/api/tgu-planning/plans?${query}`;
return fetchJson<TguPlan[]>(path);
}
export async function sendPlanDecision(
planId: string,
decision: TguPlanDecision,
reason?: string
): Promise<TguPlanDecisionMessage> {
const query = new URLSearchParams({ decision });
if (reason?.trim()) {
query.set("reason", reason.trim());
}
return fetchJson<TguPlanDecisionMessage>(
`/api/tgu-planning/plans/${encodeURIComponent(planId)}/decision?${query}`,
{ method: "POST" },
true
);
}
async function fetchJson<T>(url: string, init?: RequestInit, decisionRequest = false): Promise<T> {
let response: Response;
try {
response = await fetch(url, {
headers: { Accept: "application/json" },
...init
});
} catch (error) {
throw new TguApiError("Не удалось подключиться к backend API.", "network");
}
const text = await response.text();
if (!response.ok) {
throw new TguApiError(errorMessage(response.status, text, decisionRequest), "http", response.status);
}
if (!text.trim()) {
return undefined as T;
}
return JSON.parse(text) as T;
}
function errorMessage(status: number, body: string, decisionRequest: boolean): string {
if (decisionRequest && status === 404) {
return "План не найден.";
}
if (decisionRequest && status === 409) {
return "План не является активным планом, ожидающим решения, или для него нет active attempt.";
}
if (decisionRequest && status >= 500) {
return "Ошибка сервиса при отправке решения.";
}
return extractErrorText(body) || "Ошибка backend API.";
}
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 ["reason", "message", "error", "detail"]) {
const value = parsed[field];
if (typeof value === "string" && value.trim()) {
return value;
}
}
} catch {
return trimmed;
}
return trimmed;
}