Init
This commit is contained in:
@@ -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")}")
|
||||
}
|
||||
}
|
||||
+93
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+46
@@ -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)
|
||||
}
|
||||
}
|
||||
+138
@@ -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()
|
||||
}
|
||||
}
|
||||
+14
@@ -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
|
||||
}
|
||||
}
|
||||
+37
@@ -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)
|
||||
|
||||
}
|
||||
+21
@@ -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)
|
||||
}
|
||||
+126
@@ -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)
|
||||
|
||||
}
|
||||
+30
@@ -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)
|
||||
|
||||
}
|
||||
+110
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+58
@@ -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
|
||||
)
|
||||
}
|
||||
+36
@@ -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)
|
||||
)
|
||||
}
|
||||
+77
@@ -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)
|
||||
)
|
||||
}
|
||||
+56
@@ -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
|
||||
)
|
||||
}
|
||||
+20
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+66
@@ -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()
|
||||
}
|
||||
+70
@@ -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()
|
||||
}
|
||||
}
|
||||
+39
@@ -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
|
||||
}
|
||||
}
|
||||
+27
@@ -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>
|
||||
}
|
||||
+22
@@ -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>
|
||||
|
||||
}
|
||||
+22
@@ -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>
|
||||
}
|
||||
+21
@@ -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>
|
||||
}
|
||||
+49
@@ -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>
|
||||
}
|
||||
+97
@@ -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)
|
||||
)
|
||||
}
|
||||
+46
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
+882
@@ -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
|
||||
)
|
||||
+86
@@ -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) }
|
||||
}
|
||||
}
|
||||
+176
@@ -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);
|
||||
+51
@@ -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);
|
||||
+23
@@ -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);
|
||||
+13
@@ -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() {
|
||||
}
|
||||
|
||||
}
|
||||
+106
@@ -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
|
||||
)
|
||||
}
|
||||
+96
@@ -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
|
||||
}
|
||||
}
|
||||
+125
@@ -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 }
|
||||
}
|
||||
}
|
||||
+56
@@ -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
|
||||
@@ -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,111 @@
|
||||
|
||||
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 {
|
||||
jvmToolchain((property("versions.java") as String).toInt())
|
||||
compilerOptions {
|
||||
freeCompilerArgs.addAll("-Xjsr305=strict")
|
||||
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.fromTarget(property("versions.java") as String))
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
compileOnly {
|
||||
extendsFrom(configurations.annotationProcessor.get())
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":libs:pcp-types-lib"))
|
||||
implementation(project(":libs:ballistics-lib"))
|
||||
|
||||
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("org.springframework.kafka:spring-kafka")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
||||
implementation("org.springframework.cloud:spring-cloud-starter-config")
|
||||
implementation("org.springframework.boot:spring-boot-starter-flyway")
|
||||
implementation("org.flywaydb:flyway-database-postgresql")
|
||||
runtimeOnly("org.postgresql:postgresql")
|
||||
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")}")
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package space.nstart.pcp.pcp_satellites_service
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.scheduling.annotation.EnableAsync
|
||||
|
||||
@EnableAsync
|
||||
@SpringBootApplication
|
||||
class PcpComplexMissionServiceApplication
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<PcpComplexMissionServiceApplication>(*args)
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.configuration
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "complex-plan")
|
||||
class ComplexPlanProperties {
|
||||
var batchChunkSize: Int = 100
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package space.nstart.pcp.pcp_satellites_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)
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.configuration
|
||||
|
||||
import org.apache.kafka.clients.consumer.ConsumerConfig
|
||||
import org.apache.kafka.common.serialization.StringDeserializer
|
||||
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
|
||||
|
||||
@ConditionalOnProperty(name = ["spring.kafka.bootstrap-servers"], matchIfMissing = false)
|
||||
@Configuration
|
||||
class KafkaConfig {
|
||||
|
||||
@Value("\${spring.kafka.bootstrap-servers}")
|
||||
private lateinit var bootstrapServers: String
|
||||
|
||||
@Value("\${spring.kafka.consumer.group-id}")
|
||||
private lateinit var groupId: String
|
||||
|
||||
@Bean
|
||||
fun consumerFactory(): ConsumerFactory<String, String> {
|
||||
val props = hashMapOf<String, Any>()
|
||||
props[ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG] = bootstrapServers
|
||||
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, String> =
|
||||
ConcurrentKafkaListenerContainerFactory<String, String>()
|
||||
.apply { setConsumerFactory(consumerFactory()) }
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.configuration
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import java.time.Clock
|
||||
|
||||
@Configuration
|
||||
class TimeConfiguration {
|
||||
|
||||
@Bean
|
||||
fun clock(): Clock = Clock.systemUTC()
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package space.nstart.pcp.pcp_satellites_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
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.configuration
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.util.unit.DataSize
|
||||
import org.springframework.web.reactive.function.client.WebClient
|
||||
|
||||
@Configuration
|
||||
class WebClientConfig(
|
||||
@param:Value("\${spring.codec.max-in-memory-size:20MB}")
|
||||
private val maxInMemorySize: DataSize
|
||||
) {
|
||||
|
||||
@Bean
|
||||
fun webClientBuilder(): WebClient.Builder =
|
||||
WebClient.builder()
|
||||
.codecs { configurer ->
|
||||
configurer.defaultCodecs().maxInMemorySize(maxInMemorySize.toBytes().toInt())
|
||||
}
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.controller
|
||||
|
||||
import jakarta.validation.Valid
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.web.bind.annotation.DeleteMapping
|
||||
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.ResponseStatus
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestHeader
|
||||
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_satellites_service.dto.ComplexPlanRunFilterDTO
|
||||
import space.nstart.pcp.pcp_satellites_service.dto.ComplexPlanRunResponseDTO
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.ComplexPlanRunStatus
|
||||
import space.nstart.pcp.pcp_satellites_service.service.ComplexMissionService
|
||||
import space.nstart.pcp.pcp_satellites_service.service.ComplexPlanRunPersistenceService
|
||||
import space.nstart.pcp.pcp_satellites_service.service.ComplexPlanRunOrchestrationService
|
||||
import space.nstart.pcp.pcp_satellites_service.service.ComplexPlanRunQueryService
|
||||
import space.nstart.pcp.pcp_satellites_service.service.SatelliteModeQueryService
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.ComplexPlanProcessRequestDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageStrategy
|
||||
import tools.jackson.databind.json.JsonMapper
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/com-plan")
|
||||
class ComplexPlanController {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||
private val objectMapper = JsonMapper.builder().findAndAddModules().build()
|
||||
|
||||
@Autowired
|
||||
private lateinit var complexMissionService: ComplexMissionService
|
||||
|
||||
@Autowired
|
||||
private lateinit var complexPlanRunOrchestrationService: ComplexPlanRunOrchestrationService
|
||||
|
||||
@Autowired
|
||||
private lateinit var complexPlanRunPersistenceService: ComplexPlanRunPersistenceService
|
||||
|
||||
@Autowired
|
||||
private lateinit var complexPlanRunQueryService: ComplexPlanRunQueryService
|
||||
|
||||
@Autowired
|
||||
private lateinit var satelliteModeQueryService: SatelliteModeQueryService
|
||||
|
||||
@PostMapping("/process")
|
||||
fun calc(
|
||||
@Valid @RequestBody body: ComplexPlanProcessRequestDTO,
|
||||
@RequestHeader(name = "X-Requested-By", required = false) requestedByHeader: String?,
|
||||
@RequestHeader(name = "X-User", required = false) xUserHeader: String?,
|
||||
@RequestHeader(name = "X-Forwarded-User", required = false) forwardedUserHeader: String?
|
||||
): ComplexPlanRunResponseDTO {
|
||||
val requestedBy = requestedByHeader
|
||||
?: xUserHeader
|
||||
?: forwardedUserHeader
|
||||
val request = body.copy(coverageStrategy = body.coverageStrategy ?: SlotCoverageStrategy.GREEDY)
|
||||
logger.info(
|
||||
"Complex plan process request started: interval=[{} - {}], satelliteId={}, satelliteIds={}, coverageSource={}, countLat={}, countLong={}, coverageStrategy={}, requestedBy={}",
|
||||
request.intervalStart,
|
||||
request.intervalEnd,
|
||||
request.satelliteId,
|
||||
request.satelliteIds,
|
||||
request.coverageSource,
|
||||
request.countLat,
|
||||
request.countLong,
|
||||
request.coverageStrategy,
|
||||
requestedBy
|
||||
)
|
||||
lateinit var response: ComplexPlanRunResponseDTO
|
||||
val durationMs = measureTimeMillis {
|
||||
response = complexPlanRunOrchestrationService.process(
|
||||
request = request,
|
||||
requestedBy = requestedBy
|
||||
)
|
||||
}
|
||||
logger.info(
|
||||
"Complex plan process completed: runId={}, status={}, durationMs={}, responseBody={}",
|
||||
response.runId,
|
||||
response.status,
|
||||
durationMs,
|
||||
objectMapper.writeValueAsString(response)
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
@PostMapping("/clear")
|
||||
fun clear() = complexMissionService.clear()
|
||||
|
||||
@GetMapping("/runs/{id}")
|
||||
fun run(@PathVariable id: Long) =
|
||||
complexPlanRunQueryService.getRun(id)
|
||||
|
||||
@GetMapping("/runs/{id}/modes")
|
||||
fun runModes(@PathVariable id: Long) =
|
||||
satelliteModeQueryService.findModesByRun(id)
|
||||
|
||||
@DeleteMapping("/runs/{id}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
fun deleteRun(@PathVariable id: Long) {
|
||||
complexPlanRunPersistenceService.deleteRun(id)
|
||||
}
|
||||
|
||||
@GetMapping("/runs")
|
||||
fun runs(
|
||||
@RequestParam(required = false) status: ComplexPlanRunStatus?,
|
||||
@RequestParam(required = false) createdFrom: LocalDateTime?,
|
||||
@RequestParam(required = false) createdTo: LocalDateTime?,
|
||||
@RequestParam(required = false) satelliteId: Long?,
|
||||
@RequestParam(required = false) intervalStart: LocalDateTime?,
|
||||
@RequestParam(required = false) intervalEnd: LocalDateTime?
|
||||
) = complexPlanRunQueryService.findRuns(
|
||||
ComplexPlanRunFilterDTO(
|
||||
status = status,
|
||||
createdFrom = createdFrom,
|
||||
createdTo = createdTo,
|
||||
satelliteId = satelliteId,
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd
|
||||
)
|
||||
)
|
||||
|
||||
@GetMapping("/cell-with-mars/{cell_number}")
|
||||
fun paintCellWithMars(@PathVariable("cell_number") cellNumber: Long) =
|
||||
satelliteModeQueryService.buildCellPaintGeometry(cellNumber)
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.controller
|
||||
|
||||
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_satellites_service.configuration.CustomErrorException
|
||||
import space.nstart.pcp.pcp_satellites_service.configuration.CustomValidationException
|
||||
import space.nstart.pcp.pcp_satellites_service.service.ComplexMissionService
|
||||
import space.nstart.pcp.pcp_satellites_service.service.ComplexPlanQueryService
|
||||
import space.nstart.pcp.pcp_satellites_service.service.SatelliteService
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.EarthCellCalculationRequestDTO
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/satellites")
|
||||
class SatelliteController {
|
||||
|
||||
@Autowired
|
||||
private lateinit var complexMissionService: ComplexMissionService
|
||||
|
||||
@Autowired
|
||||
private lateinit var satelliteService: SatelliteService
|
||||
|
||||
@Autowired
|
||||
private lateinit var complexPlanQueryService: ComplexPlanQueryService
|
||||
|
||||
@GetMapping
|
||||
fun all() = satelliteService.all()
|
||||
|
||||
@GetMapping("/{satellite_id}")
|
||||
fun byId(@PathVariable("satellite_id") id: Long) = satelliteService.byId(id)
|
||||
|
||||
@GetMapping("/{satellite_id}/mission")
|
||||
fun planById(@PathVariable("satellite_id") id: Long) =
|
||||
complexPlanQueryService.findMissionModes(requireSatelliteId(id))
|
||||
|
||||
@GetMapping("/{satellite_id}/mission/statistics")
|
||||
fun missionStatistics(@PathVariable("satellite_id") id: Long) =
|
||||
complexPlanQueryService.missionStatistics(requireSatelliteId(id))
|
||||
|
||||
@GetMapping("/{satellite_id}/mission-for-paint")
|
||||
fun planByIdForPaint(@PathVariable("satellite_id") id: Long) =
|
||||
complexPlanQueryService.buildPaintGeometry(requireSatelliteId(id))
|
||||
|
||||
@GetMapping("/{satellite_id}/modes")
|
||||
fun modesBySatelliteAndInterval(
|
||||
@PathVariable("satellite_id") id: Long,
|
||||
@RequestParam("intervalStart") intervalStart: LocalDateTime,
|
||||
@RequestParam("intervalEnd") intervalEnd: LocalDateTime,
|
||||
@RequestParam("runId", required = false) runId: Long?
|
||||
) = complexPlanQueryService.findModesByInterval(
|
||||
listOf(requireSatelliteId(id)),
|
||||
intervalStart,
|
||||
intervalEnd,
|
||||
runId
|
||||
)
|
||||
|
||||
@GetMapping("/{satellite_id}/snapshots")
|
||||
fun snapshotsBySatellite(
|
||||
@PathVariable("satellite_id") id: Long,
|
||||
@RequestParam("intervalStart", required = false) intervalStart: LocalDateTime?,
|
||||
@RequestParam("intervalEnd", required = false) intervalEnd: LocalDateTime?,
|
||||
@RequestParam("activeOnly", defaultValue = "false") activeOnly: Boolean
|
||||
) = complexPlanQueryService.findSnapshots(
|
||||
satelliteId = requireSatelliteId(id),
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
activeOnly = activeOnly
|
||||
)
|
||||
|
||||
@GetMapping("/{satellite_id}/snapshots/active")
|
||||
fun activeSnapshotBySatellite(
|
||||
@PathVariable("satellite_id") id: Long,
|
||||
@RequestParam("intervalStart", required = false) intervalStart: LocalDateTime?,
|
||||
@RequestParam("intervalEnd", required = false) intervalEnd: LocalDateTime?
|
||||
) = complexPlanQueryService.findActiveSnapshot(
|
||||
satelliteId = requireSatelliteId(id),
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd
|
||||
)
|
||||
|
||||
@GetMapping("/snapshots")
|
||||
fun snapshots(
|
||||
@RequestParam("satelliteId", required = false) satelliteId: Long?,
|
||||
@RequestParam("intervalStart", required = false) intervalStart: LocalDateTime?,
|
||||
@RequestParam("intervalEnd", required = false) intervalEnd: LocalDateTime?,
|
||||
@RequestParam("activeOnly", defaultValue = "false") activeOnly: Boolean
|
||||
) = complexPlanQueryService.findSnapshots(
|
||||
satelliteId = satelliteId,
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
activeOnly = activeOnly
|
||||
)
|
||||
|
||||
@GetMapping("/snapshots/{snapshot_id}")
|
||||
fun snapshot(
|
||||
@PathVariable("snapshot_id") snapshotId: Long
|
||||
) = complexPlanQueryService.findSnapshot(snapshotId)
|
||||
|
||||
@GetMapping("/snapshots/{snapshot_id}/modes")
|
||||
fun modesBySnapshot(
|
||||
@PathVariable("snapshot_id") snapshotId: Long,
|
||||
@RequestParam("intervalStart", required = false) intervalStart: LocalDateTime?,
|
||||
@RequestParam("intervalEnd", required = false) intervalEnd: LocalDateTime?
|
||||
) = complexPlanQueryService.findModesBySnapshot(snapshotId, intervalStart, intervalEnd)
|
||||
|
||||
@GetMapping("/modes")
|
||||
fun modesByInterval(
|
||||
@RequestParam("intervalStart") intervalStart: LocalDateTime,
|
||||
@RequestParam("intervalEnd") intervalEnd: LocalDateTime,
|
||||
@RequestParam("satelliteId", required = false) satelliteId: Long?,
|
||||
@RequestParam("satelliteIds", required = false) satelliteIds: List<Long>?
|
||||
) = complexPlanQueryService.findModesByInterval(
|
||||
buildSatelliteFilter(satelliteId, satelliteIds),
|
||||
intervalStart,
|
||||
intervalEnd
|
||||
)
|
||||
|
||||
@PostMapping("prepare")
|
||||
fun prepare() = complexMissionService.clear()
|
||||
|
||||
private fun buildSatelliteFilter(satelliteId: Long?, satelliteIds: List<Long>?): List<Long> {
|
||||
val ids = buildList {
|
||||
satelliteId?.let { add(it) }
|
||||
satelliteIds?.let { addAll(it) }
|
||||
}.distinct()
|
||||
|
||||
if (ids.isEmpty()) {
|
||||
throw CustomValidationException("Необходимо задать satelliteId или satelliteIds")
|
||||
}
|
||||
|
||||
return ids
|
||||
}
|
||||
|
||||
private fun requireSatelliteId(id: Long): Long {
|
||||
satelliteService.byId(id) ?: throw CustomErrorException("Нет КА с заданным идентификаторм $id")
|
||||
return id
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.dto
|
||||
|
||||
import java.time.LocalDateTime
|
||||
|
||||
data class ComplexPlanRunCreateDTO(
|
||||
val intervalStart: LocalDateTime,
|
||||
val intervalEnd: LocalDateTime,
|
||||
val satelliteIds: List<Long>,
|
||||
val requestedBy: String?,
|
||||
val requestPayload: String?
|
||||
)
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.dto
|
||||
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.ComplexPlanRunStatus
|
||||
import java.time.LocalDateTime
|
||||
|
||||
data class ComplexPlanRunFilterDTO(
|
||||
val status: ComplexPlanRunStatus? = null,
|
||||
val createdFrom: LocalDateTime? = null,
|
||||
val createdTo: LocalDateTime? = null,
|
||||
val satelliteId: Long? = null,
|
||||
val intervalStart: LocalDateTime? = null,
|
||||
val intervalEnd: LocalDateTime? = null
|
||||
)
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.dto
|
||||
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.ComplexPlanRunStatus
|
||||
import java.time.LocalDateTime
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
data class ComplexPlanRunResponseDTO(
|
||||
val runId: Long,
|
||||
val status: ComplexPlanRunStatus,
|
||||
val intervalStart: LocalDateTime,
|
||||
val intervalEnd: LocalDateTime,
|
||||
val satelliteIds: List<Long>,
|
||||
val requestedBy: String?,
|
||||
val createdAt: OffsetDateTime,
|
||||
val startedAt: OffsetDateTime?,
|
||||
val finishedAt: OffsetDateTime?,
|
||||
val durationMs: Long?,
|
||||
val errorMessage: String?,
|
||||
val calculatedModesCount: Int,
|
||||
val bookedModesCount: Int,
|
||||
val complanModesCount: Int,
|
||||
val mixedModesCount: Int,
|
||||
val affectedSatellitesCount: Int,
|
||||
val processedCellsCount: Int,
|
||||
val bookedSlotLinksCount: Int,
|
||||
val snapshotIds: List<Long>,
|
||||
val requestPayload: String?,
|
||||
val totalRequestsArea: Double = 0.0,
|
||||
val coveredArea: Double = 0.0,
|
||||
val coveredAreaPercent: Double = 0.0
|
||||
)
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.dto
|
||||
|
||||
data class ComplexPlanRunStatisticsDTO(
|
||||
val calculatedModesCount: Int = 0,
|
||||
val bookedModesCount: Int = 0,
|
||||
val complanModesCount: Int = 0,
|
||||
val mixedModesCount: Int = 0,
|
||||
val affectedSatellitesCount: Int = 0,
|
||||
val processedCellsCount: Int = 0,
|
||||
val bookedSlotLinksCount: Int = 0,
|
||||
val totalRequestsArea: Double = 0.0,
|
||||
val coveredArea: Double = 0.0,
|
||||
val coveredAreaPercent: Double = 0.0
|
||||
)
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.dto
|
||||
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.ComplexPlanRunStatus
|
||||
import java.time.LocalDateTime
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
data class ComplexPlanRunSummaryDTO(
|
||||
val runId: Long,
|
||||
val status: ComplexPlanRunStatus,
|
||||
val intervalStart: LocalDateTime,
|
||||
val intervalEnd: LocalDateTime,
|
||||
val satelliteIds: List<Long>,
|
||||
val requestedBy: String?,
|
||||
val createdAt: OffsetDateTime,
|
||||
val startedAt: OffsetDateTime?,
|
||||
val finishedAt: OffsetDateTime?,
|
||||
val durationMs: Long?,
|
||||
val errorMessage: String?,
|
||||
val calculatedModesCount: Int,
|
||||
val bookedModesCount: Int,
|
||||
val complanModesCount: Int,
|
||||
val mixedModesCount: Int,
|
||||
val affectedSatellitesCount: Int,
|
||||
val processedCellsCount: Int,
|
||||
val bookedSlotLinksCount: Int,
|
||||
val snapshotIds: List<Long>,
|
||||
val totalRequestsArea: Double = 0.0,
|
||||
val coveredArea: Double = 0.0,
|
||||
val coveredAreaPercent: Double = 0.0
|
||||
)
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.entity
|
||||
|
||||
import jakarta.persistence.CascadeType
|
||||
import jakarta.persistence.Column
|
||||
import jakarta.persistence.Entity
|
||||
import jakarta.persistence.EnumType
|
||||
import jakarta.persistence.Enumerated
|
||||
import jakarta.persistence.FetchType
|
||||
import jakarta.persistence.GeneratedValue
|
||||
import jakarta.persistence.GenerationType
|
||||
import jakarta.persistence.Id
|
||||
import jakarta.persistence.Index
|
||||
import jakarta.persistence.OneToMany
|
||||
import jakarta.persistence.Table
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Entity
|
||||
@Table(
|
||||
name = "complex_plan_run",
|
||||
indexes = [
|
||||
Index(name = "idx_complex_plan_run_status", columnList = "status"),
|
||||
Index(name = "idx_complex_plan_run_created_at", columnList = "created_at"),
|
||||
Index(name = "idx_complex_plan_run_interval_start", columnList = "interval_start"),
|
||||
Index(name = "idx_complex_plan_run_interval_end", columnList = "interval_end")
|
||||
]
|
||||
)
|
||||
class ComplexPlanRunEntity(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var id: Long? = null,
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "status", nullable = false, length = 20)
|
||||
var status: ComplexPlanRunStatus = ComplexPlanRunStatus.CREATED,
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
var createdAt: LocalDateTime = LocalDateTime.now(),
|
||||
|
||||
@Column(name = "started_at")
|
||||
var startedAt: LocalDateTime? = null,
|
||||
|
||||
@Column(name = "finished_at")
|
||||
var finishedAt: LocalDateTime? = null,
|
||||
|
||||
@Column(name = "duration_ms")
|
||||
var durationMs: Long? = null,
|
||||
|
||||
@Column(name = "interval_start", nullable = false)
|
||||
var intervalStart: LocalDateTime = LocalDateTime.now(),
|
||||
|
||||
@Column(name = "interval_end", nullable = false)
|
||||
var intervalEnd: LocalDateTime = LocalDateTime.now(),
|
||||
|
||||
@Column(name = "requested_by")
|
||||
var requestedBy: String? = null,
|
||||
|
||||
@Column(name = "error_message", columnDefinition = "TEXT")
|
||||
var errorMessage: String? = null,
|
||||
|
||||
@Column(name = "calculated_modes_count", nullable = false)
|
||||
var calculatedModesCount: Int = 0,
|
||||
|
||||
@Column(name = "booked_modes_count", nullable = false)
|
||||
var bookedModesCount: Int = 0,
|
||||
|
||||
@Column(name = "complan_modes_count", nullable = false)
|
||||
var complanModesCount: Int = 0,
|
||||
|
||||
@Column(name = "mixed_modes_count", nullable = false)
|
||||
var mixedModesCount: Int = 0,
|
||||
|
||||
@Column(name = "affected_satellites_count", nullable = false)
|
||||
var affectedSatellitesCount: Int = 0,
|
||||
|
||||
@Column(name = "processed_cells_count", nullable = false)
|
||||
var processedCellsCount: Int = 0,
|
||||
|
||||
@Column(name = "booked_slot_links_count", nullable = false)
|
||||
var bookedSlotLinksCount: Int = 0,
|
||||
|
||||
@Column(name = "total_requests_area", nullable = false)
|
||||
var totalRequestsArea: Double = 0.0,
|
||||
|
||||
@Column(name = "covered_area", nullable = false)
|
||||
var coveredArea: Double = 0.0,
|
||||
|
||||
@Column(name = "covered_area_percent", nullable = false)
|
||||
var coveredAreaPercent: Double = 0.0,
|
||||
|
||||
@Column(name = "request_payload", columnDefinition = "TEXT")
|
||||
var requestPayload: String? = null,
|
||||
|
||||
@OneToMany(
|
||||
mappedBy = "run",
|
||||
cascade = [CascadeType.ALL],
|
||||
orphanRemoval = true,
|
||||
fetch = FetchType.LAZY
|
||||
)
|
||||
var satellites: MutableList<ComplexPlanRunSatelliteEntity> = mutableListOf(),
|
||||
|
||||
@OneToMany(
|
||||
mappedBy = "complexPlanRun",
|
||||
fetch = FetchType.LAZY
|
||||
)
|
||||
var snapshots: MutableList<SatelliteModeSnapshotEntity> = mutableListOf()
|
||||
)
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.entity
|
||||
|
||||
import jakarta.persistence.Column
|
||||
import jakarta.persistence.Entity
|
||||
import jakarta.persistence.FetchType
|
||||
import jakarta.persistence.GeneratedValue
|
||||
import jakarta.persistence.GenerationType
|
||||
import jakarta.persistence.Id
|
||||
import jakarta.persistence.Index
|
||||
import jakarta.persistence.JoinColumn
|
||||
import jakarta.persistence.ManyToOne
|
||||
import jakarta.persistence.Table
|
||||
import jakarta.persistence.UniqueConstraint
|
||||
|
||||
@Entity
|
||||
@Table(
|
||||
name = "complex_plan_run_satellite",
|
||||
uniqueConstraints = [
|
||||
UniqueConstraint(
|
||||
name = "uq_complex_plan_run_satellite_run_satellite",
|
||||
columnNames = ["run_id", "satellite_id"]
|
||||
)
|
||||
],
|
||||
indexes = [
|
||||
Index(name = "idx_complex_plan_run_satellite_run_id", columnList = "run_id"),
|
||||
Index(name = "idx_complex_plan_run_satellite_satellite_id", columnList = "satellite_id")
|
||||
]
|
||||
)
|
||||
class ComplexPlanRunSatelliteEntity(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var id: Long? = null,
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "run_id", nullable = false)
|
||||
var run: ComplexPlanRunEntity? = null,
|
||||
|
||||
@Column(name = "satellite_id", nullable = false)
|
||||
var satelliteId: Long = 0
|
||||
)
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.entity
|
||||
|
||||
enum class ComplexPlanRunStatus {
|
||||
CREATED,
|
||||
RUNNING,
|
||||
COMPLETED,
|
||||
FAILED
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.entity
|
||||
|
||||
import jakarta.persistence.Column
|
||||
import jakarta.persistence.Entity
|
||||
import jakarta.persistence.FetchType
|
||||
import jakarta.persistence.GeneratedValue
|
||||
import jakarta.persistence.GenerationType
|
||||
import jakarta.persistence.Id
|
||||
import jakarta.persistence.JoinColumn
|
||||
import jakarta.persistence.ManyToOne
|
||||
import jakarta.persistence.Table
|
||||
import jakarta.persistence.UniqueConstraint
|
||||
import jakarta.persistence.Index
|
||||
|
||||
@Entity
|
||||
@Table(
|
||||
name = "satellite_mode_booked_slot",
|
||||
uniqueConstraints = [
|
||||
UniqueConstraint(
|
||||
name = "uq_satellite_mode_booked_slot_mode_booked_slot",
|
||||
columnNames = ["mode_id", "booked_slot_id"]
|
||||
)
|
||||
],
|
||||
indexes = [
|
||||
Index(name = "idx_satellite_mode_booked_slot_mode_id", columnList = "mode_id"),
|
||||
Index(name = "idx_satellite_mode_booked_slot_booked_slot_id", columnList = "booked_slot_id")
|
||||
]
|
||||
)
|
||||
class SatelliteModeBookedSlotEntity(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var id: Long? = null,
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "mode_id", nullable = false)
|
||||
var mode: SatelliteModeEntity? = null,
|
||||
|
||||
@Column(name = "booked_slot_id", nullable = false)
|
||||
var bookedSlotId: Long = 0
|
||||
)
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.entity
|
||||
|
||||
import jakarta.persistence.Column
|
||||
import jakarta.persistence.Entity
|
||||
import jakarta.persistence.FetchType
|
||||
import jakarta.persistence.GeneratedValue
|
||||
import jakarta.persistence.GenerationType
|
||||
import jakarta.persistence.Id
|
||||
import jakarta.persistence.JoinColumn
|
||||
import jakarta.persistence.ManyToOne
|
||||
import jakarta.persistence.Table
|
||||
import jakarta.persistence.UniqueConstraint
|
||||
import jakarta.persistence.Index
|
||||
|
||||
@Entity
|
||||
@Table(
|
||||
name = "satellite_mode_cell",
|
||||
uniqueConstraints = [
|
||||
UniqueConstraint(
|
||||
name = "uq_satellite_mode_cell_mode_cell",
|
||||
columnNames = ["mode_id", "cell_num"]
|
||||
)
|
||||
],
|
||||
indexes = [
|
||||
Index(name = "idx_satellite_mode_cell_mode_id", columnList = "mode_id"),
|
||||
Index(name = "idx_satellite_mode_cell_cell_num", columnList = "cell_num")
|
||||
]
|
||||
)
|
||||
class SatelliteModeCellEntity(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var id: Long? = null,
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "mode_id", nullable = false)
|
||||
var mode: SatelliteModeEntity? = null,
|
||||
|
||||
@Column(name = "cell_num", nullable = false)
|
||||
var cellNum: Long = 0
|
||||
)
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.entity
|
||||
|
||||
import jakarta.persistence.CascadeType
|
||||
import jakarta.persistence.Column
|
||||
import jakarta.persistence.Entity
|
||||
import jakarta.persistence.EnumType
|
||||
import jakarta.persistence.Enumerated
|
||||
import jakarta.persistence.FetchType
|
||||
import jakarta.persistence.GeneratedValue
|
||||
import jakarta.persistence.GenerationType
|
||||
import jakarta.persistence.Id
|
||||
import jakarta.persistence.OneToMany
|
||||
import jakarta.persistence.JoinColumn
|
||||
import jakarta.persistence.ManyToOne
|
||||
import jakarta.persistence.Table
|
||||
import jakarta.persistence.Index
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Entity
|
||||
@Table(
|
||||
name = "satellite_modes",
|
||||
indexes = [
|
||||
Index(name = "idx_satellite_modes_satellite_id", columnList = "satellite_id"),
|
||||
Index(name = "idx_satellite_modes_start_time", columnList = "start_time"),
|
||||
Index(name = "idx_satellite_modes_end_time", columnList = "end_time"),
|
||||
Index(name = "idx_satellite_modes_source", columnList = "source")
|
||||
]
|
||||
)
|
||||
class SatelliteModeEntity(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var id: Long? = null,
|
||||
|
||||
@Column(name = "satellite_id", nullable = false)
|
||||
var satelliteId: Long = 0,
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "source", nullable = false, length = 20)
|
||||
var source: SatelliteModeSource = SatelliteModeSource.COMPLAN,
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "type", nullable = false, length = 20)
|
||||
var type: SatelliteModeType = SatelliteModeType.SURVEY,
|
||||
|
||||
@Column(name = "start_time", nullable = false)
|
||||
var startTime: LocalDateTime = LocalDateTime.now(),
|
||||
|
||||
@Column(name = "end_time", nullable = false)
|
||||
var endTime: LocalDateTime = LocalDateTime.now(),
|
||||
|
||||
@Column(name = "revolution", nullable = false)
|
||||
var revolution: Long = 0,
|
||||
|
||||
@Column(name = "lat", nullable = false)
|
||||
var lat: Double = 0.0,
|
||||
|
||||
@Column(name = "longitude", nullable = false)
|
||||
var longitude: Double = 0.0,
|
||||
|
||||
@Column(name = "duration", nullable = false)
|
||||
var duration: Double = 0.0,
|
||||
|
||||
@Column(name = "contour_wkt", columnDefinition = "TEXT")
|
||||
var contourWkt: String? = null,
|
||||
|
||||
@Column(name = "roll", nullable = false)
|
||||
var roll: Double = 0.0,
|
||||
|
||||
@Column(name = "cell_num", nullable = false)
|
||||
var cellNum: Long = -1,
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "snapshot_id", nullable = false)
|
||||
var snapshot: SatelliteModeSnapshotEntity? = null,
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "complex_plan_run_id")
|
||||
var complexPlanRun: ComplexPlanRunEntity? = null,
|
||||
|
||||
@OneToMany(
|
||||
mappedBy = "mode",
|
||||
cascade = [CascadeType.ALL],
|
||||
orphanRemoval = true,
|
||||
fetch = FetchType.LAZY
|
||||
)
|
||||
var bookedSlots: MutableList<SatelliteModeBookedSlotEntity> = mutableListOf(),
|
||||
|
||||
@OneToMany(
|
||||
mappedBy = "mode",
|
||||
cascade = [CascadeType.ALL],
|
||||
orphanRemoval = true,
|
||||
fetch = FetchType.LAZY
|
||||
)
|
||||
var cells: MutableList<SatelliteModeCellEntity> = mutableListOf()
|
||||
)
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.entity
|
||||
|
||||
import jakarta.persistence.CascadeType
|
||||
import jakarta.persistence.Column
|
||||
import jakarta.persistence.Entity
|
||||
import jakarta.persistence.FetchType
|
||||
import jakarta.persistence.GeneratedValue
|
||||
import jakarta.persistence.GenerationType
|
||||
import jakarta.persistence.Id
|
||||
import jakarta.persistence.Index
|
||||
import jakarta.persistence.JoinColumn
|
||||
import jakarta.persistence.ManyToOne
|
||||
import jakarta.persistence.OneToMany
|
||||
import jakarta.persistence.Table
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Entity
|
||||
@Table(
|
||||
name = "satellite_mode_snapshots",
|
||||
indexes = [
|
||||
Index(name = "idx_satellite_mode_snapshots_satellite_id", columnList = "satellite_id"),
|
||||
Index(name = "idx_satellite_mode_snapshots_interval_start", columnList = "interval_start"),
|
||||
Index(name = "idx_satellite_mode_snapshots_interval_end", columnList = "interval_end"),
|
||||
Index(name = "idx_satellite_mode_snapshots_is_active", columnList = "is_active"),
|
||||
Index(name = "idx_satellite_mode_snapshots_run_id", columnList = "complex_plan_run_id")
|
||||
]
|
||||
)
|
||||
class SatelliteModeSnapshotEntity(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var id: Long? = null,
|
||||
|
||||
@Column(name = "satellite_id", nullable = false)
|
||||
var satelliteId: Long = 0,
|
||||
|
||||
@Column(name = "interval_start", nullable = false)
|
||||
var intervalStart: LocalDateTime = LocalDateTime.now(),
|
||||
|
||||
@Column(name = "interval_end", nullable = false)
|
||||
var intervalEnd: LocalDateTime = LocalDateTime.now(),
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
var createdAt: LocalDateTime = LocalDateTime.now(),
|
||||
|
||||
@Column(name = "is_active", nullable = false)
|
||||
var isActive: Boolean = false,
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "complex_plan_run_id")
|
||||
var complexPlanRun: ComplexPlanRunEntity? = null,
|
||||
|
||||
@Column(name = "comment", columnDefinition = "TEXT")
|
||||
var comment: String? = null,
|
||||
|
||||
@OneToMany(
|
||||
mappedBy = "snapshot",
|
||||
cascade = [CascadeType.ALL],
|
||||
orphanRemoval = true,
|
||||
fetch = FetchType.LAZY
|
||||
)
|
||||
var modes: MutableList<SatelliteModeEntity> = mutableListOf()
|
||||
)
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.entity
|
||||
|
||||
enum class SatelliteModeSource {
|
||||
SLOTS,
|
||||
COMPLAN,
|
||||
MIXED,
|
||||
MANUAL
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.entity
|
||||
|
||||
enum class SatelliteModeType {
|
||||
SURVEY
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.model
|
||||
|
||||
class BLS(
|
||||
val captureAngle : Double = 1.5,
|
||||
val sunAngleMin : Double = -90.0,
|
||||
val durationMin : Double = 10.0,
|
||||
val durationMax : Double = 300.0,
|
||||
val mmi : Double = 10.0,
|
||||
val dailyMaxDuration : Double = 35.0 * 60,
|
||||
val revolutionMaxDuration : Double = 5.0 * 60
|
||||
){
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.model
|
||||
|
||||
import org.locationtech.jts.index.bintree.Interval
|
||||
import space.nstart.pcp.pcp_satellites_service.model.mode.SurveyMode
|
||||
import java.time.LocalDateTime
|
||||
|
||||
class LongTermMission(
|
||||
val surveys : MutableList<SurveyMode> = mutableListOf()
|
||||
) {
|
||||
|
||||
fun hasResources(tStart : LocalDateTime, tStop : LocalDateTime, bls: BLS) : Boolean{
|
||||
|
||||
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.model
|
||||
|
||||
import ballistics.flightLine.PointOnEarthCalculator
|
||||
import ballistics.orbitalPoints.timeStepper.RungeStepper
|
||||
import ballistics.types.EarthType
|
||||
import ballistics.types.ModDVType
|
||||
import ballistics.types.OrbitalPoint
|
||||
import ballistics.types.Orientation
|
||||
import ballistics.types.THBLPoint
|
||||
import ballistics.types.WorkCSType
|
||||
import org.locationtech.jts.geom.Coordinate
|
||||
import org.locationtech.jts.geom.GeometryFactory
|
||||
import org.locationtech.jts.io.WKTWriter
|
||||
import org.springframework.stereotype.Component
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteInfoDTO
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.div
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.PI
|
||||
import kotlin.times
|
||||
|
||||
|
||||
@Component
|
||||
class SatelliteModel(
|
||||
val satelliteId : Long = 0,
|
||||
val name : String = "",
|
||||
val red : Short = 255,
|
||||
val green : Short = 0,
|
||||
val blue : Short = 0,
|
||||
val longMission : LongTermMission = LongTermMission(),
|
||||
val bls: BLS = BLS(),
|
||||
val scanTLE : Boolean = false
|
||||
|
||||
) {
|
||||
|
||||
fun toDTO() = SatelliteInfoDTO(
|
||||
satelliteId,
|
||||
name,
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
bls.captureAngle,
|
||||
scanTLE,
|
||||
bls.durationMax.toLong(),
|
||||
ceil(bls.mmi).toLong()
|
||||
)
|
||||
|
||||
|
||||
fun contourByTime(
|
||||
tn : Double,
|
||||
roll: Double,
|
||||
capture : Double,
|
||||
points : List<OrbitalPoint>
|
||||
) : String {
|
||||
|
||||
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
|
||||
fun contour(
|
||||
tn : Double,
|
||||
tk : Double,
|
||||
roll: Double,
|
||||
capture : Double,
|
||||
points : List<OrbitalPoint>) : String{
|
||||
//val vp = points.filter { p -> p.t>=tn && p.t <= tk }
|
||||
val stepper: RungeStepper = RungeStepper(points.toMutableList(), EarthType.PZ90d02)
|
||||
var t = tn
|
||||
val vp = mutableListOf<OrbitalPoint>()
|
||||
while(t <= tk) {
|
||||
stepper.calculate(t)?.let { vp.add(it) }
|
||||
|
||||
t += 1.0
|
||||
}
|
||||
val r = vp.map {
|
||||
viewParams(it, roll + capture, 0.0) ?: THBLPoint(
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
)
|
||||
}
|
||||
val l = vp.map {
|
||||
viewParams(it, roll - capture, 0.0) ?: THBLPoint(
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
)
|
||||
}
|
||||
val geom = mutableListOf<Coordinate>()
|
||||
for (rp in r)
|
||||
geom.add(Coordinate(rp.long * 180 / PI, rp.lat * 180 / PI, 0.0))
|
||||
for (lp in l.reversed())
|
||||
geom.add(Coordinate(lp.long * 180 / PI, lp.lat * 180 / PI, 0.0))
|
||||
if (!geom.isEmpty())
|
||||
geom.add(Coordinate(geom.first().x, geom.first().y))
|
||||
val geometryFactory = GeometryFactory()
|
||||
val shell = geometryFactory.createLinearRing(geom.toTypedArray())
|
||||
val polygon = geometryFactory.createPolygon(shell)
|
||||
val wktWriter = WKTWriter()
|
||||
return wktWriter.write(polygon)
|
||||
}
|
||||
|
||||
fun viewParams(pos : OrbitalPoint, gamma : Double, tang : Double) : THBLPoint?{
|
||||
val calculator = PointOnEarthCalculator(EarthType.PZ90d02, WorkCSType.WCSOrbit)
|
||||
return calculator.pointOnEarth(pos, Orientation(tang * PI / 180.0, gamma * PI / 180.0, 0.0))
|
||||
}
|
||||
|
||||
fun hasResources(dat : LocalDate) : Boolean{
|
||||
if (longMission.surveys
|
||||
.filter { it.time.toLocalDate() == dat }
|
||||
.sumOf { view -> view.duration } >= bls.dailyMaxDuration - bls.durationMin
|
||||
)
|
||||
return false
|
||||
return true
|
||||
}
|
||||
|
||||
fun canBeUsed(view : SquareViewParamDTO) : Boolean{
|
||||
|
||||
if (longMission.surveys
|
||||
.filter { it.time.toLocalDate() == view.timeBegin.toLocalDate() }
|
||||
.sumOf { view -> view.duration } > bls.dailyMaxDuration
|
||||
)
|
||||
return false
|
||||
if (longMission.surveys
|
||||
.filter { it.revolution == view.revolutionBegin }
|
||||
.sumOf { view -> view.duration } > bls.revolutionMaxDuration
|
||||
)
|
||||
return false
|
||||
// val intersections = longMission.surveys
|
||||
// .filter { surv ->
|
||||
// val tn1 = surv.time.plusSeconds(surv.duration.toLong() + bls.mmi.toLong())
|
||||
// val tk1 = surv.time.minusSeconds(bls.mmi.toLong())
|
||||
// val tn2 = view.timeBegin
|
||||
// val tk2 = view.timeEnd
|
||||
//
|
||||
// val intersects = (tk1 <= tn2 && tk2 <= tn1)
|
||||
// intersects
|
||||
// }
|
||||
// return intersections.isEmpty()
|
||||
return true
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.model.mode
|
||||
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeSource
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.modes.SurveyModeInfoDTO
|
||||
import java.time.LocalDateTime
|
||||
|
||||
class SurveyMode(
|
||||
val revolution : Long = 1,
|
||||
val time : LocalDateTime = LocalDateTime.now(),
|
||||
val timeStop : LocalDateTime = LocalDateTime.now(),
|
||||
val roll : Double = 0.0,
|
||||
val latitude : Double = 0.0,
|
||||
val longitude : Double = 0.0,
|
||||
val duration : Double = 0.0,
|
||||
val contourWKT : String = "",
|
||||
val cellNum : Long = 0,
|
||||
val source: SatelliteModeSource = SatelliteModeSource.COMPLAN,
|
||||
val bookedSlotIds: List<Long> = emptyList(),
|
||||
val cellNums: List<Long> = emptyList()
|
||||
) {
|
||||
fun toDTO() = SurveyModeInfoDTO(
|
||||
revolution = revolution,
|
||||
time = time,
|
||||
timStop = timeStop,
|
||||
roll = roll,
|
||||
duration = duration,
|
||||
contourWKT = contourWKT,
|
||||
slotIds = bookedSlotIds,
|
||||
latitude = latitude,
|
||||
longitude = longitude
|
||||
)
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.repository
|
||||
|
||||
import org.springframework.data.jpa.repository.EntityGraph
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
import org.springframework.data.repository.query.Param
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.ComplexPlanRunEntity
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.ComplexPlanRunStatus
|
||||
import java.time.LocalDateTime
|
||||
|
||||
interface ComplexPlanRunRepository : JpaRepository<ComplexPlanRunEntity, Long>, JpaSpecificationExecutor<ComplexPlanRunEntity> {
|
||||
|
||||
@EntityGraph(attributePaths = ["satellites"])
|
||||
@Query("select run from ComplexPlanRunEntity run where run.id = :id")
|
||||
fun findDetailedById(@Param("id") id: Long): ComplexPlanRunEntity?
|
||||
|
||||
@EntityGraph(attributePaths = ["satellites"])
|
||||
fun findAllByStatusIn(statuses: Collection<ComplexPlanRunStatus>): List<ComplexPlanRunEntity>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
select distinct run.id
|
||||
from ComplexPlanRunEntity run
|
||||
join run.satellites satellite
|
||||
where run.status = :status
|
||||
and run.id <> :excludedRunId
|
||||
and satellite.satelliteId in :satelliteIds
|
||||
and run.intervalStart < :intervalEnd
|
||||
and :intervalStart < run.intervalEnd
|
||||
order by run.id
|
||||
"""
|
||||
)
|
||||
fun findOverlappingRunIdsByStatusAndSatelliteIds(
|
||||
@Param("status") status: ComplexPlanRunStatus,
|
||||
@Param("satelliteIds") satelliteIds: Collection<Long>,
|
||||
@Param("intervalStart") intervalStart: LocalDateTime,
|
||||
@Param("intervalEnd") intervalEnd: LocalDateTime,
|
||||
@Param("excludedRunId") excludedRunId: Long
|
||||
): List<Long>
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.repository
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.ComplexPlanRunSatelliteEntity
|
||||
|
||||
interface ComplexPlanRunSatelliteRepository : JpaRepository<ComplexPlanRunSatelliteEntity, Long> {
|
||||
|
||||
fun countBySatelliteId(satelliteId: Long): Long
|
||||
|
||||
fun deleteAllBySatelliteId(satelliteId: Long): Long
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.repository
|
||||
|
||||
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_satellites_service.entity.SatelliteModeBookedSlotEntity
|
||||
|
||||
interface SatelliteModeBookedSlotRepository : JpaRepository<SatelliteModeBookedSlotEntity, Long> {
|
||||
fun findAllByMode_IdIn(modeIds: Collection<Long>): List<SatelliteModeBookedSlotEntity>
|
||||
|
||||
@Modifying
|
||||
@Query("delete from SatelliteModeBookedSlotEntity entity where entity.mode.id in :modeIds")
|
||||
fun deleteAllByModeIds(@Param("modeIds") modeIds: Collection<Long>): Int
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.repository
|
||||
|
||||
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_satellites_service.entity.SatelliteModeCellEntity
|
||||
|
||||
interface SatelliteModeCellRepository : JpaRepository<SatelliteModeCellEntity, Long> {
|
||||
fun findAllByMode_IdIn(modeIds: Collection<Long>): List<SatelliteModeCellEntity>
|
||||
|
||||
@Modifying
|
||||
@Query("delete from SatelliteModeCellEntity entity where entity.mode.id in :modeIds")
|
||||
fun deleteAllByModeIds(@Param("modeIds") modeIds: Collection<Long>): Int
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package space.nstart.pcp.pcp_satellites_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_satellites_service.entity.SatelliteModeEntity
|
||||
import java.time.LocalDateTime
|
||||
|
||||
interface SatelliteModeRepository : JpaRepository<SatelliteModeEntity, Long> {
|
||||
|
||||
fun findAllBySnapshot_IdInOrderByStartTimeAsc(
|
||||
snapshotIds: Collection<Long>
|
||||
): List<SatelliteModeEntity>
|
||||
|
||||
fun findAllBySnapshot_IdOrderByStartTimeAsc(snapshotId: Long): List<SatelliteModeEntity>
|
||||
|
||||
fun findAllBySnapshot_SatelliteIdInAndSnapshot_IsActiveTrueAndEndTimeGreaterThanAndStartTimeLessThanOrderByStartTimeAsc(
|
||||
satelliteIds: Collection<Long>,
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime
|
||||
): List<SatelliteModeEntity>
|
||||
|
||||
fun findAllBySnapshot_SatelliteIdInAndSnapshot_IsActiveTrueAndRevolutionInOrderByStartTimeAsc(
|
||||
satelliteIds: Collection<Long>,
|
||||
revolutions: Collection<Long>
|
||||
): List<SatelliteModeEntity>
|
||||
|
||||
fun findAllBySnapshot_SatelliteIdAndSnapshot_IsActiveTrueAndRevolutionInOrderByStartTimeAsc(
|
||||
satelliteId: Long,
|
||||
revolutions: Collection<Long>
|
||||
): List<SatelliteModeEntity>
|
||||
|
||||
fun findAllBySnapshot_SatelliteIdAndSnapshot_IsActiveTrueOrderByStartTimeAsc(satelliteId: Long): List<SatelliteModeEntity>
|
||||
|
||||
fun findAllByCellNumOrderByStartTimeAsc(cellNum: Long): List<SatelliteModeEntity>
|
||||
|
||||
fun findAllByComplexPlanRun_IdOrderByStartTimeAsc(runId: Long): List<SatelliteModeEntity>
|
||||
|
||||
fun countByComplexPlanRun_Id(runId: Long): Long
|
||||
|
||||
fun countBySatelliteId(satelliteId: Long): Long
|
||||
|
||||
@Query(
|
||||
"""
|
||||
select entity.id
|
||||
from SatelliteModeEntity entity
|
||||
where entity.satelliteId in :satelliteIds
|
||||
and entity.startTime < :intervalEnd
|
||||
and entity.endTime > :intervalStart
|
||||
"""
|
||||
)
|
||||
fun findIntersectingIdsBySatelliteIdsAndInterval(
|
||||
@Param("satelliteIds") satelliteIds: Collection<Long>,
|
||||
@Param("intervalStart") intervalStart: LocalDateTime,
|
||||
@Param("intervalEnd") intervalEnd: LocalDateTime
|
||||
): List<Long>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
select distinct entity
|
||||
from SatelliteModeEntity entity
|
||||
join entity.cells cell
|
||||
where cell.cellNum = :cellNum
|
||||
and entity.snapshot.isActive = true
|
||||
order by entity.startTime asc
|
||||
"""
|
||||
)
|
||||
fun findAllByRelatedCellNumOrderByStartTimeAsc(@Param("cellNum") cellNum: Long): List<SatelliteModeEntity>
|
||||
|
||||
@Modifying
|
||||
@Transactional
|
||||
@Query(
|
||||
"""
|
||||
DELETE FROM satellite_modes
|
||||
WHERE satellite_id IN (:satelliteIds)
|
||||
AND start_time < :intervalEnd
|
||||
AND end_time > :intervalStart
|
||||
""",
|
||||
nativeQuery = true
|
||||
)
|
||||
fun deleteIntersectingBySatelliteIdsAndInterval(
|
||||
@Param("satelliteIds") satelliteIds: Collection<Long>,
|
||||
@Param("intervalStart") intervalStart: LocalDateTime,
|
||||
@Param("intervalEnd") intervalEnd: LocalDateTime
|
||||
): Int
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.repository
|
||||
|
||||
import jakarta.transaction.Transactional
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor
|
||||
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_satellites_service.entity.SatelliteModeSnapshotEntity
|
||||
import java.time.LocalDateTime
|
||||
|
||||
interface SatelliteModeSnapshotRepository :
|
||||
JpaRepository<SatelliteModeSnapshotEntity, Long>,
|
||||
JpaSpecificationExecutor<SatelliteModeSnapshotEntity> {
|
||||
|
||||
fun findAllByComplexPlanRun_IdOrderByIdAsc(runId: Long): List<SatelliteModeSnapshotEntity>
|
||||
|
||||
fun findAllByComplexPlanRun_IdAndSatelliteIdInOrderBySatelliteIdAscCreatedAtDesc(
|
||||
runId: Long,
|
||||
satelliteIds: Collection<Long>
|
||||
): List<SatelliteModeSnapshotEntity>
|
||||
|
||||
fun findAllBySatelliteIdInAndIsActiveTrueOrderBySatelliteIdAscCreatedAtDesc(
|
||||
satelliteIds: Collection<Long>
|
||||
): List<SatelliteModeSnapshotEntity>
|
||||
|
||||
fun findFirstBySatelliteIdAndIsActiveTrueOrderByCreatedAtDescIdDesc(satelliteId: Long): SatelliteModeSnapshotEntity?
|
||||
|
||||
fun countBySatelliteId(satelliteId: Long): Long
|
||||
|
||||
fun deleteAllBySatelliteId(satelliteId: Long): Long
|
||||
|
||||
@Modifying
|
||||
@Transactional
|
||||
@Query(
|
||||
"""
|
||||
update SatelliteModeSnapshotEntity snapshot
|
||||
set snapshot.isActive = false
|
||||
where snapshot.satelliteId = :satelliteId
|
||||
and snapshot.isActive = true
|
||||
"""
|
||||
)
|
||||
fun deactivateActiveSnapshots(@Param("satelliteId") satelliteId: Long): Int
|
||||
|
||||
@Query(
|
||||
"""
|
||||
select snapshot.id
|
||||
from SatelliteModeSnapshotEntity snapshot
|
||||
where snapshot.satelliteId in :satelliteIds
|
||||
and snapshot.intervalStart < :intervalEnd
|
||||
and snapshot.intervalEnd > :intervalStart
|
||||
"""
|
||||
)
|
||||
fun findIntersectingIdsBySatelliteIdsAndInterval(
|
||||
@Param("satelliteIds") satelliteIds: Collection<Long>,
|
||||
@Param("intervalStart") intervalStart: LocalDateTime,
|
||||
@Param("intervalEnd") intervalEnd: LocalDateTime
|
||||
): List<Long>
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.web.reactive.function.client.WebClient
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ExactTimePositionRequestDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ObjViewRequestDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamsWithPointsDTO
|
||||
|
||||
|
||||
@Service
|
||||
class BallisticsService(webClientBuilderProvider: ObjectProvider<WebClient.Builder>){
|
||||
private val webClientBuilder: WebClient.Builder = webClientBuilderProvider.ifAvailable ?: WebClient.builder()
|
||||
|
||||
@Value("\${settings.ballistics-service:ic-service}")
|
||||
val url = ""
|
||||
|
||||
private fun client() = webClientBuilder.baseUrl(url).build()
|
||||
|
||||
|
||||
fun mplSquare(req : ObjViewRequestDTO) =
|
||||
client()
|
||||
.post()
|
||||
.uri("/api/obj-view/mpl-square")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(req)
|
||||
.retrieve()
|
||||
.bodyToFlux(SquareViewParamDTO::class.java)
|
||||
.toIterable()
|
||||
|
||||
|
||||
|
||||
fun cellCovering(cell : Long) =
|
||||
client()
|
||||
.get()
|
||||
.uri("/api/obj-view/cell-coverage/{cell}", cell)
|
||||
.retrieve()
|
||||
.bodyToFlux(SquareViewParamsWithPointsDTO::class.java)
|
||||
.toIterable()
|
||||
|
||||
|
||||
fun exactTime(sat : Long, req : ExactTimePositionRequestDTO) =
|
||||
client()
|
||||
.post()
|
||||
.uri("/api/satellites/{satelliteId}/extract-time", sat)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(req)
|
||||
.retrieve()
|
||||
.bodyToFlux(OrbPointDTO::class.java)
|
||||
.toIterable()
|
||||
|
||||
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import ballistics.types.OrbitalPoint
|
||||
import ballistics.utils.fromDateTime
|
||||
import ballistics.utils.math.Vector3D
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.stereotype.Service
|
||||
import space.nstart.pcp.pcp_satellites_service.configuration.CustomErrorException
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeSource
|
||||
import space.nstart.pcp.pcp_satellites_service.model.mode.SurveyMode
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ExactTimePositionRequestDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ObjDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ObjViewRequestDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.CoverageResponseDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.CoverageResponseStatus
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.EarthCellCalculationRequestDTO
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.math.abs
|
||||
|
||||
|
||||
@Service
|
||||
class ComplexMissionService {
|
||||
|
||||
@Autowired
|
||||
private lateinit var satelliteService: SatelliteService
|
||||
@Autowired
|
||||
private lateinit var ballisticsService: BallisticsService
|
||||
private val logger : Logger = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
@Autowired
|
||||
private lateinit var complexPlanCalculationService: ComplexPlanCalculationService
|
||||
|
||||
fun clear() =
|
||||
complexPlanCalculationService.clear()
|
||||
|
||||
fun process(tn : LocalDateTime, tk : LocalDateTime, satsAvail : List<Long>?){
|
||||
complexPlanCalculationService.process(tn, tk, satsAvail)
|
||||
}
|
||||
}
|
||||
+616
@@ -0,0 +1,616 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import org.locationtech.jts.geom.Geometry
|
||||
import org.locationtech.jts.io.WKTReader
|
||||
import org.locationtech.jts.io.WKTWriter
|
||||
import org.locationtech.jts.operation.union.UnaryUnionOp
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import space.nstart.pcp.pcp_satellites_service.configuration.ComplexPlanProperties
|
||||
import space.nstart.pcp.pcp_satellites_service.configuration.CustomValidationException
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeSource
|
||||
import space.nstart.pcp.pcp_satellites_service.model.LongTermMission
|
||||
import space.nstart.pcp.pcp_satellites_service.model.SatelliteModel
|
||||
import space.nstart.pcp.pcp_satellites_service.model.mode.SurveyMode
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.CoverageCalculationSource
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.EarthCellWithRequestsDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.OccupiedIntervalDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageStrategy
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageTargetDTO
|
||||
import java.time.Duration
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
@Service
|
||||
class ComplexPlanCalculationService(
|
||||
private val coverageBatchClient: CoverageBatchClient,
|
||||
private val coverageCalculationClientResolver: CoverageCalculationClientResolver,
|
||||
private val earthGridService: EarthGridService,
|
||||
private val satelliteService: SatelliteService,
|
||||
private val complexPlanQueryService: ComplexPlanQueryService,
|
||||
private val complexPlanPersistenceService: ComplexPlanPersistenceService,
|
||||
private val complexPlanProperties: ComplexPlanProperties,
|
||||
private val surveyPlacementService: SurveyPlacementService
|
||||
) {
|
||||
|
||||
private val logger: Logger = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
fun process(
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime,
|
||||
satelliteIds: List<Long>?,
|
||||
sun: Double? = null,
|
||||
coverageSource: CoverageCalculationSource = CoverageCalculationSource.SLOTS,
|
||||
countLat: Int? = null,
|
||||
countLong: Int? = null,
|
||||
coverageStrategy: SlotCoverageStrategy? = null
|
||||
): ComplexPlanCalculationResult =
|
||||
process(
|
||||
runId = null,
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
satelliteIds = satelliteIds,
|
||||
sun = sun,
|
||||
coverageSource = coverageSource,
|
||||
countLat = countLat,
|
||||
countLong = countLong,
|
||||
coverageStrategy = coverageStrategy
|
||||
)
|
||||
|
||||
fun process(
|
||||
runId: Long?,
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime,
|
||||
satelliteIds: List<Long>?,
|
||||
sun: Double? = null,
|
||||
coverageSource: CoverageCalculationSource = CoverageCalculationSource.SLOTS,
|
||||
countLat: Int? = null,
|
||||
countLong: Int? = null,
|
||||
coverageStrategy: SlotCoverageStrategy? = null
|
||||
): ComplexPlanCalculationResult {
|
||||
val calculationStartedAtMs = System.currentTimeMillis()
|
||||
val selectedSatelliteIds = (satelliteIds ?: satelliteService.satellites.map { it.satelliteId }).distinct()
|
||||
if (selectedSatelliteIds.isEmpty()) {
|
||||
logger.info("Расчет комплексного плана пропущен: runId={}, не задано ни одного спутника", runId)
|
||||
return ComplexPlanCalculationResult()
|
||||
}
|
||||
lateinit var workingSatellites: List<SatelliteModel>
|
||||
val buildSatellitesMs = measureTimeMillis {
|
||||
workingSatellites = buildWorkingSatellites(selectedSatelliteIds)
|
||||
}
|
||||
logger.info(
|
||||
"Stage satellites-build: runId={}, requestedSatellites={}, workingSatellites={}, durationMs={}",
|
||||
runId,
|
||||
selectedSatelliteIds.size,
|
||||
workingSatellites.size,
|
||||
buildSatellitesMs
|
||||
)
|
||||
val coverageCalculationClient = coverageCalculationClientResolver.resolve(coverageSource)
|
||||
|
||||
logger.info(
|
||||
"Старт расчета комплексного плана: runId={}, satellites={}, interval=[{} - {}], chunkSize={}, sun={}, coverageSource={}, countLat={}, countLong={}, coverageStrategy={}",
|
||||
runId,
|
||||
selectedSatelliteIds.size,
|
||||
intervalStart,
|
||||
intervalEnd,
|
||||
complexPlanProperties.batchChunkSize,
|
||||
sun,
|
||||
coverageSource,
|
||||
countLat,
|
||||
countLong,
|
||||
coverageStrategy
|
||||
)
|
||||
logger.info("Шаг snapshot: runId={}, загрузка рабочего состояния вне пересчитываемого интервала", runId)
|
||||
val snapshotMs = measureTimeMillis {
|
||||
initializeWorkingModes(workingSatellites, intervalStart, intervalEnd)
|
||||
}
|
||||
logger.info(
|
||||
"Stage snapshot-load: runId={}, satellites={}, durationMs={}",
|
||||
runId,
|
||||
workingSatellites.size,
|
||||
snapshotMs
|
||||
)
|
||||
|
||||
try {
|
||||
var cells = emptyList<EarthCellWithRequestsDTO>()
|
||||
val cellsLoadMs = measureTimeMillis {
|
||||
cells = earthGridService.cells(countLat, countLong)
|
||||
.toList()
|
||||
.sortedWith(
|
||||
compareByDescending<EarthCellWithRequestsDTO> { it.importance }
|
||||
.thenByDescending { it.requests.count() }
|
||||
.thenBy { it.num }
|
||||
)
|
||||
}
|
||||
logger.info("Stage cells-load: runId={}, cells={}, durationMs={}", runId, cells.size, cellsLoadMs)
|
||||
val requestGeometry = buildRequestsGeometry(cells)
|
||||
val totalRequestsArea = requestGeometry?.area ?: 0.0
|
||||
logger.info(
|
||||
"Stage requests-area: runId={}, totalRequestsArea={}",
|
||||
runId,
|
||||
totalRequestsArea
|
||||
)
|
||||
|
||||
logger.info("Шаг batch: runId={}, загрузка booked-маршрутов из slots-service", runId)
|
||||
val plannedModesMs = measureTimeMillis {
|
||||
addPlannedModes(workingSatellites, intervalStart, intervalEnd)
|
||||
}
|
||||
logger.info(
|
||||
"Stage planned-modes-load: runId={}, satellites={}, durationMs={}",
|
||||
runId,
|
||||
workingSatellites.size,
|
||||
plannedModesMs
|
||||
)
|
||||
|
||||
val chunkSize = complexPlanProperties.batchChunkSize.coerceAtLeast(1)
|
||||
val chunkCount = (cells.size + chunkSize - 1) / chunkSize
|
||||
logger.info("Stage chunk-prepare: runId={}, chunkSize={}, chunks={}", runId, chunkSize, chunkCount)
|
||||
|
||||
var processedCells = 0
|
||||
var acceptedSlots = 0
|
||||
for ((chunkIndex, chunk) in cells.chunked(chunkSize).withIndex()) {
|
||||
if (!hasRemainingCapacity(workingSatellites, intervalStart, intervalEnd)) {
|
||||
logger.info(
|
||||
"Ранняя остановка перед chunk {}: runId={}, ресурс ОГ исчерпан, processedCells={}",
|
||||
chunkIndex + 1,
|
||||
runId,
|
||||
processedCells
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
val targetsChunk = buildTargetsChunk(chunk)
|
||||
var coverageByTargetId = emptyMap<Long, List<space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO>>()
|
||||
val occupiedIntervals = buildOccupiedIntervalsSnapshot(workingSatellites)
|
||||
val coverageFetchMs = measureTimeMillis {
|
||||
coverageByTargetId = coverageCalculationClient.coverageModes(
|
||||
targets = targetsChunk,
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
satelliteIds = selectedSatelliteIds,
|
||||
occupiedIntervals = occupiedIntervals,
|
||||
sun = sun,
|
||||
coverageStrategy = coverageStrategy
|
||||
)
|
||||
}
|
||||
logger.info(
|
||||
"Stage chunk-coverage-fetch: runId={}, chunk={}/{}, cells={}, targets={}, occupiedIntervals={}, returnedTargets={}, durationMs={}",
|
||||
runId,
|
||||
chunkIndex + 1,
|
||||
chunkCount,
|
||||
chunk.size,
|
||||
targetsChunk.size,
|
||||
occupiedIntervals.size,
|
||||
coverageByTargetId.size,
|
||||
coverageFetchMs
|
||||
)
|
||||
|
||||
var acceptedForChunk = 0
|
||||
val chunkProcessMs = measureTimeMillis {
|
||||
acceptedForChunk = processChunk(
|
||||
workingSatellites = workingSatellites,
|
||||
cells = chunk,
|
||||
coverageByTargetId = coverageByTargetId,
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd
|
||||
)
|
||||
}
|
||||
processedCells += chunk.size
|
||||
acceptedSlots += acceptedForChunk
|
||||
logger.info(
|
||||
"Stage chunk-process: runId={}, chunk={}/{}, cells={}, acceptedModes={}, durationMs={}",
|
||||
runId,
|
||||
chunkIndex + 1,
|
||||
chunkCount,
|
||||
chunk.size,
|
||||
acceptedForChunk,
|
||||
chunkProcessMs
|
||||
)
|
||||
coverageByTargetId = emptyMap()
|
||||
|
||||
if (shouldStop(workingSatellites, intervalStart, intervalEnd)) {
|
||||
logger.info(
|
||||
"Ранняя остановка после chunk {}: runId={}, ресурс ОГ исчерпан, processedCells={}, acceptedModes={}",
|
||||
chunkIndex + 1,
|
||||
runId,
|
||||
processedCells,
|
||||
acceptedSlots
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var saveResult = SatelliteModeSaveResult()
|
||||
val persistMs = measureTimeMillis {
|
||||
saveResult = saveCalculatedModes(
|
||||
runId = runId,
|
||||
workingSatellites = workingSatellites,
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd
|
||||
)
|
||||
}
|
||||
logger.info(
|
||||
"Stage persist-modes: runId={}, processedCells={}, acceptedModes={}, persistedModes={}, durationMs={}",
|
||||
runId,
|
||||
processedCells,
|
||||
acceptedSlots,
|
||||
saveResult.calculatedModesCount,
|
||||
persistMs
|
||||
)
|
||||
var coveredArea = 0.0
|
||||
val coverageAreaMs = measureTimeMillis {
|
||||
coveredArea = calculateCoveredArea(workingSatellites, intervalStart, intervalEnd)
|
||||
}
|
||||
val coveredAreaPercent = coveredAreaPercent(totalRequestsArea, coveredArea)
|
||||
logger.info(
|
||||
"Stage coverage-area: runId={}, totalRequestsArea={}, coveredArea={}, coveredAreaPercent={}, durationMs={}",
|
||||
runId,
|
||||
totalRequestsArea,
|
||||
coveredArea,
|
||||
coveredAreaPercent,
|
||||
coverageAreaMs
|
||||
)
|
||||
logger.info(
|
||||
"Расчет комплексного плана завершен успешно: runId={}, calculatedModes={}, bookedModes={}, complanModes={}, mixedModes={}, affectedSatellites={}, processedCells={}, bookedSlotLinks={}, totalRequestsArea={}, coveredArea={}, coveredAreaPercent={}, durationMs={}",
|
||||
runId,
|
||||
saveResult.calculatedModesCount,
|
||||
saveResult.bookedModesCount,
|
||||
saveResult.complanModesCount,
|
||||
saveResult.mixedModesCount,
|
||||
saveResult.affectedSatellitesCount,
|
||||
saveResult.processedCellsCount,
|
||||
saveResult.bookedSlotLinksCount,
|
||||
totalRequestsArea,
|
||||
coveredArea,
|
||||
coveredAreaPercent,
|
||||
System.currentTimeMillis() - calculationStartedAtMs
|
||||
)
|
||||
return ComplexPlanCalculationResult(
|
||||
calculatedModesCount = saveResult.calculatedModesCount,
|
||||
bookedModesCount = saveResult.bookedModesCount,
|
||||
complanModesCount = saveResult.complanModesCount,
|
||||
mixedModesCount = saveResult.mixedModesCount,
|
||||
affectedSatellitesCount = saveResult.affectedSatellitesCount,
|
||||
processedCellsCount = saveResult.processedCellsCount,
|
||||
bookedSlotLinksCount = saveResult.bookedSlotLinksCount,
|
||||
totalRequestsArea = totalRequestsArea,
|
||||
coveredArea = coveredArea,
|
||||
coveredAreaPercent = coveredAreaPercent
|
||||
)
|
||||
} finally {
|
||||
logger.info(
|
||||
"Очистка локального рабочего состояния расчета: runId={}, elapsedMs={}",
|
||||
runId,
|
||||
System.currentTimeMillis() - calculationStartedAtMs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
logger.info("Полная очистка persisted и временного состояния complex plan")
|
||||
complexPlanPersistenceService.clearAll()
|
||||
satelliteService.clear()
|
||||
}
|
||||
|
||||
private fun addPlannedModes(
|
||||
workingSatellites: List<SatelliteModel>,
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime
|
||||
) {
|
||||
val satelliteIds = workingSatellites.map { it.satelliteId }
|
||||
val plannedModesBySatelliteId = coverageBatchClient.plannedModes(satelliteIds, intervalStart, intervalEnd)
|
||||
val totalPlannedModes = plannedModesBySatelliteId.values.sumOf { it.size }
|
||||
logger.info(
|
||||
"Получено {} booked-маршрутов из slots-service для {} спутников",
|
||||
totalPlannedModes,
|
||||
plannedModesBySatelliteId.size
|
||||
)
|
||||
|
||||
workingSatellites.forEach { satellite ->
|
||||
val plannedForSatellite = plannedModesBySatelliteId[satellite.satelliteId].orEmpty()
|
||||
plannedModesBySatelliteId[satellite.satelliteId]
|
||||
.orEmpty()
|
||||
.forEach { slot ->
|
||||
surveyPlacementService.mergeSurvey(
|
||||
existingSurveys = satellite.longMission.surveys,
|
||||
candidateSurvey = SurveyMode(
|
||||
revolution = slot.revolution,
|
||||
time = slot.tn,
|
||||
timeStop = slot.tk,
|
||||
roll = slot.roll,
|
||||
latitude = slot.latitude,
|
||||
longitude = slot.longitude,
|
||||
duration = Duration.between(slot.tn, slot.tk).seconds.toDouble(),
|
||||
contourWKT = slot.contour,
|
||||
cellNum = -1,
|
||||
source = SatelliteModeSource.SLOTS,
|
||||
bookedSlotIds = slot.slotIds,
|
||||
cellNums = emptyList()
|
||||
),
|
||||
satellite = satellite
|
||||
)
|
||||
}
|
||||
logger.info(
|
||||
"Добавлены booked-маршруты для satelliteId={}: count={}",
|
||||
satellite.satelliteId,
|
||||
plannedForSatellite.size
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildCoverageTargets(cells: List<EarthCellWithRequestsDTO>): List<SlotCoverageTargetDTO> =
|
||||
cells.map { cell ->
|
||||
SlotCoverageTargetDTO(
|
||||
targetId = cell.targetId(),
|
||||
wkt = cellTargetContour(cell)
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildTargetsChunk(cells: List<EarthCellWithRequestsDTO>): List<SlotCoverageTargetDTO> =
|
||||
buildCoverageTargets(cells)
|
||||
|
||||
private fun buildOccupiedIntervalsSnapshot(workingSatellites: List<SatelliteModel>): List<OccupiedIntervalDTO> =
|
||||
workingSatellites
|
||||
.asSequence()
|
||||
.flatMap { satellite ->
|
||||
satellite.longMission.surveys.asSequence().map { survey ->
|
||||
OccupiedIntervalDTO(
|
||||
satelliteId = satellite.satelliteId,
|
||||
startTime = survey.time,
|
||||
endTime = survey.timeStop,
|
||||
roll = survey.roll,
|
||||
source = survey.source.name
|
||||
)
|
||||
}
|
||||
}
|
||||
.sortedWith(compareBy<OccupiedIntervalDTO> { it.satelliteId }.thenBy { it.startTime }.thenBy { it.endTime })
|
||||
.toList()
|
||||
|
||||
private fun processChunk(
|
||||
workingSatellites: List<SatelliteModel>,
|
||||
cells: List<EarthCellWithRequestsDTO>,
|
||||
coverageByTargetId: Map<Long, List<space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO>>,
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime
|
||||
): Int {
|
||||
var acceptedModes = 0
|
||||
|
||||
for (cell in cells) {
|
||||
val resourceAvailableSatelliteIds = collectResourceAvailableSatelliteIds(workingSatellites, intervalStart, intervalEnd)
|
||||
if (resourceAvailableSatelliteIds.isEmpty()) {
|
||||
break
|
||||
}
|
||||
|
||||
val activeSatelliteIds = resourceAvailableSatelliteIds.toSet()
|
||||
coverageByTargetId[cell.targetId()]
|
||||
.orEmpty()
|
||||
.asSequence()
|
||||
.filter { slot -> activeSatelliteIds.contains(slot.satelliteId) }
|
||||
.forEach { slot ->
|
||||
val satellite = workingSatellites.find { it.satelliteId == slot.satelliteId }
|
||||
?: throw CustomValidationException("KA ${slot.satelliteId} не зарегистророван")
|
||||
|
||||
val decision = surveyPlacementService.tryPlaceSurvey(
|
||||
existingSurveys = satellite.longMission.surveys,
|
||||
candidateSurvey = SurveyMode(
|
||||
revolution = slot.revolution,
|
||||
time = slot.tn,
|
||||
timeStop = slot.tk,
|
||||
roll = slot.roll,
|
||||
latitude = slot.latitude,
|
||||
longitude = slot.longitude,
|
||||
duration = Duration.between(slot.tn, slot.tk).seconds.toDouble(),
|
||||
contourWKT = slot.contour,
|
||||
cellNum = cell.num,
|
||||
source = SatelliteModeSource.COMPLAN,
|
||||
cellNums = listOf(cell.num)
|
||||
),
|
||||
satellite = satellite
|
||||
)
|
||||
if (decision.accepted) {
|
||||
acceptedModes++
|
||||
} else {
|
||||
logger.debug(
|
||||
"Candidate rejected by placement: satelliteId={}, cellNum={}, revolution={}, reason={}",
|
||||
slot.satelliteId,
|
||||
cell.num,
|
||||
slot.revolution,
|
||||
decision.rejectionReason
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return acceptedModes
|
||||
}
|
||||
|
||||
private fun hasRemainingCapacity(
|
||||
workingSatellites: List<SatelliteModel>,
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime
|
||||
): Boolean =
|
||||
collectResourceAvailableSatelliteIds(workingSatellites, intervalStart, intervalEnd).isNotEmpty()
|
||||
|
||||
private fun shouldStop(
|
||||
workingSatellites: List<SatelliteModel>,
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime
|
||||
): Boolean = !hasRemainingCapacity(workingSatellites, intervalStart, intervalEnd)
|
||||
|
||||
private fun initializeWorkingModes(
|
||||
workingSatellites: List<SatelliteModel>,
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime
|
||||
) {
|
||||
val workingModes = complexPlanQueryService.loadConstraintModes(
|
||||
workingSatellites.map { it.satelliteId },
|
||||
intervalStart,
|
||||
intervalEnd
|
||||
)
|
||||
workingSatellites.forEach { satellite ->
|
||||
satellite.longMission.surveys.clear()
|
||||
satellite.longMission.surveys.addAll(workingModes[satellite.satelliteId]
|
||||
.orEmpty()
|
||||
.filter { mode -> mode.timeStop <= intervalStart || mode.time >= intervalEnd || mode.source == SatelliteModeSource.MANUAL})
|
||||
logger.info(
|
||||
"Загружено рабочее состояние для satelliteId={}: modesOutsideInterval={}",
|
||||
satellite.satelliteId,
|
||||
satellite.longMission.surveys.size
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveCalculatedModes(
|
||||
runId: Long?,
|
||||
workingSatellites: List<SatelliteModel>,
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime
|
||||
): SatelliteModeSaveResult {
|
||||
val satelliteIds = workingSatellites.map { it.satelliteId }
|
||||
val surveysBySatelliteId = workingSatellites
|
||||
.associate { satellite -> satellite.satelliteId to satellite.longMission.surveys.toList() }
|
||||
val totalModes = surveysBySatelliteId.values.sumOf { it.size }
|
||||
logger.info(
|
||||
"Подготовлены данные к сохранению: runId={}, satellites={}, totalModes={}",
|
||||
runId,
|
||||
surveysBySatelliteId.keys,
|
||||
totalModes
|
||||
)
|
||||
|
||||
return complexPlanPersistenceService.replaceModesForInterval(
|
||||
satelliteIds = satelliteIds,
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
surveysBySatelliteId = surveysBySatelliteId,
|
||||
complexPlanRunId = runId
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildWorkingSatellites(satelliteIds: List<Long>): List<SatelliteModel> =
|
||||
satelliteService.satellites
|
||||
.filter { satelliteIds.contains(it.satelliteId) }
|
||||
.map { satellite ->
|
||||
SatelliteModel(
|
||||
satelliteId = satellite.satelliteId,
|
||||
name = satellite.name,
|
||||
red = satellite.red,
|
||||
green = satellite.green,
|
||||
blue = satellite.blue,
|
||||
longMission = LongTermMission(),
|
||||
bls = satellite.bls,
|
||||
scanTLE = satellite.scanTLE
|
||||
)
|
||||
}
|
||||
|
||||
private fun collectResourceAvailableSatelliteIds(
|
||||
workingSatellites: List<SatelliteModel>,
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime
|
||||
): List<Long> {
|
||||
val availableSatelliteIds = linkedSetOf<Long>()
|
||||
var day = intervalStart.toLocalDate()
|
||||
val lastDay = intervalEnd.toLocalDate().minusDays(1)
|
||||
|
||||
while (day <= lastDay) {
|
||||
workingSatellites.forEach { satellite ->
|
||||
if (satellite.hasResources(day)) {
|
||||
availableSatelliteIds += satellite.satelliteId
|
||||
}
|
||||
}
|
||||
day = day.plusDays(1)
|
||||
}
|
||||
|
||||
return availableSatelliteIds.toList()
|
||||
}
|
||||
|
||||
private fun cellTargetContour(cell: EarthCellWithRequestsDTO): String {
|
||||
val requestContours = cell.requests
|
||||
.map { it.contour.trim() }
|
||||
.filter { it.isNotEmpty() }
|
||||
|
||||
if (requestContours.isEmpty()) {
|
||||
return cell.contour
|
||||
}
|
||||
|
||||
return requestContours.drop(1).fold(requestContours.first()) { merged, contour ->
|
||||
unionContours(merged, contour)
|
||||
}
|
||||
}
|
||||
|
||||
private fun unionContours(baseContour: String, contourToAdd: String): String {
|
||||
return try {
|
||||
val reader = WKTReader()
|
||||
val writer = WKTWriter()
|
||||
writer.write(reader.read(baseContour).union(reader.read(contourToAdd)))
|
||||
} catch (e: Exception) {
|
||||
logger.warn("Не удалось объединить контуры маршрутов: {}", e.message)
|
||||
baseContour
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildRequestsGeometry(cells: List<EarthCellWithRequestsDTO>): Geometry? {
|
||||
val requestContours = cells
|
||||
.asSequence()
|
||||
.flatMap { cell -> cell.requests.asSequence() }
|
||||
.distinctBy { request -> requestFragmentKey(request.id, request.requestId, request.contour) }
|
||||
.map { request -> request.contour }
|
||||
.filter { contour -> contour.isNotBlank() }
|
||||
.toList()
|
||||
|
||||
return unionGeometries(requestContours, "request")
|
||||
}
|
||||
|
||||
private fun requestFragmentKey(id: Long, requestId: String, contour: String): String =
|
||||
if (id > 0) {
|
||||
"fragment:$id"
|
||||
} else {
|
||||
"request:${requestId.takeIf { it.isNotBlank() } ?: "unknown"}:$contour"
|
||||
}
|
||||
|
||||
private fun calculateCoveredArea(
|
||||
workingSatellites: List<SatelliteModel>,
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime
|
||||
): Double {
|
||||
val coveredContours = workingSatellites
|
||||
.asSequence()
|
||||
.flatMap { satellite -> satellite.longMission.surveys.asSequence() }
|
||||
.filter { survey -> survey.source != SatelliteModeSource.MANUAL }
|
||||
.filter { survey -> survey.time < intervalEnd && intervalStart < survey.timeStop }
|
||||
.map { survey -> survey.contourWKT }
|
||||
.filter { contour -> contour.isNotBlank() }
|
||||
.toList()
|
||||
|
||||
return unionGeometries(coveredContours, "covered")?.area ?: 0.0
|
||||
}
|
||||
|
||||
private fun unionGeometries(contours: List<String>, context: String): Geometry? {
|
||||
val reader = WKTReader()
|
||||
val geometries = contours.mapNotNull { contour ->
|
||||
runCatching { reader.read(contour) }
|
||||
.onFailure { e -> logger.warn("Не удалось прочитать WKT {} geometry: {}", context, e.message) }
|
||||
.getOrNull()
|
||||
?.takeIf { geometry -> !geometry.isEmpty }
|
||||
}
|
||||
|
||||
if (geometries.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return runCatching {
|
||||
UnaryUnionOp.union(geometries)
|
||||
}.onFailure { e ->
|
||||
logger.warn("Не удалось объединить WKT {} geometries: {}", context, e.message)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private fun coveredAreaPercent(totalRequestsArea: Double, coveredArea: Double): Double =
|
||||
if (totalRequestsArea <= 0.0 || coveredArea <= 0.0) {
|
||||
0.0
|
||||
} else {
|
||||
coveredArea / totalRequestsArea * 100.0
|
||||
}
|
||||
|
||||
private fun EarthCellWithRequestsDTO.targetId(): Long = if (id > 0) id else num
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import java.sql.Connection
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.sql.DataSource
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
@Service
|
||||
class ComplexPlanExecutionLockService(
|
||||
private val dataSource: DataSource
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private const val LOCK_NAMESPACE = 4_204_202_411L
|
||||
private val fallbackLocks = ConcurrentHashMap.newKeySet<Long>()
|
||||
}
|
||||
|
||||
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
fun tryAcquire(satelliteIds: Collection<Long>): ExecutionLockHandle? {
|
||||
val normalizedSatelliteIds = satelliteIds.distinct().sorted()
|
||||
if (normalizedSatelliteIds.isEmpty()) {
|
||||
logger.info("Complex plan execution lock skipped: empty satellite list")
|
||||
return ExecutionLockHandle {}
|
||||
}
|
||||
|
||||
var handle: ExecutionLockHandle? = null
|
||||
val durationMs = measureTimeMillis {
|
||||
val connection = dataSource.connection
|
||||
handle = try {
|
||||
val postgres = connection.metaData.databaseProductName.contains("PostgreSQL", ignoreCase = true)
|
||||
if (postgres) {
|
||||
acquirePostgresLocks(connection, normalizedSatelliteIds)
|
||||
} else {
|
||||
connection.close()
|
||||
acquireFallbackLocks(normalizedSatelliteIds)
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
runCatching { connection.close() }
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
logger.info(
|
||||
"Complex plan execution lock acquire completed: satellites={}, acquired={}, durationMs={}",
|
||||
normalizedSatelliteIds,
|
||||
handle != null,
|
||||
durationMs
|
||||
)
|
||||
return handle
|
||||
}
|
||||
|
||||
private fun acquirePostgresLocks(connection: Connection, satelliteIds: List<Long>): ExecutionLockHandle? {
|
||||
val acquiredKeys = mutableListOf<Long>()
|
||||
try {
|
||||
satelliteIds.forEach { satelliteId ->
|
||||
val lockKey = advisoryKey(satelliteId)
|
||||
if (!tryAcquirePostgresLock(connection, lockKey)) {
|
||||
releasePostgresLocks(connection, acquiredKeys.asReversed())
|
||||
connection.close()
|
||||
logger.info(
|
||||
"Complex plan execution lock busy: satelliteId={}, acquiredSatelliteIds={}",
|
||||
satelliteId,
|
||||
acquiredKeys.map(::satelliteIdFromKey)
|
||||
)
|
||||
return null
|
||||
}
|
||||
acquiredKeys += lockKey
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
releasePostgresLocks(connection, acquiredKeys.asReversed())
|
||||
connection.close()
|
||||
throw ex
|
||||
}
|
||||
|
||||
return ExecutionLockHandle {
|
||||
releasePostgresLocks(connection, acquiredKeys.asReversed())
|
||||
connection.close()
|
||||
}
|
||||
}
|
||||
|
||||
private fun acquireFallbackLocks(satelliteIds: List<Long>): ExecutionLockHandle? {
|
||||
val acquiredKeys = mutableListOf<Long>()
|
||||
satelliteIds.forEach { satelliteId ->
|
||||
val lockKey = advisoryKey(satelliteId)
|
||||
if (!fallbackLocks.add(lockKey)) {
|
||||
acquiredKeys.forEach(fallbackLocks::remove)
|
||||
logger.info(
|
||||
"Complex plan execution fallback lock busy: satelliteId={}, acquiredSatelliteIds={}",
|
||||
satelliteId,
|
||||
acquiredKeys.map(::satelliteIdFromKey)
|
||||
)
|
||||
return null
|
||||
}
|
||||
acquiredKeys += lockKey
|
||||
}
|
||||
|
||||
return ExecutionLockHandle {
|
||||
acquiredKeys.forEach(fallbackLocks::remove)
|
||||
}
|
||||
}
|
||||
|
||||
private fun tryAcquirePostgresLock(connection: Connection, lockKey: Long): Boolean =
|
||||
connection.prepareStatement("select pg_try_advisory_lock(?)").use { statement ->
|
||||
statement.setLong(1, lockKey)
|
||||
statement.executeQuery().use { resultSet ->
|
||||
resultSet.next()
|
||||
resultSet.getBoolean(1)
|
||||
}
|
||||
}
|
||||
|
||||
private fun releasePostgresLocks(connection: Connection, lockKeys: List<Long>) {
|
||||
lockKeys.forEach { lockKey ->
|
||||
runCatching {
|
||||
connection.prepareStatement("select pg_advisory_unlock(?)").use { statement ->
|
||||
statement.setLong(1, lockKey)
|
||||
statement.executeQuery().use { resultSet -> resultSet.next() }
|
||||
}
|
||||
}.onFailure { ex ->
|
||||
logger.warn("Failed to release complex plan advisory lock: key={}", lockKey, ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun advisoryKey(satelliteId: Long): Long = LOCK_NAMESPACE * 1_000_003L + satelliteId
|
||||
|
||||
private fun satelliteIdFromKey(lockKey: Long): Long = lockKey - LOCK_NAMESPACE * 1_000_003L
|
||||
|
||||
fun interface ExecutionLockHandle : AutoCloseable {
|
||||
override fun close()
|
||||
}
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.ComplexPlanRunRepository
|
||||
import space.nstart.pcp.pcp_satellites_service.model.mode.SurveyMode
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
@Service
|
||||
class ComplexPlanPersistenceService(
|
||||
private val satelliteModePersistenceService: SatelliteModePersistenceService,
|
||||
private val complexPlanRunRepository: ComplexPlanRunRepository
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
fun clearModesForInterval(
|
||||
satelliteIds: Collection<Long>,
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime
|
||||
) {
|
||||
val durationMs = measureTimeMillis {
|
||||
satelliteModePersistenceService.deleteModesForInterval(
|
||||
satelliteIds = satelliteIds,
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd
|
||||
)
|
||||
}
|
||||
logger.info(
|
||||
"Complex plan persistence clear-modes completed: satellites={}, interval=[{} - {}], durationMs={}",
|
||||
satelliteIds.distinct(),
|
||||
intervalStart,
|
||||
intervalEnd,
|
||||
durationMs
|
||||
)
|
||||
}
|
||||
|
||||
fun saveModesForInterval(
|
||||
satelliteIds: Collection<Long>,
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime,
|
||||
surveysBySatelliteId: Map<Long, List<SurveyMode>>,
|
||||
complexPlanRunId: Long? = null
|
||||
): SatelliteModeSaveResult {
|
||||
lateinit var result: SatelliteModeSaveResult
|
||||
val durationMs = measureTimeMillis {
|
||||
result = satelliteModePersistenceService.saveModesForInterval(
|
||||
satelliteIds = satelliteIds,
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
surveysBySatelliteId = surveysBySatelliteId,
|
||||
complexPlanRun = complexPlanRunId?.let { complexPlanRunRepository.getReferenceById(it) }
|
||||
)
|
||||
}
|
||||
logger.info(
|
||||
"Complex plan persistence save-modes completed: runId={}, satellites={}, modes={}, durationMs={}",
|
||||
complexPlanRunId,
|
||||
satelliteIds.distinct(),
|
||||
result.calculatedModesCount,
|
||||
durationMs
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
fun replaceModesForInterval(
|
||||
satelliteIds: Collection<Long>,
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime,
|
||||
surveysBySatelliteId: Map<Long, List<SurveyMode>>,
|
||||
complexPlanRunId: Long? = null
|
||||
): SatelliteModeSaveResult {
|
||||
lateinit var result: SatelliteModeSaveResult
|
||||
val durationMs = measureTimeMillis {
|
||||
result = satelliteModePersistenceService.replaceModesForInterval(
|
||||
satelliteIds = satelliteIds,
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
surveysBySatelliteId = surveysBySatelliteId,
|
||||
complexPlanRun = complexPlanRunId?.let { complexPlanRunRepository.getReferenceById(it) }
|
||||
)
|
||||
}
|
||||
logger.info(
|
||||
"Complex plan persistence replace-modes completed: runId={}, satellites={}, modes={}, durationMs={}",
|
||||
complexPlanRunId,
|
||||
satelliteIds.distinct(),
|
||||
result.calculatedModesCount,
|
||||
durationMs
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
fun clearAll() {
|
||||
val durationMs = measureTimeMillis {
|
||||
satelliteModePersistenceService.clearAll()
|
||||
}
|
||||
logger.info("Complex plan persistence clear-all completed: durationMs={}", durationMs)
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import org.springframework.stereotype.Service
|
||||
import space.nstart.pcp.pcp_satellites_service.model.mode.SurveyMode
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.MissionStatisticsDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SatelliteModeResponseDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SatelliteModeSnapshotResponseDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.modes.SurveyModeInfoDTO
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Service
|
||||
class ComplexPlanQueryService(
|
||||
private val satelliteModeQueryService: SatelliteModeQueryService
|
||||
) {
|
||||
|
||||
fun findMissionModes(satelliteId: Long): List<SurveyModeInfoDTO> =
|
||||
satelliteModeQueryService.findMissionModes(satelliteId)
|
||||
|
||||
fun missionStatistics(satelliteId: Long): MissionStatisticsDTO =
|
||||
satelliteModeQueryService.missionStatistics(satelliteId)
|
||||
|
||||
fun buildPaintGeometry(satelliteId: Long): String =
|
||||
satelliteModeQueryService.buildPaintGeometry(satelliteId)
|
||||
|
||||
fun buildCellPaintGeometry(cellNum: Long): String =
|
||||
satelliteModeQueryService.buildCellPaintGeometry(cellNum)
|
||||
|
||||
fun findModesByInterval(
|
||||
satelliteIds: Collection<Long>,
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime,
|
||||
runId: Long? = null
|
||||
): List<SatelliteModeResponseDTO> =
|
||||
satelliteModeQueryService.findModesByInterval(satelliteIds, intervalStart, intervalEnd, runId)
|
||||
|
||||
fun findSnapshot(snapshotId: Long): SatelliteModeSnapshotResponseDTO =
|
||||
satelliteModeQueryService.findSnapshot(snapshotId)
|
||||
|
||||
fun findSnapshots(
|
||||
satelliteId: Long?,
|
||||
intervalStart: LocalDateTime?,
|
||||
intervalEnd: LocalDateTime?,
|
||||
activeOnly: Boolean
|
||||
): List<SatelliteModeSnapshotResponseDTO> =
|
||||
satelliteModeQueryService.findSnapshots(
|
||||
satelliteId = satelliteId,
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
activeOnly = activeOnly
|
||||
)
|
||||
|
||||
fun findActiveSnapshot(
|
||||
satelliteId: Long,
|
||||
intervalStart: LocalDateTime? = null,
|
||||
intervalEnd: LocalDateTime? = null
|
||||
): SatelliteModeSnapshotResponseDTO? =
|
||||
satelliteModeQueryService.findActiveSnapshot(satelliteId, intervalStart, intervalEnd)
|
||||
|
||||
fun findModesBySnapshot(
|
||||
snapshotId: Long,
|
||||
intervalStart: LocalDateTime? = null,
|
||||
intervalEnd: LocalDateTime? = null
|
||||
): List<SatelliteModeResponseDTO> =
|
||||
satelliteModeQueryService.findModesBySnapshot(snapshotId, intervalStart, intervalEnd)
|
||||
|
||||
fun loadWorkingModes(
|
||||
satelliteIds: Collection<Long>,
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime
|
||||
): Map<Long, MutableList<SurveyMode>> =
|
||||
satelliteModeQueryService.loadWorkingModes(satelliteIds, intervalStart, intervalEnd)
|
||||
|
||||
fun loadConstraintModes(
|
||||
satelliteIds: Collection<Long>,
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime
|
||||
): Map<Long, MutableList<SurveyMode>> =
|
||||
satelliteModeQueryService.loadConstraintModes(satelliteIds, intervalStart, intervalEnd)
|
||||
|
||||
fun loadDayConstraintModes(
|
||||
satelliteIds: Collection<Long>,
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime
|
||||
): Map<Long, MutableList<SurveyMode>> =
|
||||
satelliteModeQueryService.loadDayConstraintModes(satelliteIds, intervalStart, intervalEnd)
|
||||
|
||||
fun loadRevolutionConstraintModes(
|
||||
satelliteIds: Collection<Long>,
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime
|
||||
): Map<Long, MutableList<SurveyMode>> =
|
||||
satelliteModeQueryService.loadRevolutionConstraintModes(satelliteIds, intervalStart, intervalEnd)
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import org.springframework.stereotype.Component
|
||||
import space.nstart.pcp.pcp_satellites_service.dto.ComplexPlanRunResponseDTO
|
||||
import space.nstart.pcp.pcp_satellites_service.dto.ComplexPlanRunSummaryDTO
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.ComplexPlanRunEntity
|
||||
import java.time.LocalDateTime
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
|
||||
@Component
|
||||
class ComplexPlanRunMapper {
|
||||
|
||||
fun toResponse(entity: ComplexPlanRunEntity): ComplexPlanRunResponseDTO =
|
||||
ComplexPlanRunResponseDTO(
|
||||
runId = entity.id ?: 0,
|
||||
status = entity.status,
|
||||
intervalStart = entity.intervalStart,
|
||||
intervalEnd = entity.intervalEnd,
|
||||
satelliteIds = satelliteIds(entity),
|
||||
requestedBy = entity.requestedBy,
|
||||
createdAt = entity.createdAt.toUtcOffset(),
|
||||
startedAt = entity.startedAt?.toUtcOffset(),
|
||||
finishedAt = entity.finishedAt?.toUtcOffset(),
|
||||
durationMs = entity.durationMs,
|
||||
errorMessage = entity.errorMessage,
|
||||
calculatedModesCount = entity.calculatedModesCount,
|
||||
bookedModesCount = entity.bookedModesCount,
|
||||
complanModesCount = entity.complanModesCount,
|
||||
mixedModesCount = entity.mixedModesCount,
|
||||
affectedSatellitesCount = entity.affectedSatellitesCount,
|
||||
processedCellsCount = entity.processedCellsCount,
|
||||
bookedSlotLinksCount = entity.bookedSlotLinksCount,
|
||||
snapshotIds = snapshotIds(entity),
|
||||
requestPayload = entity.requestPayload,
|
||||
totalRequestsArea = entity.totalRequestsArea,
|
||||
coveredArea = entity.coveredArea,
|
||||
coveredAreaPercent = entity.coveredAreaPercent
|
||||
)
|
||||
|
||||
fun toSummary(entity: ComplexPlanRunEntity): ComplexPlanRunSummaryDTO =
|
||||
ComplexPlanRunSummaryDTO(
|
||||
runId = entity.id ?: 0,
|
||||
status = entity.status,
|
||||
intervalStart = entity.intervalStart,
|
||||
intervalEnd = entity.intervalEnd,
|
||||
satelliteIds = satelliteIds(entity),
|
||||
requestedBy = entity.requestedBy,
|
||||
createdAt = entity.createdAt.toUtcOffset(),
|
||||
startedAt = entity.startedAt?.toUtcOffset(),
|
||||
finishedAt = entity.finishedAt?.toUtcOffset(),
|
||||
durationMs = entity.durationMs,
|
||||
errorMessage = entity.errorMessage,
|
||||
calculatedModesCount = entity.calculatedModesCount,
|
||||
bookedModesCount = entity.bookedModesCount,
|
||||
complanModesCount = entity.complanModesCount,
|
||||
mixedModesCount = entity.mixedModesCount,
|
||||
affectedSatellitesCount = entity.affectedSatellitesCount,
|
||||
processedCellsCount = entity.processedCellsCount,
|
||||
bookedSlotLinksCount = entity.bookedSlotLinksCount,
|
||||
snapshotIds = snapshotIds(entity),
|
||||
totalRequestsArea = entity.totalRequestsArea,
|
||||
coveredArea = entity.coveredArea,
|
||||
coveredAreaPercent = entity.coveredAreaPercent
|
||||
)
|
||||
|
||||
private fun satelliteIds(entity: ComplexPlanRunEntity): List<Long> =
|
||||
entity.satellites
|
||||
.map { it.satelliteId }
|
||||
.distinct()
|
||||
.sorted()
|
||||
|
||||
private fun snapshotIds(entity: ComplexPlanRunEntity): List<Long> =
|
||||
entity.snapshots
|
||||
.mapNotNull { it.id }
|
||||
.distinct()
|
||||
.sorted()
|
||||
|
||||
private fun LocalDateTime.toUtcOffset(): OffsetDateTime =
|
||||
atOffset(ZoneOffset.UTC)
|
||||
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import space.nstart.pcp.pcp_satellites_service.configuration.CustomValidationException
|
||||
import space.nstart.pcp.pcp_satellites_service.dto.ComplexPlanRunCreateDTO
|
||||
import space.nstart.pcp.pcp_satellites_service.dto.ComplexPlanRunResponseDTO
|
||||
import space.nstart.pcp.pcp_satellites_service.dto.ComplexPlanRunStatisticsDTO
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.ComplexPlanRunEntity
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.ComplexPlanProcessRequestDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.CoverageCalculationSource
|
||||
import tools.jackson.databind.json.JsonMapper
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
@Service
|
||||
class ComplexPlanRunOrchestrationService(
|
||||
private val complexPlanRunPersistenceService: ComplexPlanRunPersistenceService,
|
||||
private val complexPlanRunQueryService: ComplexPlanRunQueryService,
|
||||
private val complexPlanCalculationService: ComplexPlanCalculationService,
|
||||
private val satelliteService: SatelliteService,
|
||||
private val complexPlanExecutionLockService: ComplexPlanExecutionLockService
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||
private val objectMapper = JsonMapper.builder().findAndAddModules().build()
|
||||
|
||||
fun process(request: ComplexPlanProcessRequestDTO, requestedBy: String?): ComplexPlanRunResponseDTO {
|
||||
lateinit var selectedSatelliteIds: List<Long>
|
||||
val resolveSatelliteIdsMs = measureTimeMillis {
|
||||
selectedSatelliteIds = resolveSatelliteIds(request)
|
||||
}
|
||||
logger.info(
|
||||
"Complex plan orchestration stage resolve-satellites completed: satellites={}, durationMs={}",
|
||||
selectedSatelliteIds,
|
||||
resolveSatelliteIdsMs
|
||||
)
|
||||
|
||||
lateinit var createdRun: ComplexPlanRunEntity
|
||||
val createRunMs = measureTimeMillis {
|
||||
createdRun = complexPlanRunPersistenceService.createRun(
|
||||
ComplexPlanRunCreateDTO(
|
||||
intervalStart = request.intervalStart,
|
||||
intervalEnd = request.intervalEnd,
|
||||
satelliteIds = selectedSatelliteIds,
|
||||
requestedBy = requestedBy,
|
||||
requestPayload = objectMapper.writeValueAsString(request)
|
||||
)
|
||||
)
|
||||
}
|
||||
val runId = createdRun.id ?: error("Complex plan run id is null after create")
|
||||
logger.info(
|
||||
"Complex plan orchestration stage create-run completed: runId={}, durationMs={}",
|
||||
runId,
|
||||
createRunMs
|
||||
)
|
||||
|
||||
var startupLock: ComplexPlanExecutionLockService.ExecutionLockHandle? = null
|
||||
val acquireLockMs = measureTimeMillis {
|
||||
startupLock = complexPlanExecutionLockService.tryAcquire(selectedSatelliteIds)
|
||||
}
|
||||
logger.info(
|
||||
"Complex plan orchestration stage acquire-lock completed: runId={}, acquired={}, durationMs={}",
|
||||
runId,
|
||||
startupLock != null,
|
||||
acquireLockMs
|
||||
)
|
||||
if (startupLock == null) {
|
||||
val errorMessage = "Complex plan recalculation is already running for at least one requested satellite"
|
||||
complexPlanRunPersistenceService.markFailed(runId, errorMessage)
|
||||
throw CustomValidationException(errorMessage)
|
||||
}
|
||||
|
||||
try {
|
||||
lateinit var overlappingRunIds: List<Long>
|
||||
val overlapCheckMs = measureTimeMillis {
|
||||
overlappingRunIds = complexPlanRunPersistenceService.findOverlappingRunningRunIds(
|
||||
satelliteIds = selectedSatelliteIds,
|
||||
intervalStart = request.intervalStart,
|
||||
intervalEnd = request.intervalEnd,
|
||||
excludedRunId = runId
|
||||
)
|
||||
}
|
||||
logger.info(
|
||||
"Complex plan orchestration stage overlap-check completed: runId={}, overlaps={}, durationMs={}",
|
||||
runId,
|
||||
overlappingRunIds,
|
||||
overlapCheckMs
|
||||
)
|
||||
if (overlappingRunIds.isNotEmpty()) {
|
||||
val errorMessage =
|
||||
"Complex plan recalculation is already running for overlapping interval, runIds=$overlappingRunIds"
|
||||
complexPlanRunPersistenceService.markFailed(runId, errorMessage)
|
||||
throw CustomValidationException(errorMessage)
|
||||
}
|
||||
|
||||
val markRunningMs = measureTimeMillis {
|
||||
complexPlanRunPersistenceService.markRunning(runId)
|
||||
}
|
||||
logger.info(
|
||||
"Complex plan orchestration stage mark-running completed: runId={}, durationMs={}",
|
||||
runId,
|
||||
markRunningMs
|
||||
)
|
||||
} finally {
|
||||
startupLock?.close()
|
||||
}
|
||||
|
||||
val result = try {
|
||||
lateinit var calculationResult: ComplexPlanCalculationResult
|
||||
val calculationMs = measureTimeMillis {
|
||||
calculationResult = complexPlanCalculationService.process(
|
||||
runId = runId,
|
||||
intervalStart = request.intervalStart,
|
||||
intervalEnd = request.intervalEnd,
|
||||
satelliteIds = selectedSatelliteIds,
|
||||
sun = request.sun,
|
||||
coverageSource = request.coverageSource ?: CoverageCalculationSource.SLOTS,
|
||||
countLat = request.countLat,
|
||||
countLong = request.countLong,
|
||||
coverageStrategy = request.coverageStrategy
|
||||
)
|
||||
}
|
||||
logger.info(
|
||||
"Complex plan orchestration stage calculation completed: runId={}, calculatedModes={}, durationMs={}",
|
||||
runId,
|
||||
calculationResult.calculatedModesCount,
|
||||
calculationMs
|
||||
)
|
||||
calculationResult
|
||||
} catch (ex: Exception) {
|
||||
complexPlanRunPersistenceService.markFailed(runId, ex.message ?: ex.javaClass.simpleName)
|
||||
logger.error("Complex plan execution failed: runId={}", runId, ex)
|
||||
throw ex
|
||||
}
|
||||
val markCompletedMs = measureTimeMillis {
|
||||
complexPlanRunPersistenceService.markCompleted(
|
||||
runId = runId,
|
||||
statistics = result.toStatistics()
|
||||
)
|
||||
}
|
||||
logger.info(
|
||||
"Complex plan orchestration stage mark-completed completed: runId={}, durationMs={}",
|
||||
runId,
|
||||
markCompletedMs
|
||||
)
|
||||
|
||||
lateinit var response: ComplexPlanRunResponseDTO
|
||||
val loadResponseMs = measureTimeMillis {
|
||||
response = complexPlanRunQueryService.getRun(runId)
|
||||
}
|
||||
logger.info(
|
||||
"Complex plan orchestration stage load-response completed: runId={}, durationMs={}",
|
||||
runId,
|
||||
loadResponseMs
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
private fun resolveSatelliteIds(request: ComplexPlanProcessRequestDTO): List<Long> =
|
||||
buildList {
|
||||
request.satelliteId?.let { add(it) }
|
||||
addAll(request.satelliteIds)
|
||||
}.distinct().ifEmpty { satelliteService.satellites.map { it.satelliteId }.distinct() }
|
||||
}
|
||||
|
||||
data class ComplexPlanCalculationResult(
|
||||
val calculatedModesCount: Int = 0,
|
||||
val bookedModesCount: Int = 0,
|
||||
val complanModesCount: Int = 0,
|
||||
val mixedModesCount: Int = 0,
|
||||
val affectedSatellitesCount: Int = 0,
|
||||
val processedCellsCount: Int = 0,
|
||||
val bookedSlotLinksCount: Int = 0,
|
||||
val totalRequestsArea: Double = 0.0,
|
||||
val coveredArea: Double = 0.0,
|
||||
val coveredAreaPercent: Double = 0.0
|
||||
) {
|
||||
fun toStatistics(): ComplexPlanRunStatisticsDTO =
|
||||
ComplexPlanRunStatisticsDTO(
|
||||
calculatedModesCount = calculatedModesCount,
|
||||
bookedModesCount = bookedModesCount,
|
||||
complanModesCount = complanModesCount,
|
||||
mixedModesCount = mixedModesCount,
|
||||
affectedSatellitesCount = affectedSatellitesCount,
|
||||
processedCellsCount = processedCellsCount,
|
||||
bookedSlotLinksCount = bookedSlotLinksCount,
|
||||
totalRequestsArea = totalRequestsArea,
|
||||
coveredArea = coveredArea,
|
||||
coveredAreaPercent = coveredAreaPercent
|
||||
)
|
||||
}
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import space.nstart.pcp.pcp_satellites_service.configuration.CustomValidationException
|
||||
import space.nstart.pcp.pcp_satellites_service.dto.ComplexPlanRunCreateDTO
|
||||
import space.nstart.pcp.pcp_satellites_service.dto.ComplexPlanRunStatisticsDTO
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.ComplexPlanRunEntity
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.ComplexPlanRunSatelliteEntity
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.ComplexPlanRunStatus
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.ComplexPlanRunRepository
|
||||
import java.time.Clock
|
||||
import java.time.Duration
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Service
|
||||
class ComplexPlanRunPersistenceService(
|
||||
private val complexPlanRunRepository: ComplexPlanRunRepository,
|
||||
private val clock: Clock
|
||||
) {
|
||||
|
||||
companion object {
|
||||
const val APPLICATION_SHUTDOWN_ERROR_MESSAGE = "Run marked as FAILED because the application was shutting down"
|
||||
const val STARTUP_RECOVERY_ERROR_MESSAGE = "Marked as FAILED on startup: previous process terminated before completion"
|
||||
}
|
||||
|
||||
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
@Transactional
|
||||
fun createRun(createDTO: ComplexPlanRunCreateDTO): ComplexPlanRunEntity {
|
||||
validateInterval(createDTO.intervalStart, createDTO.intervalEnd)
|
||||
val now = LocalDateTime.now(clock)
|
||||
val entity = ComplexPlanRunEntity(
|
||||
status = ComplexPlanRunStatus.CREATED,
|
||||
createdAt = now,
|
||||
intervalStart = createDTO.intervalStart,
|
||||
intervalEnd = createDTO.intervalEnd,
|
||||
requestedBy = createDTO.requestedBy,
|
||||
requestPayload = createDTO.requestPayload
|
||||
)
|
||||
entity.satellites = createDTO.satelliteIds
|
||||
.distinct()
|
||||
.sorted()
|
||||
.map { satelliteId ->
|
||||
ComplexPlanRunSatelliteEntity(
|
||||
run = entity,
|
||||
satelliteId = satelliteId
|
||||
)
|
||||
}
|
||||
.toMutableList()
|
||||
|
||||
val saved = complexPlanRunRepository.save(entity)
|
||||
logger.info(
|
||||
"complexPlanRun created: runId={}, satellites={}, interval=[{} - {}], requestedBy={}",
|
||||
saved.id,
|
||||
saved.satellites.map { it.satelliteId },
|
||||
saved.intervalStart,
|
||||
saved.intervalEnd,
|
||||
saved.requestedBy
|
||||
)
|
||||
return saved
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun markRunning(runId: Long): ComplexPlanRunEntity {
|
||||
val entity = getRunOrThrow(runId)
|
||||
entity.status = ComplexPlanRunStatus.RUNNING
|
||||
entity.startedAt = LocalDateTime.now(clock)
|
||||
entity.finishedAt = null
|
||||
entity.durationMs = null
|
||||
entity.errorMessage = null
|
||||
logger.info("complexPlanRun started: runId={}, startedAt={}", runId, entity.startedAt)
|
||||
return entity
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun markCompleted(runId: Long, statistics: ComplexPlanRunStatisticsDTO): ComplexPlanRunEntity {
|
||||
val entity = getRunOrThrow(runId)
|
||||
entity.status = ComplexPlanRunStatus.COMPLETED
|
||||
entity.finishedAt = LocalDateTime.now(clock)
|
||||
entity.durationMs = calculateDurationMs(entity.startedAt, entity.finishedAt)
|
||||
entity.errorMessage = null
|
||||
applyStatistics(entity, statistics)
|
||||
logger.info(
|
||||
"complexPlanRun completed: runId={}, durationMs={}, calculatedModes={}, bookedModes={}, complanModes={}, mixedModes={}, satellites={}, processedCells={}, bookedSlotLinks={}",
|
||||
runId,
|
||||
entity.durationMs,
|
||||
entity.calculatedModesCount,
|
||||
entity.bookedModesCount,
|
||||
entity.complanModesCount,
|
||||
entity.mixedModesCount,
|
||||
entity.affectedSatellitesCount,
|
||||
entity.processedCellsCount,
|
||||
entity.bookedSlotLinksCount
|
||||
)
|
||||
return entity
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun markFailed(runId: Long, errorMessage: String?): ComplexPlanRunEntity {
|
||||
val entity = getRunOrThrow(runId)
|
||||
entity.status = ComplexPlanRunStatus.FAILED
|
||||
entity.finishedAt = LocalDateTime.now(clock)
|
||||
entity.durationMs = calculateDurationMs(entity.startedAt, entity.finishedAt)
|
||||
entity.errorMessage = errorMessage?.take(4000)
|
||||
logger.info(
|
||||
"complexPlanRun failed: runId={}, durationMs={}, error={}",
|
||||
runId,
|
||||
entity.durationMs,
|
||||
entity.errorMessage
|
||||
)
|
||||
return entity
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun markActiveRunsFailedOnShutdown(
|
||||
errorMessage: String = APPLICATION_SHUTDOWN_ERROR_MESSAGE
|
||||
): Int {
|
||||
val interruptedStatuses = listOf(
|
||||
ComplexPlanRunStatus.CREATED,
|
||||
ComplexPlanRunStatus.RUNNING
|
||||
)
|
||||
val now = LocalDateTime.now(clock)
|
||||
val interruptedRuns = complexPlanRunRepository.findAllByStatusIn(interruptedStatuses)
|
||||
|
||||
interruptedRuns.forEach { entity ->
|
||||
entity.status = ComplexPlanRunStatus.FAILED
|
||||
entity.finishedAt = now
|
||||
entity.durationMs = calculateDurationMs(entity.startedAt, entity.finishedAt)
|
||||
entity.errorMessage = errorMessage.take(4000)
|
||||
}
|
||||
|
||||
if (interruptedRuns.isNotEmpty()) {
|
||||
logger.info(
|
||||
"complexPlanRun shutdown-fail: affectedRuns={}, runIds={}",
|
||||
interruptedRuns.size,
|
||||
interruptedRuns.mapNotNull { it.id }.sorted()
|
||||
)
|
||||
}
|
||||
|
||||
return interruptedRuns.size
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun markRunningRunsFailedOnStartup(
|
||||
errorMessage: String = STARTUP_RECOVERY_ERROR_MESSAGE
|
||||
): Int {
|
||||
val now = LocalDateTime.now(clock)
|
||||
val staleRuns = complexPlanRunRepository.findAllByStatusIn(listOf(ComplexPlanRunStatus.RUNNING))
|
||||
|
||||
staleRuns.forEach { entity ->
|
||||
entity.status = ComplexPlanRunStatus.FAILED
|
||||
entity.finishedAt = entity.finishedAt ?: now
|
||||
entity.durationMs = calculateDurationMs(entity.startedAt, entity.finishedAt)
|
||||
entity.errorMessage = errorMessage.take(4000)
|
||||
}
|
||||
|
||||
if (staleRuns.isNotEmpty()) {
|
||||
logger.info(
|
||||
"complexPlanRun startup-recovery: affectedRuns={}, runIds={}",
|
||||
staleRuns.size,
|
||||
staleRuns.mapNotNull { it.id }.sorted()
|
||||
)
|
||||
}
|
||||
|
||||
return staleRuns.size
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun getRun(runId: Long): ComplexPlanRunEntity {
|
||||
val entity = complexPlanRunRepository.findDetailedById(runId)
|
||||
?: throw CustomValidationException("Complex plan run $runId not found")
|
||||
entity.snapshots.size
|
||||
return entity
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun findOverlappingRunningRunIds(
|
||||
satelliteIds: Collection<Long>,
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime,
|
||||
excludedRunId: Long
|
||||
): List<Long> {
|
||||
validateInterval(intervalStart, intervalEnd)
|
||||
if (satelliteIds.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
return complexPlanRunRepository.findOverlappingRunIdsByStatusAndSatelliteIds(
|
||||
status = ComplexPlanRunStatus.RUNNING,
|
||||
satelliteIds = satelliteIds.distinct(),
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
excludedRunId = excludedRunId
|
||||
)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun deleteRun(runId: Long) {
|
||||
if (!complexPlanRunRepository.existsById(runId)) {
|
||||
throw CustomValidationException("Complex plan run $runId not found")
|
||||
}
|
||||
|
||||
complexPlanRunRepository.deleteById(runId)
|
||||
logger.info("complexPlanRun deleted: runId={}", runId)
|
||||
}
|
||||
|
||||
private fun getRunOrThrow(runId: Long): ComplexPlanRunEntity =
|
||||
complexPlanRunRepository.findById(runId)
|
||||
.orElseThrow { CustomValidationException("Complex plan run $runId not found") }
|
||||
|
||||
private fun applyStatistics(entity: ComplexPlanRunEntity, statistics: ComplexPlanRunStatisticsDTO) {
|
||||
entity.calculatedModesCount = statistics.calculatedModesCount
|
||||
entity.bookedModesCount = statistics.bookedModesCount
|
||||
entity.complanModesCount = statistics.complanModesCount
|
||||
entity.mixedModesCount = statistics.mixedModesCount
|
||||
entity.affectedSatellitesCount = statistics.affectedSatellitesCount
|
||||
entity.processedCellsCount = statistics.processedCellsCount
|
||||
entity.bookedSlotLinksCount = statistics.bookedSlotLinksCount
|
||||
entity.totalRequestsArea = statistics.totalRequestsArea
|
||||
entity.coveredArea = statistics.coveredArea
|
||||
entity.coveredAreaPercent = statistics.coveredAreaPercent
|
||||
}
|
||||
|
||||
private fun calculateDurationMs(startedAt: LocalDateTime?, finishedAt: LocalDateTime?): Long? {
|
||||
if (startedAt == null || finishedAt == null) {
|
||||
return null
|
||||
}
|
||||
return Duration.between(startedAt, finishedAt).toMillis().coerceAtLeast(0)
|
||||
}
|
||||
|
||||
private fun validateInterval(intervalStart: LocalDateTime, intervalEnd: LocalDateTime) {
|
||||
if (intervalEnd.isBefore(intervalStart)) {
|
||||
throw CustomValidationException("Параметр intervalEnd должен быть больше или равен intervalStart")
|
||||
}
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import jakarta.persistence.criteria.JoinType
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.data.domain.Sort
|
||||
import org.springframework.data.jpa.domain.Specification
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import space.nstart.pcp.pcp_satellites_service.configuration.CustomValidationException
|
||||
import space.nstart.pcp.pcp_satellites_service.dto.ComplexPlanRunFilterDTO
|
||||
import space.nstart.pcp.pcp_satellites_service.dto.ComplexPlanRunResponseDTO
|
||||
import space.nstart.pcp.pcp_satellites_service.dto.ComplexPlanRunSummaryDTO
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.ComplexPlanRunRepository
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeSnapshotRepository
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
@Service
|
||||
class ComplexPlanRunQueryService(
|
||||
private val complexPlanRunRepository: ComplexPlanRunRepository,
|
||||
private val satelliteModeSnapshotRepository: SatelliteModeSnapshotRepository,
|
||||
private val complexPlanRunMapper: ComplexPlanRunMapper
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun getRun(runId: Long): ComplexPlanRunResponseDTO {
|
||||
lateinit var response: ComplexPlanRunResponseDTO
|
||||
val durationMs = measureTimeMillis {
|
||||
val run = complexPlanRunRepository.findDetailedById(runId)
|
||||
?: throw CustomValidationException("Complex plan run $runId not found")
|
||||
run.snapshots = satelliteModeSnapshotRepository.findAllByComplexPlanRun_IdOrderByIdAsc(runId).toMutableList()
|
||||
response = complexPlanRunMapper.toResponse(run)
|
||||
}
|
||||
logger.info(
|
||||
"complexPlanRun response loaded: runId={}, status={}, snapshots={}, durationMs={}",
|
||||
runId,
|
||||
response.status,
|
||||
response.snapshotIds.size,
|
||||
durationMs
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun findRuns(filter: ComplexPlanRunFilterDTO): List<ComplexPlanRunSummaryDTO> =
|
||||
complexPlanRunRepository.findAll(
|
||||
buildSpecification(filter),
|
||||
Sort.by(
|
||||
Sort.Order.desc("createdAt"),
|
||||
Sort.Order.desc("id")
|
||||
)
|
||||
).map(complexPlanRunMapper::toSummary)
|
||||
|
||||
private fun buildSpecification(filter: ComplexPlanRunFilterDTO): Specification<space.nstart.pcp.pcp_satellites_service.entity.ComplexPlanRunEntity> =
|
||||
Specification { root, query, criteriaBuilder ->
|
||||
val predicates = mutableListOf<jakarta.persistence.criteria.Predicate>()
|
||||
query.distinct(true)
|
||||
|
||||
filter.status?.let { status ->
|
||||
predicates += criteriaBuilder.equal(root.get<space.nstart.pcp.pcp_satellites_service.entity.ComplexPlanRunStatus>("status"), status)
|
||||
}
|
||||
filter.createdFrom?.let { createdFrom ->
|
||||
predicates += criteriaBuilder.greaterThanOrEqualTo(root.get("createdAt"), createdFrom)
|
||||
}
|
||||
filter.createdTo?.let { createdTo ->
|
||||
predicates += criteriaBuilder.lessThanOrEqualTo(root.get("createdAt"), createdTo)
|
||||
}
|
||||
filter.intervalStart?.let { intervalStart ->
|
||||
predicates += criteriaBuilder.greaterThanOrEqualTo(root.get("intervalStart"), intervalStart)
|
||||
}
|
||||
filter.intervalEnd?.let { intervalEnd ->
|
||||
predicates += criteriaBuilder.lessThanOrEqualTo(root.get("intervalEnd"), intervalEnd)
|
||||
}
|
||||
filter.satelliteId?.let { satelliteId ->
|
||||
val satellitesJoin = root.join<Any, Any>("satellites", JoinType.LEFT)
|
||||
predicates += criteriaBuilder.equal(satellitesJoin.get<Long>("satelliteId"), satelliteId)
|
||||
}
|
||||
|
||||
criteriaBuilder.and(*predicates.toTypedArray())
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.dao.DataAccessException
|
||||
import org.springframework.context.event.ContextClosedEvent
|
||||
import org.springframework.context.event.EventListener
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class ComplexPlanRunShutdownHandler(
|
||||
private val complexPlanRunPersistenceService: ComplexPlanRunPersistenceService
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
@EventListener(ContextClosedEvent::class)
|
||||
fun onContextClosed() {
|
||||
try {
|
||||
val updatedRuns = complexPlanRunPersistenceService.markActiveRunsFailedOnShutdown()
|
||||
if (updatedRuns > 0) {
|
||||
logger.info("complexPlanRun shutdown handler completed: updatedRuns={}", updatedRuns)
|
||||
}
|
||||
} catch (ex: DataAccessException) {
|
||||
logger.warn("complexPlanRun shutdown handler skipped due to data access issue: {}", ex.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||
import org.springframework.context.event.EventListener
|
||||
import org.springframework.dao.DataAccessException
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class ComplexPlanRunStartupRecovery(
|
||||
private val complexPlanRunPersistenceService: ComplexPlanRunPersistenceService
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
@EventListener(ApplicationReadyEvent::class)
|
||||
fun onApplicationReady() {
|
||||
try {
|
||||
val recoveredRuns = complexPlanRunPersistenceService.markRunningRunsFailedOnStartup()
|
||||
if (recoveredRuns > 0) {
|
||||
logger.info("complexPlanRun startup recovery completed: recoveredRuns={}", recoveredRuns)
|
||||
}
|
||||
} catch (ex: DataAccessException) {
|
||||
logger.warn("complexPlanRun startup recovery skipped due to data access issue: {}", ex.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.web.reactive.function.client.WebClientRequestException
|
||||
import org.springframework.web.reactive.function.client.WebClient
|
||||
import org.springframework.web.reactive.function.client.bodyToFlux
|
||||
import reactor.core.publisher.Mono
|
||||
import space.nstart.pcp.pcp_satellites_service.configuration.CustomErrorException
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.CoverageCalculationSource
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SatelliteMissionBatchRequestDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SatelliteMissionBatchResponseDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.OccupiedIntervalDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageBatchRequestDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageBatchResponseDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageStrategy
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageTargetDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SurveySlotDTO
|
||||
import tools.jackson.databind.ObjectMapper
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
@Service
|
||||
class CoverageBatchClient(webClientBuilderProvider: ObjectProvider<WebClient.Builder>) : CoverageCalculationClient {
|
||||
|
||||
override val source: CoverageCalculationSource = CoverageCalculationSource.SLOTS
|
||||
|
||||
private val webClientBuilder: WebClient.Builder = webClientBuilderProvider.ifAvailable ?: WebClient.builder()
|
||||
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
private data class CoverageBatchCallResult(
|
||||
val response: List<SlotCoverageBatchResponseDTO>,
|
||||
val durationMs: Long,
|
||||
val coverageStrategy: SlotCoverageStrategy
|
||||
)
|
||||
|
||||
private class CoverageBatchRemoteException(
|
||||
val statusCode: Int,
|
||||
message: String
|
||||
) : RuntimeException(message)
|
||||
|
||||
@Value("\${settings.slots-service:slots-service}")
|
||||
val url = ""
|
||||
|
||||
private fun client() = webClientBuilder.baseUrl(url).build()
|
||||
|
||||
fun plannedModes(
|
||||
satelliteIds: List<Long>,
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime
|
||||
): Map<Long, List<SurveySlotDTO>> {
|
||||
if (satelliteIds.isEmpty()) {
|
||||
return emptyMap()
|
||||
}
|
||||
|
||||
val selectedSatelliteIds = satelliteIds.distinct()
|
||||
logger.info(
|
||||
"slots-service plannedModes batch start: satellites={}, interval=[{} - {}]",
|
||||
selectedSatelliteIds.size,
|
||||
intervalStart,
|
||||
intervalEnd
|
||||
)
|
||||
|
||||
var response = emptyList<SatelliteMissionBatchResponseDTO>()
|
||||
val durationMs = measureTimeMillis {
|
||||
response = client()
|
||||
.post()
|
||||
.uri("/api/slots/mission/satellites")
|
||||
.bodyValue(
|
||||
SatelliteMissionBatchRequestDTO(
|
||||
satelliteIds = selectedSatelliteIds,
|
||||
timeStart = intervalStart,
|
||||
timeStop = intervalEnd
|
||||
)
|
||||
)
|
||||
.retrieve()
|
||||
.onStatus({ status -> status.isError }) { response ->
|
||||
response.bodyToMono(String::class.java)
|
||||
.defaultIfEmpty("error")
|
||||
.flatMap { body -> Mono.error(CustomErrorException(extractErrorMessage(body))) }
|
||||
}
|
||||
.bodyToFlux<SatelliteMissionBatchResponseDTO>()
|
||||
.collectList()
|
||||
.block()
|
||||
.orEmpty()
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"slots-service plannedModes batch finish: satellites={}, chains={}, durationMs={}",
|
||||
selectedSatelliteIds.size,
|
||||
response.sumOf { it.slots.size },
|
||||
durationMs
|
||||
)
|
||||
|
||||
return response.associate { it.satelliteId to it.slots }
|
||||
}
|
||||
|
||||
override fun coverageModes(
|
||||
targets: List<SlotCoverageTargetDTO>,
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime,
|
||||
satelliteIds: List<Long>,
|
||||
occupiedIntervals: List<OccupiedIntervalDTO>,
|
||||
sun: Double?,
|
||||
coverageStrategy: SlotCoverageStrategy?
|
||||
): Map<Long, List<SlotDTO>> {
|
||||
if (targets.isEmpty() || satelliteIds.isEmpty()) {
|
||||
return emptyMap()
|
||||
}
|
||||
|
||||
val selectedSatelliteIds = satelliteIds.distinct()
|
||||
logger.info(
|
||||
"slots-service coverage batch start: targets={}, satellites={}, occupiedIntervals={}, interval=[{} - {}], sun={}, coverageStrategy={}",
|
||||
targets.size,
|
||||
selectedSatelliteIds.size,
|
||||
occupiedIntervals.size,
|
||||
intervalStart,
|
||||
intervalEnd,
|
||||
sun,
|
||||
coverageStrategy
|
||||
)
|
||||
|
||||
val effectiveStrategy = coverageStrategy ?: SlotCoverageStrategy.GREEDY
|
||||
|
||||
val result = try {
|
||||
requestCoverageModes(
|
||||
targets = targets,
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
selectedSatelliteIds = selectedSatelliteIds,
|
||||
occupiedIntervals = occupiedIntervals,
|
||||
sun = sun,
|
||||
coverageStrategy = effectiveStrategy
|
||||
)
|
||||
} catch (ex: Exception) {
|
||||
if (effectiveStrategy == SlotCoverageStrategy.CONTINUOUS && isContinuousFallbackCandidate(ex)) {
|
||||
logger.warn(
|
||||
"slots-service coverage batch CONTINUOUS failed, fallback to GREEDY: targets={}, satellites={}, occupiedIntervals={}, interval=[{} - {}], causeType={}, cause={}",
|
||||
targets.size,
|
||||
selectedSatelliteIds.size,
|
||||
occupiedIntervals.size,
|
||||
intervalStart,
|
||||
intervalEnd,
|
||||
ex::class.simpleName,
|
||||
ex.message
|
||||
)
|
||||
requestCoverageModes(
|
||||
targets = targets,
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
selectedSatelliteIds = selectedSatelliteIds,
|
||||
occupiedIntervals = occupiedIntervals,
|
||||
sun = sun,
|
||||
coverageStrategy = SlotCoverageStrategy.GREEDY
|
||||
)
|
||||
} else {
|
||||
throw toCustomError(ex)
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"slots-service coverage batch finish: targets={}, returnedTargets={}, candidateSlots={}, durationMs={}, effectiveCoverageStrategy={}",
|
||||
targets.size,
|
||||
result.response.size,
|
||||
result.response.sumOf { it.slots.size },
|
||||
result.durationMs,
|
||||
result.coverageStrategy
|
||||
)
|
||||
|
||||
return result.response.associate { it.targetId to it.slots }
|
||||
}
|
||||
|
||||
private fun requestCoverageModes(
|
||||
targets: List<SlotCoverageTargetDTO>,
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime,
|
||||
selectedSatelliteIds: List<Long>,
|
||||
occupiedIntervals: List<OccupiedIntervalDTO>,
|
||||
sun: Double?,
|
||||
coverageStrategy: SlotCoverageStrategy
|
||||
): CoverageBatchCallResult {
|
||||
var response = emptyList<SlotCoverageBatchResponseDTO>()
|
||||
val durationMs = measureTimeMillis {
|
||||
response = client()
|
||||
.post()
|
||||
.uri("/api/slots/poly-cover/batch")
|
||||
.bodyValue(
|
||||
SlotCoverageBatchRequestDTO(
|
||||
items = targets,
|
||||
timeStart = intervalStart,
|
||||
timeStop = intervalEnd,
|
||||
cov = true,
|
||||
satellites = selectedSatelliteIds,
|
||||
occupiedIntervals = occupiedIntervals,
|
||||
sun = sun,
|
||||
coverageStrategy = coverageStrategy
|
||||
)
|
||||
)
|
||||
.retrieve()
|
||||
.onStatus({ status -> status.isError }) { response ->
|
||||
val statusCode = response.statusCode().value()
|
||||
response.bodyToMono(String::class.java)
|
||||
.defaultIfEmpty("error")
|
||||
.flatMap { body ->
|
||||
Mono.error(CoverageBatchRemoteException(statusCode, extractErrorMessage(body)))
|
||||
}
|
||||
}
|
||||
.bodyToFlux<SlotCoverageBatchResponseDTO>()
|
||||
.collectList()
|
||||
.block()
|
||||
.orEmpty()
|
||||
}
|
||||
return CoverageBatchCallResult(
|
||||
response = response,
|
||||
durationMs = durationMs,
|
||||
coverageStrategy = coverageStrategy
|
||||
)
|
||||
}
|
||||
|
||||
private fun isContinuousFallbackCandidate(ex: Exception): Boolean =
|
||||
when (ex) {
|
||||
is CoverageBatchRemoteException -> ex.statusCode >= 500
|
||||
is WebClientRequestException -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
private fun toCustomError(ex: Exception): CustomErrorException =
|
||||
when (ex) {
|
||||
is CustomErrorException -> ex
|
||||
is CoverageBatchRemoteException -> CustomErrorException(ex.message ?: "error")
|
||||
else -> CustomErrorException(ex.message ?: "error")
|
||||
}
|
||||
|
||||
private fun extractErrorMessage(body: String): String {
|
||||
val text = body.trim()
|
||||
if (text.isEmpty()) return "error"
|
||||
|
||||
return runCatching {
|
||||
val node = ObjectMapper().readTree(text)
|
||||
when {
|
||||
node.hasNonNull("error") -> node["error"].asText()
|
||||
node.hasNonNull("message") -> node["message"].asText()
|
||||
else -> text
|
||||
}
|
||||
}.getOrDefault(text)
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.CoverageCalculationSource
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.OccupiedIntervalDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageStrategy
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageTargetDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO
|
||||
import java.time.LocalDateTime
|
||||
|
||||
interface CoverageCalculationClient {
|
||||
val source: CoverageCalculationSource
|
||||
|
||||
fun coverageModes(
|
||||
targets: List<SlotCoverageTargetDTO>,
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime,
|
||||
satelliteIds: List<Long>,
|
||||
occupiedIntervals: List<OccupiedIntervalDTO> = emptyList(),
|
||||
sun: Double? = null,
|
||||
coverageStrategy: SlotCoverageStrategy? = null
|
||||
): Map<Long, List<SlotDTO>>
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import org.springframework.stereotype.Service
|
||||
import space.nstart.pcp.pcp_satellites_service.configuration.CustomValidationException
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.CoverageCalculationSource
|
||||
|
||||
@Service
|
||||
class CoverageCalculationClientResolver(
|
||||
clients: List<CoverageCalculationClient>
|
||||
) {
|
||||
private val clientsBySource = clients.associateBy { it.source }
|
||||
|
||||
fun resolve(source: CoverageCalculationSource): CoverageCalculationClient =
|
||||
clientsBySource[source]
|
||||
?: throw CustomValidationException("Unsupported coverage calculation source: $source")
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.ObjectProvider
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.web.reactive.function.client.WebClient
|
||||
import reactor.core.publisher.Mono
|
||||
import space.nstart.pcp.pcp_satellites_service.configuration.CustomErrorException
|
||||
import space.nstart.pcp.pcp_types_lib.dto.coverage.CoverageSchemeCalculateRequestDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.coverage.CoverageSchemeResponseDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.CoverageCalculationSource
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.OccupiedIntervalDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageStrategy
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageTargetDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SurveySlotDTO
|
||||
import tools.jackson.databind.ObjectMapper
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
@Service
|
||||
class CoverageSchemeCalculationClient(webClientBuilderProvider: ObjectProvider<WebClient.Builder>) : CoverageCalculationClient {
|
||||
|
||||
override val source: CoverageCalculationSource = CoverageCalculationSource.COVERAGE_SCHEME
|
||||
|
||||
private val webClientBuilder: WebClient.Builder = webClientBuilderProvider.ifAvailable ?: WebClient.builder()
|
||||
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
@Value("\${settings.coverage-scheme-service:pcp-coverage-scheme-service}")
|
||||
val url = ""
|
||||
|
||||
private fun client() = webClientBuilder.baseUrl(url).build()
|
||||
|
||||
override fun coverageModes(
|
||||
targets: List<SlotCoverageTargetDTO>,
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime,
|
||||
satelliteIds: List<Long>,
|
||||
occupiedIntervals: List<OccupiedIntervalDTO>,
|
||||
sun: Double?,
|
||||
coverageStrategy: SlotCoverageStrategy?
|
||||
): Map<Long, List<SlotDTO>> {
|
||||
if (targets.isEmpty() || satelliteIds.isEmpty()) {
|
||||
return emptyMap()
|
||||
}
|
||||
|
||||
val selectedSatelliteIds = satelliteIds.distinct()
|
||||
|
||||
logger.info(
|
||||
"coverage-scheme-service coverage start: targets={}, satellites={}, occupiedIntervals={}, interval=[{} - {}]",
|
||||
targets.size,
|
||||
selectedSatelliteIds.size,
|
||||
occupiedIntervals.size,
|
||||
intervalStart,
|
||||
intervalEnd
|
||||
)
|
||||
|
||||
val result = linkedMapOf<Long, List<SlotDTO>>()
|
||||
val durationMs = measureTimeMillis {
|
||||
targets.forEach { target ->
|
||||
lateinit var response: CoverageSchemeResponseDTO
|
||||
val targetDurationMs = measureTimeMillis {
|
||||
response = calculateTarget(
|
||||
target = target,
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
satelliteIds = selectedSatelliteIds,
|
||||
sun = sun
|
||||
)
|
||||
}
|
||||
result[target.targetId] = response.coverageScheme.map { it.toSlotDTO() }
|
||||
logger.info(
|
||||
"coverage-scheme-service target calculated: targetId={}, candidateSlots={}, durationMs={}",
|
||||
target.targetId,
|
||||
response.coverageScheme.size,
|
||||
targetDurationMs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"coverage-scheme-service coverage finish: targets={}, returnedTargets={}, candidateSlots={}, durationMs={}",
|
||||
targets.size,
|
||||
result.size,
|
||||
result.values.sumOf { it.size },
|
||||
durationMs
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private fun calculateTarget(
|
||||
target: SlotCoverageTargetDTO,
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime,
|
||||
satelliteIds: List<Long>,
|
||||
sun: Double?
|
||||
): CoverageSchemeResponseDTO =
|
||||
client()
|
||||
.post()
|
||||
.uri("/api/coverage-schemes/calculate")
|
||||
.bodyValue(
|
||||
CoverageSchemeCalculateRequestDTO(
|
||||
polygonWkt = target.wkt,
|
||||
satelliteIds = satelliteIds,
|
||||
timeStart = intervalStart,
|
||||
timeEnd = intervalEnd,
|
||||
sunAngle = sun
|
||||
)
|
||||
)
|
||||
.retrieve()
|
||||
.onStatus({ status -> status.isError }) { response ->
|
||||
response.bodyToMono(String::class.java)
|
||||
.defaultIfEmpty("error")
|
||||
.flatMap { body -> Mono.error(CustomErrorException(extractErrorMessage(body))) }
|
||||
}
|
||||
.bodyToMono(CoverageSchemeResponseDTO::class.java)
|
||||
.block()
|
||||
?: CoverageSchemeResponseDTO()
|
||||
|
||||
private fun SurveySlotDTO.toSlotDTO() =
|
||||
SlotDTO(
|
||||
satelliteId = satelliteId,
|
||||
tn = tn,
|
||||
tk = tk,
|
||||
roll = roll,
|
||||
contour = contour,
|
||||
revolution = revolution,
|
||||
revolutionSign = revolutionSign,
|
||||
latitude = latitude,
|
||||
longitude = longitude
|
||||
)
|
||||
|
||||
private fun extractErrorMessage(body: String): String {
|
||||
val text = body.trim()
|
||||
if (text.isEmpty()) return "error"
|
||||
|
||||
return runCatching {
|
||||
val node = ObjectMapper().readTree(text)
|
||||
when {
|
||||
node.hasNonNull("error") -> node["error"].asText()
|
||||
node.hasNonNull("message") -> node["message"].asText()
|
||||
else -> text
|
||||
}
|
||||
}.getOrDefault(text)
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import org.springframework.beans.factory.ObjectProvider
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.web.reactive.function.client.WebClient
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.CellRequestDOT
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.EarthCellWithRequestsDTO
|
||||
import java.util.UUID
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
|
||||
@Service
|
||||
/**
|
||||
* Request/grid reader backed by pcp-request-service /v1 cells API.
|
||||
*/
|
||||
class EarthGridService(webClientBuilderProvider: ObjectProvider<WebClient.Builder>) {
|
||||
private val webClientBuilder: WebClient.Builder = webClientBuilderProvider.ifAvailable ?: WebClient.builder()
|
||||
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
@Value("\${settings.request-service:\${settings.earth-grid-service:pcp-request-service}}")
|
||||
val url = ""
|
||||
|
||||
private fun client() = webClientBuilder.baseUrl(url).build()
|
||||
|
||||
fun cells(countLat: Int? = null, countLong: Int? = null): Iterable<EarthCellWithRequestsDTO> {
|
||||
var cells = emptyList<EarthCellWithRequestsDTO>()
|
||||
val durationMs = measureTimeMillis {
|
||||
cells = client()
|
||||
.get()
|
||||
.uri(cellsWithRequestsUri(countLat, countLong))
|
||||
.retrieve()
|
||||
.bodyToMono(CellsWithRequestsResponseDto::class.java)
|
||||
.block()
|
||||
?.items
|
||||
.orEmpty()
|
||||
.map { cell -> cell.toEarthCellWithRequestsDto() }
|
||||
}
|
||||
logger.info(
|
||||
"request-service v1 cells with requests loaded: cells={}, durationMs={}",
|
||||
cells.size,
|
||||
durationMs
|
||||
)
|
||||
return cells
|
||||
}
|
||||
|
||||
private fun cellsWithRequestsUri(countLat: Int?, countLong: Int?): String {
|
||||
val params = mutableListOf("minImportance=$REQUEST_GRID_MIN_IMPORTANCE")
|
||||
countLat?.let { params += "countLat=$it" }
|
||||
countLong?.let { params += "countLong=$it" }
|
||||
return "/v1/cells/with-requests?${params.joinToString("&")}"
|
||||
}
|
||||
|
||||
private fun CellWithRequestsResponseDto.toEarthCellWithRequestsDto(): EarthCellWithRequestsDTO =
|
||||
EarthCellWithRequestsDTO(
|
||||
id = cellNum,
|
||||
num = cellNum,
|
||||
latitude = latitude,
|
||||
longitude = longitude,
|
||||
importance = importance,
|
||||
contour = contour,
|
||||
requests = requests.map { request -> request.toCellRequestDto() },
|
||||
)
|
||||
|
||||
private fun CellRequestFragmentResponseDto.toCellRequestDto(): CellRequestDOT =
|
||||
CellRequestDOT(
|
||||
requestId = requestId.toString(),
|
||||
covPercent = coveragePercent,
|
||||
importance = importance,
|
||||
contour = contour,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class CellsWithRequestsResponseDto(
|
||||
val items: List<CellWithRequestsResponseDto> = emptyList(),
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class CellWithRequestsResponseDto(
|
||||
val cellNum: Long = 0,
|
||||
val latitude: Double = 0.0,
|
||||
val longitude: Double = 0.0,
|
||||
val importance: Double = 0.0,
|
||||
val contour: String = "",
|
||||
val requests: List<CellRequestFragmentResponseDto> = emptyList(),
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class CellRequestFragmentResponseDto(
|
||||
val requestId: UUID = UUID(0L, 0L),
|
||||
val contour: String = "",
|
||||
val coveragePercent: Double = 0.0,
|
||||
val importance: Double = 0.0,
|
||||
)
|
||||
|
||||
private const val REQUEST_GRID_MIN_IMPORTANCE = "0.001"
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.web.reactive.function.client.WebClient
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteBatchRequestDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteSummaryDTO
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
interface SatelliteCatalogClient {
|
||||
fun allSatellites(): List<SatelliteDTO>
|
||||
}
|
||||
|
||||
@Service
|
||||
class SatelliteCatalogWebClient(
|
||||
webClientBuilderProvider: ObjectProvider<WebClient.Builder>
|
||||
) : SatelliteCatalogClient {
|
||||
|
||||
private val webClientBuilder: WebClient.Builder = webClientBuilderProvider.ifAvailable ?: WebClient.builder()
|
||||
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
@Value("\${settings.satellite-catalog-service:pcp-satellite-catalog-service}")
|
||||
private val satelliteCatalogServiceUrl = ""
|
||||
|
||||
override fun allSatellites(): List<SatelliteDTO> {
|
||||
var summaries = emptyList<SatelliteSummaryDTO>()
|
||||
val summariesMs = measureTimeMillis {
|
||||
summaries = webClientBuilder.build()
|
||||
.get()
|
||||
.uri("$satelliteCatalogServiceUrl/api/satellites")
|
||||
.retrieve()
|
||||
.bodyToFlux(SatelliteSummaryDTO::class.java)
|
||||
.collectList()
|
||||
.block()
|
||||
.orEmpty()
|
||||
}
|
||||
logger.info(
|
||||
"satellite-catalog-service summaries loaded: satellites={}, durationMs={}",
|
||||
summaries.size,
|
||||
summariesMs
|
||||
)
|
||||
if (summaries.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
var satellites = emptyList<SatelliteDTO>()
|
||||
val detailsMs = measureTimeMillis {
|
||||
satellites = webClientBuilder.build()
|
||||
.post()
|
||||
.uri("$satelliteCatalogServiceUrl/api/satellites/batch")
|
||||
.bodyValue(SatelliteBatchRequestDTO(ids = summaries.map { it.id }))
|
||||
.retrieve()
|
||||
.bodyToFlux(SatelliteDTO::class.java)
|
||||
.collectList()
|
||||
.block()
|
||||
.orEmpty()
|
||||
}
|
||||
logger.info(
|
||||
"satellite-catalog-service details loaded: requested={}, returned={}, durationMs={}",
|
||||
summaries.size,
|
||||
satellites.size,
|
||||
detailsMs
|
||||
)
|
||||
return satellites
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.ObjectProvider
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||
import org.springframework.kafka.annotation.KafkaListener
|
||||
import org.springframework.stereotype.Component
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteDeletedEventDTO
|
||||
import space.nstart.pcp.pcp_types_lib.message.KafkaMessage
|
||||
import space.nstart.pcp.pcp_types_lib.message.PcpKafkaEvent
|
||||
|
||||
private const val SATELLITE_DELETED_CONSUMER_GROUP = "pcp-complex-mission-service-satellite-deleted"
|
||||
|
||||
@ConditionalOnProperty(name = ["spring.kafka.bootstrap-servers"], matchIfMissing = false)
|
||||
@Component
|
||||
class SatelliteDeletedKafkaListener(
|
||||
objectMapperProvider: ObjectProvider<ObjectMapper>,
|
||||
private val satelliteDeletedService: SatelliteDeletedService
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||
private val objectMapper = objectMapperProvider.ifAvailable ?: jacksonObjectMapper().findAndRegisterModules()
|
||||
|
||||
@KafkaListener(
|
||||
topics = ["\${app.kafka.topics.satellites:pcp.satellites}"],
|
||||
groupId = SATELLITE_DELETED_CONSUMER_GROUP
|
||||
)
|
||||
fun consume(message: String) {
|
||||
try {
|
||||
val kafkaMessage = objectMapper.readValue(
|
||||
message,
|
||||
object : TypeReference<KafkaMessage<SatelliteDeletedEventDTO>>() {}
|
||||
)
|
||||
if (kafkaMessage.type != PcpKafkaEvent.SatelliteDeletedEvent) {
|
||||
logger.debug("Ignoring Kafka message with unsupported type {}", kafkaMessage.type)
|
||||
return
|
||||
}
|
||||
|
||||
val event = kafkaMessage.data
|
||||
logger.info(
|
||||
"Received SatelliteDeletedEvent in pcp-complex-mission-service: satelliteId={}, noradId={}, eventId={}, traceId={}, source={}, consumerGroup={}",
|
||||
event.satelliteId,
|
||||
event.noradId,
|
||||
kafkaMessage.id,
|
||||
kafkaMessage.traceId,
|
||||
kafkaMessage.source,
|
||||
SATELLITE_DELETED_CONSUMER_GROUP
|
||||
)
|
||||
event.deleteIdentifiers().forEach { satelliteId ->
|
||||
satelliteDeletedService.deleteSatelliteData(satelliteId)
|
||||
}
|
||||
logger.info(
|
||||
"SatelliteDeletedEvent processed in pcp-complex-mission-service: satelliteId={}, noradId={}, eventId={}, traceId={}, source={}, consumerGroup={}",
|
||||
event.satelliteId,
|
||||
event.noradId,
|
||||
kafkaMessage.id,
|
||||
kafkaMessage.traceId,
|
||||
kafkaMessage.source,
|
||||
SATELLITE_DELETED_CONSUMER_GROUP
|
||||
)
|
||||
} catch (exception: Exception) {
|
||||
logger.error("Failed to process SatelliteDeletedEvent in pcp-complex-mission-service", exception)
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
|
||||
private fun SatelliteDeletedEventDTO.deleteIdentifiers(): List<Long> =
|
||||
listOfNotNull(satelliteId, noradId).distinct()
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.ComplexPlanRunSatelliteRepository
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeRepository
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeSnapshotRepository
|
||||
|
||||
@Service
|
||||
class SatelliteDeletedService(
|
||||
private val satelliteModeSnapshotRepository: SatelliteModeSnapshotRepository,
|
||||
private val satelliteModeRepository: SatelliteModeRepository,
|
||||
private val complexPlanRunSatelliteRepository: ComplexPlanRunSatelliteRepository
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
@Transactional
|
||||
fun deleteSatelliteData(satelliteId: Long): SatelliteComplexMissionDeletionSummary {
|
||||
val modesDeletedByCascade = satelliteModeRepository.countBySatelliteId(satelliteId)
|
||||
val snapshotsBeforeDelete = satelliteModeSnapshotRepository.countBySatelliteId(satelliteId)
|
||||
val runLinksBeforeDelete = complexPlanRunSatelliteRepository.countBySatelliteId(satelliteId)
|
||||
|
||||
val snapshotsDeleted = satelliteModeSnapshotRepository.deleteAllBySatelliteId(satelliteId)
|
||||
val runLinksDeleted = complexPlanRunSatelliteRepository.deleteAllBySatelliteId(satelliteId)
|
||||
val summary = SatelliteComplexMissionDeletionSummary(
|
||||
satelliteId = satelliteId,
|
||||
snapshotsDeleted = snapshotsDeleted,
|
||||
modesDeletedByCascade = modesDeletedByCascade,
|
||||
runLinksDeleted = runLinksDeleted
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Deleted complex mission data for satellite: satelliteId={}, snapshotsDeleted={}, snapshotsBeforeDelete={}, modesDeletedByCascade={}, runLinksDeleted={}, runLinksBeforeDelete={}",
|
||||
summary.satelliteId,
|
||||
summary.snapshotsDeleted,
|
||||
snapshotsBeforeDelete,
|
||||
summary.modesDeletedByCascade,
|
||||
summary.runLinksDeleted,
|
||||
runLinksBeforeDelete
|
||||
)
|
||||
return summary
|
||||
}
|
||||
}
|
||||
|
||||
data class SatelliteComplexMissionDeletionSummary(
|
||||
val satelliteId: Long,
|
||||
val snapshotsDeleted: Long,
|
||||
val modesDeletedByCascade: Long,
|
||||
val runLinksDeleted: Long
|
||||
)
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import org.locationtech.jts.io.WKTReader
|
||||
import org.springframework.stereotype.Component
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.ComplexPlanRunEntity
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeBookedSlotEntity
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeCellEntity
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeEntity
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeSnapshotEntity
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeType
|
||||
import space.nstart.pcp.pcp_satellites_service.model.mode.SurveyMode
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SatelliteModeResponseDTO
|
||||
|
||||
@Component
|
||||
class SatelliteModeMapper {
|
||||
|
||||
fun toEntity(
|
||||
satelliteId: Long,
|
||||
survey: SurveyMode,
|
||||
snapshot: SatelliteModeSnapshotEntity? = null,
|
||||
complexPlanRun: ComplexPlanRunEntity? = null
|
||||
): SatelliteModeEntity {
|
||||
val (latitude, longitude) = resolveCoordinates(survey)
|
||||
val entity = SatelliteModeEntity(
|
||||
satelliteId = satelliteId,
|
||||
source = survey.source,
|
||||
type = SatelliteModeType.SURVEY,
|
||||
startTime = survey.time,
|
||||
endTime = survey.timeStop,
|
||||
revolution = survey.revolution,
|
||||
lat = latitude,
|
||||
longitude = longitude,
|
||||
duration = survey.duration,
|
||||
contourWkt = survey.contourWKT,
|
||||
roll = survey.roll,
|
||||
cellNum = survey.primaryCellNum(),
|
||||
snapshot = snapshot,
|
||||
complexPlanRun = complexPlanRun
|
||||
)
|
||||
|
||||
entity.bookedSlots = survey.bookedSlotIds
|
||||
.filter { it > 0 }
|
||||
.distinct()
|
||||
.map { bookedSlotId ->
|
||||
SatelliteModeBookedSlotEntity(
|
||||
mode = entity,
|
||||
bookedSlotId = bookedSlotId
|
||||
)
|
||||
}
|
||||
.toMutableList()
|
||||
|
||||
entity.cells = survey.cellNumsForPersistence()
|
||||
.map { cellNum ->
|
||||
SatelliteModeCellEntity(
|
||||
mode = entity,
|
||||
cellNum = cellNum
|
||||
)
|
||||
}
|
||||
.toMutableList()
|
||||
|
||||
return entity
|
||||
}
|
||||
|
||||
fun toSurveyMode(
|
||||
entity: SatelliteModeEntity,
|
||||
bookedSlotIds: List<Long> = emptyList(),
|
||||
cellNums: List<Long> = emptyList()
|
||||
): SurveyMode =
|
||||
SurveyMode(
|
||||
revolution = entity.revolution,
|
||||
time = entity.startTime,
|
||||
timeStop = entity.endTime,
|
||||
roll = entity.roll,
|
||||
latitude = entity.lat,
|
||||
longitude = entity.longitude,
|
||||
duration = entity.duration,
|
||||
contourWKT = entity.contourWkt ?: "",
|
||||
cellNum = primaryCellNum(cellNums, entity.cellNum),
|
||||
source = entity.source,
|
||||
bookedSlotIds = bookedSlotIds,
|
||||
cellNums = normalizeCellNums(cellNums, entity.cellNum)
|
||||
)
|
||||
|
||||
fun toResponse(
|
||||
entity: SatelliteModeEntity,
|
||||
bookedSlotIds: List<Long> = emptyList(),
|
||||
cellNums: List<Long> = emptyList()
|
||||
): SatelliteModeResponseDTO =
|
||||
SatelliteModeResponseDTO(
|
||||
id = entity.id ?: 0,
|
||||
satelliteId = entity.satelliteId,
|
||||
source = entity.source.name,
|
||||
type = entity.type.name,
|
||||
startTime = entity.startTime,
|
||||
endTime = entity.endTime,
|
||||
revolution = entity.revolution,
|
||||
lat = entity.lat,
|
||||
longitude = entity.longitude,
|
||||
duration = entity.duration,
|
||||
contourWkt = entity.contourWkt,
|
||||
roll = entity.roll,
|
||||
cellNum = primaryCellNum(cellNums, entity.cellNum),
|
||||
cellNums = normalizeCellNums(cellNums, entity.cellNum),
|
||||
bookedSlotIds = bookedSlotIds,
|
||||
snapshotId = entity.snapshot?.id
|
||||
)
|
||||
|
||||
private fun SurveyMode.primaryCellNum(): Long =
|
||||
cellNumsForPersistence().singleOrNull() ?: -1
|
||||
|
||||
private fun SurveyMode.cellNumsForPersistence(): List<Long> =
|
||||
normalizeCellNums(cellNums, cellNum)
|
||||
|
||||
private fun normalizeCellNums(cellNums: List<Long>, fallbackCellNum: Long): List<Long> {
|
||||
val normalized = cellNums
|
||||
.ifEmpty { if (fallbackCellNum > 0) listOf(fallbackCellNum) else emptyList() }
|
||||
.filter { it > 0 }
|
||||
.distinct()
|
||||
.sorted()
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
private fun primaryCellNum(cellNums: List<Long>, fallbackCellNum: Long): Long =
|
||||
normalizeCellNums(cellNums, fallbackCellNum).singleOrNull() ?: -1
|
||||
|
||||
private fun resolveCoordinates(survey: SurveyMode): Pair<Double, Double> {
|
||||
if (survey.latitude != 0.0 || survey.longitude != 0.0) {
|
||||
return survey.latitude to survey.longitude
|
||||
}
|
||||
if (survey.contourWKT.isBlank()) {
|
||||
return survey.latitude to survey.longitude
|
||||
}
|
||||
|
||||
return runCatching {
|
||||
val centroid = WKTReader().read(survey.contourWKT).centroid
|
||||
centroid.y to centroid.x
|
||||
}.getOrDefault(survey.latitude to survey.longitude)
|
||||
}
|
||||
}
|
||||
+321
@@ -0,0 +1,321 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.ComplexPlanRunEntity
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeEntity
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeSource
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeSnapshotEntity
|
||||
import space.nstart.pcp.pcp_satellites_service.configuration.CustomValidationException
|
||||
import space.nstart.pcp.pcp_satellites_service.model.mode.SurveyMode
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeBookedSlotRepository
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeCellRepository
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeRepository
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeSnapshotRepository
|
||||
import java.time.Clock
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
@Service
|
||||
class SatelliteModePersistenceService(
|
||||
private val satelliteModeRepository: SatelliteModeRepository,
|
||||
private val satelliteModeBookedSlotRepository: SatelliteModeBookedSlotRepository,
|
||||
private val satelliteModeCellRepository: SatelliteModeCellRepository,
|
||||
private val satelliteModeSnapshotRepository: SatelliteModeSnapshotRepository,
|
||||
private val satelliteModeMapper: SatelliteModeMapper,
|
||||
private val clock: Clock
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
@Transactional
|
||||
fun deleteModesForInterval(
|
||||
satelliteIds: Collection<Long>,
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime
|
||||
) {
|
||||
validateInterval(intervalStart, intervalEnd)
|
||||
|
||||
val selectedSatelliteIds = satelliteIds.distinct()
|
||||
if (selectedSatelliteIds.isEmpty()) {
|
||||
logger.info("Удаление режимов пропущено: пустой список спутников")
|
||||
return
|
||||
}
|
||||
|
||||
var modeIds = emptyList<Long>()
|
||||
var snapshotIds = emptyList<Long>()
|
||||
val lookupMs = measureTimeMillis {
|
||||
modeIds = satelliteModeRepository.findIntersectingIdsBySatelliteIdsAndInterval(
|
||||
selectedSatelliteIds,
|
||||
intervalStart,
|
||||
intervalEnd
|
||||
)
|
||||
snapshotIds = satelliteModeSnapshotRepository.findIntersectingIdsBySatelliteIdsAndInterval(
|
||||
selectedSatelliteIds,
|
||||
intervalStart,
|
||||
intervalEnd
|
||||
)
|
||||
}
|
||||
logger.info(
|
||||
"Satellite mode delete stage lookup completed: satellites={}, snapshots={}, modes={}, durationMs={}",
|
||||
selectedSatelliteIds,
|
||||
snapshotIds.size,
|
||||
modeIds.size,
|
||||
lookupMs
|
||||
)
|
||||
|
||||
if (modeIds.isEmpty() && snapshotIds.isEmpty()) {
|
||||
logger.info(
|
||||
"Для удаления не найдено пересекающихся snapshot/result-set: satellites={}, interval=[{} - {}]",
|
||||
selectedSatelliteIds,
|
||||
intervalStart,
|
||||
intervalEnd
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"Удаление legacy snapshot/result-set: satellites={}, interval=[{} - {}], snapshots={}, modes={}",
|
||||
selectedSatelliteIds,
|
||||
intervalStart,
|
||||
intervalEnd,
|
||||
snapshotIds.size,
|
||||
modeIds.size
|
||||
)
|
||||
val deleteMs = measureTimeMillis {
|
||||
if (modeIds.isNotEmpty()) {
|
||||
satelliteModeBookedSlotRepository.deleteAllByModeIds(modeIds)
|
||||
satelliteModeCellRepository.deleteAllByModeIds(modeIds)
|
||||
satelliteModeRepository.deleteIntersectingBySatelliteIdsAndInterval(
|
||||
selectedSatelliteIds,
|
||||
intervalStart,
|
||||
intervalEnd
|
||||
)
|
||||
}
|
||||
if (snapshotIds.isNotEmpty()) {
|
||||
satelliteModeSnapshotRepository.deleteAllByIdInBatch(snapshotIds)
|
||||
}
|
||||
}
|
||||
logger.info("Удаление legacy snapshot/result-set завершено: durationMs={}", deleteMs)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun saveModesForInterval(
|
||||
satelliteIds: Collection<Long>,
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime,
|
||||
surveysBySatelliteId: Map<Long, List<SurveyMode>>,
|
||||
complexPlanRun: ComplexPlanRunEntity? = null
|
||||
): SatelliteModeSaveResult {
|
||||
validateInterval(intervalStart, intervalEnd)
|
||||
|
||||
val selectedSatelliteIds = satelliteIds.distinct()
|
||||
if (selectedSatelliteIds.isEmpty()) {
|
||||
logger.info("Сохранение режимов пропущено: пустой список спутников")
|
||||
return SatelliteModeSaveResult()
|
||||
}
|
||||
|
||||
return saveSnapshotModes(
|
||||
satelliteIds = selectedSatelliteIds,
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
surveysBySatelliteId = surveysBySatelliteId,
|
||||
complexPlanRun = complexPlanRun,
|
||||
activateSnapshot = true
|
||||
)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun replaceModesForInterval(
|
||||
satelliteIds: Collection<Long>,
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime,
|
||||
surveysBySatelliteId: Map<Long, List<SurveyMode>>,
|
||||
complexPlanRun: ComplexPlanRunEntity? = null
|
||||
): SatelliteModeSaveResult {
|
||||
validateInterval(intervalStart, intervalEnd)
|
||||
val selectedSatelliteIds = satelliteIds.distinct()
|
||||
if (selectedSatelliteIds.isEmpty()) {
|
||||
logger.info("Atomic replace режимов пропущен: пустой список спутников")
|
||||
return SatelliteModeSaveResult()
|
||||
}
|
||||
|
||||
return saveSnapshotModes(
|
||||
satelliteIds = selectedSatelliteIds,
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
surveysBySatelliteId = surveysBySatelliteId,
|
||||
complexPlanRun = complexPlanRun,
|
||||
activateSnapshot = true
|
||||
)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun clearAll() {
|
||||
logger.info("Полная очистка таблиц satellite_mode_snapshots и satellite_modes")
|
||||
val durationMs = measureTimeMillis {
|
||||
satelliteModeBookedSlotRepository.deleteAllInBatch()
|
||||
satelliteModeCellRepository.deleteAllInBatch()
|
||||
satelliteModeRepository.deleteAllInBatch()
|
||||
satelliteModeSnapshotRepository.deleteAllInBatch()
|
||||
}
|
||||
logger.info("Полная очистка таблиц satellite_mode_snapshots и satellite_modes завершена: durationMs={}", durationMs)
|
||||
}
|
||||
|
||||
private fun saveSnapshotModes(
|
||||
satelliteIds: Collection<Long>,
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime,
|
||||
surveysBySatelliteId: Map<Long, List<SurveyMode>>,
|
||||
complexPlanRun: ComplexPlanRunEntity?,
|
||||
activateSnapshot: Boolean
|
||||
): SatelliteModeSaveResult {
|
||||
lateinit var snapshots: List<SatelliteModeSnapshotEntity>
|
||||
val createSnapshotsMs = measureTimeMillis {
|
||||
snapshots = satelliteIds.map { satelliteId ->
|
||||
SatelliteModeSnapshotEntity(
|
||||
satelliteId = satelliteId,
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
createdAt = LocalDateTime.now(clock),
|
||||
isActive = false,
|
||||
complexPlanRun = complexPlanRun
|
||||
)
|
||||
}
|
||||
satelliteModeSnapshotRepository.saveAll(snapshots)
|
||||
}
|
||||
logger.info(
|
||||
"Satellite mode save stage snapshots-created: snapshots={}, durationMs={}",
|
||||
snapshots.size,
|
||||
createSnapshotsMs
|
||||
)
|
||||
|
||||
lateinit var entities: List<SatelliteModeEntity>
|
||||
val mapEntitiesMs = measureTimeMillis {
|
||||
entities = snapshots.flatMap { snapshot ->
|
||||
surveysBySatelliteId[snapshot.satelliteId]
|
||||
.orEmpty()
|
||||
.filter { intersects(it.time, it.timeStop, intervalStart, intervalEnd) }
|
||||
.map { survey ->
|
||||
satelliteModeMapper.toEntity(
|
||||
satelliteId = snapshot.satelliteId,
|
||||
survey = survey,
|
||||
snapshot = snapshot,
|
||||
complexPlanRun = complexPlanRun
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.info(
|
||||
"Satellite mode save stage entities-mapped: snapshots={}, modes={}, durationMs={}",
|
||||
snapshots.size,
|
||||
entities.size,
|
||||
mapEntitiesMs
|
||||
)
|
||||
|
||||
if (entities.isNotEmpty()) {
|
||||
logger.info(
|
||||
"Сохранение stage 4 snapshot: satellites={}, interval=[{} - {}], snapshots={}, modes={}",
|
||||
satelliteIds,
|
||||
intervalStart,
|
||||
intervalEnd,
|
||||
snapshots.size,
|
||||
entities.size
|
||||
)
|
||||
val saveModesMs = measureTimeMillis {
|
||||
satelliteModeRepository.saveAll(entities)
|
||||
}
|
||||
logger.info(
|
||||
"Satellite mode save stage modes-saved: modes={}, durationMs={}",
|
||||
entities.size,
|
||||
saveModesMs
|
||||
)
|
||||
} else {
|
||||
logger.info(
|
||||
"Сохранение stage 4 snapshot без режимов: satellites={}, interval=[{} - {}], snapshots={}",
|
||||
satelliteIds,
|
||||
intervalStart,
|
||||
intervalEnd,
|
||||
snapshots.size
|
||||
)
|
||||
}
|
||||
|
||||
val activeSnapshotIds = if (activateSnapshot) {
|
||||
val activateMs = measureTimeMillis {
|
||||
snapshots.forEach { snapshot ->
|
||||
satelliteModeSnapshotRepository.deactivateActiveSnapshots(snapshot.satelliteId)
|
||||
snapshot.isActive = true
|
||||
}
|
||||
}
|
||||
logger.info(
|
||||
"Satellite mode save stage snapshots-activated: snapshots={}, durationMs={}",
|
||||
snapshots.size,
|
||||
activateMs
|
||||
)
|
||||
snapshots.mapNotNull { it.id }
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"Сохранение stage 4 snapshot завершено: snapshots={}, activeSnapshots={}",
|
||||
snapshots.mapNotNull { it.id },
|
||||
activeSnapshotIds
|
||||
)
|
||||
return SatelliteModeSaveResult.from(
|
||||
entities = entities,
|
||||
snapshotIds = snapshots.mapNotNull { it.id },
|
||||
activeSnapshotIds = activeSnapshotIds
|
||||
)
|
||||
}
|
||||
|
||||
private fun validateInterval(intervalStart: LocalDateTime, intervalEnd: LocalDateTime) {
|
||||
if (intervalEnd.isBefore(intervalStart)) {
|
||||
throw CustomValidationException("Параметр intervalEnd должен быть больше или равен intervalStart")
|
||||
}
|
||||
}
|
||||
|
||||
private fun intersects(
|
||||
start1: LocalDateTime,
|
||||
end1: LocalDateTime,
|
||||
start2: LocalDateTime,
|
||||
end2: LocalDateTime
|
||||
): Boolean = start1 < end2 && start2 < end1
|
||||
}
|
||||
|
||||
data class SatelliteModeSaveResult(
|
||||
val calculatedModesCount: Int = 0,
|
||||
val bookedModesCount: Int = 0,
|
||||
val complanModesCount: Int = 0,
|
||||
val mixedModesCount: Int = 0,
|
||||
val affectedSatellitesCount: Int = 0,
|
||||
val processedCellsCount: Int = 0,
|
||||
val bookedSlotLinksCount: Int = 0,
|
||||
val modeIds: List<Long> = emptyList(),
|
||||
val snapshotIds: List<Long> = emptyList(),
|
||||
val activeSnapshotIds: List<Long> = emptyList()
|
||||
) {
|
||||
companion object {
|
||||
fun from(
|
||||
entities: List<SatelliteModeEntity>,
|
||||
snapshotIds: List<Long>,
|
||||
activeSnapshotIds: List<Long>
|
||||
): SatelliteModeSaveResult =
|
||||
SatelliteModeSaveResult(
|
||||
calculatedModesCount = entities.size,
|
||||
bookedModesCount = entities.count { it.source == SatelliteModeSource.SLOTS },
|
||||
complanModesCount = entities.count { it.source == SatelliteModeSource.COMPLAN },
|
||||
mixedModesCount = entities.count { it.source == SatelliteModeSource.MIXED },
|
||||
affectedSatellitesCount = entities.map { it.satelliteId }.distinct().size,
|
||||
processedCellsCount = entities.flatMap { entity -> entity.cells.map { it.cellNum } }
|
||||
.filter { it > 0 }
|
||||
.distinct()
|
||||
.size,
|
||||
bookedSlotLinksCount = entities.sumOf { it.bookedSlots.size },
|
||||
modeIds = entities.mapNotNull { it.id },
|
||||
snapshotIds = snapshotIds,
|
||||
activeSnapshotIds = activeSnapshotIds
|
||||
)
|
||||
}
|
||||
}
|
||||
+511
@@ -0,0 +1,511 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import org.locationtech.jts.io.WKTReader
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.data.domain.Sort
|
||||
import org.springframework.data.jpa.domain.Specification
|
||||
import org.springframework.stereotype.Service
|
||||
import space.nstart.pcp.pcp_satellites_service.configuration.CustomValidationException
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeEntity
|
||||
import space.nstart.pcp.pcp_satellites_service.model.mode.SurveyMode
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeBookedSlotRepository
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeCellRepository
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeRepository
|
||||
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeSnapshotRepository
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.DailyDurationDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.MissionStatisticsDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.RevolutionDurationDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SatelliteModeResponseDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SatelliteModeSnapshotResponseDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.modes.SurveyModeInfoDTO
|
||||
import java.time.Duration
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.LocalTime
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
@Service
|
||||
class SatelliteModeQueryService(
|
||||
private val satelliteModeRepository: SatelliteModeRepository,
|
||||
private val satelliteModeSnapshotRepository: SatelliteModeSnapshotRepository,
|
||||
private val satelliteModeBookedSlotRepository: SatelliteModeBookedSlotRepository,
|
||||
private val satelliteModeCellRepository: SatelliteModeCellRepository,
|
||||
private val satelliteModeMapper: SatelliteModeMapper,
|
||||
private val satelliteModeSnapshotMapper: SatelliteModeSnapshotMapper
|
||||
) {
|
||||
|
||||
private data class ModeKey(
|
||||
val id: Long?,
|
||||
val satelliteId: Long,
|
||||
val startTime: LocalDateTime,
|
||||
val endTime: LocalDateTime
|
||||
)
|
||||
|
||||
private val wktReader = WKTReader()
|
||||
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
fun findMissionModes(satelliteId: Long): List<SurveyModeInfoDTO> =
|
||||
satelliteModeRepository.findAllBySnapshot_SatelliteIdAndSnapshot_IsActiveTrueOrderByStartTimeAsc(satelliteId)
|
||||
.let { modes ->
|
||||
val cellNumsByModeId = cellNumsByModeId(modes.mapNotNull { it.id })
|
||||
modes.map { entity ->
|
||||
satelliteModeMapper.toSurveyMode(
|
||||
entity = entity,
|
||||
cellNums = cellNumsByModeId[entity.id].orEmpty()
|
||||
).toDTO()
|
||||
}
|
||||
}
|
||||
|
||||
fun missionStatistics(satelliteId: Long): MissionStatisticsDTO {
|
||||
val surveys = satelliteModeRepository.findAllBySnapshot_SatelliteIdAndSnapshot_IsActiveTrueOrderByStartTimeAsc(satelliteId)
|
||||
.let { modes ->
|
||||
val cellNumsByModeId = cellNumsByModeId(modes.mapNotNull { it.id })
|
||||
modes.map { entity ->
|
||||
satelliteModeMapper.toSurveyMode(
|
||||
entity = entity,
|
||||
cellNums = cellNumsByModeId[entity.id].orEmpty()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return MissionStatisticsDTO(
|
||||
modesCount = surveys.size,
|
||||
totalContourArea = surveys.sumOf { survey -> contourArea(survey.contourWKT) },
|
||||
revolutionDurations = surveys
|
||||
.groupBy { it.revolution }
|
||||
.toSortedMap()
|
||||
.map { (revolution, items) ->
|
||||
RevolutionDurationDTO(
|
||||
revolution = revolution,
|
||||
totalDuration = items.sumOf { it.duration }
|
||||
)
|
||||
},
|
||||
dailyDurations = surveys
|
||||
.flatMap { survey -> splitSurveyDurationByDay(survey) }
|
||||
.groupBy({ it.first }, { it.second })
|
||||
.toSortedMap()
|
||||
.map { (date, durations) ->
|
||||
DailyDurationDTO(
|
||||
date = date,
|
||||
totalDuration = durations.sum()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun buildPaintGeometry(satelliteId: Long): String {
|
||||
val contours = satelliteModeRepository.findAllBySnapshot_SatelliteIdAndSnapshot_IsActiveTrueOrderByStartTimeAsc(satelliteId)
|
||||
.mapNotNull { it.contourWkt?.takeIf(String::isNotBlank) }
|
||||
|
||||
if (contours.isEmpty()) {
|
||||
return "GEOMETRYCOLLECTION EMPTY"
|
||||
}
|
||||
|
||||
return contours.joinToString(
|
||||
prefix = "GEOMETRYCOLLECTION(",
|
||||
postfix = ")"
|
||||
)
|
||||
}
|
||||
|
||||
fun buildCellPaintGeometry(cellNum: Long): String {
|
||||
val contours = satelliteModeRepository.findAllByRelatedCellNumOrderByStartTimeAsc(cellNum)
|
||||
.mapNotNull { it.contourWkt?.takeIf(String::isNotBlank) }
|
||||
|
||||
if (contours.isEmpty()) {
|
||||
return "GEOMETRYCOLLECTION EMPTY"
|
||||
}
|
||||
|
||||
return contours.joinToString(
|
||||
prefix = "GEOMETRYCOLLECTION(",
|
||||
postfix = ")"
|
||||
)
|
||||
}
|
||||
|
||||
fun findModesByInterval(
|
||||
satelliteIds: Collection<Long>,
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime,
|
||||
runId: Long? = null
|
||||
): List<SatelliteModeResponseDTO> {
|
||||
validateInterval(intervalStart, intervalEnd)
|
||||
if (satelliteIds.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val distinctSatelliteIds = satelliteIds.distinct()
|
||||
val snapshots = if (runId == null) {
|
||||
satelliteModeSnapshotRepository
|
||||
.findAllBySatelliteIdInAndIsActiveTrueOrderBySatelliteIdAscCreatedAtDesc(distinctSatelliteIds)
|
||||
} else {
|
||||
satelliteModeSnapshotRepository
|
||||
.findAllByComplexPlanRun_IdAndSatelliteIdInOrderBySatelliteIdAscCreatedAtDesc(
|
||||
runId,
|
||||
distinctSatelliteIds
|
||||
)
|
||||
}
|
||||
val snapshotIds = snapshots.distinctBy { it.satelliteId }.mapNotNull { it.id }
|
||||
|
||||
return findModesBySnapshotIds(snapshotIds)
|
||||
.filter { mode -> mode.startTime < intervalEnd && intervalStart < mode.endTime }
|
||||
}
|
||||
|
||||
fun findModesByRun(runId: Long): List<SatelliteModeResponseDTO> {
|
||||
val modes = satelliteModeRepository.findAllByComplexPlanRun_IdOrderByStartTimeAsc(runId)
|
||||
val bookedSlotsByModeId = bookedSlotsByModeId(modes.mapNotNull { it.id })
|
||||
val cellNumsByModeId = cellNumsByModeId(modes.mapNotNull { it.id })
|
||||
|
||||
return modes.map { mode ->
|
||||
satelliteModeMapper.toResponse(
|
||||
entity = mode,
|
||||
bookedSlotIds = bookedSlotsByModeId[mode.id].orEmpty(),
|
||||
cellNums = cellNumsByModeId[mode.id].orEmpty()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun findSnapshot(snapshotId: Long): SatelliteModeSnapshotResponseDTO =
|
||||
satelliteModeSnapshotMapper.toResponse(
|
||||
satelliteModeSnapshotRepository.findById(snapshotId)
|
||||
.orElseThrow { CustomValidationException("Satellite mode snapshot $snapshotId not found") }
|
||||
)
|
||||
|
||||
fun findSnapshots(
|
||||
satelliteId: Long?,
|
||||
intervalStart: LocalDateTime?,
|
||||
intervalEnd: LocalDateTime?,
|
||||
activeOnly: Boolean
|
||||
): List<SatelliteModeSnapshotResponseDTO> =
|
||||
satelliteModeSnapshotRepository.findAll(
|
||||
buildSnapshotSpecification(
|
||||
satelliteId = satelliteId,
|
||||
intervalStart = intervalStart,
|
||||
intervalEnd = intervalEnd,
|
||||
activeOnly = activeOnly
|
||||
),
|
||||
Sort.by(
|
||||
Sort.Order.asc("satelliteId"),
|
||||
Sort.Order.desc("intervalStart"),
|
||||
Sort.Order.desc("createdAt"),
|
||||
Sort.Order.desc("id")
|
||||
)
|
||||
).map(satelliteModeSnapshotMapper::toResponse)
|
||||
|
||||
fun findActiveSnapshot(
|
||||
satelliteId: Long,
|
||||
intervalStart: LocalDateTime? = null,
|
||||
intervalEnd: LocalDateTime? = null
|
||||
): SatelliteModeSnapshotResponseDTO? {
|
||||
if ((intervalStart == null) != (intervalEnd == null)) {
|
||||
throw CustomValidationException("Параметры intervalStart и intervalEnd должны передаваться вместе")
|
||||
}
|
||||
if (intervalStart != null && intervalEnd != null) {
|
||||
validateInterval(intervalStart, intervalEnd)
|
||||
}
|
||||
return satelliteModeSnapshotRepository
|
||||
.findFirstBySatelliteIdAndIsActiveTrueOrderByCreatedAtDescIdDesc(satelliteId)
|
||||
?.let(satelliteModeSnapshotMapper::toResponse)
|
||||
}
|
||||
|
||||
fun findModesBySnapshot(
|
||||
snapshotId: Long,
|
||||
intervalStart: LocalDateTime? = null,
|
||||
intervalEnd: LocalDateTime? = null
|
||||
): List<SatelliteModeResponseDTO> {
|
||||
if ((intervalStart == null) != (intervalEnd == null)) {
|
||||
throw CustomValidationException("Параметры intervalStart и intervalEnd должны передаваться вместе")
|
||||
}
|
||||
if (intervalStart != null && intervalEnd != null) {
|
||||
validateInterval(intervalStart, intervalEnd)
|
||||
}
|
||||
return findModesBySnapshotIds(listOf(snapshotId))
|
||||
.filter { mode ->
|
||||
if (intervalStart == null || intervalEnd == null) {
|
||||
true
|
||||
} else {
|
||||
mode.startTime < intervalEnd && intervalStart < mode.endTime
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadWorkingModes(
|
||||
satelliteIds: Collection<Long>,
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime
|
||||
): Map<Long, MutableList<SurveyMode>> =
|
||||
loadConstraintModes(satelliteIds, intervalStart, intervalEnd)
|
||||
|
||||
fun loadConstraintModes(
|
||||
satelliteIds: Collection<Long>,
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime
|
||||
): Map<Long, MutableList<SurveyMode>> {
|
||||
validateInterval(intervalStart, intervalEnd)
|
||||
if (satelliteIds.isEmpty()) {
|
||||
return emptyMap()
|
||||
}
|
||||
|
||||
val distinctSatelliteIds = satelliteIds.distinct()
|
||||
lateinit var dayModes: List<SatelliteModeEntity>
|
||||
val dayModesMs = measureTimeMillis {
|
||||
dayModes = loadDayConstraintModeEntities(distinctSatelliteIds, intervalStart, intervalEnd)
|
||||
}
|
||||
logger.info(
|
||||
"Satellite mode query stage day-constraints loaded: satellites={}, modes={}, durationMs={}",
|
||||
distinctSatelliteIds,
|
||||
dayModes.size,
|
||||
dayModesMs
|
||||
)
|
||||
|
||||
lateinit var revolutionModes: List<SatelliteModeEntity>
|
||||
val revolutionModesMs = measureTimeMillis {
|
||||
revolutionModes = loadRevolutionConstraintModeEntities(dayModes)
|
||||
}
|
||||
logger.info(
|
||||
"Satellite mode query stage revolution-constraints loaded: satellites={}, modes={}, durationMs={}",
|
||||
distinctSatelliteIds,
|
||||
revolutionModes.size,
|
||||
revolutionModesMs
|
||||
)
|
||||
|
||||
val modes = (dayModes + revolutionModes)
|
||||
.distinctBy { entity -> ModeKey(entity.id, entity.satelliteId, entity.startTime, entity.endTime) }
|
||||
.sortedBy { it.startTime }
|
||||
|
||||
lateinit var bookedSlotsByModeId: Map<Long, List<Long>>
|
||||
lateinit var cellNumsByModeId: Map<Long, List<Long>>
|
||||
val linksMs = measureTimeMillis {
|
||||
val modeIds = modes.mapNotNull { it.id }
|
||||
bookedSlotsByModeId = bookedSlotsByModeId(modeIds)
|
||||
cellNumsByModeId = cellNumsByModeId(modeIds)
|
||||
}
|
||||
logger.info(
|
||||
"Satellite mode query stage mode-links loaded: modes={}, bookedSlotLinks={}, cellLinks={}, durationMs={}",
|
||||
modes.size,
|
||||
bookedSlotsByModeId.values.sumOf { it.size },
|
||||
cellNumsByModeId.values.sumOf { it.size },
|
||||
linksMs
|
||||
)
|
||||
|
||||
lateinit var result: Map<Long, MutableList<SurveyMode>>
|
||||
val mapMs = measureTimeMillis {
|
||||
result = modes
|
||||
.groupBy { it.satelliteId }
|
||||
.mapValues { (_, entities) ->
|
||||
entities.map { entity ->
|
||||
satelliteModeMapper.toSurveyMode(
|
||||
entity = entity,
|
||||
bookedSlotIds = bookedSlotsByModeId[entity.id].orEmpty(),
|
||||
cellNums = cellNumsByModeId[entity.id].orEmpty()
|
||||
)
|
||||
}.toMutableList()
|
||||
}
|
||||
}
|
||||
logger.info(
|
||||
"Satellite mode query stage constraints mapped: satellites={}, modes={}, durationMs={}",
|
||||
result.keys,
|
||||
result.values.sumOf { it.size },
|
||||
mapMs
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
fun loadDayConstraintModes(
|
||||
satelliteIds: Collection<Long>,
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime
|
||||
): Map<Long, MutableList<SurveyMode>> {
|
||||
lateinit var result: Map<Long, MutableList<SurveyMode>>
|
||||
val durationMs = measureTimeMillis {
|
||||
result = mapConstraintEntitiesToSurveys(loadDayConstraintModeEntities(satelliteIds, intervalStart, intervalEnd))
|
||||
}
|
||||
logger.info(
|
||||
"Satellite mode query day-constraint modes loaded: satellites={}, modes={}, durationMs={}",
|
||||
satelliteIds.distinct(),
|
||||
result.values.sumOf { it.size },
|
||||
durationMs
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
fun loadRevolutionConstraintModes(
|
||||
satelliteIds: Collection<Long>,
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime
|
||||
): Map<Long, MutableList<SurveyMode>> {
|
||||
lateinit var result: Map<Long, MutableList<SurveyMode>>
|
||||
val durationMs = measureTimeMillis {
|
||||
val dayModes = loadDayConstraintModeEntities(satelliteIds, intervalStart, intervalEnd)
|
||||
result = mapConstraintEntitiesToSurveys(loadRevolutionConstraintModeEntities(dayModes))
|
||||
}
|
||||
logger.info(
|
||||
"Satellite mode query revolution-constraint modes loaded: satellites={}, modes={}, durationMs={}",
|
||||
satelliteIds.distinct(),
|
||||
result.values.sumOf { it.size },
|
||||
durationMs
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
private fun loadDayConstraintModeEntities(
|
||||
satelliteIds: Collection<Long>,
|
||||
intervalStart: LocalDateTime,
|
||||
intervalEnd: LocalDateTime
|
||||
): List<SatelliteModeEntity> {
|
||||
validateInterval(intervalStart, intervalEnd)
|
||||
if (satelliteIds.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val dayStart = intervalStart.toLocalDate().atStartOfDay()
|
||||
val lastTouchedDate = touchedDateRange(intervalStart, intervalEnd).last()
|
||||
val dayEndExclusive = lastTouchedDate.plusDays(1).atStartOfDay()
|
||||
return satelliteModeRepository
|
||||
.findAllBySnapshot_SatelliteIdInAndSnapshot_IsActiveTrueAndEndTimeGreaterThanAndStartTimeLessThanOrderByStartTimeAsc(
|
||||
satelliteIds.distinct(),
|
||||
dayStart,
|
||||
dayEndExclusive
|
||||
)
|
||||
}
|
||||
|
||||
private fun loadRevolutionConstraintModeEntities(dayModes: List<SatelliteModeEntity>): List<SatelliteModeEntity> =
|
||||
dayModes
|
||||
.groupBy { it.satelliteId }
|
||||
.flatMap { (satelliteId, entities) ->
|
||||
val revolutions = entities.map { it.revolution }.toSet()
|
||||
if (revolutions.isEmpty()) {
|
||||
emptyList()
|
||||
} else {
|
||||
satelliteModeRepository.findAllBySnapshot_SatelliteIdAndSnapshot_IsActiveTrueAndRevolutionInOrderByStartTimeAsc(
|
||||
satelliteId,
|
||||
revolutions
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapConstraintEntitiesToSurveys(modes: List<SatelliteModeEntity>): Map<Long, MutableList<SurveyMode>> {
|
||||
if (modes.isEmpty()) {
|
||||
return emptyMap()
|
||||
}
|
||||
|
||||
val uniqueModes = modes
|
||||
.distinctBy { entity -> ModeKey(entity.id, entity.satelliteId, entity.startTime, entity.endTime) }
|
||||
.sortedBy { it.startTime }
|
||||
val bookedSlotsByModeId = bookedSlotsByModeId(uniqueModes.mapNotNull { it.id })
|
||||
val cellNumsByModeId = cellNumsByModeId(uniqueModes.mapNotNull { it.id })
|
||||
|
||||
return uniqueModes
|
||||
.groupBy { it.satelliteId }
|
||||
.mapValues { (_, entities) ->
|
||||
entities.map { entity ->
|
||||
satelliteModeMapper.toSurveyMode(
|
||||
entity = entity,
|
||||
bookedSlotIds = bookedSlotsByModeId[entity.id].orEmpty(),
|
||||
cellNums = cellNumsByModeId[entity.id].orEmpty()
|
||||
)
|
||||
}.toMutableList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun findModesBySnapshotIds(snapshotIds: Collection<Long>): List<SatelliteModeResponseDTO> {
|
||||
if (snapshotIds.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val modes = satelliteModeRepository.findAllBySnapshot_IdInOrderByStartTimeAsc(snapshotIds.distinct())
|
||||
val bookedSlotsByModeId = bookedSlotsByModeId(modes.mapNotNull { it.id })
|
||||
val cellNumsByModeId = cellNumsByModeId(modes.mapNotNull { it.id })
|
||||
return modes.map { mode ->
|
||||
satelliteModeMapper.toResponse(
|
||||
entity = mode,
|
||||
bookedSlotIds = bookedSlotsByModeId[mode.id].orEmpty(),
|
||||
cellNums = cellNumsByModeId[mode.id].orEmpty()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun bookedSlotsByModeId(modeIds: Collection<Long>): Map<Long, List<Long>> {
|
||||
if (modeIds.isEmpty()) {
|
||||
return emptyMap()
|
||||
}
|
||||
|
||||
return satelliteModeBookedSlotRepository.findAllByMode_IdIn(modeIds)
|
||||
.groupBy(
|
||||
keySelector = { it.mode?.id ?: -1L },
|
||||
valueTransform = { it.bookedSlotId }
|
||||
)
|
||||
.mapValues { (_, bookedSlotIds) ->
|
||||
bookedSlotIds.filter { it > 0 }.distinct()
|
||||
}
|
||||
}
|
||||
|
||||
private fun cellNumsByModeId(modeIds: Collection<Long>): Map<Long, List<Long>> {
|
||||
if (modeIds.isEmpty()) {
|
||||
return emptyMap()
|
||||
}
|
||||
|
||||
return satelliteModeCellRepository.findAllByMode_IdIn(modeIds)
|
||||
.groupBy(
|
||||
keySelector = { it.mode?.id ?: -1L },
|
||||
valueTransform = { it.cellNum }
|
||||
)
|
||||
.mapValues { (_, cellNums) ->
|
||||
cellNums.filter { it > 0 }.distinct().sorted()
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildSnapshotSpecification(
|
||||
satelliteId: Long?,
|
||||
intervalStart: LocalDateTime?,
|
||||
intervalEnd: LocalDateTime?,
|
||||
activeOnly: Boolean
|
||||
): Specification<space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeSnapshotEntity> =
|
||||
Specification { root, _, criteriaBuilder ->
|
||||
val predicates = mutableListOf<jakarta.persistence.criteria.Predicate>()
|
||||
satelliteId?.let { predicates += criteriaBuilder.equal(root.get<Long>("satelliteId"), it) }
|
||||
intervalStart?.let { predicates += criteriaBuilder.equal(root.get<LocalDateTime>("intervalStart"), it) }
|
||||
intervalEnd?.let { predicates += criteriaBuilder.equal(root.get<LocalDateTime>("intervalEnd"), it) }
|
||||
if (activeOnly) {
|
||||
predicates += criteriaBuilder.isTrue(root.get("isActive"))
|
||||
}
|
||||
criteriaBuilder.and(*predicates.toTypedArray())
|
||||
}
|
||||
|
||||
private fun validateInterval(intervalStart: LocalDateTime, intervalEnd: LocalDateTime) {
|
||||
if (intervalEnd.isBefore(intervalStart)) {
|
||||
throw CustomValidationException("Параметр intervalEnd должен быть больше или равен intervalStart")
|
||||
}
|
||||
}
|
||||
|
||||
private fun touchedDateRange(intervalStart: LocalDateTime, intervalEnd: LocalDateTime): List<LocalDate> {
|
||||
val lastTouchedDate = when {
|
||||
intervalEnd.isEqual(intervalStart) -> intervalStart.toLocalDate()
|
||||
intervalEnd.toLocalTime() == LocalTime.MIDNIGHT -> intervalEnd.minusNanos(1).toLocalDate()
|
||||
else -> intervalEnd.minusNanos(1).toLocalDate()
|
||||
}
|
||||
return generateSequence(intervalStart.toLocalDate()) { current ->
|
||||
current.takeIf { it < lastTouchedDate }?.plusDays(1)
|
||||
}.toList()
|
||||
}
|
||||
|
||||
private fun contourArea(contourWkt: String): Double =
|
||||
if (contourWkt.isBlank()) 0.0
|
||||
else runCatching { wktReader.read(contourWkt).area }.getOrDefault(0.0)
|
||||
|
||||
private fun splitSurveyDurationByDay(survey: SurveyMode): List<Pair<LocalDate, Double>> {
|
||||
if (!survey.timeStop.isAfter(survey.time)) {
|
||||
return listOf(survey.time.toLocalDate() to survey.duration)
|
||||
}
|
||||
|
||||
val durations = mutableListOf<Pair<LocalDate, Double>>()
|
||||
var current = survey.time
|
||||
|
||||
while (current.toLocalDate() < survey.timeStop.toLocalDate()) {
|
||||
val nextDayStart = current.toLocalDate().plusDays(1).atStartOfDay()
|
||||
durations += current.toLocalDate() to Duration.between(current, nextDayStart).toSeconds().toDouble()
|
||||
current = nextDayStart
|
||||
}
|
||||
|
||||
durations += current.toLocalDate() to Duration.between(current, survey.timeStop).toSeconds().toDouble()
|
||||
return durations
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import org.springframework.stereotype.Component
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeSnapshotEntity
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.SatelliteModeSnapshotResponseDTO
|
||||
|
||||
@Component
|
||||
class SatelliteModeSnapshotMapper {
|
||||
|
||||
fun toResponse(entity: SatelliteModeSnapshotEntity): SatelliteModeSnapshotResponseDTO =
|
||||
SatelliteModeSnapshotResponseDTO(
|
||||
id = entity.id ?: 0,
|
||||
satelliteId = entity.satelliteId,
|
||||
intervalStart = entity.intervalStart,
|
||||
intervalEnd = entity.intervalEnd,
|
||||
createdAt = entity.createdAt,
|
||||
isActive = entity.isActive,
|
||||
complexPlanRunId = entity.complexPlanRun?.id
|
||||
)
|
||||
}
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import org.locationtech.jts.io.WKTReader
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import space.nstart.pcp.pcp_satellites_service.configuration.CustomErrorException
|
||||
import space.nstart.pcp.pcp_satellites_service.model.BLS
|
||||
import space.nstart.pcp.pcp_satellites_service.model.LongTermMission
|
||||
import space.nstart.pcp.pcp_satellites_service.model.SatelliteModel
|
||||
import space.nstart.pcp.pcp_satellites_service.model.mode.SurveyMode
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.DailyDurationDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.MissionStatisticsDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.RevolutionDurationDTO
|
||||
import java.time.Duration
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
@Service
|
||||
class SatelliteService(
|
||||
private val satelliteCatalogClient: SatelliteCatalogClient
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||
private val wktReader = WKTReader()
|
||||
private val satellitesById = linkedMapOf<Long, SatelliteModel>()
|
||||
|
||||
val satellites: List<SatelliteModel>
|
||||
get() = ensureLoaded().values.toList()
|
||||
|
||||
fun clear() {
|
||||
satellites.forEach { satellite -> satellite.longMission.surveys.clear() }
|
||||
}
|
||||
|
||||
fun all() = satellites.map { sat -> sat.toDTO() }
|
||||
|
||||
fun byId(id: Long) = ensureLoaded()[id]?.toDTO()
|
||||
|
||||
fun complexMission(id: Long) =
|
||||
ensureLoaded()[id]
|
||||
?.longMission?.surveys?.sortedBy { survey -> survey.time }?.map { survey -> survey.toDTO() }
|
||||
?: throw CustomErrorException("Нет КА с заданным идентификаторм $id")
|
||||
|
||||
fun missionStatistics(id: Long): MissionStatisticsDTO {
|
||||
val mission = ensureLoaded()[id]
|
||||
?.longMission
|
||||
?: throw CustomErrorException("Нет КА с заданным идентификаторм $id")
|
||||
|
||||
return MissionStatisticsDTO(
|
||||
modesCount = mission.surveys.size,
|
||||
totalContourArea = mission.surveys.sumOf { survey -> contourArea(survey.contourWKT) },
|
||||
revolutionDurations = mission.surveys
|
||||
.groupBy { it.revolution }
|
||||
.toSortedMap()
|
||||
.map { (revolution, surveys) ->
|
||||
RevolutionDurationDTO(
|
||||
revolution = revolution,
|
||||
totalDuration = surveys.sumOf { survey -> survey.duration }
|
||||
)
|
||||
},
|
||||
dailyDurations = mission.surveys
|
||||
.flatMap { survey -> splitSurveyDurationByDay(survey) }
|
||||
.groupBy({ it.first }, { it.second })
|
||||
.toSortedMap()
|
||||
.map { (date, durations) ->
|
||||
DailyDurationDTO(
|
||||
date = date,
|
||||
totalDuration = durations.sum()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun complexMissionForPaint(id: Long): String {
|
||||
val plan = ensureLoaded()[id]
|
||||
?.longMission?.surveys
|
||||
?: throw CustomErrorException("Нет КА с заданным идентификаторм $id")
|
||||
|
||||
var geom = "GEOMETRYCOLLECTION("
|
||||
plan.forEach { survey ->
|
||||
geom += "${survey.contourWKT},"
|
||||
}
|
||||
geom = geom.dropLast(1)
|
||||
geom += ")"
|
||||
|
||||
return geom
|
||||
}
|
||||
|
||||
fun hasResources(
|
||||
tStart: LocalDateTime,
|
||||
tStop: LocalDateTime,
|
||||
available: List<Long>,
|
||||
sats: MutableList<Long>
|
||||
): Boolean {
|
||||
sats.clear()
|
||||
var currentDay = tStart.toLocalDate()
|
||||
val lastDay = tStop.toLocalDate().minusDays(1)
|
||||
var hasResource = false
|
||||
|
||||
while (currentDay <= lastDay) {
|
||||
satellites.forEach { satellite ->
|
||||
if (available.contains(satellite.satelliteId) && satellite.hasResources(currentDay)) {
|
||||
hasResource = true
|
||||
if (!sats.contains(satellite.satelliteId)) {
|
||||
sats.add(satellite.satelliteId)
|
||||
}
|
||||
}
|
||||
}
|
||||
currentDay = currentDay.plusDays(1)
|
||||
}
|
||||
return hasResource
|
||||
}
|
||||
|
||||
private fun ensureLoaded(): Map<Long, SatelliteModel> {
|
||||
if (satellitesById.isNotEmpty()) {
|
||||
logger.info("Satellite cache hit: satellites={}", satellitesById.size)
|
||||
return satellitesById
|
||||
}
|
||||
|
||||
synchronized(satellitesById) {
|
||||
if (satellitesById.isEmpty()) {
|
||||
val durationMs = measureTimeMillis {
|
||||
satelliteCatalogClient.allSatellites()
|
||||
.sortedBy { it.id }
|
||||
.forEach { satellite ->
|
||||
satellitesById[satellite.id] = satellite.toSatelliteModel()
|
||||
}
|
||||
}
|
||||
logger.info(
|
||||
"Satellite cache loaded: satellites={}, durationMs={}",
|
||||
satellitesById.size,
|
||||
durationMs
|
||||
)
|
||||
}
|
||||
}
|
||||
return satellitesById
|
||||
}
|
||||
|
||||
private fun SatelliteDTO.toSatelliteModel() = SatelliteModel(
|
||||
satelliteId = id,
|
||||
name = name,
|
||||
red = visualization.red,
|
||||
green = visualization.green,
|
||||
blue = visualization.blue,
|
||||
longMission = LongTermMission(),
|
||||
bls = observationProfile?.let {
|
||||
BLS(
|
||||
captureAngle = it.captureAngle,
|
||||
sunAngleMin = it.sunAngleMin,
|
||||
durationMin = it.durationMinSeconds.toDouble(),
|
||||
durationMax = it.durationMaxSeconds.toDouble(),
|
||||
mmi = it.mmiSeconds.toDouble(),
|
||||
dailyMaxDuration = it.dailyMaxDurationSeconds.toDouble(),
|
||||
revolutionMaxDuration = it.revolutionMaxDurationSeconds.toDouble()
|
||||
)
|
||||
} ?: BLS(),
|
||||
scanTLE = scanTle
|
||||
)
|
||||
|
||||
private fun contourArea(contourWkt: String): Double =
|
||||
if (contourWkt.isBlank()) {
|
||||
0.0
|
||||
} else {
|
||||
runCatching { wktReader.read(contourWkt).area }.getOrDefault(0.0)
|
||||
}
|
||||
|
||||
private fun splitSurveyDurationByDay(survey: SurveyMode): List<Pair<java.time.LocalDate, Double>> {
|
||||
if (!survey.timeStop.isAfter(survey.time)) {
|
||||
return listOf(survey.time.toLocalDate() to survey.duration)
|
||||
}
|
||||
|
||||
val durations = mutableListOf<Pair<java.time.LocalDate, Double>>()
|
||||
var current = survey.time
|
||||
|
||||
while (current.toLocalDate() < survey.timeStop.toLocalDate()) {
|
||||
val nextDayStart = current.toLocalDate().plusDays(1).atStartOfDay()
|
||||
durations += current.toLocalDate() to Duration.between(current, nextDayStart).toSeconds().toDouble()
|
||||
current = nextDayStart
|
||||
}
|
||||
|
||||
durations += current.toLocalDate() to Duration.between(current, survey.timeStop).toSeconds().toDouble()
|
||||
return durations
|
||||
}
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.web.reactive.function.client.WebClient
|
||||
import org.springframework.web.reactive.function.client.bodyToFlux
|
||||
import org.springframework.web.util.UriComponentsBuilder
|
||||
import reactor.core.publisher.Mono
|
||||
import space.nstart.pcp.pcp_satellites_service.configuration.CustomErrorException
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.BookedSlotDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.BookingRequestDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SurveySlotDTO
|
||||
import tools.jackson.databind.ObjectMapper
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Service
|
||||
class SlotService(webClientBuilderProvider: ObjectProvider<WebClient.Builder>) {
|
||||
private val webClientBuilder: WebClient.Builder = webClientBuilderProvider.ifAvailable ?: WebClient.builder()
|
||||
|
||||
@Value("\${settings.slots-service:slots-service}")
|
||||
val url = ""
|
||||
|
||||
private fun client() = webClientBuilder.baseUrl(url).build()
|
||||
|
||||
fun slots(wkt : String, tn : LocalDateTime, tk : LocalDateTime, sign : RevolutionSign?, cov : Boolean?, sats : List<Long>) =
|
||||
(
|
||||
if (sign == null) {
|
||||
client()
|
||||
.get()
|
||||
.uri(
|
||||
UriComponentsBuilder
|
||||
.fromUriString("/api/slots/poly-cover")
|
||||
.queryParam("wkt", wkt)
|
||||
.queryParam("timeStart", tn)
|
||||
.queryParam("timeStop", tk)
|
||||
.queryParam("cov", cov)
|
||||
.apply {
|
||||
sats.forEach { queryParam("satellites", it) }
|
||||
}
|
||||
.build()
|
||||
.toUriString()
|
||||
)
|
||||
} else {
|
||||
client()
|
||||
.get()
|
||||
.uri(
|
||||
"/api/slots/poly-cover?wkt={wkt}&timeStart={timeStart}&timeStop={timeStop}&revSign={revSign}&cov={cov}",
|
||||
wkt,
|
||||
tn,
|
||||
tk,
|
||||
sign,
|
||||
cov
|
||||
)
|
||||
}
|
||||
)
|
||||
.retrieve()
|
||||
.onStatus({ status -> status.isError }) { response ->
|
||||
response.bodyToMono(String::class.java)
|
||||
.defaultIfEmpty("error")
|
||||
.flatMap { body -> Mono.error(CustomErrorException(extractErrorMessage(body))) }
|
||||
}
|
||||
.bodyToFlux<SlotDTO>()
|
||||
|
||||
|
||||
fun plan(id : Long, tn : LocalDateTime, tk : LocalDateTime) =
|
||||
client()
|
||||
.get()
|
||||
.uri(
|
||||
"/api/slots/mission/satellite?satellite_id={satelliteId}&timeStart={timeStart}&timeStop={timeStop}",
|
||||
id,
|
||||
tn,
|
||||
tk
|
||||
)
|
||||
.retrieve()
|
||||
.onStatus({ status -> status.isError }) { response ->
|
||||
response.bodyToMono(String::class.java)
|
||||
.defaultIfEmpty("error")
|
||||
.flatMap { body -> Mono.error(CustomErrorException(extractErrorMessage(body))) }
|
||||
}
|
||||
.bodyToFlux< SurveySlotDTO>()
|
||||
|
||||
|
||||
fun bookReq(req: BookingRequestDTO): List<BookedSlotDTO> =
|
||||
client()
|
||||
.post()
|
||||
.uri("/api/slots/booking/request")
|
||||
.bodyValue(req)
|
||||
.retrieve()
|
||||
.bodyToFlux(BookedSlotDTO::class.java)
|
||||
.collectList()
|
||||
.block() ?: emptyList()
|
||||
|
||||
fun cancelReq(requestId: String): Int =
|
||||
client()
|
||||
.delete()
|
||||
.uri("/api/slots/booking/request?requestId={requestId}", requestId)
|
||||
.retrieve()
|
||||
.onStatus({ status -> status.isError }) { response ->
|
||||
response.bodyToMono(String::class.java)
|
||||
.defaultIfEmpty("error")
|
||||
.flatMap { body -> Mono.error(CustomErrorException(extractErrorMessage(body))) }
|
||||
}
|
||||
.bodyToMono(Int::class.java)
|
||||
.block() ?: 0
|
||||
|
||||
private fun extractErrorMessage(body: String): String {
|
||||
val text = body.trim()
|
||||
if (text.isEmpty()) return "error"
|
||||
|
||||
return runCatching {
|
||||
val node = ObjectMapper().readTree(text)
|
||||
when {
|
||||
node.hasNonNull("error") -> node["error"].asText()
|
||||
node.hasNonNull("message") -> node["message"].asText()
|
||||
else -> text
|
||||
}
|
||||
}.getOrDefault(text)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user