сравнение TLE
This commit is contained in:
+3
@@ -18,6 +18,9 @@ class CatalogPageController {
|
||||
@GetMapping("/satellites/pdcm")
|
||||
fun satellitesPdcmPage(): String = "satellites-pdcm"
|
||||
|
||||
@GetMapping("/tle-comparison")
|
||||
fun tleComparisonPage(): String = "tle-comparison"
|
||||
|
||||
@GetMapping("/current-plans")
|
||||
fun currentPlansPage(): String = "current-plans"
|
||||
|
||||
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
package space.nstart.pcp.slots_service.controller
|
||||
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO
|
||||
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.TleCompareResponseDTO
|
||||
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.TleTextRequestDTO
|
||||
import space.nstart.pcp.slots_service.service.BallisticsService
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Locale
|
||||
import kotlin.math.abs
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/tle-comparison")
|
||||
class TleComparisonController(
|
||||
private val ballisticsService: BallisticsService
|
||||
) {
|
||||
|
||||
@PostMapping("/parse")
|
||||
fun parse(@RequestBody request: TleTextRequestDTO): TleParsedDTO =
|
||||
ballisticsService.parseTle(parseTleText(request.tle))
|
||||
|
||||
@PostMapping("/compare")
|
||||
fun compare(@RequestBody request: TleCompareRequestDTO): TleCompareResponseDTO {
|
||||
val first = ballisticsService.parseTle(parseTleText(request.first))
|
||||
val second = ballisticsService.parseTle(parseTleText(request.second))
|
||||
return TleCompareResponseDTO(
|
||||
first = first,
|
||||
second = second,
|
||||
rows = buildRows(first, second)
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseTleText(raw: String): TLEDTO {
|
||||
val lines = raw.lines()
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotBlank() }
|
||||
|
||||
if (lines.isEmpty()) {
|
||||
throw CustomValidationException("Введите TLE")
|
||||
}
|
||||
|
||||
val firstIndex = lines.indexOfFirst { it.startsWith("1 ") }
|
||||
val secondIndex = lines.indexOfFirst { it.startsWith("2 ") }
|
||||
if (firstIndex < 0 || secondIndex < 0) {
|
||||
throw CustomValidationException("TLE должен содержать строки 1 и 2")
|
||||
}
|
||||
|
||||
val first = lines[firstIndex]
|
||||
val second = lines[secondIndex]
|
||||
val header = lines.take(firstIndex)
|
||||
.firstOrNull { !it.startsWith("1 ") && !it.startsWith("2 ") }
|
||||
|
||||
if (first.length < 69 || second.length < 69) {
|
||||
throw CustomValidationException("Строки TLE должны содержать не менее 69 символов")
|
||||
}
|
||||
|
||||
return TLEDTO(
|
||||
header = header,
|
||||
first = first.take(69),
|
||||
second = second.take(69)
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildRows(first: TleParsedDTO, second: TleParsedDTO): List<TleCompareRowDTO> = listOf(
|
||||
textRow("satName", "Название", first.satName, second.satName),
|
||||
textRow("noradId", "NORAD ID", first.noradId?.toString(), second.noradId?.toString()),
|
||||
textRow("revolution", "Номер витка", first.revolution?.toString(), second.revolution?.toString()),
|
||||
timeRow("time", "Эпоха", first.time, second.time),
|
||||
numberRow("inclination", "Наклонение", first.inclination, second.inclination),
|
||||
numberRow("ascendingNode", "ВУЗ", first.ascendingNode, second.ascendingNode),
|
||||
numberRow("argPerigee", "Аргумент перигея", first.argPerigee, second.argPerigee),
|
||||
numberRow("meanAnomaly", "Средняя аномалия", first.meanAnomaly, second.meanAnomaly),
|
||||
numberRow("eccentricity", "Эксцентриситет", first.eccentricity, second.eccentricity),
|
||||
numberRow("perigee", "Перигей", first.perigee, second.perigee),
|
||||
numberRow("apogee", "Апогей", first.apogee, second.apogee),
|
||||
numberRow("period", "Период", first.period, second.period),
|
||||
numberRow("semiMajor", "Большая полуось", first.semiMajor, second.semiMajor),
|
||||
numberRow("semiMinor", "Малая полуось", first.semiMinor, second.semiMinor),
|
||||
numberRow("meanMotion", "Среднее движение", first.meanMotion, second.meanMotion),
|
||||
numberRow("meanMotionTLE", "Среднее движение TLE", first.meanMotionTLE, second.meanMotionTLE)
|
||||
)
|
||||
|
||||
private fun textRow(code: String, name: String, first: String?, second: String?): TleCompareRowDTO =
|
||||
TleCompareRowDTO(
|
||||
code = code,
|
||||
name = name,
|
||||
first = first,
|
||||
second = second,
|
||||
delta = if (first != null && second != null && first != second) "изменено" else null
|
||||
)
|
||||
|
||||
private fun timeRow(code: String, name: String, first: LocalDateTime?, second: LocalDateTime?): TleCompareRowDTO {
|
||||
val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME
|
||||
val delta = if (first != null && second != null) {
|
||||
val seconds = java.time.Duration.between(first, second).seconds
|
||||
formatSecondsDelta(seconds)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
return TleCompareRowDTO(code, name, first?.format(formatter), second?.format(formatter), delta)
|
||||
}
|
||||
|
||||
private fun numberRow(code: String, name: String, first: Double?, second: Double?): TleCompareRowDTO =
|
||||
TleCompareRowDTO(
|
||||
code = code,
|
||||
name = name,
|
||||
first = first?.formatNumber(),
|
||||
second = second?.formatNumber(),
|
||||
delta = if (first != null && second != null) (second - first).formatSignedNumber() else null
|
||||
)
|
||||
|
||||
private fun Double.formatNumber(): String = String.format(Locale.US, "%.8f", this)
|
||||
|
||||
private fun Double.formatSignedNumber(): String {
|
||||
val sign = if (this > 0) "+" else ""
|
||||
return sign + String.format(Locale.US, "%.8f", this)
|
||||
}
|
||||
|
||||
private fun formatSecondsDelta(seconds: Long): String {
|
||||
val sign = if (seconds > 0) "+" else if (seconds < 0) "-" else ""
|
||||
val absolute = abs(seconds)
|
||||
val hours = absolute / 3600
|
||||
val minutes = (absolute % 3600) / 60
|
||||
val secs = absolute % 60
|
||||
return "$sign${hours}ч ${minutes}м ${secs}с"
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package space.nstart.pcp.slots_service.dto.tlecomparison
|
||||
|
||||
import java.time.LocalDateTime
|
||||
|
||||
class TleTextRequestDTO(
|
||||
var tle: String = ""
|
||||
)
|
||||
|
||||
class TleCompareRequestDTO(
|
||||
var first: String = "",
|
||||
var second: String = ""
|
||||
)
|
||||
|
||||
data class TleParsedDTO(
|
||||
val satName: String? = null,
|
||||
val noradId: Long? = null,
|
||||
val revolution: Long? = null,
|
||||
val time: LocalDateTime? = null,
|
||||
val inclination: Double? = null,
|
||||
val perigee: Double? = null,
|
||||
val apogee: Double? = null,
|
||||
val argPerigee: Double? = null,
|
||||
val eccentricity: Double? = null,
|
||||
val major: Double? = null,
|
||||
val minor: Double? = null,
|
||||
val meanAnomaly: Double? = null,
|
||||
val period: Double? = null,
|
||||
val semiMajor: Double? = null,
|
||||
val semiMinor: Double? = null,
|
||||
val ascendingNode: Double? = null,
|
||||
val meanMotion: Double? = null,
|
||||
val meanMotionTLE: Double? = null
|
||||
)
|
||||
|
||||
data class TleCompareRowDTO(
|
||||
val code: String,
|
||||
val name: String,
|
||||
val first: String?,
|
||||
val second: String?,
|
||||
val delta: String? = null
|
||||
)
|
||||
|
||||
data class TleCompareResponseDTO(
|
||||
val first: TleParsedDTO,
|
||||
val second: TleParsedDTO,
|
||||
val rows: List<TleCompareRowDTO>
|
||||
)
|
||||
+12
@@ -10,6 +10,8 @@ import space.nstart.pcp.pcp_types_lib.dto.ballistics.AscNodeDTO
|
||||
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.SatelliteOrbitAvailabilityDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO
|
||||
import space.nstart.pcp.slots_service.dto.tlecomparison.TleParsedDTO
|
||||
import space.nstart.pcp.slots_service.configuration.CustomErrorException
|
||||
import java.net.URI
|
||||
import java.time.LocalDateTime
|
||||
@@ -68,6 +70,16 @@ class BallisticsService (webClientBuilderProvider: ObjectProvider<WebClient.Buil
|
||||
?.firstOrNull()
|
||||
?: throw CustomErrorException("Не удалось получить положение спутника $id на время $time")
|
||||
|
||||
fun parseTle(tle: TLEDTO): TleParsedDTO =
|
||||
webClientBuilder.build()
|
||||
.post()
|
||||
.uri("$url/api/tle/parse")
|
||||
.bodyValue(tle)
|
||||
.retrieve()
|
||||
.bodyToMono(TleParsedDTO::class.java)
|
||||
.block()
|
||||
?: throw CustomErrorException("Не удалось разобрать TLE")
|
||||
|
||||
private fun withTimeInterval(
|
||||
path: String,
|
||||
timeStart: LocalDateTime?,
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
.tle-comparison-page {
|
||||
min-height: calc(100vh - 5.5rem);
|
||||
}
|
||||
|
||||
.tle-input-card,
|
||||
.tle-result-card {
|
||||
border: 0;
|
||||
box-shadow: 0 0.5rem 1.25rem rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.tle-input-widget {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tle-textarea {
|
||||
min-height: 11rem;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 0.9rem;
|
||||
resize: vertical;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.tle-reference {
|
||||
border: 1px solid rgba(148, 163, 184, 0.3);
|
||||
border-radius: 0.75rem;
|
||||
background: rgba(248, 250, 252, 0.82);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tle-reference-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 0.55rem 0.75rem;
|
||||
border-bottom: 1px solid rgba(148, 163, 184, 0.25);
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.tle-reference-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.tle-reference-row span {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.tle-reference-row strong {
|
||||
text-align: right;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.tle-parse-status {
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.tle-parse-status.tle-parse-ok {
|
||||
color: #198754 !important;
|
||||
}
|
||||
|
||||
.tle-parse-status.tle-parse-error {
|
||||
color: #dc3545 !important;
|
||||
}
|
||||
|
||||
.tle-result-table-wrap {
|
||||
max-height: calc(100vh - 30rem);
|
||||
min-height: 18rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.tle-result-table-wrap thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.tle-delta-positive {
|
||||
color: #0d6efd;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tle-delta-negative {
|
||||
color: #dc3545;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@media (max-width: 1199.98px) {
|
||||
.tle-result-table-wrap {
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
(() => {
|
||||
const state = {
|
||||
first: null,
|
||||
second: null,
|
||||
parseTimers: {}
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
bindEvents();
|
||||
updateReference('first', null);
|
||||
updateReference('second', null);
|
||||
});
|
||||
|
||||
function bindEvents() {
|
||||
const first = el('tle-first-text');
|
||||
const second = el('tle-second-text');
|
||||
first?.addEventListener('input', () => scheduleParse('first'));
|
||||
second?.addEventListener('input', () => scheduleParse('second'));
|
||||
el('tle-compare-submit')?.addEventListener('click', compareTle);
|
||||
el('tle-compare-clear')?.addEventListener('click', clearPage);
|
||||
}
|
||||
|
||||
function scheduleParse(side) {
|
||||
window.clearTimeout(state.parseTimers[side]);
|
||||
const text = getTleText(side);
|
||||
if (!text.trim()) {
|
||||
state[side] = null;
|
||||
updateReference(side, null);
|
||||
return;
|
||||
}
|
||||
setStatus(side, 'Разбор...', 'text-muted');
|
||||
state.parseTimers[side] = window.setTimeout(() => parseTle(side), 350);
|
||||
}
|
||||
|
||||
async function parseTle(side) {
|
||||
try {
|
||||
const parsed = await postJson('/api/tle-comparison/parse', { tle: getTleText(side) });
|
||||
state[side] = parsed;
|
||||
updateReference(side, parsed);
|
||||
setStatus(side, 'Разобрано', 'tle-parse-ok');
|
||||
} catch (error) {
|
||||
state[side] = null;
|
||||
updateReference(side, null);
|
||||
setStatus(side, error.message || 'Ошибка разбора', 'tle-parse-error');
|
||||
}
|
||||
}
|
||||
|
||||
async function compareTle() {
|
||||
showAlert('');
|
||||
const first = getTleText('first');
|
||||
const second = getTleText('second');
|
||||
if (!first.trim() || !second.trim()) {
|
||||
showAlert('Введите оба TLE для сравнения.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
setCompareLoading(true);
|
||||
try {
|
||||
const response = await postJson('/api/tle-comparison/compare', { first, second });
|
||||
state.first = response.first;
|
||||
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) {
|
||||
showAlert(error.message || 'Не удалось выполнить сравнение TLE.', 'danger');
|
||||
} finally {
|
||||
setCompareLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function renderComparison(rows) {
|
||||
const body = el('tle-comparison-result-body');
|
||||
const empty = el('tle-comparison-empty');
|
||||
const wrap = el('tle-comparison-table-wrap');
|
||||
const meta = el('tle-comparison-result-meta');
|
||||
if (!body || !empty || !wrap) return;
|
||||
|
||||
if (!rows.length) {
|
||||
body.innerHTML = '';
|
||||
empty.classList.remove('d-none');
|
||||
wrap.classList.add('d-none');
|
||||
if (meta) meta.textContent = 'Нет данных для сравнения.';
|
||||
return;
|
||||
}
|
||||
|
||||
body.innerHTML = rows.map((row) => `
|
||||
<tr>
|
||||
<td>${escapeHtml(row.name || row.code || '')}</td>
|
||||
<td>${escapeHtml(emptyToDash(row.first))}</td>
|
||||
<td>${escapeHtml(emptyToDash(row.second))}</td>
|
||||
<td class="${deltaClass(row.delta)}">${escapeHtml(emptyToDash(row.delta))}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
empty.classList.add('d-none');
|
||||
wrap.classList.remove('d-none');
|
||||
if (meta) {
|
||||
const firstEpoch = formatDateTime(state.first?.time);
|
||||
const secondEpoch = formatDateTime(state.second?.time);
|
||||
meta.textContent = `Эпохи: TLE 1 — ${firstEpoch || '—'}, TLE 2 — ${secondEpoch || '—'}`;
|
||||
}
|
||||
}
|
||||
|
||||
function updateReference(side, parsed) {
|
||||
setText(`tle-${side}-epoch`, formatDateTime(parsed?.time) || '—');
|
||||
setText(`tle-${side}-norad`, parsed?.noradId ?? '—');
|
||||
setText(`tle-${side}-revolution`, parsed?.revolution ?? '—');
|
||||
if (!parsed) {
|
||||
const text = getTleText(side);
|
||||
setStatus(side, text.trim() ? 'Ошибка разбора' : 'Нет данных', text.trim() ? 'tle-parse-error' : 'text-muted');
|
||||
}
|
||||
}
|
||||
|
||||
function clearPage() {
|
||||
el('tle-first-text').value = '';
|
||||
el('tle-second-text').value = '';
|
||||
state.first = null;
|
||||
state.second = null;
|
||||
updateReference('first', null);
|
||||
updateReference('second', null);
|
||||
showAlert('');
|
||||
renderComparison([]);
|
||||
setText('tle-comparison-result-meta', 'Нажмите “Сравнить” после ввода двух TLE.');
|
||||
}
|
||||
|
||||
async function postJson(url, payload) {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (!response.ok) {
|
||||
let message = `HTTP ${response.status}`;
|
||||
try {
|
||||
const error = await response.json();
|
||||
message = error.error || Object.values(error)[0] || message;
|
||||
} catch (_) {
|
||||
message = await response.text() || message;
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function getTleText(side) {
|
||||
return el(`tle-${side}-text`)?.value || '';
|
||||
}
|
||||
|
||||
function setStatus(side, text, cssClass) {
|
||||
const node = el(`tle-${side}-status`);
|
||||
if (!node) return;
|
||||
node.className = `tle-parse-status ${cssClass || 'text-muted'}`;
|
||||
node.textContent = text;
|
||||
}
|
||||
|
||||
function setCompareLoading(loading) {
|
||||
const button = el('tle-compare-submit');
|
||||
if (!button) return;
|
||||
button.disabled = loading;
|
||||
button.textContent = loading ? 'Сравнение...' : 'Сравнить';
|
||||
}
|
||||
|
||||
function showAlert(message, type = 'info') {
|
||||
const container = el('tle-comparison-alert');
|
||||
if (!container) return;
|
||||
container.innerHTML = message
|
||||
? `<div class="alert alert-${type} alert-dismissible fade show" role="alert">
|
||||
${escapeHtml(message)}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Закрыть"></button>
|
||||
</div>`
|
||||
: '';
|
||||
}
|
||||
|
||||
function formatDateTime(value) {
|
||||
if (!value) return '';
|
||||
return String(value).replace('T', ' ');
|
||||
}
|
||||
|
||||
function emptyToDash(value) {
|
||||
return value === null || value === undefined || value === '' ? '—' : value;
|
||||
}
|
||||
|
||||
function deltaClass(value) {
|
||||
const text = String(value || '').trim();
|
||||
if (text.startsWith('+')) return 'tle-delta-positive';
|
||||
if (text.startsWith('-')) return 'tle-delta-negative';
|
||||
return '';
|
||||
}
|
||||
|
||||
function setText(id, value) {
|
||||
const node = el(id);
|
||||
if (node) node.textContent = value;
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? '')
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
function el(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
})();
|
||||
@@ -46,6 +46,7 @@
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="/satellites">Управление</a></li>
|
||||
<li><a class="dropdown-item" href="/satellites/pdcm">ПДЦМ</a></li>
|
||||
<li><a class="dropdown-item" href="/tle-comparison">Сравнение TLE</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.w3.org/1999/xhtml"
|
||||
layout:decorate="~{layouts/main-layout}">
|
||||
<head>
|
||||
<title>Сравнение TLE</title>
|
||||
<link rel="stylesheet" href="/css/catalog.css"/>
|
||||
<link rel="stylesheet" href="/css/tle-comparison.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<th:block layout:fragment="content">
|
||||
<div id="tle-comparison-page" class="catalog-page tle-comparison-page py-3">
|
||||
<div class="d-flex justify-content-between align-items-start flex-wrap gap-3 mb-4">
|
||||
<div>
|
||||
<h2 class="mb-1">Сравнение TLE</h2>
|
||||
<div class="catalog-helper">Вставьте два набора TLE, проверьте эпохи и сравните параметры, полученные через ballistics-service.</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tle-comparison-alert"></div>
|
||||
|
||||
<div class="card catalog-card tle-input-card mb-4">
|
||||
<div class="card-header">
|
||||
<div class="catalog-section-title">Ввод данных</div>
|
||||
<div class="catalog-helper mt-1">Можно вставить две или три строки TLE. Заголовок определяется автоматически, строки 1 и 2 обязательны.</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-4">
|
||||
<div class="col-12 col-xl-6">
|
||||
<div class="tle-input-widget h-100">
|
||||
<div class="d-flex justify-content-between align-items-center gap-3 mb-2">
|
||||
<label for="tle-first-text" class="form-label mb-0">TLE 1</label>
|
||||
<span id="tle-first-status" class="tle-parse-status text-muted">Нет данных</span>
|
||||
</div>
|
||||
<textarea id="tle-first-text"
|
||||
class="form-control tle-textarea"
|
||||
spellcheck="false"
|
||||
placeholder="ISS (ZARYA) 1 25544U 98067A ... 2 25544 ..."></textarea>
|
||||
<div class="tle-reference mt-3">
|
||||
<div class="tle-reference-row">
|
||||
<span>Эпоха TLE</span>
|
||||
<strong id="tle-first-epoch">—</strong>
|
||||
</div>
|
||||
<div class="tle-reference-row">
|
||||
<span>NORAD</span>
|
||||
<strong id="tle-first-norad">—</strong>
|
||||
</div>
|
||||
<div class="tle-reference-row">
|
||||
<span>Виток</span>
|
||||
<strong id="tle-first-revolution">—</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-xl-6">
|
||||
<div class="tle-input-widget h-100">
|
||||
<div class="d-flex justify-content-between align-items-center gap-3 mb-2">
|
||||
<label for="tle-second-text" class="form-label mb-0">TLE 2</label>
|
||||
<span id="tle-second-status" class="tle-parse-status text-muted">Нет данных</span>
|
||||
</div>
|
||||
<textarea id="tle-second-text"
|
||||
class="form-control tle-textarea"
|
||||
spellcheck="false"
|
||||
placeholder="ISS (ZARYA) 1 25544U 98067A ... 2 25544 ..."></textarea>
|
||||
<div class="tle-reference mt-3">
|
||||
<div class="tle-reference-row">
|
||||
<span>Эпоха TLE</span>
|
||||
<strong id="tle-second-epoch">—</strong>
|
||||
</div>
|
||||
<div class="tle-reference-row">
|
||||
<span>NORAD</span>
|
||||
<strong id="tle-second-norad">—</strong>
|
||||
</div>
|
||||
<div class="tle-reference-row">
|
||||
<span>Виток</span>
|
||||
<strong id="tle-second-revolution">—</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card catalog-card tle-result-card">
|
||||
<div class="card-header d-flex justify-content-between align-items-start gap-3 flex-wrap">
|
||||
<div>
|
||||
<div class="catalog-section-title">Результаты</div>
|
||||
<div id="tle-comparison-result-meta" class="catalog-helper mt-1">Нажмите “Сравнить” после ввода двух TLE.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div id="tle-comparison-empty" class="catalog-empty">Результаты сравнения появятся здесь.</div>
|
||||
<div id="tle-comparison-table-wrap" class="table-responsive tle-result-table-wrap d-none">
|
||||
<table class="table table-hover mb-0 align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Параметр</th>
|
||||
<th>TLE 1</th>
|
||||
<th>TLE 2</th>
|
||||
<th>Разница</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tle-comparison-result-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/tle_comparison_scripts.js"></script>
|
||||
</th:block>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user