Files
dc-observatio/services/pcp-tgu-ui-service/src/api/ballisticsApi.ts
T
Дмитрий Соловьев 3765efb4fe feat(ui): перевести фронт с pcp-ui-service на источники, снять gateway catch-all
Запросы tguApi.ts (platforms/plans/decision/dismiss/restore/confirm-layin)
и flight-line переведены с прокси /api/tgu-planning через умирающий
pcp-ui-service на прямые вызовы pcp-tgu-service (/api/pcp-tgu, decision и
confirm-layin — на тест-контроллер /test/tgu/*) и pcp-ballistics-service.
flight-line перенесён в ballisticsApi.ts. editorApi.ts modes переведён с
/api/current-plans на /api/pcp-mission. UX-перевод ошибок решения уже жил во
фронте (errorMessage в tguApi.ts), серверная обвязка ui-service дублировала.

Из config-repo/pcp-gateway-service.yaml снят маршрут ui-catch-all: не-матчнутый
/api/** теперь отдаёт 404 на gateway, а не уходит в pcp-ui-service.

Шаг 8 (вывод сервиса из эксплуатации: compose/helm/CI/реестр + удаление кода)
отложен до проверки репойнта на живом стенде; сервис пока задеплоен без
маршрутов. См. docs/план-перевода-с-pcp-ui-service.md и задачу #40 бэклога.
2026-06-10 14:47:05 +03:00

261 lines
8.2 KiB
TypeScript

import { TguApiError } from '../model/tguTypes'
import { toLocalIso } from '../utils/localTime'
import { apiFetch } from './apiFetch'
const BASE = '/api/pcp-ballistics'
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'
}
/**
* Трасса полёта КА (flight-line) на интервале для отрисовки на карте/таймлайне из
* pcp-ballistics-service (GET /api/satellites/{norad}/flight-line). Шаг трассы 60 с.
* @param noradId NORAD-идентификатор КА.
* @param fromMs Начало интервала, мс от эпохи.
* @param toMs Конец интервала, мс от эпохи.
*
* @returns Точки трассы полёта на интервале.
*/
export async function fetchFlightLine(
noradId: string,
fromMs: number,
toMs: number,
): Promise<FlightLinePoint[]> {
const params = new URLSearchParams({
time_start: toLocalDateTime(fromMs),
time_stop: toLocalDateTime(toMs),
})
const url = `${BASE}/api/satellites/${encodeURIComponent(noradId)}/flight-line?${params.toString()}`
let response: Response
try {
response = await apiFetch(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 FlightLinePoint[]
}
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). Возвращает по одному моменту видимости на виток.
* @param noradId NORAD-идентификатор КА.
* @param target Цель (координаты точки) для расчёта видимости.
* @param fromMs Начало интервала, мс от эпохи.
* @param toMs Конец интервала, мс от эпохи.
* @param filters Фильтры по углу Солнца и крену (необязательны).
*
* @returns По одному моменту видимости на виток на интервале.
*/
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 apiFetch(`${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).
* @param noradId NORAD-идентификатор КА.
* @param fromMs Начало интервала, мс от эпохи.
* @param toMs Конец интервала, мс от эпохи.
*
* @returns Зоны радиовидимости КА на интервале (помечены номером станции).
*/
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 apiFetch(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.
* @param ms Время в миллисекундах от эпохи.
*
* @returns Строка LocalDateTime без зоны.
*/
function toLocalDateTime(ms: number): string {
return toLocalIso(ms)
}
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
}