4ab765b0ee
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>
125 lines
3.4 KiB
TypeScript
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;
|
|
}
|