сравнение TLE
This commit is contained in:
@@ -39,6 +39,7 @@ settings:
|
|||||||
dynamic-plan-service: ${pcp.services.dynamic-plan}
|
dynamic-plan-service: ${pcp.services.dynamic-plan}
|
||||||
satellite-catalog-service: ${pcp.services.satellite-catalog}
|
satellite-catalog-service: ${pcp.services.satellite-catalog}
|
||||||
ballistics-service: ${pcp.services.ballistics}
|
ballistics-service: ${pcp.services.ballistics}
|
||||||
|
tle-monitoring-service: ${pcp.services.tle-monitoring}
|
||||||
stations-service: ${pcp.services.stations}
|
stations-service: ${pcp.services.stations}
|
||||||
slots-service: ${pcp.services.slots}
|
slots-service: ${pcp.services.slots}
|
||||||
mission-service: ${pcp.services.mission}
|
mission-service: ${pcp.services.mission}
|
||||||
|
|||||||
+3
@@ -26,4 +26,7 @@ class CatalogPageController {
|
|||||||
|
|
||||||
@GetMapping("/bookings")
|
@GetMapping("/bookings")
|
||||||
fun bookingsPage(): String = "bookings"
|
fun bookingsPage(): String = "bookings"
|
||||||
|
|
||||||
|
@GetMapping("/tle-analysis")
|
||||||
|
fun tleAnalysisPage(): String = "tle-analysis"
|
||||||
}
|
}
|
||||||
|
|||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
package space.nstart.pcp.slots_service.controller
|
||||||
|
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
import space.nstart.pcp.slots_service.service.TleAnalysisService
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/tle-analysis")
|
||||||
|
class TleAnalysisController(
|
||||||
|
private val tleAnalysisService: TleAnalysisService
|
||||||
|
) {
|
||||||
|
@GetMapping("/satellites/{satelliteId}")
|
||||||
|
fun analyzeSatellite(@PathVariable satelliteId: Long) = tleAnalysisService.analyzeSatellite(satelliteId)
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
package space.nstart.pcp.slots_service.dto.tleanalysis
|
||||||
|
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
data class TleAnalysisResponseDTO(
|
||||||
|
val satelliteId: Long,
|
||||||
|
val rows: List<TleAnalysisRowDTO>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class TleAnalysisRowDTO(
|
||||||
|
val index: Int,
|
||||||
|
val epoch: LocalDateTime,
|
||||||
|
val revolution: Long,
|
||||||
|
val noradId: Long,
|
||||||
|
val tleHeader: String?,
|
||||||
|
val radiusVector: RadiusVectorDTO,
|
||||||
|
val radiusNorm: Double,
|
||||||
|
val discrepancy: RadiusVectorDiscrepancyDTO?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class RadiusVectorDTO(
|
||||||
|
val x: Double,
|
||||||
|
val y: Double,
|
||||||
|
val z: Double
|
||||||
|
)
|
||||||
|
|
||||||
|
data class RadiusVectorDiscrepancyDTO(
|
||||||
|
val previousEpoch: LocalDateTime,
|
||||||
|
val dx: Double,
|
||||||
|
val dy: Double,
|
||||||
|
val dz: Double,
|
||||||
|
val norm: Double
|
||||||
|
)
|
||||||
+94
@@ -0,0 +1,94 @@
|
|||||||
|
package space.nstart.pcp.slots_service.service
|
||||||
|
|
||||||
|
import ballistics.Ballistics
|
||||||
|
import ballistics.orbitalPoints.timeStepper.TLEStepper
|
||||||
|
import ballistics.types.EarthType
|
||||||
|
import ballistics.types.TLE
|
||||||
|
import ballistics.utils.fromDateTime
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEExtensionDTO
|
||||||
|
import space.nstart.pcp.slots_service.configuration.CustomErrorException
|
||||||
|
import space.nstart.pcp.slots_service.dto.tleanalysis.RadiusVectorDTO
|
||||||
|
import space.nstart.pcp.slots_service.dto.tleanalysis.RadiusVectorDiscrepancyDTO
|
||||||
|
import space.nstart.pcp.slots_service.dto.tleanalysis.TleAnalysisResponseDTO
|
||||||
|
import space.nstart.pcp.slots_service.dto.tleanalysis.TleAnalysisRowDTO
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class TleAnalysisService(
|
||||||
|
private val tleMonitoringService: TleMonitoringService
|
||||||
|
) {
|
||||||
|
fun analyzeSatellite(satelliteId: Long): TleAnalysisResponseDTO {
|
||||||
|
val records = tleMonitoringService.satelliteTles(satelliteId)
|
||||||
|
.mapNotNull { parseRecord(it) }
|
||||||
|
.sortedBy { it.epoch }
|
||||||
|
|
||||||
|
val rows = records.mapIndexed { index, record ->
|
||||||
|
val currentPoint = calculatePoint(record.tle, record.epoch)
|
||||||
|
val radiusVector = RadiusVectorDTO(
|
||||||
|
x = currentPoint.r.x,
|
||||||
|
y = currentPoint.r.y,
|
||||||
|
z = currentPoint.r.z
|
||||||
|
)
|
||||||
|
|
||||||
|
val discrepancy = records.getOrNull(index - 1)?.let { previous ->
|
||||||
|
val previousPointAtCurrentEpoch = calculatePoint(previous.tle, record.epoch)
|
||||||
|
val dx = currentPoint.r.x - previousPointAtCurrentEpoch.r.x
|
||||||
|
val dy = currentPoint.r.y - previousPointAtCurrentEpoch.r.y
|
||||||
|
val dz = currentPoint.r.z - previousPointAtCurrentEpoch.r.z
|
||||||
|
RadiusVectorDiscrepancyDTO(
|
||||||
|
previousEpoch = previous.epoch,
|
||||||
|
dx = dx,
|
||||||
|
dy = dy,
|
||||||
|
dz = dz,
|
||||||
|
norm = norm(dx, dy, dz)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
TleAnalysisRowDTO(
|
||||||
|
index = index + 1,
|
||||||
|
epoch = record.epoch,
|
||||||
|
revolution = record.revolution,
|
||||||
|
noradId = record.noradId,
|
||||||
|
tleHeader = record.tle.header,
|
||||||
|
radiusVector = radiusVector,
|
||||||
|
radiusNorm = norm(radiusVector.x, radiusVector.y, radiusVector.z),
|
||||||
|
discrepancy = discrepancy
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return TleAnalysisResponseDTO(
|
||||||
|
satelliteId = satelliteId,
|
||||||
|
rows = rows
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseRecord(source: TLEExtensionDTO): ParsedTleRecord? = runCatching {
|
||||||
|
val tle = source.tle
|
||||||
|
val params = Ballistics().getTLEParams(TLE(tle.first, tle.second), tle.header ?: "")
|
||||||
|
ParsedTleRecord(
|
||||||
|
tle = tle,
|
||||||
|
epoch = params.time,
|
||||||
|
revolution = source.revolution.takeIf { it > 0 } ?: params.revolution,
|
||||||
|
noradId = params.noradId
|
||||||
|
)
|
||||||
|
}.getOrNull()
|
||||||
|
|
||||||
|
private fun calculatePoint(tle: TLEDTO, time: LocalDateTime) = runCatching {
|
||||||
|
val stepper = TLEStepper(tle.first, tle.second, EarthType.PZ90d02)
|
||||||
|
stepper.calculate(fromDateTime(time))
|
||||||
|
}.getOrElse { ex ->
|
||||||
|
throw CustomErrorException("Ошибка расчета положения по TLE на время $time: ${ex.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun norm(x: Double, y: Double, z: Double): Double = sqrt(x * x + y * y + z * z)
|
||||||
|
|
||||||
|
private data class ParsedTleRecord(
|
||||||
|
val tle: TLEDTO,
|
||||||
|
val epoch: LocalDateTime,
|
||||||
|
val revolution: Long,
|
||||||
|
val noradId: Long
|
||||||
|
)
|
||||||
|
}
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
package space.nstart.pcp.slots_service.service
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.ObjectProvider
|
||||||
|
import org.springframework.beans.factory.annotation.Value
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.web.reactive.function.client.WebClient
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEExtensionDTO
|
||||||
|
import space.nstart.pcp.slots_service.configuration.CustomErrorException
|
||||||
|
import tools.jackson.databind.ObjectMapper
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class TleMonitoringService(webClientBuilderProvider: ObjectProvider<WebClient.Builder>) {
|
||||||
|
private val webClientBuilder: WebClient.Builder = webClientBuilderProvider.ifAvailable ?: WebClient.builder()
|
||||||
|
|
||||||
|
@Value("\${settings.tle-monitoring-service:tle-monitoring-service}")
|
||||||
|
private val tleMonitoringUrl: String = ""
|
||||||
|
|
||||||
|
fun satelliteTles(satelliteId: Long): List<TLEExtensionDTO> =
|
||||||
|
webClientBuilder.build()
|
||||||
|
.get()
|
||||||
|
.uri("$tleMonitoringUrl/v1/api/tle/satellite/$satelliteId")
|
||||||
|
.retrieve()
|
||||||
|
.onStatus({ status -> status.isError }, ::mapError)
|
||||||
|
.bodyToFlux(TLEExtensionDTO::class.java)
|
||||||
|
.collectList()
|
||||||
|
.block()
|
||||||
|
?: emptyList()
|
||||||
|
|
||||||
|
private fun mapError(response: org.springframework.web.reactive.function.client.ClientResponse): Mono<Throwable> =
|
||||||
|
response.bodyToMono(String::class.java)
|
||||||
|
.defaultIfEmpty("error")
|
||||||
|
.flatMap { body -> Mono.error(CustomErrorException(extractErrorMessage(body))) }
|
||||||
|
|
||||||
|
private fun extractErrorMessage(body: String): String {
|
||||||
|
val text = body.trim()
|
||||||
|
if (text.isEmpty()) return "error"
|
||||||
|
|
||||||
|
return runCatching {
|
||||||
|
val node = ObjectMapper().readTree(text)
|
||||||
|
when {
|
||||||
|
node.has("message") -> node.get("message").asText()
|
||||||
|
node.has("error") -> node.get("error").asText()
|
||||||
|
else -> text
|
||||||
|
}
|
||||||
|
}.getOrDefault(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
.tle-analysis-page {
|
||||||
|
height: calc(100vh - 4.75rem);
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-analysis-layout {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-analysis-layout > [class*="col-"] {
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-analysis-sidebar,
|
||||||
|
.tle-analysis-results-card {
|
||||||
|
min-height: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-analysis-satellites-wrap,
|
||||||
|
.tle-analysis-results-wrap {
|
||||||
|
overflow: auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-analysis-satellites-wrap {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-analysis-results-wrap {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-analysis-satellites-table tbody tr,
|
||||||
|
.tle-analysis-results-table tbody tr {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-analysis-satellites-table tbody tr.active {
|
||||||
|
--bs-table-bg: rgba(13, 110, 253, 0.12);
|
||||||
|
--bs-table-hover-bg: rgba(13, 110, 253, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-analysis-vector {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-analysis-discrepancy {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1199.98px) {
|
||||||
|
.tle-analysis-page {
|
||||||
|
height: auto;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tle-analysis-satellites-wrap,
|
||||||
|
.tle-analysis-results-wrap {
|
||||||
|
max-height: 60vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
(function () {
|
||||||
|
const state = {
|
||||||
|
satellites: [],
|
||||||
|
selectedSatelliteId: null,
|
||||||
|
analysis: null,
|
||||||
|
loading: false
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
bindEvents();
|
||||||
|
await loadSatellites();
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindEvents() {
|
||||||
|
document.getElementById('tle-analysis-refresh')?.addEventListener('click', async () => {
|
||||||
|
await loadSatellites();
|
||||||
|
if (state.selectedSatelliteId) {
|
||||||
|
await loadAnalysis(state.selectedSatelliteId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.getElementById('tle-analysis-satellite-filter')?.addEventListener('input', renderSatellites);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSatellites() {
|
||||||
|
showAlert('Загрузка списка спутников...', 'info');
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/catalog/satellites');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(await errorText(response));
|
||||||
|
}
|
||||||
|
state.satellites = await response.json();
|
||||||
|
renderSatellites();
|
||||||
|
clearAlert();
|
||||||
|
} catch (error) {
|
||||||
|
showAlert(`Не удалось загрузить спутники: ${error.message}`, 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAnalysis(satelliteId) {
|
||||||
|
state.loading = true;
|
||||||
|
state.analysis = null;
|
||||||
|
renderResults();
|
||||||
|
showAlert('Формирование анализа TLE...', 'info');
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/tle-analysis/satellites/${encodeURIComponent(satelliteId)}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(await errorText(response));
|
||||||
|
}
|
||||||
|
state.analysis = await response.json();
|
||||||
|
renderResults();
|
||||||
|
clearAlert();
|
||||||
|
} catch (error) {
|
||||||
|
state.analysis = null;
|
||||||
|
renderResults();
|
||||||
|
showAlert(`Не удалось сформировать анализ TLE: ${error.message}`, 'danger');
|
||||||
|
} finally {
|
||||||
|
state.loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSatellites() {
|
||||||
|
const tbody = document.getElementById('tle-analysis-satellites-body');
|
||||||
|
if (!tbody) return;
|
||||||
|
|
||||||
|
const filter = normalize(document.getElementById('tle-analysis-satellite-filter')?.value || '');
|
||||||
|
const satellites = state.satellites.filter((satellite) => {
|
||||||
|
if (!filter) return true;
|
||||||
|
return normalize([
|
||||||
|
satellite.id,
|
||||||
|
satellite.noradId,
|
||||||
|
satellite.code,
|
||||||
|
satellite.name,
|
||||||
|
satellite.typeCode
|
||||||
|
].join(' ')).includes(filter);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (satellites.length === 0) {
|
||||||
|
tbody.innerHTML = `<tr><td colspan="3" class="catalog-empty">Спутники не найдены</td></tr>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody.innerHTML = satellites.map((satellite) => {
|
||||||
|
const active = Number(satellite.id) === Number(state.selectedSatelliteId) ? 'active' : '';
|
||||||
|
const title = escapeHtml(satellite.name || satellite.code || `КА ${satellite.id}`);
|
||||||
|
return `
|
||||||
|
<tr class="${active}" data-satellite-id="${escapeHtml(satellite.id)}">
|
||||||
|
<td>${escapeHtml(satellite.id)}</td>
|
||||||
|
<td>
|
||||||
|
<div class="catalog-main-text">${title}</div>
|
||||||
|
<div class="catalog-helper">${escapeHtml(satellite.code || '')} ${satellite.typeCode ? ' / ' + escapeHtml(satellite.typeCode) : ''}</div>
|
||||||
|
</td>
|
||||||
|
<td>${satellite.noradId == null ? '—' : escapeHtml(satellite.noradId)}</td>
|
||||||
|
</tr>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
tbody.querySelectorAll('tr[data-satellite-id]').forEach((row) => {
|
||||||
|
row.addEventListener('click', async () => {
|
||||||
|
const satelliteId = Number(row.dataset.satelliteId);
|
||||||
|
state.selectedSatelliteId = satelliteId;
|
||||||
|
state.analysis = null;
|
||||||
|
renderSatellites();
|
||||||
|
renderSelectedSatellite();
|
||||||
|
await loadAnalysis(satelliteId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSelectedSatellite() {
|
||||||
|
const label = document.getElementById('tle-analysis-selected-satellite');
|
||||||
|
if (!label) return;
|
||||||
|
const satellite = state.satellites.find((item) => Number(item.id) === Number(state.selectedSatelliteId));
|
||||||
|
if (!satellite) {
|
||||||
|
label.textContent = 'Выберите спутник слева.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const name = satellite.name || satellite.code || `КА ${satellite.id}`;
|
||||||
|
label.textContent = `КА ${satellite.id}: ${name}${satellite.noradId == null ? '' : `, NORAD ${satellite.noradId}`}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderResults() {
|
||||||
|
renderSelectedSatellite();
|
||||||
|
const empty = document.getElementById('tle-analysis-empty');
|
||||||
|
const wrap = document.getElementById('tle-analysis-table-wrap');
|
||||||
|
const tbody = document.getElementById('tle-analysis-results-body');
|
||||||
|
const summary = document.getElementById('tle-analysis-summary');
|
||||||
|
if (!empty || !wrap || !tbody || !summary) return;
|
||||||
|
|
||||||
|
if (state.loading) {
|
||||||
|
empty.textContent = 'Формирование анализа TLE...';
|
||||||
|
empty.classList.remove('d-none');
|
||||||
|
wrap.classList.add('d-none');
|
||||||
|
summary.textContent = '';
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = state.analysis?.rows || [];
|
||||||
|
summary.textContent = rows.length ? `Записей TLE: ${rows.length}` : '';
|
||||||
|
|
||||||
|
if (!state.selectedSatelliteId) {
|
||||||
|
empty.textContent = 'Выберите спутник для загрузки TLE.';
|
||||||
|
empty.classList.remove('d-none');
|
||||||
|
wrap.classList.add('d-none');
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rows.length === 0) {
|
||||||
|
empty.textContent = 'Для выбранного спутника TLE не найдены.';
|
||||||
|
empty.classList.remove('d-none');
|
||||||
|
wrap.classList.add('d-none');
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
empty.classList.add('d-none');
|
||||||
|
wrap.classList.remove('d-none');
|
||||||
|
tbody.innerHTML = rows.map(renderAnalysisRow).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAnalysisRow(row) {
|
||||||
|
const r = row.radiusVector || {};
|
||||||
|
const discrepancy = row.discrepancy;
|
||||||
|
const diffText = discrepancy
|
||||||
|
? `<div class="tle-analysis-discrepancy">${formatNumber(discrepancy.norm, 3)}</div>
|
||||||
|
<div class="catalog-helper tle-analysis-vector">Δx=${formatNumber(discrepancy.dx, 3)}, Δy=${formatNumber(discrepancy.dy, 3)}, Δz=${formatNumber(discrepancy.dz, 3)}</div>
|
||||||
|
<div class="catalog-helper">к предыдущей эпохе ${escapeHtml(formatDateTime(discrepancy.previousEpoch))}</div>`
|
||||||
|
: '<span class="catalog-helper">не рассчитывается</span>';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td>${escapeHtml(row.index)}</td>
|
||||||
|
<td>
|
||||||
|
<div>${escapeHtml(formatDateTime(row.epoch))}</div>
|
||||||
|
${row.tleHeader ? `<div class="catalog-helper">${escapeHtml(row.tleHeader)}</div>` : ''}
|
||||||
|
</td>
|
||||||
|
<td>${escapeHtml(row.revolution)}</td>
|
||||||
|
<td>${escapeHtml(row.noradId)}</td>
|
||||||
|
<td class="tle-analysis-vector">
|
||||||
|
x=${formatNumber(r.x, 3)}<br>
|
||||||
|
y=${formatNumber(r.y, 3)}<br>
|
||||||
|
z=${formatNumber(r.z, 3)}
|
||||||
|
</td>
|
||||||
|
<td>${formatNumber(row.radiusNorm, 3)}</td>
|
||||||
|
<td>${diffText}</td>
|
||||||
|
</tr>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalize(value) {
|
||||||
|
return String(value || '').trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(value) {
|
||||||
|
if (!value) return '—';
|
||||||
|
return String(value).replace('T', ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatNumber(value, fractionDigits) {
|
||||||
|
const number = Number(value);
|
||||||
|
if (!Number.isFinite(number)) return '—';
|
||||||
|
return number.toLocaleString('ru-RU', {
|
||||||
|
minimumFractionDigits: fractionDigits,
|
||||||
|
maximumFractionDigits: fractionDigits
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function errorText(response) {
|
||||||
|
const text = await response.text();
|
||||||
|
if (!text) return `${response.status} ${response.statusText}`;
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(text);
|
||||||
|
return json.message || json.error || text;
|
||||||
|
} catch (_) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAlert(message, type) {
|
||||||
|
const alert = document.getElementById('tle-analysis-alert');
|
||||||
|
if (!alert) return;
|
||||||
|
alert.innerHTML = `<div class="alert alert-${type} py-2" role="alert">${escapeHtml(message)}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAlert() {
|
||||||
|
const alert = document.getElementById('tle-analysis-alert');
|
||||||
|
if (alert) alert.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value ?? '')
|
||||||
|
.replaceAll('&', '&')
|
||||||
|
.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>')
|
||||||
|
.replaceAll('"', '"')
|
||||||
|
.replaceAll("'", ''');
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -47,6 +47,7 @@
|
|||||||
<li><a class="dropdown-item" href="/satellites">Управление</a></li>
|
<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="/satellites/pdcm">ПДЦМ</a></li>
|
||||||
<li><a class="dropdown-item" href="/tle-comparison">Сравнение TLE</a></li>
|
<li><a class="dropdown-item" href="/tle-comparison">Сравнение TLE</a></li>
|
||||||
|
<li><a class="dropdown-item" href="/tle-analysis">Анализ TLE</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<!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-analysis.css"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<th:block layout:fragment="content">
|
||||||
|
<div id="tle-analysis-page" class="catalog-page tle-analysis-page py-3">
|
||||||
|
<div class="d-flex justify-content-between align-items-start flex-wrap gap-3 mb-3">
|
||||||
|
<div>
|
||||||
|
<h2 class="mb-1">Анализ TLE</h2>
|
||||||
|
<div class="catalog-helper">Анализ изменения TLE выбранного спутника по разнице рассчитанных радиус-векторов.</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2 flex-wrap">
|
||||||
|
<button id="tle-analysis-refresh" type="button" class="btn btn-outline-secondary">Обновить</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tle-analysis-alert"></div>
|
||||||
|
|
||||||
|
<div class="row g-3 tle-analysis-layout">
|
||||||
|
<div class="col-12 col-xl-4">
|
||||||
|
<div class="card catalog-card tle-analysis-sidebar h-100">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="catalog-section-title">Спутники</div>
|
||||||
|
<div class="catalog-helper mt-1">Выберите КА для формирования аналитической таблицы TLE.</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-3">
|
||||||
|
<label for="tle-analysis-satellite-filter" class="form-label">Фильтр</label>
|
||||||
|
<input id="tle-analysis-satellite-filter"
|
||||||
|
type="search"
|
||||||
|
class="form-control form-control-sm"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="ID, NORAD, код, имя, тип">
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0 tle-analysis-satellites-wrap">
|
||||||
|
<table class="table table-hover mb-0 tle-analysis-satellites-table">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Спутник</th>
|
||||||
|
<th>NORAD</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="tle-analysis-satellites-body"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-xl-8">
|
||||||
|
<div class="card catalog-card tle-analysis-results-card h-100">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-start gap-3 flex-wrap">
|
||||||
|
<div>
|
||||||
|
<div class="catalog-section-title">Таблица анализа TLE</div>
|
||||||
|
<div id="tle-analysis-selected-satellite" class="catalog-helper mt-1">Выберите спутник слева.</div>
|
||||||
|
</div>
|
||||||
|
<div id="tle-analysis-summary" class="catalog-helper text-end"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0 tle-analysis-results-wrap">
|
||||||
|
<div id="tle-analysis-empty" class="catalog-empty">Выберите спутник для загрузки TLE.</div>
|
||||||
|
<div id="tle-analysis-table-wrap" class="table-responsive d-none">
|
||||||
|
<table class="table table-hover table-sm mb-0 tle-analysis-results-table">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Эпоха</th>
|
||||||
|
<th>Виток</th>
|
||||||
|
<th>NORAD</th>
|
||||||
|
<th>Радиус-вектор, м</th>
|
||||||
|
<th>|r|, м</th>
|
||||||
|
<th>Расхождение, м</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="tle-analysis-results-body"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
<th:block layout:fragment="scripts">
|
||||||
|
<script src="/tle_analysis_scripts.js"></script>
|
||||||
|
</th:block>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+27
-6
@@ -25,12 +25,33 @@ class TLEController {
|
|||||||
private lateinit var celestrakService: CelesTrakService
|
private lateinit var celestrakService: CelesTrakService
|
||||||
|
|
||||||
@GetMapping()
|
@GetMapping()
|
||||||
fun all() = tleRepository.findAll().map { tle ->
|
fun all() = tleRepository.findAll().map { tle -> tle.toDto() }
|
||||||
val t = tle.tle.split("\r\n")
|
|
||||||
TLEExtensionDTO(
|
@GetMapping("/satellite/{satelliteId}")
|
||||||
tle.satelliteId,
|
fun bySatellite(@PathVariable satelliteId: Long) =
|
||||||
tle.revolution,
|
tleRepository.findAllBySatelliteIdOrderByTimeTleAsc(satelliteId).map { tle -> tle.toDto() }
|
||||||
TLEDTO(t[0], t[1], t[2])
|
|
||||||
|
private fun space.nstart.pcp.tle_monitoring_service.entity.TLEEntity.toDto(): TLEExtensionDTO {
|
||||||
|
val lines = tle.lines().map { it.trimEnd() }.filter { it.isNotBlank() }
|
||||||
|
val header = when {
|
||||||
|
lines.size >= 3 -> lines[0]
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
val first = when {
|
||||||
|
lines.size >= 3 -> lines[1]
|
||||||
|
lines.size >= 2 -> lines[0]
|
||||||
|
else -> " ".repeat(69)
|
||||||
|
}
|
||||||
|
val second = when {
|
||||||
|
lines.size >= 3 -> lines[2]
|
||||||
|
lines.size >= 2 -> lines[1]
|
||||||
|
else -> " ".repeat(69)
|
||||||
|
}
|
||||||
|
|
||||||
|
return TLEExtensionDTO(
|
||||||
|
satelliteId,
|
||||||
|
revolution,
|
||||||
|
TLEDTO(header, first, second)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
@@ -9,6 +9,8 @@ interface TLERepository : JpaRepository<TLEEntity, Long> {
|
|||||||
|
|
||||||
fun findAllBySatelliteId(id : Long) : List<TLEEntity>
|
fun findAllBySatelliteId(id : Long) : List<TLEEntity>
|
||||||
|
|
||||||
|
fun findAllBySatelliteIdOrderByTimeTleAsc(id: Long): List<TLEEntity>
|
||||||
|
|
||||||
fun countBySatelliteId(id: Long): Long
|
fun countBySatelliteId(id: Long): Long
|
||||||
|
|
||||||
fun deleteAllBySatelliteId(id: Long): Long
|
fun deleteAllBySatelliteId(id: Long): Long
|
||||||
|
|||||||
Reference in New Issue
Block a user