Редактор: ручное добавление маршрутов съёмки и сбросов
Режим «Сброс» теперь рабочий: загружает ЗРВ станций (ballisticsApi.fetchSatelliteRva), даёт оператору выбрать зону, затем показывает съёмки плана, заканчивающиеся строго до начала зоны, и сохраняет сброс в плане (missionPlanningApi.saveDropRoute → DropPanel). Старый фейковый DownlinkPanel на stationPasses убран из потока. Режим «Съёмка» сохраняет маршрут через saveSurveyRoute. Времена ЗРВ приходят как LocalDateTime без зоны и трактуются как UTC (helper localDateTimeMs), чтобы сравнивать с временами съёмок. В vite-прокси добавлены pcp-ballistics, pcp-mission и stations.
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
import { TguApiError } from "../model/tguTypes";
|
||||
|
||||
const BASE = "/api/pcp-ballistics";
|
||||
|
||||
export type PointVisibilityParam = {
|
||||
/** NORAD-идентификатор КА. */
|
||||
noradId: number;
|
||||
/** Идентификатор объекта (цели) из запроса. */
|
||||
objectId: string;
|
||||
/** Момент видимости (локальное время, ISO без зоны). */
|
||||
time: string;
|
||||
/** Номер витка. */
|
||||
revolution: number;
|
||||
/** Крен (gamma), град. */
|
||||
gamma: number;
|
||||
/** Угол Солнца, град. */
|
||||
sunAngle: number;
|
||||
/** Наклонная дальность. */
|
||||
range: number;
|
||||
/** Направление витка. */
|
||||
revSign: "ASC" | "DESC";
|
||||
};
|
||||
|
||||
export type PointVisibilityFilters = {
|
||||
/** Минимальный угол Солнца, град. */
|
||||
sunAngleMin?: number;
|
||||
/** Минимальный крен, град. */
|
||||
rollMin?: number;
|
||||
/** Максимальный крен, град. */
|
||||
rollMax?: number;
|
||||
};
|
||||
|
||||
export type GeoPointTarget = {
|
||||
id?: string;
|
||||
lat: number;
|
||||
lon: number;
|
||||
height?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Параметры видимости точки на заданном интервале времени из pcp-ballistics-service
|
||||
* (POST /api/obj-view/mpl-point). Возвращает по одному моменту видимости на виток.
|
||||
*/
|
||||
export async function fetchPointVisibility(
|
||||
noradId: number,
|
||||
target: GeoPointTarget,
|
||||
fromMs: number,
|
||||
toMs: number,
|
||||
filters: PointVisibilityFilters = {}
|
||||
): Promise<PointVisibilityParam[]> {
|
||||
const body = {
|
||||
satellites: [noradId],
|
||||
timeStart: toLocalDateTime(fromMs),
|
||||
timeStop: toLocalDateTime(toMs),
|
||||
obj: {
|
||||
id: target.id ?? "",
|
||||
position: {
|
||||
lat: target.lat,
|
||||
long: target.lon,
|
||||
height: target.height ?? 0
|
||||
}
|
||||
},
|
||||
sunAngleMin: filters.sunAngleMin ?? null,
|
||||
rollMin: filters.rollMin ?? null,
|
||||
rollMax: filters.rollMax ?? null
|
||||
};
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(`${BASE}/api/obj-view/mpl-point`, {
|
||||
method: "POST",
|
||||
headers: { Accept: "application/json", "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
} catch {
|
||||
throw new TguApiError("Не удалось подключиться к сервису баллистики.", "network");
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new TguApiError(extractErrorText(text) || "Ошибка сервиса баллистики.", "http", response.status);
|
||||
}
|
||||
|
||||
if (!text.trim()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return JSON.parse(text) as PointVisibilityParam[];
|
||||
}
|
||||
|
||||
/** Целеуказание (момент/угол места/азимут/дальность) границы зоны радиовидимости. */
|
||||
export type RvaTargetPosition = {
|
||||
/** Момент (локальное время, ISO без зоны). */
|
||||
time: string;
|
||||
/** Угол места, град. */
|
||||
elevation: number;
|
||||
/** Азимут, град. */
|
||||
azimuth: number;
|
||||
/** Наклонная дальность. */
|
||||
range: number;
|
||||
};
|
||||
|
||||
/** Зона радиовидимости (ЗРВ) станции для КА на витке. */
|
||||
export type RvaZone = {
|
||||
/** NORAD-идентификатор КА. */
|
||||
noradId: number;
|
||||
/** Номер станции приёма (совпадает с number из /api/stations). */
|
||||
stationId: number;
|
||||
/** Номер витка. */
|
||||
revolution: number;
|
||||
/** Вход в зону. */
|
||||
onStart: RvaTargetPosition;
|
||||
/** Максимум (кульминация). */
|
||||
onMaximum: RvaTargetPosition;
|
||||
/** Выход из зоны. */
|
||||
onStop: RvaTargetPosition;
|
||||
/** Длительность зоны, секунды. */
|
||||
duration: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Зоны радиовидимости (ЗРВ) для КА на заданном интервале из pcp-ballistics-service
|
||||
* (GET /api/satellites/{norad}/rva). Баллистика считает зоны по реальным станциям приёма
|
||||
* (каталог берёт у сервиса станций); каждая зона помечена номером станции (stationId).
|
||||
*/
|
||||
export async function fetchSatelliteRva(
|
||||
noradId: number,
|
||||
fromMs: number,
|
||||
toMs: number
|
||||
): Promise<RvaZone[]> {
|
||||
const params = new URLSearchParams({
|
||||
time_start: toLocalDateTime(fromMs),
|
||||
time_stop: toLocalDateTime(toMs)
|
||||
});
|
||||
const url = `${BASE}/api/satellites/${noradId}/rva?${params.toString()}`;
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(url, { headers: { Accept: "application/json" } });
|
||||
} catch {
|
||||
throw new TguApiError("Не удалось подключиться к сервису баллистики.", "network");
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new TguApiError(extractErrorText(text) || "Ошибка сервиса баллистики.", "http", response.status);
|
||||
}
|
||||
if (!text.trim()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return JSON.parse(text) as RvaZone[];
|
||||
}
|
||||
|
||||
/** LocalDateTime без зоны: YYYY-MM-DDTHH:mm:ss. */
|
||||
function toLocalDateTime(ms: number): string {
|
||||
return new Date(ms).toISOString().slice(0, 19);
|
||||
}
|
||||
|
||||
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 ["message", "error", "reason", "detail"]) {
|
||||
const value = parsed[field];
|
||||
if (typeof value === "string" && value.trim()) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return trimmed;
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import { TguApiError } from "../model/tguTypes";
|
||||
|
||||
const BASE = "/api/pcp-mission";
|
||||
|
||||
/** Способ построения окна маршрута относительно момента видимости. */
|
||||
export type SurveyRouteAnchor = "CENTERED" | "FORWARD";
|
||||
|
||||
export type BuildSurveyRouteParams = {
|
||||
/** Идентификатор КА (NORAD — тот же, что в obj-view/mpl-point). */
|
||||
satelliteId: number;
|
||||
/** Момент видимости (локальное время, ISO без зоны). */
|
||||
time: string;
|
||||
/** Длительность съёмки, секунды. */
|
||||
durationSeconds: number;
|
||||
/** Крен (gamma), градусы. */
|
||||
roll: number;
|
||||
/** Угол захвата (полуширина полосы), градусы. */
|
||||
captureAngle: number;
|
||||
/** Номер витка (0 — определить по орбитальным точкам). */
|
||||
revolution?: number;
|
||||
/** Способ построения окна относительно момента видимости. */
|
||||
anchor?: SurveyRouteAnchor;
|
||||
/**
|
||||
* Начало интервала плана (локальное время, ISO без зоны). Передаётся при сохранении,
|
||||
* чтобы при автосоздании миссии/плана задать корректное окно (для пересечений/наложений).
|
||||
*/
|
||||
planStart?: string;
|
||||
/** Конец интервала плана (локальное время, ISO без зоны). См. planStart. */
|
||||
planEnd?: string;
|
||||
};
|
||||
|
||||
export type SurveyRoute = {
|
||||
satelliteId: number;
|
||||
/** Начало съёмки (локальное время, ISO без зоны). */
|
||||
timeStart: string;
|
||||
/** Длительность съёмки, секунды. */
|
||||
duration: number;
|
||||
revolution: number;
|
||||
/** Крен (gamma), градусы. */
|
||||
roll: number;
|
||||
/** Угол захвата, градусы. */
|
||||
captureAngle: number;
|
||||
/** Широта центра контура, градусы. */
|
||||
lat: number;
|
||||
/** Долгота центра контура, градусы. */
|
||||
longitude: number;
|
||||
/** Контур полосы съёмки, WKT POLYGON (lon lat, ...). */
|
||||
contourWkt: string;
|
||||
/** Идентификатор сохранённого режима съёмки в БД (есть только после сохранения). */
|
||||
surveyModeId?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Построение одного маршрута съёмки по параметрам видимости точки без сохранения
|
||||
* (pcp-mission-planing-service, POST /api/missions/survey-route/build). Live-превью.
|
||||
*/
|
||||
export async function buildSurveyRoute(params: BuildSurveyRouteParams): Promise<SurveyRoute> {
|
||||
return postSurveyRoute(`${BASE}/api/missions/survey-route/build`, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Построение и сохранение маршрута съёмки в БД по идентификатору выбранного плана
|
||||
* (pcp-mission-planing-service, POST /api/missions/{planId}/survey-route).
|
||||
* planId — идентификатор выбранного плана в pcp-tgu-service (= mission_id).
|
||||
*/
|
||||
export async function saveSurveyRoute(planId: string, params: BuildSurveyRouteParams): Promise<SurveyRoute> {
|
||||
return postSurveyRoute(`${BASE}/api/missions/${encodeURIComponent(planId)}/survey-route`, params);
|
||||
}
|
||||
|
||||
export type SaveDropRouteParams = {
|
||||
/** Идентификатор КА (NORAD). */
|
||||
satelliteId: number;
|
||||
/** Идентификатор/номер станции приёма (из ЗРВ). */
|
||||
station: string;
|
||||
/** Номер витка зоны радиовидимости. */
|
||||
revolution: number;
|
||||
/** Начало зоны (локальное время, ISO без зоны). */
|
||||
zoneStart: string;
|
||||
/** Конец зоны (локальное время, ISO без зоны). */
|
||||
zoneStop: string;
|
||||
/** Идентификаторы режимов съёмки (survey_modes), сбрасываемых в этой зоне. */
|
||||
surveyModeIds: number[];
|
||||
/** Начало интервала плана (для автосоздания миссии/плана). */
|
||||
planStart?: string;
|
||||
/** Конец интервала плана. */
|
||||
planEnd?: string;
|
||||
};
|
||||
|
||||
/** Сохранённый режим сброса (ответ /drop-route). */
|
||||
export type DropMode = {
|
||||
id: number;
|
||||
planId: number;
|
||||
timeStart: string | null;
|
||||
revolution: number;
|
||||
type: "DROP";
|
||||
station: string | null;
|
||||
duration: number;
|
||||
surveys: number[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Ручное добавление сброса по выбранной зоне радиовидимости
|
||||
* (pcp-mission-planing-service, POST /api/missions/{planId}/drop-route).
|
||||
* planId — идентификатор выбранного плана в pcp-tgu-service (= mission_id).
|
||||
*/
|
||||
export async function saveDropRoute(planId: string, params: SaveDropRouteParams): Promise<DropMode> {
|
||||
const body = {
|
||||
satelliteId: params.satelliteId,
|
||||
station: params.station,
|
||||
revolution: params.revolution,
|
||||
zoneStart: params.zoneStart,
|
||||
zoneStop: params.zoneStop,
|
||||
surveyModeIds: params.surveyModeIds,
|
||||
...(params.planStart ? { planStart: params.planStart } : {}),
|
||||
...(params.planEnd ? { planEnd: params.planEnd } : {})
|
||||
};
|
||||
|
||||
const url = `${BASE}/api/missions/${encodeURIComponent(planId)}/drop-route`;
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { Accept: "application/json", "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
} catch {
|
||||
throw new TguApiError("Не удалось подключиться к сервису планирования миссий.", "network");
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new TguApiError(extractErrorText(text) || "Ошибка сохранения сброса.", "http", response.status);
|
||||
}
|
||||
if (!text.trim()) {
|
||||
throw new TguApiError("Сервис планирования миссий вернул пустой ответ.", "http", response.status);
|
||||
}
|
||||
|
||||
return JSON.parse(text) as DropMode;
|
||||
}
|
||||
|
||||
async function postSurveyRoute(url: string, params: BuildSurveyRouteParams): Promise<SurveyRoute> {
|
||||
const body = {
|
||||
satelliteId: params.satelliteId,
|
||||
time: params.time,
|
||||
durationSeconds: params.durationSeconds,
|
||||
roll: params.roll,
|
||||
captureAngle: params.captureAngle,
|
||||
revolution: params.revolution ?? 0,
|
||||
anchor: params.anchor ?? "CENTERED",
|
||||
...(params.planStart ? { planStart: params.planStart } : {}),
|
||||
...(params.planEnd ? { planEnd: params.planEnd } : {})
|
||||
};
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { Accept: "application/json", "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
} catch {
|
||||
throw new TguApiError("Не удалось подключиться к сервису планирования миссий.", "network");
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new TguApiError(extractErrorText(text) || "Ошибка построения маршрута съёмки.", "http", response.status);
|
||||
}
|
||||
if (!text.trim()) {
|
||||
throw new TguApiError("Сервис планирования миссий вернул пустой ответ.", "http", response.status);
|
||||
}
|
||||
|
||||
return JSON.parse(text) as SurveyRoute;
|
||||
}
|
||||
|
||||
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 ["message", "error", "reason", "detail"]) {
|
||||
const value = parsed[field];
|
||||
if (typeof value === "string" && value.trim()) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return trimmed;
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
Reference in New Issue
Block a user