3765efb4fe
Запросы 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 бэклога.
261 lines
8.2 KiB
TypeScript
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
|
|
}
|