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 { 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 { 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 { 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 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 }