сравнение TLE

This commit is contained in:
emelianov
2026-05-27 12:00:52 +03:00
parent 29c06a4c9c
commit f4014a08a1
8 changed files with 247 additions and 52 deletions
@@ -0,0 +1,8 @@
package space.nstart.pcp.pcp_types_lib.dto.ballistics
import java.time.LocalDateTime
class TlePointRequestDTO(
var tle: TLEDTO = TLEDTO(),
var time: LocalDateTime = LocalDateTime.now()
)
@@ -1,15 +1,13 @@
package space.nstart.pcp.pcp_request_service.controller package space.nstart.pcp.pcp_request_service.controller
import ballistics.types.OrbitalPoint
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import space.nstart.pcp.pcp_request_service.service.TLEService import space.nstart.pcp.pcp_request_service.service.TLEService
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TlePointRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TleRvaRequestDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.TleRvaRequestDTO
@RestController @RestController
@@ -23,6 +21,9 @@ class TLEController {
@PostMapping("/parse") @PostMapping("/parse")
fun orbit(@RequestBody tle : TLEDTO) = tleService.parseTLE(tle) fun orbit(@RequestBody tle : TLEDTO) = tleService.parseTLE(tle)
@PostMapping("/point")
fun point(@RequestBody req: TlePointRequestDTO) = tleService.point(req)
@PostMapping("/rva") @PostMapping("/rva")
fun rva(@RequestBody req : TleRvaRequestDTO) = tleService.rva(req) fun rva(@RequestBody req : TleRvaRequestDTO) = tleService.rva(req)
@@ -1,7 +1,9 @@
package space.nstart.pcp.pcp_request_service.service package space.nstart.pcp.pcp_request_service.service
import ballistics.Ballistics import ballistics.Ballistics
import ballistics.orbitalPoints.timeStepper.TLEStepper
import ballistics.types.BallisticsError import ballistics.types.BallisticsError
import ballistics.types.EarthType
import ballistics.types.PPI import ballistics.types.PPI
import ballistics.types.TLE import ballistics.types.TLE
import ballistics.types.TLEParams import ballistics.types.TLEParams
@@ -15,6 +17,7 @@ import space.nstart.pcp.pcp_request_service.configuration.CustomValidationExcept
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RadioVisibilityAreaDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.RadioVisibilityAreaDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TlePointRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TargetPositionDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.TargetPositionDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TleRvaRequestDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.TleRvaRequestDTO
import kotlin.math.PI import kotlin.math.PI
@@ -47,6 +50,37 @@ class TLEService() {
fun point(req: TlePointRequestDTO): OrbPointDTO {
val bal = Ballistics()
val tleParams = try {
bal.getTLEParams(TLE(req.tle.first, req.tle.second), req.tle.header ?: "empty")
} catch (ex: Exception) {
throw CustomValidationException("Ошибка формата TLE : ${ex.message}")
}
if (req.time < tleParams.time) {
throw CustomValidationException("Время расчета должно быть не раньше эпохи TLE ${tleParams.time}")
}
val point = try {
val stepper = TLEStepper(req.tle.first, req.tle.second, EarthType.PZ90d02)
stepper.calculate(fromDateTime(req.time))
} catch (ex: Exception) {
throw CustomErrorException("Ошибка расчета положения по TLE : ${ex.message}")
}
return OrbPointDTO(
toDateTime(point.t),
point.vit.toLong(),
point.v.x,
point.v.y,
point.v.z,
point.r.x,
point.r.y,
point.r.z
)
}
fun rva(req : TleRvaRequestDTO) : Iterable<RadioVisibilityAreaDTO> { fun rva(req : TleRvaRequestDTO) : Iterable<RadioVisibilityAreaDTO> {
val bal = Ballistics() val bal = Ballistics()
@@ -4,18 +4,23 @@ import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO
import space.nstart.pcp.slots_service.configuration.CustomValidationException import space.nstart.pcp.slots_service.configuration.CustomValidationException
import space.nstart.pcp.slots_service.dto.tlecomparison.TleCompareRequestDTO import space.nstart.pcp.slots_service.dto.tlecomparison.TleCompareRequestDTO
import space.nstart.pcp.slots_service.dto.tlecomparison.TleCompareResponseDTO import space.nstart.pcp.slots_service.dto.tlecomparison.TleCompareResponseDTO
import space.nstart.pcp.slots_service.dto.tlecomparison.TleCompareRowDTO import space.nstart.pcp.slots_service.dto.tlecomparison.TleCompareRowDTO
import space.nstart.pcp.slots_service.dto.tlecomparison.TleParsedDTO import space.nstart.pcp.slots_service.dto.tlecomparison.TleParsedDTO
import space.nstart.pcp.slots_service.dto.tlecomparison.TlePositionDTO
import space.nstart.pcp.slots_service.dto.tlecomparison.TlePositionDeltaDTO
import space.nstart.pcp.slots_service.dto.tlecomparison.TleTextRequestDTO import space.nstart.pcp.slots_service.dto.tlecomparison.TleTextRequestDTO
import space.nstart.pcp.slots_service.service.BallisticsService import space.nstart.pcp.slots_service.service.BallisticsService
import java.time.Duration
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.util.Locale import java.util.Locale
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.sqrt
@RestController @RestController
@RequestMapping("/api/tle-comparison") @RequestMapping("/api/tle-comparison")
@@ -29,12 +34,38 @@ class TleComparisonController(
@PostMapping("/compare") @PostMapping("/compare")
fun compare(@RequestBody request: TleCompareRequestDTO): TleCompareResponseDTO { fun compare(@RequestBody request: TleCompareRequestDTO): TleCompareResponseDTO {
val first = ballisticsService.parseTle(parseTleText(request.first)) val firstTle = parseTleText(request.first)
val second = ballisticsService.parseTle(parseTleText(request.second)) val secondTle = parseTleText(request.second)
val firstParsed = ballisticsService.parseTle(firstTle)
val secondParsed = ballisticsService.parseTle(secondTle)
val calculationTime = request.time
?: throw CustomValidationException("Задайте время сравнения TLE")
val firstEpoch = firstParsed.time
?: throw CustomValidationException("Не удалось определить эпоху первого TLE")
val secondEpoch = secondParsed.time
?: throw CustomValidationException("Не удалось определить эпоху второго TLE")
val maxEpoch = maxOf(firstEpoch, secondEpoch)
if (calculationTime < maxEpoch) {
throw CustomValidationException("Время сравнения должно быть не раньше максимальной эпохи TLE: $maxEpoch")
}
val firstPoint = ballisticsService.tlePoint(firstTle, calculationTime).toPosition()
val secondPoint = ballisticsService.tlePoint(secondTle, calculationTime).toPosition()
val delta = TlePositionDeltaDTO(
dx = secondPoint.x - firstPoint.x,
dy = secondPoint.y - firstPoint.y,
dz = secondPoint.z - firstPoint.z,
norm = distance(secondPoint.x - firstPoint.x, secondPoint.y - firstPoint.y, secondPoint.z - firstPoint.z)
)
return TleCompareResponseDTO( return TleCompareResponseDTO(
first = first, time = calculationTime,
second = second, firstEpoch = firstEpoch,
rows = buildRows(first, second) secondEpoch = secondEpoch,
first = firstPoint,
second = secondPoint,
radiusVectorDelta = delta,
rows = buildRows(firstPoint, secondPoint, delta)
) )
} }
@@ -69,23 +100,38 @@ class TleComparisonController(
) )
} }
private fun buildRows(first: TleParsedDTO, second: TleParsedDTO): List<TleCompareRowDTO> = listOf( private fun buildRows(
textRow("satName", "Название", first.satName, second.satName), first: TlePositionDTO,
textRow("noradId", "NORAD ID", first.noradId?.toString(), second.noradId?.toString()), second: TlePositionDTO,
textRow("revolution", "Номер витка", first.revolution?.toString(), second.revolution?.toString()), delta: TlePositionDeltaDTO
timeRow("time", "Эпоха", first.time, second.time), ): List<TleCompareRowDTO> = listOf(
numberRow("inclination", "Наклонение", first.inclination, second.inclination), timeRow("time", "Время расчета", first.time, second.time),
numberRow("ascendingNode", "ВУЗ", first.ascendingNode, second.ascendingNode), textRow("revolution", "Виток", first.revolution.toString(), second.revolution.toString()),
numberRow("argPerigee", "Аргумент перигея", first.argPerigee, second.argPerigee), numberRow("x", "X радиус-вектора, м", first.x, second.x),
numberRow("meanAnomaly", "Средняя аномалия", first.meanAnomaly, second.meanAnomaly), numberRow("y", "Y радиус-вектора, м", first.y, second.y),
numberRow("eccentricity", "Эксцентриситет", first.eccentricity, second.eccentricity), numberRow("z", "Z радиус-вектора, м", first.z, second.z),
numberRow("perigee", "Перигей", first.perigee, second.perigee), numberRow("radiusNorm", "Модуль радиус-вектора, м", first.radiusNorm, second.radiusNorm),
numberRow("apogee", "Апогей", first.apogee, second.apogee), numberRow("vx", "Vx, м/с", first.vx, second.vx),
numberRow("period", "Период", first.period, second.period), numberRow("vy", "Vy, м/с", first.vy, second.vy),
numberRow("semiMajor", "Большая полуось", first.semiMajor, second.semiMajor), numberRow("vz", "Vz, м/с", first.vz, second.vz),
numberRow("semiMinor", "Малая полуось", first.semiMinor, second.semiMinor), numberRow("velocityNorm", "Модуль скорости, м/с", first.velocityNorm, second.velocityNorm),
numberRow("meanMotion", "Среднее движение", first.meanMotion, second.meanMotion), TleCompareRowDTO("dx", "ΔX = X2 - X1, м", first = null, second = null, delta = delta.dx.formatSignedNumber()),
numberRow("meanMotionTLE", "Среднее движение TLE", first.meanMotionTLE, second.meanMotionTLE) TleCompareRowDTO("dy", "ΔY = Y2 - Y1, м", first = null, second = null, delta = delta.dy.formatSignedNumber()),
TleCompareRowDTO("dz", "ΔZ = Z2 - Z1, м", first = null, second = null, delta = delta.dz.formatSignedNumber()),
TleCompareRowDTO("radiusDeltaNorm", "|Δr|, м", first = null, second = null, delta = delta.norm.formatNumber())
)
private fun OrbPointDTO.toPosition(): TlePositionDTO = TlePositionDTO(
time = time,
revolution = revolution,
x = x,
y = y,
z = z,
vx = vx,
vy = vy,
vz = vz,
radiusNorm = distance(x, y, z),
velocityNorm = distance(vx, vy, vz)
) )
private fun textRow(code: String, name: String, first: String?, second: String?): TleCompareRowDTO = private fun textRow(code: String, name: String, first: String?, second: String?): TleCompareRowDTO =
@@ -100,7 +146,7 @@ class TleComparisonController(
private fun timeRow(code: String, name: String, first: LocalDateTime?, second: LocalDateTime?): TleCompareRowDTO { private fun timeRow(code: String, name: String, first: LocalDateTime?, second: LocalDateTime?): TleCompareRowDTO {
val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME
val delta = if (first != null && second != null) { val delta = if (first != null && second != null) {
val seconds = java.time.Duration.between(first, second).seconds val seconds = Duration.between(first, second).seconds
formatSecondsDelta(seconds) formatSecondsDelta(seconds)
} else { } else {
null null
@@ -117,11 +163,13 @@ class TleComparisonController(
delta = if (first != null && second != null) (second - first).formatSignedNumber() else null delta = if (first != null && second != null) (second - first).formatSignedNumber() else null
) )
private fun Double.formatNumber(): String = String.format(Locale.US, "%.8f", this) private fun distance(x: Double, y: Double, z: Double): Double = sqrt(x * x + y * y + z * z)
private fun Double.formatNumber(): String = String.format(Locale.US, "%.6f", this)
private fun Double.formatSignedNumber(): String { private fun Double.formatSignedNumber(): String {
val sign = if (this > 0) "+" else "" val sign = if (this > 0) "+" else ""
return sign + String.format(Locale.US, "%.8f", this) return sign + String.format(Locale.US, "%.6f", this)
} }
private fun formatSecondsDelta(seconds: Long): String { private fun formatSecondsDelta(seconds: Long): String {
@@ -8,7 +8,8 @@ class TleTextRequestDTO(
class TleCompareRequestDTO( class TleCompareRequestDTO(
var first: String = "", var first: String = "",
var second: String = "" var second: String = "",
var time: LocalDateTime? = null
) )
data class TleParsedDTO( data class TleParsedDTO(
@@ -32,6 +33,19 @@ data class TleParsedDTO(
val meanMotionTLE: Double? = null val meanMotionTLE: Double? = null
) )
data class TlePositionDTO(
val time: LocalDateTime,
val revolution: Long,
val x: Double,
val y: Double,
val z: Double,
val vx: Double,
val vy: Double,
val vz: Double,
val radiusNorm: Double,
val velocityNorm: Double
)
data class TleCompareRowDTO( data class TleCompareRowDTO(
val code: String, val code: String,
val name: String, val name: String,
@@ -41,7 +55,18 @@ data class TleCompareRowDTO(
) )
data class TleCompareResponseDTO( data class TleCompareResponseDTO(
val first: TleParsedDTO, val time: LocalDateTime,
val second: TleParsedDTO, val firstEpoch: LocalDateTime,
val secondEpoch: LocalDateTime,
val first: TlePositionDTO,
val second: TlePositionDTO,
val radiusVectorDelta: TlePositionDeltaDTO,
val rows: List<TleCompareRowDTO> val rows: List<TleCompareRowDTO>
) )
data class TlePositionDeltaDTO(
val dx: Double,
val dy: Double,
val dz: Double,
val norm: Double
)
@@ -11,6 +11,7 @@ import space.nstart.pcp.pcp_types_lib.dto.ballistics.FlightLineDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteOrbitAvailabilityDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteOrbitAvailabilityDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TlePointRequestDTO
import space.nstart.pcp.slots_service.dto.tlecomparison.TleParsedDTO import space.nstart.pcp.slots_service.dto.tlecomparison.TleParsedDTO
import space.nstart.pcp.slots_service.configuration.CustomErrorException import space.nstart.pcp.slots_service.configuration.CustomErrorException
import java.net.URI import java.net.URI
@@ -80,6 +81,16 @@ class BallisticsService (webClientBuilderProvider: ObjectProvider<WebClient.Buil
.block() .block()
?: throw CustomErrorException("Не удалось разобрать TLE") ?: throw CustomErrorException("Не удалось разобрать TLE")
fun tlePoint(tle: TLEDTO, time: LocalDateTime): OrbPointDTO =
webClientBuilder.build()
.post()
.uri("$url/api/tle/point")
.bodyValue(TlePointRequestDTO(tle = tle, time = time))
.retrieve()
.bodyToMono(OrbPointDTO::class.java)
.block()
?: throw CustomErrorException("Не удалось рассчитать положение по TLE на время $time")
private fun withTimeInterval( private fun withTimeInterval(
path: String, path: String,
timeStart: LocalDateTime?, timeStart: LocalDateTime?,
@@ -9,6 +9,7 @@
bindEvents(); bindEvents();
updateReference('first', null); updateReference('first', null);
updateReference('second', null); updateReference('second', null);
updateTimeLimits();
}); });
function bindEvents() { function bindEvents() {
@@ -16,6 +17,7 @@
const second = el('tle-second-text'); const second = el('tle-second-text');
first?.addEventListener('input', () => scheduleParse('first')); first?.addEventListener('input', () => scheduleParse('first'));
second?.addEventListener('input', () => scheduleParse('second')); second?.addEventListener('input', () => scheduleParse('second'));
el('tle-comparison-time')?.addEventListener('change', validateCalculationTime);
el('tle-compare-submit')?.addEventListener('click', compareTle); el('tle-compare-submit')?.addEventListener('click', compareTle);
el('tle-compare-clear')?.addEventListener('click', clearPage); el('tle-compare-clear')?.addEventListener('click', clearPage);
} }
@@ -26,6 +28,7 @@
if (!text.trim()) { if (!text.trim()) {
state[side] = null; state[side] = null;
updateReference(side, null); updateReference(side, null);
updateTimeLimits();
return; return;
} }
setStatus(side, 'Разбор...', 'text-muted'); setStatus(side, 'Разбор...', 'text-muted');
@@ -37,10 +40,12 @@
const parsed = await postJson('/api/tle-comparison/parse', { tle: getTleText(side) }); const parsed = await postJson('/api/tle-comparison/parse', { tle: getTleText(side) });
state[side] = parsed; state[side] = parsed;
updateReference(side, parsed); updateReference(side, parsed);
updateTimeLimits();
setStatus(side, 'Разобрано', 'tle-parse-ok'); setStatus(side, 'Разобрано', 'tle-parse-ok');
} catch (error) { } catch (error) {
state[side] = null; state[side] = null;
updateReference(side, null); updateReference(side, null);
updateTimeLimits();
setStatus(side, error.message || 'Ошибка разбора', 'tle-parse-error'); setStatus(side, error.message || 'Ошибка разбора', 'tle-parse-error');
} }
} }
@@ -49,21 +54,25 @@
showAlert(''); showAlert('');
const first = getTleText('first'); const first = getTleText('first');
const second = getTleText('second'); const second = getTleText('second');
const time = getCalculationTime();
if (!first.trim() || !second.trim()) { if (!first.trim() || !second.trim()) {
showAlert('Введите оба TLE для сравнения.', 'warning'); showAlert('Введите оба TLE для сравнения.', 'warning');
return; return;
} }
if (!time) {
showAlert('Задайте время расчета.', 'warning');
return;
}
const minTime = maxEpoch();
if (minTime && time < minTime) {
showAlert(`Время расчета должно быть не раньше максимальной эпохи TLE: ${formatDateTime(minTime)}.`, 'warning');
return;
}
setCompareLoading(true); setCompareLoading(true);
try { try {
const response = await postJson('/api/tle-comparison/compare', { first, second }); const response = await postJson('/api/tle-comparison/compare', { first, second, time });
state.first = response.first; renderComparison(response);
state.second = response.second;
updateReference('first', response.first);
updateReference('second', response.second);
setStatus('first', 'Разобрано', 'tle-parse-ok');
setStatus('second', 'Разобрано', 'tle-parse-ok');
renderComparison(response.rows || []);
} catch (error) { } catch (error) {
showAlert(error.message || 'Не удалось выполнить сравнение TLE.', 'danger'); showAlert(error.message || 'Не удалось выполнить сравнение TLE.', 'danger');
} finally { } finally {
@@ -71,7 +80,8 @@
} }
} }
function renderComparison(rows) { function renderComparison(response) {
const rows = response?.rows || [];
const body = el('tle-comparison-result-body'); const body = el('tle-comparison-result-body');
const empty = el('tle-comparison-empty'); const empty = el('tle-comparison-empty');
const wrap = el('tle-comparison-table-wrap'); const wrap = el('tle-comparison-table-wrap');
@@ -98,9 +108,9 @@
empty.classList.add('d-none'); empty.classList.add('d-none');
wrap.classList.remove('d-none'); wrap.classList.remove('d-none');
if (meta) { if (meta) {
const firstEpoch = formatDateTime(state.first?.time); const deltaNorm = response.radiusVectorDelta?.norm;
const secondEpoch = formatDateTime(state.second?.time); const deltaText = Number.isFinite(deltaNorm) ? `${formatNumber(deltaNorm)} м` : '—';
meta.textContent = `Эпохи: TLE 1 — ${firstEpoch || '—'}, TLE 2 — ${secondEpoch || '—'}`; meta.textContent = `Время расчета: ${formatDateTime(response.time)}. |Δr| = ${deltaText}. Эпохи: TLE 1 — ${formatDateTime(response.firstEpoch)}, TLE 2 — ${formatDateTime(response.secondEpoch)}.`;
} }
} }
@@ -114,16 +124,46 @@
} }
} }
function updateTimeLimits() {
const input = el('tle-comparison-time');
const helper = el('tle-comparison-time-helper');
const minTime = maxEpoch();
if (!input) return;
if (!minTime) {
input.removeAttribute('min');
if (helper) helper.textContent = 'Минимальное допустимое время появится после разбора двух TLE.';
return;
}
input.min = toDateTimeLocalValue(minTime);
if (!input.value || input.value < input.min) {
input.value = input.min;
}
if (helper) helper.textContent = `Минимальное допустимое время: ${formatDateTime(minTime)}.`;
}
function validateCalculationTime() {
const input = el('tle-comparison-time');
const minTime = maxEpoch();
if (!input || !minTime || !input.value) return;
const time = getCalculationTime();
if (time && time < minTime) {
showAlert(`Время расчета должно быть не раньше максимальной эпохи TLE: ${formatDateTime(minTime)}.`, 'warning');
}
}
function clearPage() { function clearPage() {
el('tle-first-text').value = ''; el('tle-first-text').value = '';
el('tle-second-text').value = ''; el('tle-second-text').value = '';
const time = el('tle-comparison-time');
if (time) time.value = '';
state.first = null; state.first = null;
state.second = null; state.second = null;
updateReference('first', null); updateReference('first', null);
updateReference('second', null); updateReference('second', null);
updateTimeLimits();
showAlert(''); showAlert('');
renderComparison([]); renderComparison({ rows: [] });
setText('tle-comparison-result-meta', 'Нажмите “Сравнить” после ввода двух TLE.'); setText('tle-comparison-result-meta', 'Задайте время не раньше максимальной эпохи TLE и нажмите “Сравнить”.');
} }
async function postJson(url, payload) { async function postJson(url, payload) {
@@ -145,6 +185,16 @@
return response.json(); return response.json();
} }
function maxEpoch() {
if (!state.first?.time || !state.second?.time) return null;
return state.first.time >= state.second.time ? state.first.time : state.second.time;
}
function getCalculationTime() {
const value = el('tle-comparison-time')?.value;
return value ? value : null;
}
function getTleText(side) { function getTleText(side) {
return el(`tle-${side}-text`)?.value || ''; return el(`tle-${side}-text`)?.value || '';
} }
@@ -160,7 +210,7 @@
const button = el('tle-compare-submit'); const button = el('tle-compare-submit');
if (!button) return; if (!button) return;
button.disabled = loading; button.disabled = loading;
button.textContent = loading ? 'Сравнение...' : 'Сравнить'; button.textContent = loading ? 'Расчет...' : 'Рассчитать';
} }
function showAlert(message, type = 'info') { function showAlert(message, type = 'info') {
@@ -174,11 +224,20 @@
: ''; : '';
} }
function toDateTimeLocalValue(value) {
if (!value) return '';
return String(value).slice(0, 19);
}
function formatDateTime(value) { function formatDateTime(value) {
if (!value) return ''; if (!value) return '';
return String(value).replace('T', ' '); return String(value).replace('T', ' ');
} }
function formatNumber(value) {
return Number(value).toLocaleString('ru-RU', { maximumFractionDigits: 3 });
}
function emptyToDash(value) { function emptyToDash(value) {
return value === null || value === undefined || value === '' ? '—' : value; return value === null || value === undefined || value === '' ? '—' : value;
} }
@@ -13,10 +13,10 @@
<div class="d-flex justify-content-between align-items-start flex-wrap gap-3 mb-4"> <div class="d-flex justify-content-between align-items-start flex-wrap gap-3 mb-4">
<div> <div>
<h2 class="mb-1">Сравнение TLE</h2> <h2 class="mb-1">Сравнение TLE</h2>
<div class="catalog-helper">Вставьте два набора TLE, проверьте эпохи и сравните параметры, полученные через ballistics-service.</div> <div class="catalog-helper">Вставьте два набора TLE, проверьте эпохи и рассчитайте положение спутника на заданный момент времени.</div>
</div> </div>
<div class="d-flex gap-2 flex-wrap"> <div class="d-flex gap-2 flex-wrap">
<button id="tle-compare-submit" type="button" class="btn btn-primary">Сравнить</button> <button id="tle-compare-submit" type="button" class="btn btn-primary">Рассчитать</button>
<button id="tle-compare-clear" type="button" class="btn btn-outline-secondary">Очистить</button> <button id="tle-compare-clear" type="button" class="btn btn-outline-secondary">Очистить</button>
</div> </div>
</div> </div>
@@ -26,9 +26,18 @@
<div class="card catalog-card tle-input-card mb-4"> <div class="card catalog-card tle-input-card mb-4">
<div class="card-header"> <div class="card-header">
<div class="catalog-section-title">Ввод данных</div> <div class="catalog-section-title">Ввод данных</div>
<div class="catalog-helper mt-1">Можно вставить две или три строки TLE. Заголовок определяется автоматически, строки 1 и 2 обязательны.</div> <div class="catalog-helper mt-1">Можно вставить две или три строки TLE. Заголовок определяется автоматически, строки 1 и 2 обязательны. Время расчета должно быть не раньше максимальной эпохи двух TLE.</div>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row g-3 mb-4">
<div class="col-12 col-md-5 col-xl-4">
<label for="tle-comparison-time" class="form-label">Время расчета</label>
<input id="tle-comparison-time" type="datetime-local" class="form-control" step="1"/>
</div>
<div class="col-12 col-md-7 col-xl-8 d-flex align-items-end">
<div id="tle-comparison-time-helper" class="catalog-helper">Минимальное допустимое время появится после разбора двух TLE.</div>
</div>
</div>
<div class="row g-4"> <div class="row g-4">
<div class="col-12 col-xl-6"> <div class="col-12 col-xl-6">
<div class="tle-input-widget h-100"> <div class="tle-input-widget h-100">
@@ -90,11 +99,11 @@
<div class="card-header d-flex justify-content-between align-items-start gap-3 flex-wrap"> <div class="card-header d-flex justify-content-between align-items-start gap-3 flex-wrap">
<div> <div>
<div class="catalog-section-title">Результаты</div> <div class="catalog-section-title">Результаты</div>
<div id="tle-comparison-result-meta" class="catalog-helper mt-1">Нажмите “Сравнить” после ввода двух TLE.</div> <div id="tle-comparison-result-meta" class="catalog-helper mt-1">Задайте время не раньше максимальной эпохи TLE и нажмите “Сравнить”.</div>
</div> </div>
</div> </div>
<div class="card-body p-0"> <div class="card-body p-0">
<div id="tle-comparison-empty" class="catalog-empty">Результаты сравнения появятся здесь.</div> <div id="tle-comparison-empty" class="catalog-empty">Параметры положения и разность радиус-векторов появятся здесь.</div>
<div id="tle-comparison-table-wrap" class="table-responsive tle-result-table-wrap d-none"> <div id="tle-comparison-table-wrap" class="table-responsive tle-result-table-wrap d-none">
<table class="table table-hover mb-0 align-middle"> <table class="table table-hover mb-0 align-middle">
<thead class="table-light"> <thead class="table-light">
@@ -102,7 +111,7 @@
<th>Параметр</th> <th>Параметр</th>
<th>TLE 1</th> <th>TLE 1</th>
<th>TLE 2</th> <th>TLE 2</th>
<th>Разница</th> <th>Разница / значение</th>
</tr> </tr>
</thead> </thead>
<tbody id="tle-comparison-result-body"></tbody> <tbody id="tle-comparison-result-body"></tbody>