diff --git a/.gitlab/ci/pcp-ballistics-service.yml b/.gitlab/ci/pcp-ballistics-service.yml index 7c337a2..52fa9fa 100644 --- a/.gitlab/ci/pcp-ballistics-service.yml +++ b/.gitlab/ci/pcp-ballistics-service.yml @@ -95,7 +95,7 @@ variables: ballistics:test: stage: test - image: repo.nstart.cloud/library/gradle:jdk17-ubi10 + image: repo.nstart.cloud/cloud/gradle:jdk17-ubi10 services: - name: repo.nstart.cloud/postgis/postgis:16-3.4 alias: postgres @@ -124,7 +124,7 @@ ballistics:test: ballistics:build: stage: build - image: repo.nstart.cloud/library/gradle:jdk17-ubi10 + image: repo.nstart.cloud/cloud/gradle:jdk17-ubi10 extends: - .ballistics:rules:build-and-image - .ballistics:gradle-cache diff --git a/.gitlab/ci/pcp-complex-mission-service-tle.yml b/.gitlab/ci/pcp-complex-mission-service-tle.yml index 94e3b81..919bbc3 100644 --- a/.gitlab/ci/pcp-complex-mission-service-tle.yml +++ b/.gitlab/ci/pcp-complex-mission-service-tle.yml @@ -139,7 +139,7 @@ variables: complex-mission-service:test: stage: test - image: repo.nstart.cloud/library/gradle:jdk17-ubi10 + image: repo.nstart.cloud/cloud/gradle:jdk17-ubi10 extends: - .complex-mission-service:rules:test - .pcp-gradle-cache @@ -158,7 +158,7 @@ complex-mission-service:test: complex-mission-service:build: stage: build - image: repo.nstart.cloud/library/gradle:jdk17-ubi10 + image: repo.nstart.cloud/cloud/gradle:jdk17-ubi10 extends: - .complex-mission-service:rules:build-and-image - .pcp-gradle-cache @@ -300,7 +300,7 @@ complex-mission-service:deploy:master: tle-monitoring:test: stage: test - image: repo.nstart.cloud/library/gradle:jdk17-ubi10 + image: repo.nstart.cloud/cloud/gradle:jdk17-ubi10 services: - name: repo.nstart.cloud/postgis/postgis:16-3.4 alias: postgres @@ -330,7 +330,7 @@ tle-monitoring:test: tle-monitoring:build: stage: build - image: repo.nstart.cloud/library/gradle:jdk17-ubi10 + image: repo.nstart.cloud/cloud/gradle:jdk17-ubi10 extends: - .tle-monitoring:rules:build-and-image - .pcp-gradle-cache diff --git a/.gitlab/ci/pcp-coverage-scheme-service.yml b/.gitlab/ci/pcp-coverage-scheme-service.yml index 92abf30..38dca2f 100644 --- a/.gitlab/ci/pcp-coverage-scheme-service.yml +++ b/.gitlab/ci/pcp-coverage-scheme-service.yml @@ -86,7 +86,7 @@ variables: coverage-scheme:test: stage: test - image: repo.nstart.cloud/library/gradle:jdk17-ubi10 + image: repo.nstart.cloud/cloud/gradle:jdk17-ubi10 extends: - .coverage_scheme:rules:test - .coverage_scheme:gradle-cache @@ -105,7 +105,7 @@ coverage-scheme:test: coverage-scheme:build: stage: build - image: repo.nstart.cloud/library/gradle:jdk17-ubi10 + image: repo.nstart.cloud/cloud/gradle:jdk17-ubi10 extends: - .coverage_scheme:rules:build-and-image - .coverage_scheme:gradle-cache diff --git a/.gitlab/ci/pcp-dynamic-plan-service.yml b/.gitlab/ci/pcp-dynamic-plan-service.yml index 27b6c14..fafc4ac 100644 --- a/.gitlab/ci/pcp-dynamic-plan-service.yml +++ b/.gitlab/ci/pcp-dynamic-plan-service.yml @@ -87,7 +87,7 @@ variables: dynamic-plan:test: stage: test - image: repo.nstart.cloud/library/gradle:jdk17-ubi10 + image: repo.nstart.cloud/cloud/gradle:jdk17-ubi10 extends: - .dynamic_plan:rules:test - .dynamic_plan:gradle-cache @@ -106,7 +106,7 @@ dynamic-plan:test: dynamic-plan:build: stage: build - image: repo.nstart.cloud/library/gradle:jdk17-ubi10 + image: repo.nstart.cloud/cloud/gradle:jdk17-ubi10 extends: - .dynamic_plan:rules:build-and-image - .dynamic_plan:gradle-cache diff --git a/.gitlab/ci/pcp-mission-planing-service.yml b/.gitlab/ci/pcp-mission-planing-service.yml index 0d00c47..2987193 100644 --- a/.gitlab/ci/pcp-mission-planing-service.yml +++ b/.gitlab/ci/pcp-mission-planing-service.yml @@ -86,7 +86,7 @@ variables: mission-planing:test: stage: test - image: repo.nstart.cloud/library/gradle:jdk17-ubi10 + image: repo.nstart.cloud/cloud/gradle:jdk17-ubi10 extends: - .mission-planing:rules:test - .mission-planing:gradle-cache @@ -105,7 +105,7 @@ mission-planing:test: mission-planing:build: stage: build - image: repo.nstart.cloud/library/gradle:jdk17-ubi10 + image: repo.nstart.cloud/cloud/gradle:jdk17-ubi10 extends: - .mission-planing:rules:build-and-image - .mission-planing:gradle-cache diff --git a/.gitlab/ci/pcp-request-service.yml b/.gitlab/ci/pcp-request-service.yml index efdfa9d..270971c 100644 --- a/.gitlab/ci/pcp-request-service.yml +++ b/.gitlab/ci/pcp-request-service.yml @@ -85,7 +85,7 @@ variables: request:test: stage: test - image: repo.nstart.cloud/library/gradle:jdk17-ubi10 + image: repo.nstart.cloud/cloud/gradle:jdk17-ubi10 extends: - .request:rules:test - .request:gradle-cache @@ -104,7 +104,7 @@ request:test: request:build: stage: build - image: repo.nstart.cloud/library/gradle:jdk17-ubi10 + image: repo.nstart.cloud/cloud/gradle:jdk17-ubi10 extends: - .request:rules:build-and-image - .request:gradle-cache diff --git a/.gitlab/ci/pcp-satellite-catalog-service.yml b/.gitlab/ci/pcp-satellite-catalog-service.yml index 424ccea..7ce8239 100644 --- a/.gitlab/ci/pcp-satellite-catalog-service.yml +++ b/.gitlab/ci/pcp-satellite-catalog-service.yml @@ -85,7 +85,7 @@ variables: satellite-catalog:test: stage: test - image: repo.nstart.cloud/library/gradle:jdk17-ubi10 + image: repo.nstart.cloud/cloud/gradle:jdk17-ubi10 extends: - .satellite-catalog:rules:test - .satellite-catalog:gradle-cache @@ -104,7 +104,7 @@ satellite-catalog:test: satellite-catalog:build: stage: build - image: repo.nstart.cloud/library/gradle:jdk17-ubi10 + image: repo.nstart.cloud/cloud/gradle:jdk17-ubi10 extends: - .satellite-catalog:rules:build-and-image - .satellite-catalog:gradle-cache diff --git a/.gitlab/ci/pcp-spring-boot-admin-server.yml b/.gitlab/ci/pcp-spring-boot-admin-server.yml index 298be83..416929e 100644 --- a/.gitlab/ci/pcp-spring-boot-admin-server.yml +++ b/.gitlab/ci/pcp-spring-boot-admin-server.yml @@ -76,7 +76,7 @@ variables: spring-boot-admin:test: stage: test - image: repo.nstart.cloud/library/gradle:jdk17-ubi10 + image: repo.nstart.cloud/cloud/gradle:jdk17-ubi10 extends: - .spring-boot-admin:rules:test - .spring-boot-admin:gradle-cache @@ -95,7 +95,7 @@ spring-boot-admin:test: spring-boot-admin:build: stage: build - image: repo.nstart.cloud/library/gradle:jdk17-ubi10 + image: repo.nstart.cloud/cloud/gradle:jdk17-ubi10 extends: - .spring-boot-admin:rules:build-and-image - .spring-boot-admin:gradle-cache diff --git a/.gitlab/ci/pcp-stations-service.yml b/.gitlab/ci/pcp-stations-service.yml index 22b4590..7ac90bd 100644 --- a/.gitlab/ci/pcp-stations-service.yml +++ b/.gitlab/ci/pcp-stations-service.yml @@ -85,7 +85,7 @@ variables: stations:test: stage: test - image: repo.nstart.cloud/library/gradle:jdk17-ubi10 + image: repo.nstart.cloud/cloud/gradle:jdk17-ubi10 extends: - .stations:rules:test - .stations:gradle-cache @@ -104,7 +104,7 @@ stations:test: stations:build: stage: build - image: repo.nstart.cloud/library/gradle:jdk17-ubi10 + image: repo.nstart.cloud/cloud/gradle:jdk17-ubi10 extends: - .stations:rules:build-and-image - .stations:gradle-cache diff --git a/.gitlab/ci/pcp-ui-service.yml b/.gitlab/ci/pcp-ui-service.yml index 21111ae..3832885 100644 --- a/.gitlab/ci/pcp-ui-service.yml +++ b/.gitlab/ci/pcp-ui-service.yml @@ -86,7 +86,7 @@ variables: ui:test: stage: test - image: repo.nstart.cloud/library/gradle:jdk17-ubi10 + image: repo.nstart.cloud/cloud/gradle:jdk17-ubi10 extends: - .ui:rules:test - .ui:gradle-cache @@ -105,7 +105,7 @@ ui:test: ui:build: stage: build - image: repo.nstart.cloud/library/gradle:jdk17-ubi10 + image: repo.nstart.cloud/cloud/gradle:jdk17-ubi10 extends: - .ui:rules:build-and-image - .ui:gradle-cache diff --git a/.gitlab/ci/slots-service.yml b/.gitlab/ci/slots-service.yml index 52749a2..8e4b7e6 100644 --- a/.gitlab/ci/slots-service.yml +++ b/.gitlab/ci/slots-service.yml @@ -86,7 +86,7 @@ variables: slots:test: stage: test - image: repo.nstart.cloud/library/gradle:jdk17-ubi10 + image: repo.nstart.cloud/cloud/gradle:jdk17-ubi10 services: - name: repo.nstart.cloud/postgis/postgis:16-3.4 alias: postgres @@ -115,7 +115,7 @@ slots:test: slots:build: stage: build - image: repo.nstart.cloud/library/gradle:jdk17-ubi10 + image: repo.nstart.cloud/cloud/gradle:jdk17-ubi10 extends: - .slots:rules:build-and-image - .slots:gradle-cache diff --git a/.gitlab/ci/spring-cloud-config-server.yml b/.gitlab/ci/spring-cloud-config-server.yml index e1e45b8..ce7d65d 100644 --- a/.gitlab/ci/spring-cloud-config-server.yml +++ b/.gitlab/ci/spring-cloud-config-server.yml @@ -82,7 +82,7 @@ variables: config-server:test: stage: test - image: repo.nstart.cloud/library/gradle:jdk17-ubi10 + image: repo.nstart.cloud/cloud/gradle:jdk17-ubi10 extends: - .config-server:rules:test - .config-server:gradle-cache @@ -101,7 +101,7 @@ config-server:test: config-server:build: stage: build - image: repo.nstart.cloud/library/gradle:jdk17-ubi10 + image: repo.nstart.cloud/cloud/gradle:jdk17-ubi10 extends: - .config-server:rules:build-and-image - .config-server:gradle-cache diff --git a/config-repo/application-local.yaml b/config-repo/application-local.yaml index ccd2cc3..5fc6b17 100644 --- a/config-repo/application-local.yaml +++ b/config-repo/application-local.yaml @@ -3,8 +3,10 @@ pcp: infra: postgres: - host: ${PCP_POSTGRES_HOST:localhost} - port: ${PCP_POSTGRES_PORT:5432} +# host: ${PCP_POSTGRES_HOST:localhost} +# port: ${PCP_POSTGRES_PORT:5432} + host: ${PCP_POSTGRES_HOST:192.168.100.160} + port: ${PCP_POSTGRES_PORT:35400} kafka: host: ${PCP_KAFKA_HOST:localhost} port: ${PCP_KAFKA_PORT:29092} diff --git a/config-repo/pcp-mission-planing-service.yaml b/config-repo/pcp-mission-planing-service.yaml index ce70309..09c0e9a 100644 --- a/config-repo/pcp-mission-planing-service.yaml +++ b/config-repo/pcp-mission-planing-service.yaml @@ -136,7 +136,7 @@ spring: camunda: client: mode: self-managed - grpc-address: http://camunda.k8s.265.nstart.local:30901 + grpc-address: http://camunda.k8s.265.nstart.cloud:30901 auth: method: none prefer-rest-over-grpc: false diff --git a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/TleAnalysisController.kt b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/TleAnalysisController.kt index 6423084..48477a1 100644 --- a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/TleAnalysisController.kt +++ b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/controller/TleAnalysisController.kt @@ -1,8 +1,10 @@ package space.nstart.pcp.slots_service.controller +import org.slf4j.LoggerFactory 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.RequestParam import org.springframework.web.bind.annotation.RestController import space.nstart.pcp.slots_service.service.TleAnalysisService @@ -11,6 +13,30 @@ import space.nstart.pcp.slots_service.service.TleAnalysisService class TleAnalysisController( private val tleAnalysisService: TleAnalysisService ) { + private val logger = LoggerFactory.getLogger(this::class.java) + + @GetMapping("/satellites") + fun satellites() = + tleAnalysisService.satellites().also { satellites -> + logger.info("TLE analysis satellites request completed: satellites={}", satellites.size) + } + @GetMapping("/satellites/{satelliteId}") - fun analyzeSatellite(@PathVariable satelliteId: Long) = tleAnalysisService.analyzeSatellite(satelliteId) + fun analyzeSatellite( + @PathVariable satelliteId: Long, + @RequestParam(defaultValue = "5") tleCount: Int + ) = tleAnalysisService.analyzeSatellite(satelliteId, tleCount).also { response -> + logger.info( + "TLE analysis report request completed: satelliteId={}, requestedTleCount={}, rows={}", + satelliteId, + tleCount, + response.rows.size + ) + } + + @GetMapping("/satellites/{satelliteId}/analysis") + fun analyzeSatelliteDetails( + @PathVariable satelliteId: Long, + @RequestParam(defaultValue = "5") tleCount: Int + ) = analyzeSatellite(satelliteId, tleCount) } diff --git a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/dto/tleanalysis/TleAnalysisDTO.kt b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/dto/tleanalysis/TleAnalysisDTO.kt index d9b58f3..1d55f43 100644 --- a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/dto/tleanalysis/TleAnalysisDTO.kt +++ b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/dto/tleanalysis/TleAnalysisDTO.kt @@ -2,6 +2,18 @@ package space.nstart.pcp.slots_service.dto.tleanalysis import java.time.LocalDateTime +data class TleAnalysisSatelliteDTO( + val id: Long, + val catalogId: Long? = null, + val noradId: Long? = null, + val code: String = "", + val name: String = "", + val typeCode: String = "", + val active: Boolean = true, + val scanTle: Boolean = false, + val tleRecordsCount: Int = 0 +) + data class TleAnalysisResponseDTO( val satelliteId: Long, val rows: List @@ -26,6 +38,7 @@ data class RadiusVectorDTO( data class RadiusVectorDiscrepancyDTO( val previousEpoch: LocalDateTime, + val epochDifferenceHours: Double, val dx: Double, val dy: Double, val dz: Double, diff --git a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/TleAnalysisService.kt b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/TleAnalysisService.kt index 0094053..ae8eeb6 100644 --- a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/TleAnalysisService.kt +++ b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/TleAnalysisService.kt @@ -5,60 +5,183 @@ import ballistics.orbitalPoints.timeStepper.TLEStepper import ballistics.types.EarthType import ballistics.types.TLE import ballistics.utils.fromDateTime +import org.slf4j.LoggerFactory 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.pcp_types_lib.dto.satellite.SatelliteSummaryDTO 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.TleAnalysisSatelliteDTO import space.nstart.pcp.slots_service.dto.tleanalysis.TleAnalysisResponseDTO import space.nstart.pcp.slots_service.dto.tleanalysis.TleAnalysisRowDTO +import java.time.Duration import java.time.LocalDateTime import kotlin.math.sqrt +import kotlin.system.measureTimeMillis @Service class TleAnalysisService( - private val tleMonitoringService: TleMonitoringService + private val tleMonitoringService: TleMonitoringService, + private val satelliteCatalogService: SatelliteCatalogService ) { - fun analyzeSatellite(satelliteId: Long): TleAnalysisResponseDTO { - val records = tleMonitoringService.satelliteTles(satelliteId) - .mapNotNull { parseRecord(it) } - .sortedBy { it.epoch } + private val logger = LoggerFactory.getLogger(this::class.java) - 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 + fun satellites(): List { + logger.info("TLE analysis satellites list stage started") + var catalogSatellites = emptyList() + val catalogLoadMs = measureTimeMillis { + catalogSatellites = runCatching { satelliteCatalogService.allSatellites() } + .onFailure { exception -> + logger.warn("Не удалось загрузить спутники из satellite-catalog-service для анализа TLE: {}", exception.message) + } + .getOrDefault(emptyList()) + } + var tleRecords = emptyList() + val tleLoadMs = measureTimeMillis { + tleRecords = runCatching { tleMonitoringService.allTles() } + .onFailure { exception -> + logger.warn("Не удалось загрузить TLE из tle-monitoring-service для списка анализа TLE: {}", exception.message) + } + .getOrDefault(emptyList()) + } + logger.info( + "TLE analysis satellites list sources loaded: catalogSatellites={}, tleRecords={}, catalogLoadMs={}, tleLoadMs={}", + catalogSatellites.size, + tleRecords.size, + catalogLoadMs, + tleLoadMs + ) + + if (tleRecords.isEmpty()) { + val fallbackSatellites = catalogSatellites.mapNotNull { satellite -> satellite.toAnalysisSatelliteFallback() } + logger.info( + "TLE analysis satellites list built from catalog fallback: satellites={}", + fallbackSatellites.size ) + return fallbackSatellites + } - 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) + val catalogByNoradId = catalogSatellites + .mapNotNull { satellite -> satellite.noradId?.let { noradId -> noradId to satellite } } + .toMap() + val catalogById = catalogSatellites.associateBy { it.id } + + val satellites = tleRecords + .groupingBy { record -> record.satelliteId } + .eachCount() + .entries + .sortedBy { (satelliteId, _) -> satelliteId } + .map { (tleSatelliteId, recordsCount) -> + val catalogSatellite = catalogByNoradId[tleSatelliteId] ?: catalogById[tleSatelliteId] + catalogSatellite?.toAnalysisSatellite( + tleSatelliteId = tleSatelliteId, + recordsCount = recordsCount + ) ?: TleAnalysisSatelliteDTO( + id = tleSatelliteId, + noradId = tleSatelliteId, + name = "КА $tleSatelliteId", + tleRecordsCount = recordsCount ) } + logger.info( + "TLE analysis satellites list built from TLE records: satellites={}, matchedCatalogSatellites={}, unmatchedTleSatellites={}", + satellites.size, + satellites.count { it.catalogId != null }, + satellites.count { it.catalogId == null } + ) + return satellites + } - 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 + fun analyzeSatellite(satelliteId: Long, tleCount: Int = DEFAULT_TLE_ANALYSIS_COUNT): TleAnalysisResponseDTO { + val normalizedTleCount = tleCount.coerceAtLeast(1) + logger.info( + "TLE analysis report stage started: satelliteId={}, requestedTleCount={}, normalizedTleCount={}", + satelliteId, + tleCount, + normalizedTleCount + ) + + var sourceRecords = emptyList() + val sourceLoadMs = measureTimeMillis { + sourceRecords = tleMonitoringService.satelliteTles(satelliteId) + } + logger.info( + "TLE analysis report source TLE loaded: satelliteId={}, records={}, durationMs={}", + satelliteId, + sourceRecords.size, + sourceLoadMs + ) + + val parsedRecords = sourceRecords.mapNotNull { parseRecord(it) }.sortedBy { it.epoch } + val skippedRecords = sourceRecords.size - parsedRecords.size + if (skippedRecords > 0) { + logger.warn( + "TLE analysis report skipped invalid TLE records: satelliteId={}, skipped={}, sourceRecords={}", + satelliteId, + skippedRecords, + sourceRecords.size ) } + val records = parsedRecords.takeLast(normalizedTleCount) + logger.info( + "TLE analysis report records selected: satelliteId={}, parsedRecords={}, selectedRecords={}, firstEpoch={}, lastEpoch={}", + satelliteId, + parsedRecords.size, + records.size, + records.firstOrNull()?.epoch, + records.lastOrNull()?.epoch + ) + + var rows = emptyList() + val rowsBuildMs = measureTimeMillis { + rows = records.mapIndexed { index, record -> + // Сравнение пары TLE выполняется на эпоху более поздней записи. + val comparisonEpoch = record.epoch + val currentPoint = calculatePoint(record.tle, comparisonEpoch) + val radiusVector = RadiusVectorDTO( + x = currentPoint.r.x, + y = currentPoint.r.y, + z = currentPoint.r.z + ) + + val discrepancy = records.getOrNull(index - 1)?.let { previous -> + val epochDifferenceHours = Duration.between(previous.epoch, comparisonEpoch).toMillis() / MILLIS_IN_HOUR + val previousPointAtCurrentEpoch = calculatePoint(previous.tle, comparisonEpoch) + 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, + epochDifferenceHours = epochDifferenceHours, + 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 + ) + } + } + logger.info( + "TLE analysis report built: satelliteId={}, rows={}, durationMs={}", + satelliteId, + rows.size, + rowsBuildMs + ) + return TleAnalysisResponseDTO( satelliteId = satelliteId, rows = rows @@ -85,10 +208,46 @@ class TleAnalysisService( private fun norm(x: Double, y: Double, z: Double): Double = sqrt(x * x + y * y + z * z) + private fun SatelliteSummaryDTO.toAnalysisSatellite( + tleSatelliteId: Long, + recordsCount: Int + ): TleAnalysisSatelliteDTO = + TleAnalysisSatelliteDTO( + id = tleSatelliteId, + catalogId = id, + noradId = noradId ?: tleSatelliteId, + code = code, + name = name, + typeCode = typeCode, + active = active, + scanTle = scanTle, + tleRecordsCount = recordsCount + ) + + private fun SatelliteSummaryDTO.toAnalysisSatelliteFallback(): TleAnalysisSatelliteDTO? { + val analysisId = noradId ?: id.takeIf { it > 0 } ?: return null + return TleAnalysisSatelliteDTO( + id = analysisId, + catalogId = id, + noradId = noradId, + code = code, + name = name, + typeCode = typeCode, + active = active, + scanTle = scanTle, + tleRecordsCount = 0 + ) + } + private data class ParsedTleRecord( val tle: TLEDTO, val epoch: LocalDateTime, val revolution: Long, val noradId: Long ) + + companion object { + private const val DEFAULT_TLE_ANALYSIS_COUNT = 5 + private const val MILLIS_IN_HOUR = 3_600_000.0 + } } diff --git a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/TleMonitoringService.kt b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/TleMonitoringService.kt index e4317b9..20588bb 100644 --- a/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/TleMonitoringService.kt +++ b/services/pcp-ui-service/src/main/kotlin/space/nstart/pcp/slots_service/service/TleMonitoringService.kt @@ -1,5 +1,6 @@ package space.nstart.pcp.slots_service.service +import org.slf4j.LoggerFactory import org.springframework.beans.factory.ObjectProvider import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Service @@ -8,24 +9,74 @@ 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 +import kotlin.system.measureTimeMillis @Service class TleMonitoringService(webClientBuilderProvider: ObjectProvider) { private val webClientBuilder: WebClient.Builder = webClientBuilderProvider.ifAvailable ?: WebClient.builder() + private val logger = LoggerFactory.getLogger(this::class.java) @Value("\${settings.tle-monitoring-service:tle-monitoring-service}") private val tleMonitoringUrl: String = "" - fun satelliteTles(satelliteId: Long): List = - webClientBuilder.build() - .get() - .uri("$tleMonitoringUrl/v1/api/tle/satellite/$satelliteId") - .retrieve() - .onStatus({ status -> status.isError }, ::mapError) - .bodyToFlux(TLEExtensionDTO::class.java) - .collectList() - .block() - ?: emptyList() + fun allTles(): List { + logger.info("TLE monitoring client all TLE request started") + return runCatching { + var records = emptyList() + val durationMs = measureTimeMillis { + records = webClientBuilder.build() + .get() + .uri("$tleMonitoringUrl/v1/api/tle") + .retrieve() + .onStatus({ status -> status.isError }, ::mapError) + .bodyToFlux(TLEExtensionDTO::class.java) + .collectList() + .block() + ?: emptyList() + } + logger.info( + "TLE monitoring client all TLE request completed: records={}, durationMs={}", + records.size, + durationMs + ) + records + }.getOrElse { exception -> + logger.warn("TLE monitoring client all TLE request failed: {}", exception.message) + throw exception + } + } + + fun satelliteTles(satelliteId: Long): List { + logger.info("TLE monitoring client satellite TLE request started: satelliteId={}", satelliteId) + return runCatching { + var records = emptyList() + val durationMs = measureTimeMillis { + records = webClientBuilder.build() + .get() + .uri("$tleMonitoringUrl/v1/api/tle/satellite/$satelliteId") + .retrieve() + .onStatus({ status -> status.isError }, ::mapError) + .bodyToFlux(TLEExtensionDTO::class.java) + .collectList() + .block() + ?: emptyList() + } + logger.info( + "TLE monitoring client satellite TLE request completed: satelliteId={}, records={}, durationMs={}", + satelliteId, + records.size, + durationMs + ) + records + }.getOrElse { exception -> + logger.warn( + "TLE monitoring client satellite TLE request failed: satelliteId={}, error={}", + satelliteId, + exception.message + ) + throw exception + } + } private fun mapError(response: org.springframework.web.reactive.function.client.ClientResponse): Mono = response.bodyToMono(String::class.java) diff --git a/services/pcp-ui-service/src/main/resources/static/css/tle-analysis.css b/services/pcp-ui-service/src/main/resources/static/css/tle-analysis.css index c0d45de..ffa613a 100644 --- a/services/pcp-ui-service/src/main/resources/static/css/tle-analysis.css +++ b/services/pcp-ui-service/src/main/resources/static/css/tle-analysis.css @@ -1,25 +1,40 @@ .tle-analysis-page { height: calc(100vh - 4.75rem); + min-height: 0; overflow: hidden; display: flex; flex-direction: column; } .tle-analysis-layout { - flex: 1 1 auto; + flex: 1 1 0; min-height: 0; + overflow: hidden; } .tle-analysis-layout > [class*="col-"] { min-height: 0; display: flex; flex-direction: column; + overflow: hidden; } .tle-analysis-sidebar, .tle-analysis-results-card { + flex: 1 1 0; min-height: 0; width: 100%; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.tle-analysis-filter-body { + flex: 0 0 auto; +} + +.tle-analysis-filter-body .form-control { + min-height: 0; } .tle-analysis-satellites-wrap, @@ -29,11 +44,28 @@ } .tle-analysis-satellites-wrap { - flex: 1 1 auto; + flex: 1 1 0; + max-height: 100%; } .tle-analysis-results-wrap { - flex: 1 1 auto; + flex: 1 1 0; + display: flex; + flex-direction: column; + overflow: hidden; + max-height: 100%; +} + +#tle-analysis-table-wrap { + flex: 1 1 0; + height: 100%; + min-height: 0; + max-height: 100%; + overflow: auto; +} + +.tle-analysis-results-table { + min-width: 980px; } .tle-analysis-satellites-table tbody tr, diff --git a/services/pcp-ui-service/src/main/resources/static/tle_analysis_scripts.js b/services/pcp-ui-service/src/main/resources/static/tle_analysis_scripts.js index 590a0c5..47c5eb9 100644 --- a/services/pcp-ui-service/src/main/resources/static/tle_analysis_scripts.js +++ b/services/pcp-ui-service/src/main/resources/static/tle_analysis_scripts.js @@ -15,23 +15,30 @@ function bindEvents() { document.getElementById('tle-analysis-refresh')?.addEventListener('click', async () => { + state.selectedSatelliteId = null; + state.analysis = null; + state.loading = false; + renderResults(); await loadSatellites(); + }); + document.getElementById('tle-analysis-satellite-filter')?.addEventListener('input', renderSatellites); + document.getElementById('tle-analysis-count')?.addEventListener('change', async () => { 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'); + const response = await fetch('/api/tle-analysis/satellites'); if (!response.ok) { throw new Error(await errorText(response)); } - state.satellites = await response.json(); + state.satellites = normalizeSatelliteResponse(await response.json()); renderSatellites(); + renderResults(); clearAlert(); } catch (error) { showAlert(`Не удалось загрузить спутники: ${error.message}`, 'danger'); @@ -44,7 +51,8 @@ renderResults(); showAlert('Формирование анализа TLE...', 'info'); try { - const response = await fetch(`/api/tle-analysis/satellites/${encodeURIComponent(satelliteId)}`); + const params = new URLSearchParams({ tleCount: String(selectedTleCount()) }); + const response = await fetch(`/api/tle-analysis/satellites/${encodeURIComponent(satelliteId)}/analysis?${params}`); if (!response.ok) { throw new Error(await errorText(response)); } @@ -53,10 +61,10 @@ clearAlert(); } catch (error) { state.analysis = null; - renderResults(); showAlert(`Не удалось сформировать анализ TLE: ${error.message}`, 'danger'); } finally { state.loading = false; + renderResults(); } } @@ -69,10 +77,12 @@ if (!filter) return true; return normalize([ satellite.id, + satellite.catalogId, satellite.noradId, satellite.code, satellite.name, - satellite.typeCode + satellite.typeCode, + satellite.tleRecordsCount ].join(' ')).includes(filter); }); @@ -84,12 +94,15 @@ tbody.innerHTML = satellites.map((satellite) => { const active = Number(satellite.id) === Number(state.selectedSatelliteId) ? 'active' : ''; const title = escapeHtml(satellite.name || satellite.code || `КА ${satellite.id}`); + const catalogId = satellite.catalogId == null ? satellite.id : satellite.catalogId; + const tleCount = Number(satellite.tleRecordsCount || 0); return ` - ${escapeHtml(satellite.id)} + ${escapeHtml(catalogId)}
${title}
${escapeHtml(satellite.code || '')} ${satellite.typeCode ? ' / ' + escapeHtml(satellite.typeCode) : ''}
+
TLE-записей: ${escapeHtml(tleCount)}
${satellite.noradId == null ? '—' : escapeHtml(satellite.noradId)} `; @@ -116,7 +129,8 @@ return; } const name = satellite.name || satellite.code || `КА ${satellite.id}`; - label.textContent = `КА ${satellite.id}: ${name}${satellite.noradId == null ? '' : `, NORAD ${satellite.noradId}`}`; + const catalogId = satellite.catalogId == null ? satellite.id : satellite.catalogId; + label.textContent = `КА ${catalogId}: ${name}${satellite.noradId == null ? '' : `, NORAD ${satellite.noradId}`}`; } function renderResults() { @@ -184,10 +198,38 @@ z=${formatNumber(r.z, 3)} ${formatNumber(row.radiusNorm, 3)} + ${discrepancy ? formatNumber(discrepancy.epochDifferenceHours, 3) : '—'} ${diffText} `; } + function normalizeSatelliteResponse(payload) { + if (Array.isArray(payload)) { + return payload; + } + if (Array.isArray(payload?.items)) { + return payload.items; + } + if (Array.isArray(payload?.content)) { + return payload.content; + } + return []; + } + + function selectedTleCount() { + const input = document.getElementById('tle-analysis-count'); + const value = Number(input?.value || 5); + if (!Number.isFinite(value) || value < 1) { + if (input) input.value = '5'; + return 5; + } + const count = Math.floor(value); + if (input && String(count) !== input.value) { + input.value = String(count); + } + return count; + } + function normalize(value) { return String(value || '').trim().toLowerCase(); } diff --git a/services/pcp-ui-service/src/main/resources/templates/tle-analysis.html b/services/pcp-ui-service/src/main/resources/templates/tle-analysis.html index 58c83a2..92f78b0 100644 --- a/services/pcp-ui-service/src/main/resources/templates/tle-analysis.html +++ b/services/pcp-ui-service/src/main/resources/templates/tle-analysis.html @@ -15,7 +15,16 @@

