This commit is contained in:
Дмитрий Соловьев
2026-05-25 14:23:52 +03:00
parent b3a6012ebb
commit d48ddd2657
1066 changed files with 104601 additions and 3 deletions
@@ -0,0 +1,8 @@
FROM bellsoft/liberica-openjre-alpine:21.0.5
ENV JAVA_OPTS=""
ADD ./build/libs/*.jar /app.jar
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app.jar"]
@@ -0,0 +1,112 @@
group = "space.nstart.pcp"
plugins {
kotlin("jvm")
kotlin("plugin.spring")
kotlin("plugin.jpa")
kotlin("plugin.lombok")
id("org.springframework.boot")
id("io.spring.dependency-management")
id("org.sonarqube")
jacoco
// id("org.graalvm.buildtools.native")
}
version = "1.0.0"
kotlin {
compilerOptions {
freeCompilerArgs.addAll("-Xjsr305=strict")
}
}
configurations {
compileOnly {
extendsFrom(configurations.annotationProcessor.get())
}
}
dependencies {
implementation(project(":libs:pcp-types-lib"))
implementation(project(":libs:ballistics-lib"))
// implementation("org.nstart.dep265:ballistics:2.4.0")
implementation("${property("dep.spring.actuator")}")
implementation("org.springframework.boot:spring-boot-starter-logging")
implementation("org.springframework.boot:spring-boot-starter-validation")
// implementation("org.springframework.boot:spring-boot-starter-web")
implementation("jakarta.validation:jakarta.validation-api")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springdoc:springdoc-openapi-starter-webflux-ui:${property("versions.open-api")}")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.6.4")
implementation("com.github.alexej520:clipper-kotlin:1.0")
implementation("org.springframework.kafka:spring-kafka")
implementation("org.springframework.cloud:spring-cloud-starter-config")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
// implementation("org.springframework.kafka:spring-kafka")
implementation("org.springframework.boot:spring-boot-starter-flyway")
implementation("org.flywaydb:flyway-database-postgresql")
runtimeOnly("org.postgresql:postgresql")
implementation("jakarta.validation:jakarta.validation-api")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.locationtech.jts:jts-core:1.19.0")
testImplementation("junit:junit")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-test")
testImplementation("org.testcontainers:junit-jupiter")
testRuntimeOnly("com.h2database:h2")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
dependencyManagement {
imports {
mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("versions.spring.cloud")}")
mavenBom("org.testcontainers:testcontainers-bom:${property("versions.testcontainers")}")
}
}
tasks.withType<Jar> {
manifest {
attributes["Built-By"] = "nstart"
attributes["Implementation-Version"] = archiveVersion
}
}
tasks.withType<Test> {
enabled = true
useJUnitPlatform()
finalizedBy(tasks.jacocoTestReport)
systemProperty("spring.profiles.active", "test")
}
tasks.check {
dependsOn(tasks.jacocoTestCoverageVerification)
}
tasks.jacocoTestReport {
dependsOn(tasks.test)
reports {
xml.required = true
html.required = true
csv.required = false
}
}
sonar {
properties {
property("sonar.projectKey", "pcp")
property("sonar.login", "sqp_tokenExample")
property("sonar.qualitygate.wait", "${property("sonar.qualitygate.wait")}")
property("sonar.host.url", "${property("sonar.host.url")}")
}
}
@@ -0,0 +1,93 @@
package space.nstart.pcp.pcp_request_service
import org.slf4j.LoggerFactory
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.ApplicationListener
import org.springframework.core.env.ConfigurableEnvironment
import org.springframework.core.env.EnumerablePropertySource
import org.springframework.core.env.PropertySource
import org.springframework.kafka.annotation.EnableKafka
import org.springframework.scheduling.annotation.EnableAsync
@SpringBootApplication
@EnableAsync
@EnableKafka
class PcpBallisticsServiceApplication
fun main(args: Array<String>) {
runApplication<PcpBallisticsServiceApplication>(*args) {
addListeners(BallisticsStartupConfigurationLogger())
}
}
private class BallisticsStartupConfigurationLogger : ApplicationListener<ApplicationEnvironmentPreparedEvent> {
private val logger = LoggerFactory.getLogger(BallisticsStartupConfigurationLogger::class.java)
override fun onApplicationEvent(event: ApplicationEnvironmentPreparedEvent) {
val environment = event.environment
val activeProfiles = environment.activeProfiles.takeIf { it.isNotEmpty() } ?: environment.defaultProfiles
logger.info(
"Ballistics startup config client: activeProfiles={}, configServerUri={}, configProfile={}, configLabel={}",
activeProfiles.joinToString(","),
maskIfSensitive("spring.cloud.config.uri", environment.getProperty("spring.cloud.config.uri")),
environment.getProperty("spring.cloud.config.profile"),
environment.getProperty("spring.cloud.config.label"),
)
val configSources = environment.configServerPropertySources()
if (configSources.isEmpty()) {
logger.warn("Ballistics received no config server property sources")
return
}
configSources.forEach { source ->
logger.info("Ballistics received config source '{}': {}", source.name, source.printableProperties(environment))
}
}
private fun ConfigurableEnvironment.configServerPropertySources(): List<PropertySource<*>> =
propertySources
.asSequence()
.filter { it.name.startsWith("configserver:") || it.name == "configClient" }
.toList()
private fun PropertySource<*>.printableProperties(environment: ConfigurableEnvironment): String {
if (this !is EnumerablePropertySource<*>) {
return "<non-enumerable>"
}
return propertyNames
.sorted()
.joinToString(", ") { propertyName ->
formatProperty(propertyName, getProperty(propertyName), environment.getProperty(propertyName))
}
}
private fun formatProperty(propertyName: String, rawValue: Any?, resolvedValue: String?): String {
val printableResolvedValue = maskIfSensitive(propertyName, resolvedValue)
val printableRawValue = maskIfSensitive(propertyName, rawValue)
if (printableRawValue == printableResolvedValue) {
return "$propertyName=$printableResolvedValue"
}
return "$propertyName=$printableResolvedValue (raw=$printableRawValue)"
}
private fun maskIfSensitive(propertyName: String, value: Any?): String {
if (value == null) {
return "<null>"
}
val normalizedName = propertyName.lowercase()
val sensitiveMarkers = listOf("password", "passwd", "secret", "token", "credential", "private-key", "api-key", "apikey")
return if (sensitiveMarkers.any { normalizedName.contains(it) }) {
"******"
} else {
value.toString()
}
}
}
@@ -0,0 +1,46 @@
package space.nstart.pcp.pcp_request_service.configuration
import org.springframework.http.ResponseEntity
import org.springframework.validation.FieldError
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.support.WebExchangeBindException
import reactor.core.publisher.Mono
class CustomValidationException(message: String) : RuntimeException(message)
class CustomErrorException(message: String) : RuntimeException(message)
@ControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(CustomValidationException::class)
fun handleValidation(ex: CustomValidationException): ResponseEntity<Map<String, String>> {
return ResponseEntity.badRequest()
.body(mapOf("error" to ex.message!!))
}
@ExceptionHandler(CustomErrorException::class)
fun handleError(ex: CustomErrorException): ResponseEntity<Map<String, String>> {
return ResponseEntity.internalServerError()
.body(mapOf("error" to ex.message!!))
}
@ExceptionHandler(WebExchangeBindException::class)
fun handleValidationExceptions(ex: WebExchangeBindException): Mono<ResponseEntity<Map<String, String>>> {
val errors = mutableMapOf<String, String>()
ex.bindingResult.fieldErrors.forEach { error ->
errors[error.field] = error.defaultMessage ?: "Validation error"
}
return Mono.just(ResponseEntity.badRequest().body(errors))
}
@ExceptionHandler(MethodArgumentNotValidException::class)
fun handleValidationExceptions(ex: MethodArgumentNotValidException): ResponseEntity<Map<String, String>> {
val errors = mutableMapOf<String, String>()
ex.bindingResult.allErrors.forEach { error ->
val fieldName = (error as FieldError).field
val errorMessage = error.defaultMessage
errors[fieldName] = errorMessage ?: "Validation error"
}
return ResponseEntity.badRequest().body(errors)
}
}
@@ -0,0 +1,138 @@
package space.nstart.pcp.pcp_request_service.configuration
import org.apache.kafka.clients.admin.AdminClientConfig
import org.apache.kafka.clients.admin.NewTopic
import org.apache.kafka.clients.consumer.ConsumerConfig
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.clients.producer.ProducerConfig
import org.apache.kafka.common.header.Header
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.kafka.common.serialization.StringSerializer
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory
import org.springframework.kafka.core.ConsumerFactory
import org.springframework.kafka.core.DefaultKafkaConsumerFactory
import org.springframework.kafka.core.DefaultKafkaProducerFactory
import org.springframework.kafka.core.KafkaAdmin
import org.springframework.kafka.core.KafkaTemplate
import org.springframework.kafka.core.ProducerFactory
import org.springframework.kafka.listener.adapter.RecordFilterStrategy
import space.nstart.pcp.pcp_types_lib.message.PcpKafkaEvent
import java.nio.charset.StandardCharsets
import kotlin.String
@ConditionalOnProperty(name=["spring.kafka.bootstrap-servers"], matchIfMissing = false)
@Configuration
class KafkaConfig {
companion object {
const val HEADER_NAME: String = "type"
const val TARGET_CONSUMER_HEADER_NAME: String = "target_consumer"
}
@Value("\${spring.kafka.template.default-topic}")
private lateinit var defaultTopic: String
@Bean
fun tleTopic(): NewTopic =
NewTopic(defaultTopic, 1, 1)
@Value("\${spring.kafka.bootstrap-servers}")
private lateinit var addr: String
@Value("\${spring.kafka.consumer.group-id}")
private lateinit var groupId: String
@Bean
fun template(): KafkaTemplate<String, Any> {
return KafkaTemplate(producerFactory())
}
@Bean
fun kafkaAdmin(): KafkaAdmin {
val configs = hashMapOf<String, Any>()
configs[AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG] = addr
return KafkaAdmin(configs)
}
@Bean
fun consumerFactory(): ConsumerFactory<String, Any> {
val props = hashMapOf<String, Any>()
props[ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG] = addr
props[ConsumerConfig.GROUP_ID_CONFIG] = groupId
props[ConsumerConfig.AUTO_OFFSET_RESET_CONFIG] = "latest"
props[ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG] = false
props[ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG] = StringDeserializer::class.java
props[ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG] = StringDeserializer::class.java
return DefaultKafkaConsumerFactory(props)
}
@Bean
fun kafkaListenerContainerFactory(): ConcurrentKafkaListenerContainerFactory<String, Any> =
ConcurrentKafkaListenerContainerFactory<String, Any>()
.apply { this.setConsumerFactory(consumerFactory()) }
@Bean
fun producerFactory(): ProducerFactory<String, Any> {
val configProps = hashMapOf<String, Any>()
configProps[ProducerConfig.BOOTSTRAP_SERVERS_CONFIG] = addr
configProps[ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG] = StringSerializer::class.java
configProps[ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG] = StringSerializer::class.java
return DefaultKafkaProducerFactory(configProps)
}
private fun getRecordFilterStrategy(event: PcpKafkaEvent): RecordFilterStrategy<String, String> {
return RecordFilterStrategy { consumerRecord: ConsumerRecord<String, String> ->
val eventTypeHeader: Header? = consumerRecord.headers().lastHeader(HEADER_NAME)
if (eventTypeHeader != null) {
val headerValue = String(eventTypeHeader.value(), StandardCharsets.UTF_8)
return@RecordFilterStrategy event.name != headerValue
}
true
}
}
private fun getRecordFilterStrategyByConsumer(): RecordFilterStrategy<String, String> {
return RecordFilterStrategy { consumerRecord: ConsumerRecord<String, String> ->
val consumerGroupHeader: Header? = consumerRecord.headers().lastHeader(TARGET_CONSUMER_HEADER_NAME)
if (consumerGroupHeader != null) {
val headerValue = String(consumerGroupHeader.value(), StandardCharsets.UTF_8)
return@RecordFilterStrategy "respondedListener" != headerValue
}
true
}
}
@Bean("placedIcFilter")
fun placedFilter(): RecordFilterStrategy<String, String> {
return getRecordFilterStrategy(PcpKafkaEvent.ICPlacedEvent)
}
@Bean("placedIcRvFilter")
fun placedIcRvFilter(): RecordFilterStrategy<String, String> {
return getRecordFilterStrategy(PcpKafkaEvent.ICRVPlacedEvent)
}
@Bean("updatedIcFilter")
fun updatedIcFilter(): RecordFilterStrategy<String, String> {
return getRecordFilterStrategy(PcpKafkaEvent.ICUpdatedEvent)
}
@Bean("satelliteDeletedFilter")
fun satelliteDeletedFilter(): RecordFilterStrategy<String, String> {
return getRecordFilterStrategy(PcpKafkaEvent.SatelliteDeletedEvent)
}
@Bean("respondedFilter")
fun respondedFilter(): RecordFilterStrategy<String, String> {
return getRecordFilterStrategyByConsumer()
}
}
@@ -0,0 +1,14 @@
package space.nstart.pcp.pcp_request_service.configuration
import jakarta.validation.Validation
import jakarta.validation.Validator
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class ValidationConfig {
@Bean
fun validator(): Validator {
return Validation.buildDefaultValidatorFactory().validator
}
}
@@ -0,0 +1,37 @@
package space.nstart.pcp.pcp_request_service.controller
import jakarta.validation.Valid
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
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_request_service.service.SatelliteService
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ObjViewRequestDTO
@RestController
@RequestMapping("/api/obj-view")
class ObjViewController {
@Autowired
private lateinit var satelliteService: SatelliteService
@PostMapping("/mpl-square")
fun mplSquare(
@Valid @RequestBody body : ObjViewRequestDTO
) =
satelliteService.mplSquare(body)
@PostMapping("/mpl-point")
fun mplPoint(
@Valid @RequestBody body : ObjViewRequestDTO
) =
satelliteService.mplPoint(body)
@GetMapping("cell-coverage/{number}")
fun cellCov(@PathVariable number : Long) = satelliteService.getCellCovering(number)
}
@@ -0,0 +1,21 @@
package space.nstart.pcp.pcp_request_service.controller
import org.springframework.beans.factory.annotation.Autowired
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_request_service.service.RVAService
import space.nstart.pcp.pcp_types_lib.dto.ballistics.IcRvaRequestDTO
@RestController
@RequestMapping("api/rva")
class RVAController {
@Autowired
private lateinit var rvaService: RVAService
@PostMapping
fun getZRV(@RequestBody body : IcRvaRequestDTO) = rvaService.calculateRVA(body)
}
@@ -0,0 +1,126 @@
package space.nstart.pcp.pcp_request_service.controller
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import jakarta.validation.Valid
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
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.RequestParam
import org.springframework.web.bind.annotation.RestController
import space.nstart.pcp.pcp_request_service.service.SatelliteService
import space.nstart.pcp.pcp_types_lib.configuration.CustomValidationException
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ExactTimePositionRequestDTO
import java.time.LocalDateTime
@RestController
@RequestMapping("/api/satellites")
class SatelliteController {
@Autowired
private lateinit var satelliteService: SatelliteService
@PostMapping("/{satellite_id}/extract-time")
fun extractTime(
@PathVariable("satellite_id") satelliteId : Long,
@Valid @RequestBody body : ExactTimePositionRequestDTO
) =
satelliteService.exactTime(satelliteId, body)
@Operation(
summary = "Получить ПДЦМ",
description = "Возвраает ПДЦМ для заданного аппарата на заданном интервале времени с шагом 60 с"
)
@GetMapping("/{norad_id}/orbit")
fun getPoints(
@Parameter(description = "Идентификатор NORAD", example = "56756")
@PathVariable("norad_id") noradId : Long,
@Parameter(description = "Время начала (если null, то мин. время для выбранного КА)")
@RequestParam("time_start") timeStart : LocalDateTime?,
@Parameter(description = "Время конца (если null, то макс. время для выбранного КА)")
@RequestParam("time_stop") timeStrop : LocalDateTime?
) =
satelliteService.getOrbPoints(noradId, timeStart, timeStrop)
@Operation(
summary = "Получить доступные интервалы прогноза по всем КА",
description = "Возвращает минимальное и максимальное время ПДЦМ для каждого КА. Если time задан, возвращаются только КА, у которых это время попадает в доступный интервал"
)
@GetMapping("/orbit/availability")
fun getOrbitAvailability(
@Parameter(description = "Проверяемое время. Если null, возвращаются все КА, для которых есть точки ПДЦМ")
@RequestParam("time", required = false) time: LocalDateTime?
) =
satelliteService.getOrbitAvailability(time)
@GetMapping("/orbit/availability/{id}")
fun getOrbitAvailabilityById(
@PathVariable id : Long
) =
satelliteService.getOrbitAvailability(null).filter { it.satelliteId == id }.firstOrNull()?:
throw CustomValidationException("Спутник с заданным идентификатором не зарегистрирован")
@Operation(
summary = "Получить параметры прохождения ВУЗов",
description = "Возвраает массив ВУЗов для заданного аппарата на заданном интервале времени"
)
@GetMapping("/{norad_id}/asc-node")
fun getAskNodes(
@Parameter(description = "Идентификатор NORAD", example = "56756")
@PathVariable("norad_id") noradId : Long,
@Parameter(description = "Время начала (если null, то мин. время для выбранного КА)")
@RequestParam("time_start") timeStart : LocalDateTime?,
@Parameter(description = "Время конца (если null, то макс. время для выбранного КА)")
@RequestParam("time_stop") timeStrop : LocalDateTime?
) =
satelliteService.getAscNodes(noradId, timeStart, timeStrop)
@Operation(
summary = "Получить параметры трассы полета и полосы обзора",
description = "Возвращает параметры ТП и ПО на заданном интервале времен с шагом 60 с"
)
@GetMapping("/{norad_id}/flight-line")
fun getFL(
@Parameter(description = "Идентификатор NORAD", example = "56756")
@PathVariable("norad_id") noradId : Long,
@Parameter(description = "Время начала (если null, то мин. время для выбранного КА)")
@RequestParam("time_start") timeStart : LocalDateTime?,
@Parameter(description = "Время конца (если null, то макс. время для выбранного КА)")
@RequestParam("time_stop") timeStrop : LocalDateTime?
) =
satelliteService.getFL(noradId, timeStart, timeStrop, 60.0)
@Operation(
summary = "Получить параметры ЗРВ",
description = "Возвращает параметры ЗРВ на заданном интервале времен"
)
@GetMapping("/{norad_id}/rva")
fun getRVA(
@Parameter(description = "Идентификатор NORAD", example = "56756")
@PathVariable("norad_id") noradId : Long,
@Parameter(description = "Время начала (если null, то мин. время для выбранного КА)")
@RequestParam("time_start") timeStart : LocalDateTime,
@Parameter(description = "Время конца (если null, то макс. время для выбранного КА)")
@RequestParam("time_stop") timeStrop : LocalDateTime
) =
satelliteService.getRVA(noradId, timeStart, timeStrop)
@PostMapping("/{satellite_id}/clear")
fun clear(
@PathVariable("satellite_id") satelliteId : Long
) =
satelliteService.clear(satelliteId)
}
@@ -0,0 +1,30 @@
package space.nstart.pcp.pcp_request_service.controller
import ballistics.types.OrbitalPoint
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import space.nstart.pcp.pcp_request_service.service.TLEService
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TleRvaRequestDTO
@RestController
@RequestMapping("/api/tle")
class TLEController {
@Autowired
private lateinit var tleService: TLEService
@PostMapping("/parse")
fun orbit(@RequestBody tle : TLEDTO) = tleService.parseTLE(tle)
@PostMapping("/rva")
fun rva(@RequestBody req : TleRvaRequestDTO) = tleService.rva(req)
}
@@ -0,0 +1,110 @@
package space.nstart.pcp.pcp_request_service.entity
import ballistics.types.KeplerParams
import ballistics.types.RevolutionParameter
import ballistics.utils.fromDateTime
import ballistics.utils.toDateTime
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.persistence.Table
import space.nstart.pcp.pcp_types_lib.dto.ballistics.AscNodeDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.KeplersDTO
import java.time.LocalDateTime
import kotlin.math.PI
@Entity
@Table(name = "asc_node")
class AscNodeEntity(
@Id
@Column(name = "asc_node_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id : Long? = null,
@Column(nullable = false)
val satelliteId : Long = 0,
@Column(name = "t", nullable = false)
val time : LocalDateTime = LocalDateTime.now(),
@Column(nullable = false)
val revolution : Long = 0,
val longitude : Double = 0.0,
val height : Double = 0.0,
val x: Double = 0.0,
val y: Double = 0.0,
val z: Double = 0.0,
val vx: Double = 0.0,
val vy: Double = 0.0,
val vz: Double = 0.0,
) {
constructor(an : RevolutionParameter, sat : Long) : this(
null,
sat,
toDateTime(an.vuz.t),
an.vuz.vit.toLong(),
an.lVuz * 180 / PI,
an.hVuz,
an.vuz.r.x,
an.vuz.r.y,
an.vuz.r.z,
an.vuz.v.x,
an.vuz.v.y,
an.vuz.v.z,
)
private companion object {
const val SECONDS_IN_DAY = 86_400.0
const val NANOS_IN_DAY = 86_400_000_000_000L
}
private fun positiveModulo(value: Double, modulo: Double): Double {
val remainder = value % modulo
return if (remainder < 0) remainder + modulo else remainder
}
/**
* Местное среднее солнечное время на долготе longitudeRad.
* Используется та же идея, что в tmest(t, Lvuz): UTC-время суток плюс поправка по долготе.
*/
private fun localMeanSolarSeconds(epochSeconds: Double, longitudeRad: Double): Double {
val utcSecondsOfDay = positiveModulo(epochSeconds, SECONDS_IN_DAY)
val longitudeShiftSeconds = longitudeRad / (2.0 * PI) * SECONDS_IN_DAY
return positiveModulo(utcSecondsOfDay + longitudeShiftSeconds, SECONDS_IN_DAY)
}
fun tmest(t : LocalDateTime, l : Double) : LocalDateTime
{
var dl = l;
if (dl > PI)
dl = dl - 2 * PI;
var tm = t.plusSeconds(-60*60*3);
tm = tm.plusNanos((dl/2.0/PI * 86400.0*1e9).toLong());
return tm;
}
fun toDTO(keps : KeplerParams, rekv : Double) = AscNodeDTO(
time = this.time,
revolution = this.revolution.toInt(),
long = this.longitude,
height = this.height,
x = this.x,
y = this.y,
z = this.z,
vx = this.vx,
vy = this.vy,
vz = this.vz,
keps = KeplersDTO(
(keps.ael - rekv) / 1000.0,
keps.e,
keps.nakl * 180 / PI,
keps.omegab * 180 / PI,
keps.omegam * 180 / PI
),
localMeanTime = tmest(this.time, this.longitude * PI / 180.0)
)
}
@@ -0,0 +1,58 @@
package space.nstart.pcp.pcp_request_service.entity
import ballistics.utils.toDateTime
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.persistence.Table
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamDTO
import java.time.LocalDateTime
import kotlin.math.PI
@Entity
@Table(name = "cell_covering")
class EarthCellViewsEntity(
@Id
@Column(name = "cell_covering_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id : Long? = null,
@Column(nullable = false)
val satelliteId : Long = 0,
@Column(nullable = false)
val cellNum : Long = 0,
@Column(name = "rev_begin", nullable = false)
val revolutionBegin : Long = 0,
@Column(nullable = false)
val timeBegin : LocalDateTime = LocalDateTime.now(),
@Column(nullable = false)
val revSignBegin : Int = 0,
@Column(name = "rev_end", nullable = false)
val revolutionEnd: Long = 0,
@Column(nullable = false)
val timeEnd : LocalDateTime = LocalDateTime.now(),
@Column(nullable = false)
val revSignEnd : Int = 0,
@Column(name = "roll_min")
val gammaMin : Double = 0.0,
@Column(name = "roll_max")
val gammaMax : Double = 0.0,
@Column(name = "contour_wkt", nullable = false)
val contour : String = ""
) {
fun toDTO() = SquareViewParamDTO(
this.satelliteId,
this.cellNum.toString(),
this.revolutionBegin,
this.timeBegin,
if (this.revSignBegin == 0) RevolutionSign.ASC else RevolutionSign.DESC,
this.revolutionBegin,
this.timeEnd,
if (this.revSignEnd == 0) RevolutionSign.ASC else RevolutionSign.DESC,
this.gammaMin,
this.gammaMax
)
}
@@ -0,0 +1,36 @@
package space.nstart.pcp.pcp_request_service.entity
import ballistics.types.FleghtLineSector
import ballistics.utils.toDateTime
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.persistence.Table
import java.time.LocalDateTime
@Entity
@Table(name = "earth_coverage")
class EarthCoverageEntity(
@Id
@Column(name = "earth_coverage_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id : Long? = null,
@Column(nullable = false)
val satelliteId : Long = 0,
@Column(nullable = false)
val cellNumber : Long = 0,
@Column(nullable = false)
val tStart : LocalDateTime = LocalDateTime.now(),
@Column(nullable = false)
val tStop : LocalDateTime = LocalDateTime.now(),
) {
constructor(fl : FleghtLineSector, sector : Int, sat : Long) : this(
null,
sat,
sector.toLong(),
toDateTime(fl.tStart),
toDateTime(fl.tStop)
)
}
@@ -0,0 +1,77 @@
package space.nstart.pcp.pcp_request_service.entity
import ballistics.types.FlightLine
import ballistics.utils.toDateTime
import jakarta.persistence.*
import space.nstart.pcp.pcp_types_lib.dto.ballistics.FlightLineDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
import java.time.LocalDateTime
import kotlin.math.PI
@Entity
@Table(name = "flight_line")
class FlightLineEntity(
@Id
@Column(name = "flight_line_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id : Long? = null,
@Column(nullable = false)
val satelliteId : Long = 0,
@Column(name = "t", nullable = false)
val time : LocalDateTime = LocalDateTime.now(),
@Column(nullable = false)
val revolution : Long = 0,
@Column(nullable = false)
val revolutionSign: String = RevolutionSign.ASC.toString(),
val longitude : Double = 0.0,
val latitude : Double = 0.0,
val longitudeOuterLeft : Double = 0.0,
val latitudeOuterLeft : Double = 0.0,
val longitudeInnerLeft : Double = 0.0,
val latitudeInnerLeft : Double = 0.0,
val longitudeInnerRight : Double = 0.0,
val latitudeInnerRight : Double = 0.0,
val longitudeOuterRight : Double = 0.0,
val latitudeOuterRight : Double = 0.0,
) {
constructor(fl : FlightLine, sat : Long) : this(
null,
sat,
toDateTime(fl.t),
fl.vit.toLong(),
if (fl.pv == 0) RevolutionSign.ASC.toString() else RevolutionSign.DESC.toString(),
fl.flightLine.long * 180 / PI,
fl.flightLine.lat * 180 / PI,
fl.leftOuterSwath.long * 180 / PI,
fl.leftOuterSwath.lat * 180 / PI,
fl.leftInternalSwath.long * 180 / PI,
fl.leftInternalSwath.lat * 180 / PI,
fl.rightInternalSwath.long * 180 / PI,
fl.rightInternalSwath.lat * 180 / PI,
fl.rightOuterSwath.long * 180 / PI,
fl.rightOuterSwath.lat * 180 / PI,
)
fun toDTO() = FlightLineDTO(
time = time,
revolution = revolution,
lat = latitude ,
long = longitude,
latLeft = latitudeOuterLeft,
longLeft = longitudeOuterLeft,
latInnerLeft = latitudeInnerLeft,
longInnerLeft = longitudeInnerLeft,
latInnerRight = latitudeInnerRight,
longInnerRight = longitudeInnerRight,
latRight = latitudeOuterRight,
longRight = longitudeOuterRight,
revSign = RevolutionSign.valueOf(revolutionSign)
)
}
@@ -0,0 +1,56 @@
package space.nstart.pcp.pcp_request_service.entity
import ballistics.types.OrbitalPoint
import ballistics.utils.fromDateTime
import ballistics.utils.math.Vector3D
import ballistics.utils.toDateTime
import jakarta.persistence.*
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
import java.time.LocalDateTime
@Entity
@Table(name = "pdcm")
class PDCMEntity (
@Id
@Column(name = "pdcm_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id : Long? = null,
@Column(nullable = false)
val satelliteId : Long = 0,
@Column(name = "t", nullable = false)
val time : LocalDateTime = LocalDateTime.now(),
@Column(nullable = false)
val revolution : Long = 0,
val x: Double = 0.0,
val y: Double = 0.0,
val z: Double = 0.0,
val vx: Double = 0.0,
val vy: Double = 0.0,
val vz: Double = 0.0,
){
constructor(op : OrbitalPoint, sat : Long) : this(
null,
sat,
toDateTime(op.t),
op.vit.toLong(),
op.r.x,
op.r.y,
op.r.z,
op.v.x,
op.v.y,
op.v.z
)
fun toOrbitalPoint() = OrbitalPoint(
t = fromDateTime(time),
vit = revolution.toInt(),
r = Vector3D(x,y,z),
v = Vector3D(vx, vy,vz)
)
fun toDTO() = OrbPointDTO(
time = time,
revolution,
vx,vy,vz,x,y,z
)
}
@@ -0,0 +1,20 @@
package space.nstart.pcp.pcp_request_service.message
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.kafka.annotation.KafkaListener
import org.springframework.stereotype.Component
@Component
class KafkaListener {
private var log: Logger = LoggerFactory.getLogger(this::class.java)
@KafkaListener(id = "placedIcFilter", topics = ["\${spring.kafka.template.default-topic}"], filter = "placedIcFilter")
fun listener(data: String?) {
log.info("Received message [{}] in group1 with header1", data)
}
}
@@ -0,0 +1,66 @@
package space.nstart.pcp.pcp_request_service.message
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.ObjectProvider
import org.springframework.kafka.annotation.KafkaListener
import org.springframework.stereotype.Component
import space.nstart.pcp.pcp_request_service.service.SatelliteService
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteDeletedEventDTO
import tools.jackson.databind.ObjectMapper
private const val SATELLITE_DELETED_CONSUMER_GROUP = "pcp-ballistics-service-satellite-deleted"
@Component
class SatelliteDeletedKafkaListener(
objectMapperProvider: ObjectProvider<ObjectMapper>,
private val satelliteService: SatelliteService
) {
private val logger = LoggerFactory.getLogger(this::class.java)
private val objectMapper = objectMapperProvider.ifAvailable
?: throw IllegalStateException("ObjectMapper bean is required for SatelliteDeletedKafkaListener")
@KafkaListener(
id = "satelliteDeletedFilter",
topics = ["\${app.kafka.topics.satellites:pcp.satellites}"],
groupId = SATELLITE_DELETED_CONSUMER_GROUP,
filter = "satelliteDeletedFilter"
)
fun consume(message: String?) {
if (message.isNullOrBlank()) {
logger.warn("Received empty SatelliteDeletedEvent Kafka message")
return
}
try {
val rootNode = objectMapper.readTree(message)
val payloadNode = rootNode["data"] ?: throw IllegalArgumentException("В сообщении отсутствует поле data")
val event = objectMapper.treeToValue(payloadNode, SatelliteDeletedEventDTO::class.java)
logger.info(
"Received SatelliteDeletedEvent in ballistics-service: satelliteId={}, noradId={}, consumerGroup={}",
event.satelliteId,
event.noradId,
SATELLITE_DELETED_CONSUMER_GROUP
)
deleteByIdentifiers(event)
} catch (exception: Exception) {
logger.error("Failed to process SatelliteDeletedEvent in ballistics-service: {}", exception.message, exception)
throw exception
}
}
private fun deleteByIdentifiers(event: SatelliteDeletedEventDTO) {
event.deleteIdentifiers().forEach { satelliteId ->
satelliteService.clear(satelliteId)
}
logger.info(
"SatelliteDeletedEvent processed in ballistics-service: satelliteId={}, noradId={}, consumerGroup={}",
event.satelliteId,
event.noradId,
SATELLITE_DELETED_CONSUMER_GROUP
)
}
private fun SatelliteDeletedEventDTO.deleteIdentifiers(): List<Long> =
listOfNotNull(satelliteId, noradId).distinct()
}
@@ -0,0 +1,70 @@
package space.nstart.pcp.pcp_request_service.message
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.ObjectProvider
import org.springframework.kafka.annotation.KafkaListener
import org.springframework.stereotype.Component
import space.nstart.pcp.pcp_request_service.service.SatelliteIcEventService
import space.nstart.pcp.pcp_types_lib.dto.ballistics.InitialConditionsDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO
import tools.jackson.databind.ObjectMapper
import tools.jackson.databind.node.ObjectNode
@Component
class SatelliteIcRvKafkaListener(
objectMapperProvider: ObjectProvider<ObjectMapper>,
private val satelliteIcEventService: SatelliteIcEventService
) {
private val logger = LoggerFactory.getLogger(this::class.java)
private val objectMapper = objectMapperProvider.ifAvailable
?: throw IllegalStateException("ObjectMapper bean is required for SatelliteIcRvKafkaListener")
@KafkaListener(
id = "placedIcRvFilter",
topics = ["\${spring.kafka.template.default-topic}"],
filter = "placedIcRvFilter"
)
fun listener(data: String?) {
consume(data)
}
fun consume(data: String?) {
if (data.isNullOrBlank()) {
logger.warn("Received empty satellite initial conditions Kafka message")
return
}
try {
logger.info("Received satellite initial conditions Kafka message body: {}", data)
val rootNode = objectMapper.readTree(data)
val payloadNode = rootNode["data"] ?: throw IllegalArgumentException("В сообщении отсутствует поле data")
val payload = objectMapper.treeToValue(normalizePayload(payloadNode), SatelliteICDTO::class.java)
satelliteIcEventService.handlePlacedIcRv(payload)
} catch (exception: Exception) {
logger.error("Ошибка обработки сообщения о начальных условиях КА: {}", exception.message, exception)
throw exception
}
}
/**
* Keeps consumer backward compatible with already published messages where optional IC coefficients were null.
*/
private fun normalizePayload(payloadNode: tools.jackson.databind.JsonNode): tools.jackson.databind.JsonNode {
val normalizedPayload = payloadNode.deepCopy()
val initialConditionsNode = normalizedPayload["ic"]
if (initialConditionsNode is ObjectNode) {
if (initialConditionsNode["sBall"] == null || initialConditionsNode["sBall"].isNull) {
initialConditionsNode.put("sBall", DEFAULT_INITIAL_CONDITIONS.sBall)
}
if (initialConditionsNode["f81"] == null || initialConditionsNode["f81"].isNull) {
initialConditionsNode.put("f81", DEFAULT_INITIAL_CONDITIONS.f81)
}
}
return normalizedPayload
}
companion object {
private val DEFAULT_INITIAL_CONDITIONS = InitialConditionsDTO()
}
}
@@ -0,0 +1,39 @@
package space.nstart.pcp.pcp_request_service.model
import ballistics.utils.toDateTime
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamDTO
import kotlin.math.PI
data class SquareViewParamsModel(
var id: Long = 0,
var objID : String,
var pointNumb: Int = 0,
var vitN: Int = 0,
var tN: Double = 0.0,
var pvN : RevolutionSign = RevolutionSign.ASC,
var vitK: Int = 0,
var tK: Double = 0.0,
var pvK: RevolutionSign = RevolutionSign.ASC,
var krenMin: Double = 0.0,
var krenMax: Double = 0.0
): Cloneable {
fun toDTO(): SquareViewParamDTO =
SquareViewParamDTO(
this.id,
this.objID,
this.vitN.toLong(),
toDateTime(this.tN),
this.pvN,
this.vitK.toLong(),
toDateTime(this.tK),
this.pvK,
this.krenMin * 180 / PI,
this.krenMax * 180 / PI,
)
public override fun clone(): SquareViewParamsModel {
return super.clone() as SquareViewParamsModel
}
}
@@ -0,0 +1,27 @@
package space.nstart.pcp.pcp_request_service.repository
import jakarta.transaction.Transactional
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Modifying
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param
import space.nstart.pcp.pcp_request_service.entity.AscNodeEntity
import java.time.LocalDateTime
interface AscNodeRepository : JpaRepository<AscNodeEntity, Long>{
@Modifying
@Transactional
@Query("DELETE FROM asc_node WHERE satellite_id =:sat", nativeQuery = true)
fun deleteBySatId(@Param("sat") sat : Long) : Int
fun findBySatelliteId(id : Long) : List<AscNodeEntity>
fun findBySatelliteIdAndTimeBetween(id : Long, t1 : LocalDateTime, t2 : LocalDateTime) : List<AscNodeEntity>
fun findBySatelliteIdAndTimeAfter(id : Long, t : LocalDateTime): List<AscNodeEntity>
fun findBySatelliteIdAndTimeBefore(id : Long, t : LocalDateTime): List<AscNodeEntity>
}
@@ -0,0 +1,22 @@
package space.nstart.pcp.pcp_request_service.repository
import jakarta.transaction.Transactional
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Modifying
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param
import space.nstart.pcp.pcp_request_service.entity.AscNodeEntity
import space.nstart.pcp.pcp_request_service.entity.EarthCellViewsEntity
interface EarthCellViewRepository : JpaRepository<EarthCellViewsEntity, Long>{
@Modifying
@Transactional
@Query("DELETE FROM cell_covering WHERE satellite_id =:sat", nativeQuery = true)
fun deleteBySatId(@Param("sat") sat : Long) : Int
fun findBySatelliteId(id : Long) : List<EarthCellViewsEntity>
fun findByCellNum(num : Long) : List<EarthCellViewsEntity>
}
@@ -0,0 +1,22 @@
package space.nstart.pcp.pcp_request_service.repository
import jakarta.transaction.Transactional
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Modifying
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param
import space.nstart.pcp.pcp_request_service.entity.EarthCoverageEntity
import java.time.LocalDateTime
interface EarthCoverageRepository : JpaRepository<EarthCoverageEntity, Long>{
@Modifying
@Transactional
@Query("DELETE FROM earth_coverage WHERE satellite_id =:sat", nativeQuery = true)
fun deleteBySatId(@Param("sat") sat : Long) : Int
fun findBySatelliteIdAndTStartGreaterThanAndTStopLessThanAndCellNumberIn
(id : Long, tn : LocalDateTime, tk: LocalDateTime, cells : List<Long>) : List<EarthCoverageEntity>
}
@@ -0,0 +1,21 @@
package space.nstart.pcp.pcp_request_service.repository
import jakarta.transaction.Transactional
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Modifying
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param
import space.nstart.pcp.pcp_request_service.entity.FlightLineEntity
import java.time.LocalDateTime
interface FlightLineRepository : JpaRepository<FlightLineEntity, Long>{
@Modifying
@Transactional
@Query("DELETE FROM flight_line WHERE satellite_id =:sat", nativeQuery = true)
fun deleteBySatId(@Param("sat") sat : Long) : Int
fun findBySatelliteIdAndTimeBetween(id : Long, tn : LocalDateTime, tk: LocalDateTime) : List<FlightLineEntity>
}
@@ -0,0 +1,49 @@
package space.nstart.pcp.pcp_request_service.repository
import jakarta.transaction.Transactional
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Modifying
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param
import space.nstart.pcp.pcp_request_service.entity.PDCMEntity
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteOrbitAvailabilityDTO
import java.time.LocalDateTime
interface PDCMRepository : JpaRepository<PDCMEntity, Long>{
@Modifying
@Transactional
@Query("DELETE FROM pdcm WHERE satellite_id =:sat", nativeQuery = true)
fun deleteBySatId(@Param("sat") sat : Long) : Int
fun findBySatelliteIdAndTimeBetween(id : Long, tBegin : LocalDateTime, tEnd : LocalDateTime) : List<PDCMEntity>
@Query(
"""
select new space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteOrbitAvailabilityDTO(
p.satelliteId,
min(p.time),
max(p.time)
)
from PDCMEntity p
group by p.satelliteId
order by p.satelliteId
"""
)
fun findSatelliteOrbitAvailability(): List<SatelliteOrbitAvailabilityDTO>
@Query(
"""
select new space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteOrbitAvailabilityDTO(
p.satelliteId,
min(p.time),
max(p.time)
)
from PDCMEntity p
group by p.satelliteId
having :time between min(p.time) and max(p.time)
order by p.satelliteId
"""
)
fun findSatelliteOrbitAvailabilityAtTime(@Param("time") time: LocalDateTime): List<SatelliteOrbitAvailabilityDTO>
}
@@ -0,0 +1,97 @@
package space.nstart.pcp.pcp_request_service.service
import ballistics.Ballistics
import ballistics.types.BallisticsError
import ballistics.types.InitialConditions
import ballistics.types.ModDVType
import ballistics.types.OrbitalPoint
import ballistics.types.PPI
import ballistics.types.VisibilityParametersZRV
import ballistics.types.ZRV
import ballistics.utils.fromDateTime
import ballistics.utils.math.Vector3D
import ballistics.utils.toDateTime
import org.springframework.stereotype.Service
import space.nstart.pcp.pcp_request_service.configuration.CustomErrorException
import space.nstart.pcp.pcp_types_lib.dto.ballistics.IcRvaRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RadioVisibilityAreaDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TargetPositionDTO
import kotlin.math.PI
@Service
class RVAService {
fun calculateRVA(icRvaRequest : IcRvaRequestDTO) : List<RadioVisibilityAreaDTO> {
val bal = Ballistics()
val mdv = ModDVType.fromInt((icRvaRequest.modDvType ?: 1).toInt())
bal.modDVType = mdv
var result = bal.calculateOrbPoints(
InitialConditions(
OrbitalPoint(
fromDateTime(icRvaRequest.ic.orbPoint.time),
icRvaRequest.ic.orbPoint.revolution.toInt(),
Vector3D(
icRvaRequest.ic.orbPoint.x,
icRvaRequest.ic.orbPoint.y,
icRvaRequest.ic.orbPoint.z,
),
Vector3D(
icRvaRequest.ic.orbPoint.vx,
icRvaRequest.ic.orbPoint.vy,
icRvaRequest.ic.orbPoint.vz,
)
),
icRvaRequest.ic.sBall,
icRvaRequest.ic.f81
),
fromDateTime(icRvaRequest.timeStart),
fromDateTime(icRvaRequest.timeStop)
)
if (result != BallisticsError.OK)
throw CustomErrorException("Ошибка расчета ПДЦМ. Код ошибки : $result")
result = bal.calculateZRV(
listOf(
PPI(
1, 1,
icRvaRequest.station.position.lat / 180 * PI,
icRvaRequest.station.position.long / 180 * PI,
icRvaRequest.station.position.height,
icRvaRequest.station.elevationMin / 180 * PI,
icRvaRequest.station.elevationMax / 180 * PI,
null, null
)
),
fromDateTime(icRvaRequest.timeStart),
fromDateTime(icRvaRequest.timeStop)
)
if (result != BallisticsError.OK)
throw CustomErrorException("Ошибка расчета ЗРВ. Код ошибки : $result")
return bal.zrv.map { mkRVA(it, 0L) }
}
fun mkTP(p : VisibilityParametersZRV) = TargetPositionDTO(
toDateTime(p.t),
p.elevation * 180 / PI,
p.azimuth * 180 / PI,
p.range
)
fun mkRVA(p : ZRV, id : Long) = RadioVisibilityAreaDTO(
noradId = id,
stationId = p.ppi.toLong(),
revolution = p.vit.toLong(),
onStart = mkTP(p.zoneIn),
onMaximum = mkTP(p.zoneMax),
onStop = mkTP(p.zoneOut)
)
}
@@ -0,0 +1,46 @@
package space.nstart.pcp.pcp_request_service.service
import ballistics.Ballistics
import ballistics.types.BallisticsError
import ballistics.types.TLE
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import space.nstart.pcp.pcp_request_service.entity.AscNodeEntity
import space.nstart.pcp.pcp_request_service.entity.EarthCoverageEntity
import space.nstart.pcp.pcp_request_service.entity.FlightLineEntity
import space.nstart.pcp.pcp_request_service.entity.PDCMEntity
import space.nstart.pcp.pcp_request_service.repository.AscNodeRepository
import space.nstart.pcp.pcp_request_service.repository.EarthCellViewRepository
import space.nstart.pcp.pcp_request_service.repository.EarthCoverageRepository
import space.nstart.pcp.pcp_request_service.repository.FlightLineRepository
import space.nstart.pcp.pcp_request_service.repository.PDCMRepository
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.math.PI
@Service
open class SatelliteIcEventService {
private val logger = LoggerFactory.getLogger(this::class.java)
@Autowired
private lateinit var satelliteService: SatelliteService
open fun handlePlacedIcRv(message: SatelliteICDTO) {
CoroutineScope(Dispatchers.Default).launch {
satelliteService.resieveRV(message)
}
}
}
@@ -0,0 +1,882 @@
package space.nstart.pcp.pcp_request_service.service
import ballistics.Ballistics
import ballistics.orbitalPoints.timeStepper.RungeStepper
import ballistics.types.BallisticsError
import ballistics.types.EarthType
import ballistics.types.FleghtLineSector
import ballistics.types.InitialConditions
import ballistics.types.KeplerParams
import ballistics.types.OPKatObj
import ballistics.types.OrbitalPoint
import ballistics.types.PPI
import ballistics.types.PointViewParams
import ballistics.types.TLE
import ballistics.types.VisibilityParametersZRV
import ballistics.types.ZRV
import ballistics.utils.astro.AstronomerJ2000
import ballistics.utils.fromDateTime
import ballistics.utils.math.Vector3D
import ballistics.utils.toDateTime
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.nstart.dep265.tletools.zeptomoby.core.Globals.sqr
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.kafka.annotation.KafkaListener
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import space.nstart.pcp.pcp_request_service.configuration.CustomErrorException
import space.nstart.pcp.pcp_request_service.configuration.CustomValidationException
import space.nstart.pcp.pcp_request_service.entity.AscNodeEntity
import space.nstart.pcp.pcp_request_service.entity.EarthCellViewsEntity
import space.nstart.pcp.pcp_request_service.entity.EarthCoverageEntity
import space.nstart.pcp.pcp_request_service.entity.FlightLineEntity
import space.nstart.pcp.pcp_request_service.entity.PDCMEntity
import space.nstart.pcp.pcp_request_service.model.SquareViewParamsModel
import space.nstart.pcp.pcp_request_service.repository.AscNodeRepository
import space.nstart.pcp.pcp_request_service.repository.EarthCellViewRepository
import space.nstart.pcp.pcp_request_service.repository.EarthCoverageRepository
import space.nstart.pcp.pcp_request_service.repository.FlightLineRepository
import space.nstart.pcp.pcp_request_service.repository.PDCMRepository
import space.nstart.pcp.pcp_request_service.utils.ContourClipService
import space.nstart.pcp.pcp_types_lib.dto.ballistics.AscNodeDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ExactTimePositionRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.InitialConditionsDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ObjDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.PointViewParamDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.PositionDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ObjViewRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RadioVisibilityAreaDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamsWithPointsDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteOrbitAvailabilityDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TargetPositionDTO
import tools.jackson.databind.ObjectMapper
import java.time.LocalDateTime
import kotlin.collections.forEach
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.asin
import kotlin.math.atan
import kotlin.math.atan2
import kotlin.math.log
import kotlin.math.sin
import kotlin.math.sqrt
import kotlin.math.truncate
@Service
class SatelliteService {
@Value("\${settings.calculation-interval:7}")
val intervalForward : Int = 7
@Autowired
private lateinit var om: ObjectMapper
private val logger : Logger = LoggerFactory.getLogger(this::class.java)
@Autowired
private lateinit var pdcmRepository: PDCMRepository
@Autowired
private lateinit var ascNodeRepository: AscNodeRepository
@Autowired
private lateinit var flightLineRepository: FlightLineRepository
@Autowired
private lateinit var earthCoverageRepository: EarthCoverageRepository
@Autowired
private lateinit var earthCellViewRepository: EarthCellViewRepository
val astro = AstronomerJ2000(EarthType.PZ90d02)
@KafkaListener(id = "updatedIcFilter", topics = ["\${spring.kafka.template.default-topic}"], groupId = "\${spring.kafka.consumer.group-id}", filter = "updatedIcFilter")
fun listener2(data: String?) {
val tleMessage: TLEDTO =
try {
val rootNode = om.readTree(data)
val payloadNode = rootNode["data"] ?: throw IllegalArgumentException("В сообщении отсутствует поле data")
om.treeToValue(payloadNode, TLEDTO::class.java)
}
catch (e: Exception) {
logger.error("Ошибка парсинга сообщения от каталога: ${e.message}")
return
}
logger.info("Полученно сообщение об изменении TLE [{}]", tleMessage)
CoroutineScope(Dispatchers.Default).launch {
resieveTLE(tleMessage)
}
logger.info("Выход из KafkaListener")
}
suspend fun resieveTLE(tle: TLEDTO) {
val bal = Ballistics()
val params = bal.getTLEParams(TLE(tle.first, tle.second), tle.header ?: "")
logger.info("Начало расчета баллистики по TLE noradId = ${params.noradId}")
bal.rollMax = (45.0 * PI / 180.0)
bal.rollMin = (20.0 * PI / 180.0)
var br = bal.calculateOrbPoints(
TLE(tle1 = tle.first, tle.second),
86400.0 * intervalForward
)
if (br != BallisticsError.OK) {
logger.warn("Ошибка при расчете ПДЦМ для noradId=${params.noradId}. Код ошибки: $br")
return
}
pdcmRepository.deleteBySatId(params.noradId)
pdcmRepository.saveAll(bal.points.map { p -> PDCMEntity(p, params.noradId) })
logger.info("Точки орбиты КА ${params.noradId} сохранены в БД")
ascNodeRepository.deleteBySatId(params.noradId)
ascNodeRepository.saveAll(bal.revolutions.map { AscNodeEntity(it, params.noradId) })
logger.info("ВУЗы орбиты КА ${params.noradId} сохранены в БД")
val tInt = bal.points.first().t
br = bal.calculateFlightLine(tInt, tInt + 86400 * intervalForward)
if (br != BallisticsError.OK) {
logger.warn("Ошибка при расчете ТП и ПО для ka=${params.noradId}. Код ошибки: $br")
return
}
flightLineRepository.deleteBySatId(params.noradId)
flightLineRepository.saveAll(bal.flightLine.map { FlightLineEntity(it, params.noradId) })
logger.info("Параметры трассы полеты и полосы обзора КА ${params.noradId} сохранены в БД")
earthCoverageRepository.deleteBySatId(params.noradId)
earthCoverageRepository.saveAll(bal.coverings.flatMap { (k,v) -> mergeSectors(v).map { EarthCoverageEntity(it, k , params.noradId) } })
logger.info("Параметры покрытия земной поверхности КА ${params.noradId} сохранены в БД")
prepareEarthCovering(bal, params.noradId)
bal.clear()
logger.info("Расчет для КА ${params.noradId} завершился успешно")
}
private fun InitialConditionsDTO.toIC() = InitialConditions(
OrbitalPoint(
t = fromDateTime(orbPoint.time),
vit = orbPoint.revolution.toInt(),
r = Vector3D(orbPoint.x,orbPoint.y,orbPoint.z),
v = Vector3D(orbPoint.vx,orbPoint.vy,orbPoint.vz)
),
f81 = f81,
sBall = sBall
)
suspend fun resieveRV(rv: SatelliteICDTO) {
val bal = Ballistics()
logger.info("Начало расчета баллистики по RV id = ${rv.satelliteId}")
bal.rollMax = (45.0 * PI / 180.0)
bal.rollMin = (20.0 * PI / 180.0)
var br = bal.calculateOrbPoints(
rv.ic.toIC(),
fromDateTime(rv.ic.orbPoint.time),
fromDateTime(rv.ic.orbPoint.time)+ 86400 * intervalForward
)
if (br != BallisticsError.OK) {
logger.warn("Ошибка при расчете ПДЦМ для noradId=${rv.satelliteId}. Код ошибки: $br")
return
}
pdcmRepository.deleteBySatId(rv.satelliteId)
pdcmRepository.saveAll(bal.points.map { p -> PDCMEntity(p, rv.satelliteId) })
logger.info("Точки орбиты КА ${rv.satelliteId} сохранены в БД")
ascNodeRepository.deleteBySatId(rv.satelliteId)
ascNodeRepository.saveAll(bal.revolutions.map { AscNodeEntity(it, rv.satelliteId) })
logger.info("ВУЗы орбиты КА ${rv.satelliteId} сохранены в БД")
val tInt = bal.points.first().t
br = bal.calculateFlightLine(tInt, tInt + 86400 * intervalForward)
if (br != BallisticsError.OK) {
logger.warn("Ошибка при расчете ТП и ПО для ka=${rv.satelliteId}. Код ошибки: $br")
return
}
flightLineRepository.deleteBySatId(rv.satelliteId)
flightLineRepository.saveAll(bal.flightLine.map { FlightLineEntity(it, rv.satelliteId) })
logger.info("Параметры трассы полеты и полосы обзора КА ${rv.satelliteId} сохранены в БД")
earthCoverageRepository.deleteBySatId(rv.satelliteId)
earthCoverageRepository.saveAll(bal.coverings.flatMap { (k,v) -> mergeSectors(v).map { EarthCoverageEntity(it, k , rv.satelliteId) } })
logger.info("Параметры покрытия земной поверхности КА ${rv.satelliteId} сохранены в БД")
prepareEarthCovering(bal, rv.satelliteId)
bal.clear()
logger.info("Расчет для КА ${rv.satelliteId} завершился успешно")
}
fun mergeSectors(sectors: List<FleghtLineSector>): List<FleghtLineSector> {
if (sectors.isEmpty()) return emptyList()
val sorted = sectors.sortedBy { it.tStart }
val result = mutableListOf<FleghtLineSector>()
var current = sorted[0]
for (i in 1 until sorted.size) {
val next = sorted[i]
if (abs(next.tStart - current.tStop) < 2) {
// Объединяем: расширяем tStop текущего сектора
current = FleghtLineSector(current.tStart, maxOf(current.tStop, next.tStop))
} else {
result.add(current)
current = next
}
}
result.add(current)
return result
}
fun clear(id : Long): SatelliteBallisticsDeletionSummary {
val pdcmDeleted = pdcmRepository.deleteBySatId(id)
val ascNodesDeleted = ascNodeRepository.deleteBySatId(id)
val flightLinesDeleted = flightLineRepository.deleteBySatId(id)
val earthCoverageDeleted = earthCoverageRepository.deleteBySatId(id)
val earthCellViewsDeleted = earthCellViewRepository.deleteBySatId(id)
val summary = SatelliteBallisticsDeletionSummary(
satelliteId = id,
pdcmDeleted = pdcmDeleted,
ascNodesDeleted = ascNodesDeleted,
flightLinesDeleted = flightLinesDeleted,
earthCoverageDeleted = earthCoverageDeleted,
earthCellViewsDeleted = earthCellViewsDeleted
)
logger.info(
"Deleted ballistics data for satellite: satelliteId={}, pdcm={}, ascNodes={}, flightLines={}, earthCoverage={}, earthCellViews={}",
summary.satelliteId,
summary.pdcmDeleted,
summary.ascNodesDeleted,
summary.flightLinesDeleted,
summary.earthCoverageDeleted,
summary.earthCellViewsDeleted
)
return summary
}
fun exactTime(satId : Long, request : ExactTimePositionRequestDTO) = Flux.create {
sink ->
logger.info("Запрос расчета выхода на заданное время для КА $satId ")
val points = pdcmRepository.findBySatelliteIdAndTimeBetween(
satId,
request.time.minusSeconds(100),
request.timeStop?.plusSeconds(100) ?: request.time.plusSeconds(100)
)
if (points.isEmpty())
throw CustomErrorException("Нет точек орбиты для КА $satId на интервале ${request.time} - ${request.timeStop ?: request.time}")
val stepper = RungeStepper(points.map { p -> p.toOrbitalPoint() }.toMutableList(), EarthType.PZ90d02)
var t = request.time
val tk = request.timeStop ?: t.plusNanos(5)
var step = request.stepMs ?: 125
if (step < 1)
step = 125
while (t <= tk) {
val op = stepper.calculate(fromDateTime(t))
if (op == null) {
logger.error("Ошибка расчета выхода на заданное время $t")
throw CustomValidationException("Внутренняя ошибка расчета выхода на заданное время")
}
sink.next( OrbPointDTO(
toDateTime(op.t),
op.vit.toLong(),
op.v.x,
op.v.y,
op.v.z,
op.r.x,
op.r.y,
op.r.z)
)
t = t.plusNanos(step * 1000000)
}
if (request.timeStop != null && t.minusNanos(step * 1000000) < tk) {
val op = stepper.calculate(fromDateTime(tk))
if (op == null) {
logger.error("Ошибка расчета выхода на заданное время $t")
throw CustomValidationException("Внутренняя ошибка расчета выхода на заданное время")
}
sink.next(OrbPointDTO(
toDateTime(op.t),
op.vit.toLong(),
op.v.x,
op.v.y,
op.v.z,
op.r.x,
op.r.y,
op.r.z)
)
}
sink.complete()
}
fun getCellCovering(num : Long) : List<SquareViewParamsWithPointsDTO>{
return earthCellViewRepository.findByCellNum(num).map { view ->
val points = pdcmRepository.findBySatelliteIdAndTimeBetween(
view.satelliteId,
view.timeBegin.minusSeconds(100),
view.timeEnd.plusSeconds(100)
)
SquareViewParamsWithPointsDTO(
view.toDTO(),
points.map { it.toDTO() }
)
}
}
fun mplPoint(req : ObjViewRequestDTO) : Flux<PointViewParamDTO>{
return Flux.fromIterable(
req.satellites
.flatMap { sat ->
val bal = Ballistics()
bal.setOrbitalPoints(
loadOrbitalPoints(sat, req.timeStart, req.timeStop),
EarthType.PZ90d02
)
bal.rollMin = 0.0
bal.rollMax = 45.0 / 180 * PI
val indexes = mkCovIndexes(listOf(req.obj.position?: PositionDTO()))
val sectors = loadEarthCoverageSectors(sat, req.timeStart, req.timeStop, indexes)
bal.setEarthCoverage(
sectors
)
val t1 = req.timeStart
val t2 = req.timeStop
bal.sunAngleMin = req.sunAngleMin?.let { it / 180 * PI } ?: (-PI / 2.0)
val r = bal.calculateMPL(
fromDateTime(t1),
fromDateTime(t2),
listOf(OPKatObj(0,0,"",0,
(req.obj.position?.lat?:0.0) * PI / 180,
(req.obj.position?.long?:0.0) * PI / 180,
req.obj.position?.height?:0.0,
0.0, 0.0, 0.0
)
)
)
if (r != BallisticsError.OK) {
logger.error("Ошибка при расчете параметров наблюдения объекта (${req.obj}) для КА $sat. Код ошибки : $r")
listOf()
} else
bal.mpl.map { PointViewParamDTO(
noradId = sat,
objectId = req.obj.id?:"",
time = toDateTime(it.traverz),
revolution = it.vit.toLong(),
gamma = it.orientation.kren * 180 / PI,
sunAngle = it.sunAngle * 180 / PI,
range = it.range,
revSign = if (it.pv ==0 ) RevolutionSign.ASC else RevolutionSign.DESC
) }
}
)
}
fun mplSquare(req : ObjViewRequestDTO) : Flux<SquareViewParamDTO>{
logger.info("Запрос МПЛ по площадному объекту")
return Flux.fromIterable(
req.satellites
.flatMap { sat ->
val bal = Ballistics()
bal.setOrbitalPoints(
loadOrbitalPoints(sat, req.timeStart, req.timeStop),
EarthType.PZ90d02
)
bal.rollMin = 0.0
bal.rollMax = 45.0 / 180 * PI
val contourService = ContourClipService()
val contour = contourService.getObjContour(req.obj)
if (contour == null || contour.count() < 3) {
logger.error("Контур площадного объекта пуст или содержит меньше 3-х точек")
listOf()
}
else {
val indexes = mkCovIndexes(contour)
val sectors = loadEarthCoverageSectors(sat, req.timeStart, req.timeStop, indexes)
bal.setEarthCoverage(
sectors
)
val t1 = req.timeStart
val t2 = req.timeStop
bal.sunAngleMin = req.sunAngleMin?.let { it / 180 * PI } ?: (-PI / 2.0)
val r = bal.calculateMPL(
fromDateTime(t1),
fromDateTime(t2),
contourService.splitPolySides(contour.map { p ->
PositionDTO(
p.lat / 180 * PI,
p.long / 180 * PI,
p.height
)
})
)
if (r != BallisticsError.OK) {
logger.error("Ошибка при расчете параметров наблюдения площадного объекта (${req.obj}) для КА $sat. Код ошибки : $r")
listOf()
} else
squareViewFromMPL(sat, bal.mpl, req.obj.id ?: "")
}
}
).map { square -> square.toDTO() }
}
private fun squareViewFromMPL(id : Long, mpl: Iterable<PointViewParams>, n: String): Iterable<SquareViewParamsModel>{
val svp = mutableListOf<SquareViewParamsModel>()
val vp = SquareViewParamsModel(
tN = -1.0,
tK = 0.0,
objID = n,
id = id
)
sortPointViews(mpl).forEach { m ->
if (m.traverz - vp.tK > 60.0*3) {
// начало нового интервала наблюдения! Надо сохранить старый, если такой есть.
if (vp.tN > 0)
svp.add(vp.clone())
vp.tN = m.traverz
vp.vitN = m.vit
vp.tK = m.traverz
vp.vitK = m.vit
vp.pvN = if (m.pv == 0) RevolutionSign.ASC else RevolutionSign.DESC
vp.pvK = if (m.pv == 0) RevolutionSign.ASC else RevolutionSign.DESC
vp.krenMin = m.orientation.kren
vp.krenMax = m.orientation.kren
} else {
vp.tK = m.traverz
vp.vitK = m.vit
vp.pvK = if (m.pv == 0) RevolutionSign.ASC else RevolutionSign.DESC
if (vp.krenMax < m.orientation.kren)
vp.krenMax = m.orientation.kren
if (vp.krenMin > m.orientation.kren)
vp.krenMin = m.orientation.kren
}
}
if (vp.tN > 0)
svp.add(vp.clone())
return svp
}
internal fun sortPointViews(mpl: Iterable<PointViewParams>): List<PointViewParams> =
mpl.sortedWith(
compareBy<PointViewParams>(
{ it.traverz },
{ it.vit },
{ it.pointNumb },
{ it.pv },
{ it.orientation.kren }
)
)
private fun loadOrbitalPoints(satelliteId: Long, timeStart: LocalDateTime, timeStop: LocalDateTime) =
pdcmRepository.findBySatelliteIdAndTimeBetween(satelliteId, timeStart, timeStop)
.sortedWith(
compareBy<PDCMEntity>(
{ it.time },
{ it.revolution },
{ it.id ?: Long.MAX_VALUE }
)
)
.map { it.toOrbitalPoint() }
private fun loadEarthCoverageSectors(
satelliteId: Long,
timeStart: LocalDateTime,
timeStop: LocalDateTime,
cellIndexes: List<Long>
): Map<Int, MutableList<FleghtLineSector>> =
earthCoverageRepository.findBySatelliteIdAndTStartGreaterThanAndTStopLessThanAndCellNumberIn(
satelliteId,
timeStart,
timeStop,
cellIndexes
)
.sortedWith(
compareBy<EarthCoverageEntity>(
{ it.cellNumber },
{ it.tStart },
{ it.tStop },
{ it.id ?: Long.MAX_VALUE }
)
)
.groupBy { it.cellNumber.toInt() }
.entries
.sortedBy { it.key }
.associateTo(linkedMapOf()) { (cellNumber, views) ->
cellNumber to views.map { view ->
FleghtLineSector(
fromDateTime(view.tStart),
fromDateTime(view.tStop)
)
}.toMutableList()
}
fun getBInd(b: Double): Long {
var b2: Double = b * 180.0 / PI
b2 = 90.0 - b2
var k: Long = truncate(b2 / 2.0).toLong()
if (k > 89) k = 89
return k
}
fun getLInd(l2: Double): Long {
var l: Double = l2 * 180.0 / PI
if (l < 0) l += 360.0
var k: Long = truncate(l / 2.0).toLong()
if (k > 179) k = 179
return k
}
fun getInd(
b: Double,
l: Double,
): Long {
return getBInd(b) * 180 + getLInd(l)
}
fun mkCovIndexes(contour : Iterable<PositionDTO>) : List<Long>{
val indexes = mutableListOf<Long>()
var bMin = 100.0
var bMax = -100.0
var lMin = 370.0
var lMax = -370.0
contour.forEach { c ->
val b = c.lat
val l = c.long
if (b < bMin) bMin = b
if (b > bMax) bMax = b
if (l < lMin) lMin = l
if (l > lMax) lMax = l
}
while (bMin < bMax + 0.6){
var l = lMin
while (l < lMax + 0.6){
val ind = getInd(bMin * PI / 180, l * PI / 180)
if (!indexes.contains(ind))
indexes.add(ind)
l += 0.5
}
bMin += 0.5
}
return indexes
}
fun getBInd2(b: Double): Long {
var b2: Double = b * 180.0 / PI
b2 = 90.0 - b2
var k: Long = truncate(b2).toLong()
if (k > 180) k = 180
return k
}
fun getLInd2(l2: Double): Long {
var l: Double = l2 * 180.0 / PI
if (l < 0) l += 360.0
var k: Long = truncate(l).toLong()
if (k > 359) k = 359
return k
}
fun getInd2(
b: Double,
l: Double,
): Long {
return getBInd2(b) * 360 + getLInd2(l)
}
fun prepareEarthCovering(bal : Ballistics, satId : Long){
logger.info("Начало расчета покрытия ячеек земной поверхности для КА $satId")
earthCellViewRepository.deleteBySatId(satId)
logger.info("Функционал не используется. Старые результаты удалены.")
return
// val contourService = ContourClipService()
// val step = 1.0
//
// bal.sunAngleMin = -PI/2
//
// val num : Long = 1
// var b = -90 + step
// while (b <= 90){
// var l = 0.0
// val line = mutableListOf<EarthCellViewsEntity>()
// while (l <= 360){
// val cellInd = getInd2(b * PI / 180.0,l* PI / 180.0)
// val t1 = bal.points.first().t
// val t2 = bal.points.last().t
// val cont : String = "POLYGON (($l $b, ${l-1} ${b}, ${l-1} ${b-1}, $l ${b-1}, $l $b))"
// val contour = contourService.getObjContour(ObjDTO(contourWKT = cont))
// if (contour == null || contour.count() < 3) {
// logger.error("Контур площадного объекта пуст или содержит меньше 3-х точек")
// continue
// }
//
// val objs = contourService.splitPolySides(contour.map { p ->
// PositionDTO(
// p.lat / 180 * PI,
// p.long / 180 * PI,
// p.height
// )
// }
// )
// val r = bal.calculateMPL(
// t1,
// t2,
// objs
//
// )
// val views =
// if (r != BallisticsError.OK) {
// logger.error("Ошибка при расчете параметров наблюдения площадного объекта (${cellInd}) для КА $satId. Код ошибки : $r")
// listOf()
// } else
// squareViewFromMPL(satId, bal.mpl, "")
//
// for (view in views) {
// line.add(
// EarthCellViewsEntity(
// null,
// satId,
// cellInd,
// view.vitN.toLong(),
// toDateTime(view.tN),
// if(view.pvN == RevolutionSign.ASC) 0 else 1,
// view.vitK.toLong(),
// toDateTime(view.tK),
// if(view.pvK == RevolutionSign.ASC) 1 else 0,
// view.krenMin * 180 / PI,
// view.krenMax * 180 / PI,
// cont
// )
// )
// }
// l += step
// if (line.isEmpty()) {
// logger.info("нет наблюдений ячейки $cont")
// }
// earthCellViewRepository.saveAll(line)
// line.clear()
// }
// b += step
// }
// logger.info("Конец расчета покрытия ячеек земной поверхности для КА $satId")
}
fun getOrbPoints(id : Long, tn : LocalDateTime?, tk : LocalDateTime?) =
pdcmRepository.findBySatelliteIdAndTimeBetween(
id,
tn?: LocalDateTime.now(),
tk?: LocalDateTime.now().plusDays(1))
.map { it.toDTO()
}
fun getOrbitAvailability(time: LocalDateTime?): List<SatelliteOrbitAvailabilityDTO> =
if (time == null) {
pdcmRepository.findSatelliteOrbitAvailability()
} else {
pdcmRepository.findSatelliteOrbitAvailabilityAtTime(time)
}
fun getFL(id : Long, tn : LocalDateTime?, tk : LocalDateTime?, step : Double?) =
flightLineRepository.findBySatelliteIdAndTimeBetween(id,
tn?: LocalDateTime.now(),
tk?: LocalDateTime.now().plusDays(1)
)
.map { it.toDTO() }
fun getRVA(id : Long, tn : LocalDateTime, tk : LocalDateTime) : List<RadioVisibilityAreaDTO> {
val bal = Ballistics()
val points = pdcmRepository.findBySatelliteIdAndTimeBetween(id,
tn,
tk).map { it.toOrbitalPoint() }
if (points.isEmpty())
throw CustomErrorException("Нет ПДЦМ для КА $id на интервале времени $tn - $tk")
bal.setOrbitalPoints(points,
EarthType.PZ90d02)
val tstart = points.first().t
val tstop = points.last().t
bal.rollMin = 0.0
bal.rollMax = 45.0 / 180 * PI
val r = bal.calculateZRV(
listOf(PPI(1,1,0.35, 0.45, 100.0, 0.0, PI/2)),
tstart,
tstop
)
if (r != BallisticsError.OK) {
logger.error("Ошибка при расчете параметров ЗРВ : $r")
return listOf()
} else
return bal.zrv.map { mkRVA(it, id)}
}
fun mkRVA(p : ZRV, id : Long) = RadioVisibilityAreaDTO(
noradId = id,
stationId = p.ppi.toLong(),
revolution = p.vit.toLong(),
onStart = mkTP(p.zoneIn),
onMaximum = mkTP(p.zoneMax),
onStop = mkTP(p.zoneOut)
)
fun mkTP(p : VisibilityParametersZRV) = TargetPositionDTO(
toDateTime(p.t),
p.elevation * 180 / PI,
p.azimuth * 180 / PI,
p.range
)
fun calculateKeplerParams(point: OrbitalPoint): KeplerParams {
val res = KeplerParams()
val aL00 = 6.25648106E+7
val ask = astro.grinvToASK(point)
val r = sqrt(sqr(ask.r.x) + sqr(ask.r.y) + sqr(ask.r.z))
val v = sqrt(sqr(ask.v.x) + sqr(ask.v.y) + sqr(ask.v.z))
val mu = astro.earth.fM
val k = (r * sqr(v)) / mu
val sinO = (ask.r.x * ask.v.x + ask.r.y * ask.v.y + ask.r.z * ask.v.z) / (r * v)
val cosO = sqrt(1 - sqr(sinO))
val c1 = ask.r.y * ask.v.z - ask.r.z * ask.v.y
val c2 = ask.r.z * ask.v.x - ask.r.x * ask.v.z
val c3 = ask.r.x * ask.v.y - ask.r.y * ask.v.x
val c = sqrt(sqr(c1) + sqr(c2) + sqr(c3))
res.ael = r / (2 - k)
val lambda = (1 / res.ael) * sqrt(mu / res.ael)
res.t = (2 * PI * res.ael * sqrt(res.ael)) / sqrt(astro.earth.middleRadius * aL00)
res.e = sqrt(1 - k * (2 - k) * (1 - sinO * sinO))
res.v = atan((k * sinO * cosO) / (k * sqr(cosO) - 1))
res.u = atan2((ask.r.z * c), (ask.r.y * c1 - ask.r.x * c2))
res.eA = atan((sqrt(1 - sqr(res.e)) * k * sinO * cosO) / (sqr(res.e) + (k * sqr(cosO) - 1)))
res.tau = ask.t + ((res.e * sin(res.eA) - res.eA) / lambda)
res.nakl = 0.5 * PI - asin(c3 / c)
res.omegab = atan2(c1, -c2)
res.omegam = res.u - res.v
res.o = asin(sinO)
res.rA = res.ael * (1 + res.e)
res.rP = res.ael * (1 - res.e)
res.dmv = point.t
if (res.omegam < 0) res.omegam += 2 * PI
if (res.tau < 0) res.tau += 1
return res
}
fun getAscNodes(id : Long, tn : LocalDateTime?, tk : LocalDateTime?) : List<AscNodeDTO> {
return ascNodeRepository.findBySatelliteIdAndTimeBetween(
id,
tn ?: LocalDateTime.now(),
tk ?: LocalDateTime.now().plusDays(1)
)
.map {
val keps = calculateKeplerParams(
OrbitalPoint(
fromDateTime(it.time),
1,
Vector3D(it.x,it.y,it.z),
Vector3D(it.vx,it.vy,it.vz)
))
it.toDTO(keps, astro.earth.ekvRadius)
}
}
}
data class SatelliteBallisticsDeletionSummary(
val satelliteId: Long,
val pdcmDeleted: Int,
val ascNodesDeleted: Int,
val flightLinesDeleted: Int,
val earthCoverageDeleted: Int,
val earthCellViewsDeleted: Int
)
@@ -0,0 +1,86 @@
package space.nstart.pcp.pcp_request_service.service
import ballistics.Ballistics
import ballistics.types.BallisticsError
import ballistics.types.PPI
import ballistics.types.TLE
import ballistics.types.TLEParams
import ballistics.types.VisibilityParametersZRV
import ballistics.types.ZRV
import ballistics.utils.fromDateTime
import ballistics.utils.toDateTime
import org.springframework.stereotype.Service
import space.nstart.pcp.pcp_request_service.configuration.CustomErrorException
import space.nstart.pcp.pcp_request_service.configuration.CustomValidationException
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RadioVisibilityAreaDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TargetPositionDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TleRvaRequestDTO
import kotlin.math.PI
@Service
class TLEService() {
fun mkTP(p : VisibilityParametersZRV) = TargetPositionDTO(
toDateTime(p.t),
p.elevation * 180 / PI,
p.azimuth * 180 / PI,
p.range
)
fun mkRVA(p : ZRV, id : Long) = RadioVisibilityAreaDTO(
noradId = id,
stationId = p.ppi.toLong(),
revolution = p.vit.toLong(),
onStart = mkTP(p.zoneIn),
onMaximum = mkTP(p.zoneMax),
onStop = mkTP(p.zoneOut)
)
fun parseTLE(tle: TLEDTO) : TLEParams{
val bal = Ballistics()
return bal.getTLEParams(TLE(tle.first, tle.second), tle.header?:"empty")
}
fun rva(req : TleRvaRequestDTO) : Iterable<RadioVisibilityAreaDTO> {
val bal = Ballistics()
var tleParams : TLEParams
try {
tleParams = bal.getTLEParams(TLE(req.tle.first, req.tle.second))
if (req.timeStart >= req.timeStop || req.timeStart < tleParams.time)
throw CustomValidationException("Ошибка задания интервала расчета")
}catch (ex : Exception){
throw CustomValidationException("Ошибка формата TLE : ${ex.message}")
}
val duration = fromDateTime(req.timeStop) - fromDateTime(req.timeStart)
val r = bal.calculateOrbPoints(TLE(req.tle.first, req.tle.second), duration)
if (r != BallisticsError.OK)
throw CustomErrorException("Ошибка расчета ПДЦМ : $r")
val rz = bal.calculateZRV(
listOf(PPI(
req.station.number.toInt(),
0,
req.station.position.lat * PI / 180.0,
req.station.position.long * PI / 180.0,
req.station.position.height,
req.station.elevationMin * PI / 180.0,
req.station.elevationMax * PI / 180.0)
),
fromDateTime(req.timeStart),
fromDateTime(req.timeStop)
)
if (rz != BallisticsError.OK)
throw CustomErrorException("Ошибка расчета зон радио-видимости : $rz")
return bal.zrv.map { mkRVA(it, tleParams.noradId) }
}
}
@@ -0,0 +1,176 @@
package space.nstart.pcp.pcp_request_service.utils
import ballistics.types.OPKatObj
import clipper.IntPoint
import clipper.Path
import clipper.containsPoint
import org.locationtech.jts.io.WKTReader
import space.nstart.pcp.pcp_request_service.configuration.CustomValidationException
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ObjDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.PositionDTO
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.sqrt
class ContourClipService {
fun prceWKT(wkt : String) : Iterable<PositionDTO>?{
try {
val contour = mutableListOf<PositionDTO>()
val reader = WKTReader()
val geom = reader.read(wkt)
for (c in geom.coordinates)
contour.add(PositionDTO(c.y, c.x, c.z))
if (contour.size < 3)
return null
return contour
} catch (ex : Exception){
throw CustomValidationException("Ошибка пазбора WKT-формата")
}
}
fun getObjContour(obj : ObjDTO) : Iterable<PositionDTO>?{
obj.contourWKT?.let { return prceWKT(obj.contourWKT!!) }
return null
}
fun splitPolySides(poly : Iterable<PositionDTO>, internal : Boolean = true) : Iterable<OPKatObj>{
// return poly.map { p1 ->
// OPKatObj(
// 1, 1, "", 1, p1.lat, p1.long,
// 0.0, 0.0, PI / 2, -PI / 2
// )
// }
val res = mutableListOf<OPKatObj>()
for (i in 1 ..poly.count()-1){
res.addAll(splitLine(poly.elementAt(i-1), poly.elementAt(i)))
}
// if (internal)
res.addAll(splitPoly(poly, 2.0 * PI / 180.0))
return res
}
private fun splitLine(p1 : PositionDTO, p2 : PositionDTO) : Iterable<OPKatObj>{
var x1 = p1.long
var x2 = p2.long
if (abs(x1 - x2) > PI){
if (x1 < x2)
x1 += 2 * PI
else
x2 = 2 * PI
}
if (x1 > x2){
val buf = x1
x1 = x2
x2 = buf
}
var y1 = p1.lat
var y2 = p2.lat
if (y1 > y2){
val buf = y1
y1 = y2
y2 = buf
}
val dx = x2 - x1
val dy = y2 - y1
val dl = sqrt((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1))
val res = mutableListOf<OPKatObj>()
res.add(OPKatObj(1, 1, "", 1, p1.lat, p1.long, 0.0, 0.0, PI / 2, -PI / 2))
if (dl > 0.01) {
res.add(OPKatObj(1, 1, "", 2, p2.lat, p2.long, 0.0, 0.0, PI / 2, -PI / 2))
var pn = 3
val step = 0.1 * PI / 180
if (dx > dy) {
var x = x1 + step
val k = dy / dx
val b = y2 - k * x2
(0..(dx / step).toInt()).forEach { i ->
if (x < x2) {
val y = k * x + b
res.add(OPKatObj(1, 1, "", pn, y, x, 0.0, 0.0, PI / 2, -PI / 2))
++pn
}
x += step
}
} else {
if (dx < 0.000001) {
var y = y1 + step
(0..(dy / step).toInt()).forEach { i ->
if (y < y2) {
res.add(OPKatObj(1, 1, "", pn, y, x1, 0.0, 0.0, PI / 2, -PI / 2))
++pn
}
y += step
}
} else {
var y = y1 + step
val k = dy / dx
val b = y2 - k * x2
(0..(dy / step).toInt()).forEach { i ->
if (y < y2) {
val x = (y - b) / k
res.add(OPKatObj(1, 1, "", pn, y, x, 0.0, 0.0, PI / 2, -PI / 2))
++pn
}
y += step
}
}
}
}
return res
}
private fun splitPoly(poly : Iterable<PositionDTO>, step : Double? = null) : Iterable<OPKatObj> {
val res = mutableListOf<OPKatObj>()
val path = Path()
val clipVal = 1000000
var bMin = poly.minOf { it.lat }
val bMax = poly.maxOf { it.lat }
var lMin = poly.minOf { it.long }
var lMax = poly.maxOf { it.long }
for (p in poly) {
path.add(IntPoint((p.lat * clipVal).toLong(), (p.long * clipVal).toLong()))
if (step != null)
res.add(OPKatObj(1, 1, "", 1, p.lat, p.long, 0.0, 0.0, PI / 2, -PI / 2))
}
path.add(path.first())
if ((lMin < 0 && lMax > 0) && (lMax - lMin > PI)) {
val b = lMax
lMax = lMin + PI * 2
lMin = b
}
if (lMax - lMin > PI) {
val b = lMax
lMax = lMin
lMin = b - 2 * PI
}
var stepL : Double
var stepB : Double
if (step != null){
stepL = step
stepB = step
}else {
stepL = (lMax - lMin) / 10
if (stepL > 0.1 * PI / 180)
stepL = 0.1 * PI / 180
stepB = (bMax - bMin) / 10
if (stepB > 0.1 * PI / 180)
stepB = 0.1 * PI / 180
}
while (bMin <= bMax) {
var l = lMin
while (l <= lMax) {
if (path.containsPoint(IntPoint( (bMin * clipVal).toLong(), (l * clipVal).toLong())) == 1)
res.add(OPKatObj(1, 1, "", 1, bMin, l, 0.0, 0.0, PI / 2, -PI / 2))
l += stepL
}
bMin += stepB
}
return res
}
}
@@ -0,0 +1,13 @@
spring:
application:
name: pcp-ballistics-service
profiles:
default: local
config:
import: "configserver:"
cloud:
config:
uri: ${CONFIG_SERVER_URI:http://localhost:8888}
fail-fast: ${CONFIG_SERVER_FAIL_FAST:true}
profile: ${SPRING_CLOUD_CONFIG_PROFILE:${SPRING_PROFILES_ACTIVE:${spring.profiles.default}}}
label: ${SPRING_CLOUD_CONFIG_LABEL:dev}
@@ -0,0 +1,16 @@
CREATE TABLE IF NOT EXISTS pdcm (
pdcm_id BIGSERIAL PRIMARY KEY,
satellite_id BIGINT NOT NULL,
t TIMESTAMP NOT NULL,
revolution BIGINT NOT NULL,
x DOUBLE PRECISION,
y DOUBLE PRECISION,
z DOUBLE PRECISION,
vx DOUBLE PRECISION,
vy DOUBLE PRECISION,
vz DOUBLE PRECISION
);
CREATE INDEX idx_pdcm_satellite_id ON pdcm(satellite_id);
CREATE INDEX idx_pdcm_t ON pdcm(t);
CREATE INDEX idx_pdcm_revolution ON pdcm(revolution);
@@ -0,0 +1,51 @@
CREATE TABLE IF NOT EXISTS asc_node (
asc_node_id BIGSERIAL PRIMARY KEY,
satellite_id BIGINT NOT NULL,
t TIMESTAMP NOT NULL,
revolution BIGINT NOT NULL,
longitude DOUBLE PRECISION,
height DOUBLE PRECISION,
x DOUBLE PRECISION,
y DOUBLE PRECISION,
z DOUBLE PRECISION,
vx DOUBLE PRECISION,
vy DOUBLE PRECISION,
vz DOUBLE PRECISION
);
CREATE INDEX idx_asc_node_satellite_id ON asc_node(satellite_id);
CREATE INDEX idx_asc_node_t ON asc_node(t);
CREATE INDEX idx_asc_node_revolution ON asc_node(revolution);
CREATE TABLE IF NOT EXISTS flight_line (
flight_line_id BIGSERIAL PRIMARY KEY,
satellite_id BIGINT NOT NULL,
t TIMESTAMP NOT NULL,
revolution BIGINT NOT NULL,
revolution_sign VARCHAR(4) NOT NULL CHECK (revolution_sign IN ('ASC', 'DESC')),
longitude DOUBLE PRECISION,
latitude DOUBLE PRECISION,
longitude_outer_left DOUBLE PRECISION,
latitude_outer_left DOUBLE PRECISION,
longitude_inner_left DOUBLE PRECISION,
latitude_inner_left DOUBLE PRECISION,
longitude_inner_right DOUBLE PRECISION,
latitude_inner_right DOUBLE PRECISION,
longitude_outer_right DOUBLE PRECISION,
latitude_outer_right DOUBLE PRECISION
);
CREATE INDEX idx_flight_line_satellite_id ON flight_line(satellite_id);
CREATE INDEX idx_flight_line_t ON flight_line(t);
CREATE INDEX idx_flight_line_revolution ON flight_line(revolution);
CREATE TABLE IF NOT EXISTS earth_coverage (
earth_coverage_id BIGSERIAL PRIMARY KEY,
satellite_id BIGINT NOT NULL,
cell_number BIGINT NOT NULL,
t_start TIMESTAMP NOT NULL,
t_stop TIMESTAMP NOT NULL
);
CREATE INDEX idx_earth_coverage_satellite_id ON earth_coverage(satellite_id);
CREATE INDEX idx_earth_coverage_t ON earth_coverage(t_start);
CREATE INDEX idx_earth_coverage_cell_number ON earth_coverage(cell_number);
@@ -0,0 +1,23 @@
CREATE EXTENSION IF NOT EXISTS postgis;
CREATE TABLE IF NOT EXISTS cell_covering (
cell_covering_id BIGSERIAL PRIMARY KEY,
satellite_id BIGINT NOT NULL,
cell_num BIGINT NOT NULL,
rev_begin BIGINT NOT NULL,
time_begin TIMESTAMP NOT NULL,
rev_sign_begin INT NOT NULL,
rev_end BIGINT NOT NULL,
time_end TIMESTAMP NOT NULL,
rev_sign_end INT NOT NULL,
roll_min DOUBLE PRECISION,
roll_max DOUBLE PRECISION,
contour_wkt TEXT NOT NULL
);
ALTER TABLE cell_covering ADD COLUMN IF NOT EXISTS contour_geom geometry(Polygon, 4326)
GENERATED ALWAYS AS (ST_GeomFromText(contour_wkt)::geometry(Polygon, 4326)) STORED;
CREATE INDEX idx_cell_covering_cell_num ON cell_covering(cell_num);
CREATE INDEX idx_cell_covering_satellite_id ON cell_covering(satellite_id);
CREATE INDEX idx_cell_covering_contour_geom ON cell_covering USING GIST (contour_geom);
@@ -0,0 +1,13 @@
package space.nstart.pcp.pcp_request_service
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest
class PcpBallisticsServiceApplicationTests {
@Test
fun contextLoads() {
}
}
@@ -0,0 +1,106 @@
package space.nstart.pcp.pcp_request_service.message
import org.junit.jupiter.api.Test
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.readText
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
class SatelliteDeletedKafkaListenerGroupIdTest {
private val listeners = mapOf(
"slots-service" to ListenerExpectation(
path = "services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/service/SatelliteDeletedKafkaListener.kt",
groupId = "pcp-slots-service-satellite-deleted",
hasEventFilter = true
),
"pcp-ballistics-service" to ListenerExpectation(
path = "services/pcp-ballistics-service/src/main/kotlin/space/nstart/pcp/pcp_request_service/message/SatelliteDeletedKafkaListener.kt",
groupId = "pcp-ballistics-service-satellite-deleted",
hasEventFilter = true
),
"tle-monitoring-service" to ListenerExpectation(
path = "services/tle-monitoring-service/src/main/kotlin/space/nstart/pcp/tle_monitoring_service/message/SatelliteDeletedKafkaListener.kt",
groupId = "pcp-tle-monitoring-service-satellite-deleted",
hasEventFilter = true
),
"pcp-complex-mission-service" to ListenerExpectation(
path = "services/pcp-complex-mission-service/src/main/kotlin/space/nstart/pcp/pcp_satellites_service/service/SatelliteDeletedKafkaListener.kt",
groupId = "pcp-complex-mission-service-satellite-deleted"
),
"pcp-mission-planing-service" to ListenerExpectation(
path = "services/pcp-mission-planing-service/src/main/kotlin/space/nstart/pcp/pcp_mission_planing_service/service/SatelliteDeletedKafkaListener.kt",
groupId = "pcp-mission-planing-service-satellite-deleted"
),
"pcp-dynamic-plan-service" to ListenerExpectation(
path = "services/pcp-dynamic-plan-service/src/main/kotlin/space/nstart/pcp/complan/service/SatelliteDeletedKafkaListener.kt",
groupId = "pcp-dynamic-plan-service-satellite-deleted"
)
)
@Test
fun `satellite deleted listeners use unique explicit consumer groups`() {
val resolvedGroupIds = listeners.mapValues { (_, expectation) ->
val source = projectRoot().resolve(expectation.path).readText()
val annotationBody = kafkaListenerAnnotationBody(source)
val groupReference = annotationParameter(annotationBody, "groupId")
assertNotNull(groupReference, "SatelliteDeletedEvent listener must define explicit groupId: ${expectation.path}")
assertEquals("SATELLITE_DELETED_CONSUMER_GROUP", groupReference)
assertTrue(
annotationBody.contains("""topics = ["\${'$'}{app.kafka.topics.satellites:pcp.satellites}"]"""),
"SatelliteDeletedEvent listener must keep satellite topic: ${expectation.path}"
)
if (expectation.hasEventFilter) {
assertTrue(
annotationBody.contains("""filter = "satelliteDeletedFilter""""),
"SatelliteDeletedEvent listener must keep event filter: ${expectation.path}"
)
}
val groupId = consumerGroupConstantValue(source)
assertEquals(expectation.groupId, groupId)
groupId
}
assertEquals(
resolvedGroupIds.size,
resolvedGroupIds.values.toSet().size,
"SatelliteDeletedEvent consumer groups must be unique per service: $resolvedGroupIds"
)
}
private fun kafkaListenerAnnotationBody(source: String): String {
val match = Regex("@KafkaListener\\((.*?)\\)", RegexOption.DOT_MATCHES_ALL).find(source)
assertNotNull(match, "SatelliteDeletedEvent listener must have @KafkaListener")
return match.groupValues[1]
}
private fun annotationParameter(annotationBody: String, parameterName: String): String? =
Regex("""$parameterName\s*=\s*([^,\n)]+)""")
.find(annotationBody)
?.groupValues
?.get(1)
?.trim()
private fun consumerGroupConstantValue(source: String): String {
val match = Regex("""private const val SATELLITE_DELETED_CONSUMER_GROUP\s*=\s*"([^"]+)"""").find(source)
assertNotNull(match, "SatelliteDeletedEvent listener must declare SATELLITE_DELETED_CONSUMER_GROUP")
return match.groupValues[1]
}
private fun projectRoot(): Path {
generateSequence(Path.of(System.getProperty("user.dir")).toAbsolutePath()) { it.parent }
.firstOrNull { Files.exists(it.resolve("settings.gradle.kts")) }
?.let { return it }
error("Cannot locate project root from ${System.getProperty("user.dir")}")
}
private data class ListenerExpectation(
val path: String,
val groupId: String,
val hasEventFilter: Boolean = false
)
}
@@ -0,0 +1,96 @@
package space.nstart.pcp.pcp_request_service.message
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.ObjectProvider
import space.nstart.pcp.pcp_request_service.service.SatelliteIcEventService
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO
import tools.jackson.databind.ObjectMapper
class SatelliteIcRvKafkaListenerTest {
private val objectMapper = ObjectMapper()
private val satelliteIcEventService = RecordingSatelliteIcEventService()
private val listener = SatelliteIcRvKafkaListener(
objectMapperProvider = SingleObjectProvider(objectMapper),
satelliteIcEventService = satelliteIcEventService
)
@Test
fun `listener parses ICRV placed event payload and delegates to service`() {
listener.consume(
"""
{
"type": "ICRVPlacedEvent",
"data": {
"satelliteId": 56756,
"ic": {
"orbPoint": {
"time": "2026-04-16T12:00:00",
"revolution": 42,
"vx": 1.0,
"vy": 2.0,
"vz": 3.0,
"x": 4.0,
"y": 5.0,
"z": 6.0
},
"sBall": 0.7,
"f81": 140.0
}
}
}
""".trimIndent()
)
assertEquals(56756L, satelliteIcEventService.lastMessage?.satelliteId)
assertEquals(42L, satelliteIcEventService.lastMessage?.ic?.orbPoint?.revolution)
assertEquals(140.0, satelliteIcEventService.lastMessage?.ic?.f81)
}
@Test
fun `listener substitutes default initial condition coefficients when payload contains nulls`() {
listener.consume(
"""
{
"type": "ICRVPlacedEvent",
"data": {
"satelliteId": 56756,
"ic": {
"orbPoint": {
"time": "2026-04-16T12:00:00",
"revolution": 42,
"vx": 1.0,
"vy": 2.0,
"vz": 3.0,
"x": 4.0,
"y": 5.0,
"z": 6.0
},
"sBall": null,
"f81": null
}
}
}
""".trimIndent()
)
assertEquals(0.0, satelliteIcEventService.lastMessage?.ic?.sBall)
assertEquals(147.8, satelliteIcEventService.lastMessage?.ic?.f81)
}
private class RecordingSatelliteIcEventService : SatelliteIcEventService() {
var lastMessage: SatelliteICDTO? = null
override fun handlePlacedIcRv(message: SatelliteICDTO) {
lastMessage = message
}
}
private class SingleObjectProvider<T : Any>(private val value: T) : ObjectProvider<T> {
override fun getObject(vararg args: Any?): T = value
override fun getIfAvailable(): T = value
override fun getIfUnique(): T = value
override fun getObject(): T = value
}
}
@@ -0,0 +1,125 @@
package space.nstart.pcp.pcp_request_service.repository
import jakarta.persistence.EntityManager
import org.apache.kafka.clients.consumer.ConsumerConfig
import org.apache.kafka.common.serialization.StringDeserializer
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory
import org.springframework.kafka.core.ConsumerFactory
import org.springframework.kafka.core.DefaultKafkaConsumerFactory
import org.springframework.kafka.listener.adapter.RecordFilterStrategy
import org.springframework.transaction.annotation.Transactional
import space.nstart.pcp.pcp_request_service.entity.PDCMEntity
import java.time.LocalDateTime
import kotlin.test.assertEquals
@SpringBootTest(
properties = [
"spring.cloud.config.enabled=false",
"spring.cloud.config.import-check.enabled=false",
"spring.kafka.bootstrap-servers=false",
"spring.kafka.listener.auto-startup=false",
"spring.datasource.driver-class-name=org.h2.Driver",
"spring.datasource.url=jdbc:h2:mem:pdcm-repository-test;MODE=PostgreSQL;DB_CLOSE_DELAY=-1",
"spring.datasource.username=sa",
"spring.datasource.password=",
"spring.flyway.enabled=false",
"spring.jpa.hibernate.ddl-auto=create-drop",
"spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect"
]
)
@Transactional
class PDCMRepositoryTest {
@Autowired
private lateinit var entityManager: EntityManager
@Autowired
private lateinit var repository: PDCMRepository
@Test
fun `find satellite orbit availability returns all satellites with points when time is null`() {
persistPoint(11L, LocalDateTime.of(2026, 4, 14, 10, 0))
persistPoint(11L, LocalDateTime.of(2026, 4, 14, 12, 0))
persistPoint(22L, LocalDateTime.of(2026, 4, 15, 9, 30))
persistPoint(22L, LocalDateTime.of(2026, 4, 15, 18, 45))
val actual = repository.findSatelliteOrbitAvailability()
assertEquals(2, actual.size)
assertEquals(11L, actual[0].satelliteId)
assertEquals(LocalDateTime.of(2026, 4, 14, 10, 0), actual[0].timeStart)
assertEquals(LocalDateTime.of(2026, 4, 14, 12, 0), actual[0].timeStop)
assertEquals(22L, actual[1].satelliteId)
assertEquals(LocalDateTime.of(2026, 4, 15, 9, 30), actual[1].timeStart)
assertEquals(LocalDateTime.of(2026, 4, 15, 18, 45), actual[1].timeStop)
}
@Test
fun `find satellite orbit availability filters satellites whose interval contains requested time`() {
persistPoint(11L, LocalDateTime.of(2026, 4, 14, 10, 0))
persistPoint(11L, LocalDateTime.of(2026, 4, 14, 12, 0))
persistPoint(22L, LocalDateTime.of(2026, 4, 15, 9, 30))
persistPoint(22L, LocalDateTime.of(2026, 4, 15, 18, 45))
val actual = repository.findSatelliteOrbitAvailabilityAtTime(LocalDateTime.of(2026, 4, 14, 11, 0))
assertEquals(1, actual.size)
assertEquals(11L, actual.single().satelliteId)
assertEquals(LocalDateTime.of(2026, 4, 14, 10, 0), actual.single().timeStart)
assertEquals(LocalDateTime.of(2026, 4, 14, 12, 0), actual.single().timeStop)
}
private fun persistPoint(satelliteId: Long, time: LocalDateTime) {
entityManager.persist(
PDCMEntity(
satelliteId = satelliteId,
time = time,
revolution = 1,
x = 1.0,
y = 2.0,
z = 3.0,
vx = 4.0,
vy = 5.0,
vz = 6.0
)
)
entityManager.flush()
}
@TestConfiguration
class KafkaFilterStubConfiguration {
@Bean("kafkaListenerContainerFactory")
fun kafkaListenerContainerFactory(): ConcurrentKafkaListenerContainerFactory<String, String> =
ConcurrentKafkaListenerContainerFactory<String, String>().apply {
setConsumerFactory(testConsumerFactory())
setAutoStartup(false)
}
private fun testConsumerFactory(): ConsumerFactory<String, String> =
DefaultKafkaConsumerFactory(
mapOf(
ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG to "localhost:9092",
ConsumerConfig.GROUP_ID_CONFIG to "test",
ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java,
ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java
)
)
@Bean("placedIcFilter")
fun placedIcFilter(): RecordFilterStrategy<String, String> = RecordFilterStrategy { false }
@Bean("placedIcRvFilter")
fun placedIcRvFilter(): RecordFilterStrategy<String, String> = RecordFilterStrategy { false }
@Bean("updatedIcFilter")
fun updatedIcFilter(): RecordFilterStrategy<String, String> = RecordFilterStrategy { false }
@Bean("satelliteDeletedFilter")
fun satelliteDeletedFilter(): RecordFilterStrategy<String, String> = RecordFilterStrategy { false }
}
}
@@ -0,0 +1,56 @@
package space.nstart.pcp.pcp_request_service.service
import ballistics.types.Orientation
import ballistics.types.PointViewParams
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
class SatelliteServiceDeterminismTest {
@Test
fun `sortPointViews orders mpl points deterministically`() {
val service = SatelliteService()
val sorted = service.sortPointViews(
listOf(
pointView(traverz = 120.0, vit = 3, pointNumb = 2, pv = 1, kren = 0.3),
pointView(traverz = 100.0, vit = 4, pointNumb = 1, pv = 0, kren = 0.5),
pointView(traverz = 100.0, vit = 3, pointNumb = 2, pv = 0, kren = 0.4),
pointView(traverz = 100.0, vit = 3, pointNumb = 1, pv = 1, kren = 0.2)
)
)
assertEquals(
listOf(
100.0 to 3,
100.0 to 3,
100.0 to 4,
120.0 to 3
),
sorted.map { it.traverz to it.vit }
)
assertEquals(listOf(1, 2, 1, 2), sorted.map { it.pointNumb })
}
private fun pointView(
traverz: Double,
vit: Int,
pointNumb: Int,
pv: Int,
kren: Double
) = PointViewParams(
objON = 0,
objN = 0,
objUUID = "",
pointNumb = pointNumb,
vit = vit,
traverz = traverz,
latTraverz = 0.0,
longTraverz = 0.0,
orientation = Orientation(0.0, kren, 0.0),
range = 0.0,
sunAngle = 0.0,
sightAngle = 0.0,
pv = pv
)
}
@@ -0,0 +1,69 @@
spring:
application:
name: pcp-ballistics-service
cloud:
config:
enabled: false
lifecycle.timeout-per-shutdown-phase: 40s
jackson:
default-property-inclusion: non_null
kafka:
bootstrap-servers: 192.168.60.68:29092
consumer:
group-id: pcp-ballistics-service
auto-offset-reset: earliest
template:
default-topic: pcp.tle
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://192.168.60.68:5432/pcp_ballistics
username: postgres
password: password
jpa:
hibernate:
ddl-auto: validate
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
jdbc:
lob:
non_contextual_creation: true
flyway:
enabled: true
baseline-on-migrate: true
locations: classpath:db/migration
codec:
max-in-memory-size: 20MB
springdoc:
swagger-ui:
enabled: true
layout: BaseLayout
path: /swagger/ui
api-docs:
enabled: true
path: /api-docs
logging:
level:
.: ERROR
file:
name: ./logs/application.log
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
info:
enabled: true
settings:
calculation-interval: 28
ic-service: http://192.168.60.68:9080
server:
port: 7003