Новый сервис pcp-tgu-ui-service: ТГУ-фронт на headless-auth nstart + платформенный CI/деплой
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
import { TguApiError } from '../model/tguTypes'
|
||||
import { toLocalIso } from '../utils/localTime'
|
||||
import { apiFetch } from './apiFetch'
|
||||
|
||||
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). Возвращает по одному моменту видимости на виток.
|
||||
* @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
|
||||
}
|
||||
Reference in New Issue
Block a user