Анализ TLE

Анализ изменения TLE выбранного спутника по разнице рассчитанных радиус-векторов.
-
+
+
+ + +
@@ -29,8 +38,8 @@
Спутники
Выберите КА для формирования аналитической таблицы TLE.
-
- +
+ NORAD Радиус-вектор, м |r|, м + Разница Времени, ч Расхождение, м @@ -84,8 +94,6 @@
- - diff --git a/services/pcp-ui-service/src/test/kotlin/space/nstart/pcp/slots_service/controller/CatalogControllerTest.kt b/services/pcp-ui-service/src/test/kotlin/space/nstart/pcp/slots_service/controller/CatalogControllerTest.kt index 0c16504..50c0c28 100644 --- a/services/pcp-ui-service/src/test/kotlin/space/nstart/pcp/slots_service/controller/CatalogControllerTest.kt +++ b/services/pcp-ui-service/src/test/kotlin/space/nstart/pcp/slots_service/controller/CatalogControllerTest.kt @@ -140,6 +140,23 @@ class CatalogControllerTest { assertTrue(body.contains("/webjars/bootstrap/5.3.0/css/bootstrap.min.css")) } + @Test + fun `tle analysis page is rendered with page script`() { + val body = WebClient.create("http://localhost:$port") + .get() + .uri("/tle-analysis") + .retrieve() + .bodyToMono(String::class.java) + .block()!! + + assertTrue(body.contains("Анализ TLE")) + assertTrue(body.contains("tle-analysis-satellites-body")) + assertTrue(body.contains("tle-analysis-count")) + assertTrue(body.contains("Разница Времени, ч")) + assertTrue(body.contains("value=\"5\"")) + assertTrue(body.contains("/tle_analysis_scripts.js")) + } + @Test fun `stations page is rendered`() { val body = WebClient.create("http://localhost:$port") diff --git a/services/pcp-ui-service/src/test/kotlin/space/nstart/pcp/slots_service/service/TleAnalysisServiceTest.kt b/services/pcp-ui-service/src/test/kotlin/space/nstart/pcp/slots_service/service/TleAnalysisServiceTest.kt new file mode 100644 index 0000000..25924f9 --- /dev/null +++ b/services/pcp-ui-service/src/test/kotlin/space/nstart/pcp/slots_service/service/TleAnalysisServiceTest.kt @@ -0,0 +1,121 @@ +package space.nstart.pcp.slots_service.service + +import org.junit.jupiter.api.Test +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.mock +import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO +import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEExtensionDTO +import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteSummaryDTO +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class TleAnalysisServiceTest { + + private val tleMonitoringService: TleMonitoringService = mock() + private val satelliteCatalogService: SatelliteCatalogService = mock() + private val service = TleAnalysisService(tleMonitoringService, satelliteCatalogService) + + @Test + fun `satellites uses TLE satellite id and enriches rows from catalog by norad id`() { + doReturn( + listOf( + SatelliteSummaryDTO( + id = 101, + noradId = 62138, + code = "SAT-62138", + name = "Test satellite", + typeCode = "EO", + scanTle = true + ) + ) + ).`when`(satelliteCatalogService).allSatellites() + doReturn( + listOf( + TLEExtensionDTO(satelliteId = 62138, revolution = 10, tle = TLEDTO()), + TLEExtensionDTO(satelliteId = 62138, revolution = 11, tle = TLEDTO()) + ) + ).`when`(tleMonitoringService).allTles() + + val satellites = service.satellites() + + assertEquals(1, satellites.size) + assertEquals(62138, satellites.single().id) + assertEquals(101, satellites.single().catalogId) + assertEquals(62138, satellites.single().noradId) + assertEquals("SAT-62138", satellites.single().code) + assertEquals("Test satellite", satellites.single().name) + assertEquals("EO", satellites.single().typeCode) + assertEquals(true, satellites.single().scanTle) + assertEquals(2, satellites.single().tleRecordsCount) + } + + @Test + fun `satellites falls back to catalog when TLE records are empty`() { + doReturn( + listOf( + SatelliteSummaryDTO( + id = 101, + noradId = 62138, + code = "SAT-62138", + name = "Test satellite" + ) + ) + ).`when`(satelliteCatalogService).allSatellites() + doReturn(emptyList()).`when`(tleMonitoringService).allTles() + + val satellites = service.satellites() + + assertEquals(1, satellites.size) + assertEquals(62138, satellites.single().id) + assertEquals(101, satellites.single().catalogId) + assertEquals(62138, satellites.single().noradId) + assertEquals(0, satellites.single().tleRecordsCount) + } + + @Test + fun `analyze satellite uses only requested number of latest TLE records`() { + doReturn( + listOf( + tleRecord(revolution = 1, epoch = "21274.51041667"), + tleRecord(revolution = 2, epoch = "21275.51041667"), + tleRecord(revolution = 3, epoch = "21276.51041667") + ) + ).`when`(tleMonitoringService).satelliteTles(25544) + + val response = service.analyzeSatellite(25544, tleCount = 2) + + assertEquals(2, response.rows.size) + assertEquals(listOf(2L, 3L), response.rows.map { it.revolution }) + } + + @Test + fun `analyze satellite reports epoch difference between compared TLE records`() { + doReturn( + listOf( + tleRecord(revolution = 1, epoch = "21275.51041667"), + tleRecord(revolution = 2, epoch = "21276.01041667") + ) + ).`when`(tleMonitoringService).satelliteTles(25544) + + val response = service.analyzeSatellite(25544, tleCount = 2) + + assertEquals(2, response.rows.size) + val currentRow = response.rows[1] + val discrepancy = assertNotNull(currentRow.discrepancy) + assertEquals(response.rows[0].epoch, discrepancy.previousEpoch) + assertTrue(discrepancy.previousEpoch.isBefore(currentRow.epoch)) + assertEquals(12.0, discrepancy.epochDifferenceHours, 0.001) + } + + private fun tleRecord(revolution: Long, epoch: String = "21275.51041667") = + TLEExtensionDTO( + satelliteId = 25544, + revolution = revolution, + tle = TLEDTO( + header = "ISS (ZARYA)", + first = "1 25544U 98067A $epoch .00000282 00000-0 12558-4 0 9993", + second = "2 25544 51.6435 177.7258 0003783 65.7820 55.4027 15.48915330299929" + ) + ) +} diff --git a/services/tle-monitoring-service/src/main/kotlin/space/nstart/pcp/tle_monitoring_service/controller/TLEController.kt b/services/tle-monitoring-service/src/main/kotlin/space/nstart/pcp/tle_monitoring_service/controller/TLEController.kt index a5cac97..015a175 100644 --- a/services/tle-monitoring-service/src/main/kotlin/space/nstart/pcp/tle_monitoring_service/controller/TLEController.kt +++ b/services/tle-monitoring-service/src/main/kotlin/space/nstart/pcp/tle_monitoring_service/controller/TLEController.kt @@ -1,5 +1,6 @@ package space.nstart.pcp.tle_monitoring_service.controller +import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable @@ -12,12 +13,14 @@ import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEExtensionDTO import space.nstart.pcp.tle_monitoring_service.repository.TLERepository import space.nstart.pcp.tle_monitoring_service.service.CelesTrakService +import kotlin.system.measureTimeMillis @RestController @RequestMapping("v1/api/tle") class TLEController { + private val logger = LoggerFactory.getLogger(this::class.java) @Autowired private lateinit var tleRepository: TLERepository @@ -25,11 +28,35 @@ class TLEController { private lateinit var celestrakService: CelesTrakService @GetMapping() - fun all() = tleRepository.findAll().map { tle -> tle.toDto() } + fun all(): List { + logger.info("TLE API all records request started") + var records = emptyList() + val durationMs = measureTimeMillis { + records = tleRepository.findAll().map { tle -> tle.toDto() } + } + logger.info( + "TLE API all records request completed: records={}, durationMs={}", + records.size, + durationMs + ) + return records + } @GetMapping("/satellite/{satelliteId}") - fun bySatellite(@PathVariable satelliteId: Long) = - tleRepository.findAllBySatelliteIdOrderByTimeTleAsc(satelliteId).map { tle -> tle.toDto() } + fun bySatellite(@PathVariable satelliteId: Long): List { + logger.info("TLE API satellite records request started: satelliteId={}", satelliteId) + var records = emptyList() + val durationMs = measureTimeMillis { + records = tleRepository.findAllBySatelliteIdOrderByTimeTleAsc(satelliteId).map { tle -> tle.toDto() } + } + logger.info( + "TLE API satellite records request completed: satelliteId={}, records={}, durationMs={}", + satelliteId, + records.size, + durationMs + ) + return records + } private fun space.nstart.pcp.tle_monitoring_service.entity.TLEEntity.toDto(): TLEExtensionDTO { val lines = tle.lines().map { it.trimEnd() }.filter { it.isNotBlank() } @@ -72,4 +99,4 @@ class TLEController { fun tleFromSource(@RequestParam noradIn : Long, @RequestParam("message") sendMessage : Boolean) = celestrakService.getTleByNoradIdRaw(noradIn, false, sendMessage) -} \ No newline at end of file +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 94d9f87..1bd517e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -31,8 +31,8 @@ pluginManagement { maven { url = uri("https://repo.nstart.cloud/repository/maven-proxy/") } -// mavenCentral() -// gradlePluginPortal() + mavenCentral() + gradlePluginPortal() // maven { // url = uri("https://repo.nstart.local/repository/maven-proxy/") // }