Init
This commit is contained in:
+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)
|
||||
}
|
||||
}
|
||||
+564
@@ -0,0 +1,564 @@
|
||||
package space.nstart.pcp.pcp_satellites_service.service
|
||||
|
||||
import org.locationtech.jts.geom.Geometry
|
||||
import org.locationtech.jts.geom.Polygon
|
||||
import org.locationtech.jts.io.WKTReader
|
||||
import org.locationtech.jts.io.WKTWriter
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeSource
|
||||
import space.nstart.pcp.pcp_satellites_service.model.BLS
|
||||
import space.nstart.pcp.pcp_satellites_service.model.SatelliteModel
|
||||
import space.nstart.pcp.pcp_satellites_service.model.mode.SurveyMode
|
||||
import java.time.Duration
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.ceil
|
||||
|
||||
@Service
|
||||
class SurveyPlacementService {
|
||||
|
||||
data class PlacementDecision(
|
||||
val accepted: Boolean,
|
||||
val appliedSurvey: SurveyMode? = null,
|
||||
val adjustedCandidate: SurveyMode? = null,
|
||||
val absorbedSurveys: List<SurveyMode> = emptyList(),
|
||||
val trimmed: Boolean = false,
|
||||
val rejectionReason: String? = null
|
||||
)
|
||||
|
||||
private data class PlacementCandidate(
|
||||
val adjustedCandidate: SurveyMode,
|
||||
val absorbedSurveys: List<SurveyMode>,
|
||||
val mergedSurvey: SurveyMode,
|
||||
val resultingSurveys: List<SurveyMode>
|
||||
)
|
||||
|
||||
private data class PlacementValidation(
|
||||
val accepted: Boolean,
|
||||
val reason: String? = null
|
||||
)
|
||||
|
||||
private data class GeometryMergeAssessment(
|
||||
val mergeable: Boolean,
|
||||
val intersectionArea: Double? = null,
|
||||
val touchesOrIntersects: Boolean = false,
|
||||
val distance: Double? = null,
|
||||
val geometryChecked: Boolean = true
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val ROLL_EPSILON = 0.01
|
||||
private const val GEOMETRY_INTERSECTION_AREA_EPSILON = 1e-8
|
||||
private const val GEOMETRY_DISTANCE_EPSILON = 1e-6
|
||||
private const val MERGE_MAX_TIME_GAP_SECONDS = 1L
|
||||
}
|
||||
|
||||
private val logger: Logger = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
fun tryPlaceSurvey(
|
||||
existingSurveys: MutableList<SurveyMode>,
|
||||
candidateSurvey: SurveyMode,
|
||||
satellite: SatelliteModel
|
||||
): PlacementDecision {
|
||||
val constraints = satellite.bls
|
||||
val initialPlacement = buildPlacementCandidate(existingSurveys, candidateSurvey, satellite.satelliteId)
|
||||
val initialValidation = validatePlacement(initialPlacement, constraints)
|
||||
if (initialValidation.accepted) {
|
||||
applyPlacement(existingSurveys, initialPlacement)
|
||||
return PlacementDecision(
|
||||
accepted = true,
|
||||
appliedSurvey = initialPlacement.mergedSurvey,
|
||||
adjustedCandidate = initialPlacement.adjustedCandidate,
|
||||
absorbedSurveys = initialPlacement.absorbedSurveys
|
||||
)
|
||||
}
|
||||
|
||||
val adjustedPlacement = adjustPlacementToConstraints(existingSurveys, candidateSurvey, constraints, satellite.satelliteId)
|
||||
?: return PlacementDecision(
|
||||
accepted = false,
|
||||
rejectionReason = initialValidation.reason ?: "placement_rejected"
|
||||
)
|
||||
val adjustedValidation = validatePlacement(adjustedPlacement, constraints)
|
||||
if (!adjustedValidation.accepted) {
|
||||
return PlacementDecision(
|
||||
accepted = false,
|
||||
rejectionReason = adjustedValidation.reason ?: "placement_rejected"
|
||||
)
|
||||
}
|
||||
|
||||
applyPlacement(existingSurveys, adjustedPlacement)
|
||||
return PlacementDecision(
|
||||
accepted = true,
|
||||
appliedSurvey = adjustedPlacement.mergedSurvey,
|
||||
adjustedCandidate = adjustedPlacement.adjustedCandidate,
|
||||
absorbedSurveys = adjustedPlacement.absorbedSurveys,
|
||||
trimmed = adjustedPlacement.adjustedCandidate.timeStop != candidateSurvey.timeStop
|
||||
)
|
||||
}
|
||||
|
||||
fun mergeSurvey(
|
||||
existingSurveys: MutableList<SurveyMode>,
|
||||
candidateSurvey: SurveyMode,
|
||||
satellite: SatelliteModel
|
||||
): SurveyMode {
|
||||
val placement = buildPlacementCandidate(existingSurveys, candidateSurvey, satellite.satelliteId)
|
||||
applyPlacement(existingSurveys, placement)
|
||||
return placement.mergedSurvey
|
||||
}
|
||||
|
||||
private fun buildPlacementCandidate(
|
||||
existingSurveys: List<SurveyMode>,
|
||||
candidateSurvey: SurveyMode,
|
||||
satelliteId: Long
|
||||
): PlacementCandidate {
|
||||
val mergeChain = buildMergeChain(existingSurveys, candidateSurvey, satelliteId)
|
||||
val resultingSurveys = existingSurveys
|
||||
.filterNot { mergeChain.absorbedSurveys.contains(it) }
|
||||
.plus(mergeChain.mergedSurvey)
|
||||
.sortedBy { it.time }
|
||||
|
||||
return PlacementCandidate(
|
||||
adjustedCandidate = candidateSurvey,
|
||||
absorbedSurveys = mergeChain.absorbedSurveys,
|
||||
mergedSurvey = mergeChain.mergedSurvey,
|
||||
resultingSurveys = resultingSurveys
|
||||
)
|
||||
}
|
||||
|
||||
private fun adjustPlacementToConstraints(
|
||||
existingSurveys: List<SurveyMode>,
|
||||
candidateSurvey: SurveyMode,
|
||||
constraints: BLS,
|
||||
satelliteId: Long
|
||||
): PlacementCandidate? {
|
||||
val originalDurationSeconds = Duration.between(candidateSurvey.time, candidateSurvey.timeStop).seconds
|
||||
if (originalDurationSeconds <= 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
val minDurationSeconds = ceil(constraints.durationMin).toLong().coerceAtLeast(1L)
|
||||
var low = minDurationSeconds
|
||||
var high = originalDurationSeconds
|
||||
var bestPlacement: PlacementCandidate? = null
|
||||
|
||||
while (low <= high) {
|
||||
val mid = (low + high) / 2
|
||||
val adjustedCandidate = trimCandidate(candidateSurvey, mid)
|
||||
val placement = buildPlacementCandidate(existingSurveys, adjustedCandidate, satelliteId)
|
||||
val validation = validatePlacement(placement, constraints)
|
||||
if (validation.accepted) {
|
||||
bestPlacement = placement
|
||||
low = mid + 1
|
||||
} else {
|
||||
high = mid - 1
|
||||
}
|
||||
}
|
||||
|
||||
return bestPlacement
|
||||
}
|
||||
|
||||
private fun validatePlacement(
|
||||
placement: PlacementCandidate,
|
||||
constraints: BLS
|
||||
): PlacementValidation {
|
||||
val candidateDurationSeconds = Duration.between(
|
||||
placement.adjustedCandidate.time,
|
||||
placement.adjustedCandidate.timeStop
|
||||
).seconds.toDouble()
|
||||
if (candidateDurationSeconds < constraints.durationMin) {
|
||||
return PlacementValidation(false, "duration_min")
|
||||
}
|
||||
|
||||
if (placement.mergedSurvey.duration > constraints.durationMax) {
|
||||
return PlacementValidation(false, "duration_max")
|
||||
}
|
||||
|
||||
if (hasDifferentRollConflict(placement, constraints)) {
|
||||
return PlacementValidation(false, "different_roll_conflict")
|
||||
}
|
||||
|
||||
if (exceedsDailyMaxDuration(placement.resultingSurveys, constraints)) {
|
||||
return PlacementValidation(false, "daily_max_duration")
|
||||
}
|
||||
|
||||
if (exceedsRevolutionMaxDuration(placement.resultingSurveys, constraints)) {
|
||||
return PlacementValidation(false, "revolution_max_duration")
|
||||
}
|
||||
|
||||
return PlacementValidation(true)
|
||||
}
|
||||
|
||||
private fun applyPlacement(
|
||||
existingSurveys: MutableList<SurveyMode>,
|
||||
placement: PlacementCandidate
|
||||
) {
|
||||
existingSurveys.removeAll(placement.absorbedSurveys.toSet())
|
||||
existingSurveys.add(placement.mergedSurvey)
|
||||
existingSurveys.sortBy { it.time }
|
||||
}
|
||||
|
||||
private data class MergeChain(
|
||||
val absorbedSurveys: List<SurveyMode>,
|
||||
val mergedSurvey: SurveyMode
|
||||
)
|
||||
|
||||
private fun buildMergeChain(
|
||||
existingSurveys: List<SurveyMode>,
|
||||
candidateSurvey: SurveyMode,
|
||||
satelliteId: Long
|
||||
): MergeChain {
|
||||
val absorbed = linkedSetOf<SurveyMode>()
|
||||
var mergedSurvey = candidateSurvey
|
||||
var expanded = true
|
||||
|
||||
while (expanded) {
|
||||
expanded = false
|
||||
existingSurveys.forEach { existing ->
|
||||
if (absorbed.contains(existing)) {
|
||||
return@forEach
|
||||
}
|
||||
if (!sameRoll(existing.roll, candidateSurvey.roll)) {
|
||||
return@forEach
|
||||
}
|
||||
if (!canMergeInTime(existing.time, existing.timeStop, mergedSurvey.time, mergedSurvey.timeStop)) {
|
||||
return@forEach
|
||||
}
|
||||
val overlapsInTime = intersectsInTime(
|
||||
existing.time,
|
||||
existing.timeStop,
|
||||
mergedSurvey.time,
|
||||
mergedSurvey.timeStop
|
||||
)
|
||||
val geometryMergeAssessment = assessMergeableGeometry(
|
||||
leftContour = existing.contourWKT,
|
||||
rightContour = mergedSurvey.contourWKT,
|
||||
satelliteId = satelliteId,
|
||||
allowContinuityWithoutArea = !overlapsInTime
|
||||
)
|
||||
if (!geometryMergeAssessment.mergeable) {
|
||||
if (geometryMergeAssessment.geometryChecked) {
|
||||
logMergeSkippedWithoutSufficientGeometry(
|
||||
satelliteId = satelliteId,
|
||||
candidateSurvey = mergedSurvey,
|
||||
existingSurvey = existing,
|
||||
intersectionArea = geometryMergeAssessment.intersectionArea ?: 0.0,
|
||||
distance = geometryMergeAssessment.distance,
|
||||
overlapsInTime = overlapsInTime
|
||||
)
|
||||
}
|
||||
return@forEach
|
||||
}
|
||||
val mergedContour = unionContoursIfPolygon(
|
||||
baseContour = mergedSurvey.contourWKT,
|
||||
contourToAdd = existing.contourWKT,
|
||||
satelliteId = satelliteId,
|
||||
candidateSurvey = mergedSurvey,
|
||||
existingSurvey = existing
|
||||
) ?: return@forEach
|
||||
absorbed += existing
|
||||
mergedSurvey = mergeSurveys(mergedSurvey, existing, mergedContour)
|
||||
expanded = true
|
||||
}
|
||||
}
|
||||
|
||||
return MergeChain(absorbed.toList(), mergedSurvey)
|
||||
}
|
||||
|
||||
private fun mergeSurveys(
|
||||
baseSurvey: SurveyMode,
|
||||
existingSurvey: SurveyMode,
|
||||
mergedContour: String
|
||||
): SurveyMode {
|
||||
val mergedStart = minOf(baseSurvey.time, existingSurvey.time)
|
||||
val mergedStop = maxOf(baseSurvey.timeStop, existingSurvey.timeStop)
|
||||
val mergedRevolution = if (existingSurvey.time < baseSurvey.time) {
|
||||
existingSurvey.revolution
|
||||
} else {
|
||||
baseSurvey.revolution
|
||||
}
|
||||
val mergedBookedSlotIds = (baseSurvey.bookedSlotIds + existingSurvey.bookedSlotIds).distinct()
|
||||
val mergedCellNums = normalizeCellNums(baseSurvey).apply {
|
||||
addAll(normalizeCellNums(existingSurvey))
|
||||
}
|
||||
return SurveyMode(
|
||||
revolution = mergedRevolution,
|
||||
time = mergedStart,
|
||||
timeStop = mergedStop,
|
||||
roll = baseSurvey.roll,
|
||||
latitude = baseSurvey.latitude,
|
||||
longitude = baseSurvey.longitude,
|
||||
duration = Duration.between(mergedStart, mergedStop).seconds.toDouble(),
|
||||
contourWKT = mergedContour,
|
||||
cellNum = mergedCellNums.singleOrNull() ?: -1,
|
||||
source = mergeSources(baseSurvey.source, existingSurvey.source),
|
||||
bookedSlotIds = mergedBookedSlotIds,
|
||||
cellNums = mergedCellNums.toList().sorted()
|
||||
)
|
||||
}
|
||||
|
||||
private fun hasDifferentRollConflict(
|
||||
placement: PlacementCandidate,
|
||||
constraints: BLS
|
||||
): Boolean =
|
||||
placement.resultingSurveys.any { existing ->
|
||||
existing !== placement.mergedSurvey &&
|
||||
!sameRoll(existing.roll, placement.mergedSurvey.roll) &&
|
||||
intersectsWithMmi(
|
||||
existing.time,
|
||||
existing.timeStop,
|
||||
placement.mergedSurvey.time,
|
||||
placement.mergedSurvey.timeStop,
|
||||
constraints.mmi
|
||||
)
|
||||
}
|
||||
|
||||
private fun exceedsDailyMaxDuration(
|
||||
surveys: List<SurveyMode>,
|
||||
constraints: BLS
|
||||
): Boolean =
|
||||
surveys
|
||||
.flatMap { splitSurveyDurationByDay(it) }
|
||||
.groupBy({ it.first }, { it.second })
|
||||
.values
|
||||
.any { durations -> durations.sum() > constraints.dailyMaxDuration }
|
||||
|
||||
private fun exceedsRevolutionMaxDuration(
|
||||
surveys: List<SurveyMode>,
|
||||
constraints: BLS
|
||||
): Boolean =
|
||||
surveys
|
||||
.groupBy { it.revolution }
|
||||
.values
|
||||
.any { grouped -> grouped.sumOf { it.duration } > constraints.revolutionMaxDuration }
|
||||
|
||||
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).seconds.toDouble()
|
||||
current = nextDayStart
|
||||
}
|
||||
|
||||
durations += current.toLocalDate() to Duration.between(current, survey.timeStop).seconds.toDouble()
|
||||
return durations
|
||||
}
|
||||
|
||||
private fun trimCandidate(candidateSurvey: SurveyMode, allowedDurationSeconds: Long): SurveyMode {
|
||||
val trimmedEnd = candidateSurvey.time.plusSeconds(allowedDurationSeconds.coerceAtLeast(0))
|
||||
return candidateSurvey.copyWith(
|
||||
timeStop = trimmedEnd,
|
||||
duration = Duration.between(candidateSurvey.time, trimmedEnd).seconds.toDouble()
|
||||
)
|
||||
}
|
||||
|
||||
private fun sameRoll(left: Double, right: Double): Boolean = abs(left - right) <= ROLL_EPSILON
|
||||
|
||||
private fun canMergeInTime(
|
||||
start1: LocalDateTime,
|
||||
stop1: LocalDateTime,
|
||||
start2: LocalDateTime,
|
||||
stop2: LocalDateTime
|
||||
): Boolean {
|
||||
val firstStop = minOf(stop1, stop2)
|
||||
val secondStart = maxOf(start1, start2)
|
||||
if (!secondStart.isAfter(firstStop)) {
|
||||
return true
|
||||
}
|
||||
val gapSeconds = Duration.between(firstStop, secondStart).seconds
|
||||
return gapSeconds in 0..MERGE_MAX_TIME_GAP_SECONDS
|
||||
}
|
||||
|
||||
private fun intersectsInTime(
|
||||
start1: LocalDateTime,
|
||||
stop1: LocalDateTime,
|
||||
start2: LocalDateTime,
|
||||
stop2: LocalDateTime
|
||||
): Boolean = start1 < stop2 && start2 < stop1
|
||||
|
||||
private fun intersectsWithMmi(
|
||||
start1: LocalDateTime,
|
||||
stop1: LocalDateTime,
|
||||
start2: LocalDateTime,
|
||||
stop2: LocalDateTime,
|
||||
mmiSeconds: Double
|
||||
): Boolean {
|
||||
val mmi = ceil(mmiSeconds).toLong()
|
||||
return start1 <= stop2.plusSeconds(mmi) && start2 <= stop1.plusSeconds(mmi)
|
||||
}
|
||||
|
||||
private fun mergeSources(left: SatelliteModeSource, right: SatelliteModeSource): SatelliteModeSource = when {
|
||||
left == right -> left
|
||||
left == SatelliteModeSource.MIXED || right == SatelliteModeSource.MIXED -> SatelliteModeSource.MIXED
|
||||
else -> SatelliteModeSource.MIXED
|
||||
}
|
||||
|
||||
private fun normalizeCellNums(survey: SurveyMode): MutableSet<Long> =
|
||||
survey.cellNums
|
||||
.ifEmpty { if (survey.cellNum > 0) listOf(survey.cellNum) else emptyList() }
|
||||
.filter { it > 0 }
|
||||
.toMutableSet()
|
||||
|
||||
private fun assessMergeableGeometry(
|
||||
leftContour: String,
|
||||
rightContour: String,
|
||||
satelliteId: Long,
|
||||
allowContinuityWithoutArea: Boolean
|
||||
): GeometryMergeAssessment {
|
||||
return try {
|
||||
val reader = WKTReader()
|
||||
val leftGeometry = reader.read(leftContour)
|
||||
val rightGeometry = reader.read(rightContour)
|
||||
logInvalidGeometryIfNeeded(satelliteId, "left", leftGeometry)
|
||||
logInvalidGeometryIfNeeded(satelliteId, "right", rightGeometry)
|
||||
val intersectionArea = leftGeometry.intersection(rightGeometry).area
|
||||
val touchesOrIntersects = leftGeometry.intersects(rightGeometry) || leftGeometry.touches(rightGeometry)
|
||||
val distance = if (touchesOrIntersects) 0.0 else leftGeometry.distance(rightGeometry)
|
||||
GeometryMergeAssessment(
|
||||
mergeable = intersectionArea > GEOMETRY_INTERSECTION_AREA_EPSILON ||
|
||||
(allowContinuityWithoutArea && (touchesOrIntersects || distance <= GEOMETRY_DISTANCE_EPSILON)),
|
||||
intersectionArea = intersectionArea,
|
||||
touchesOrIntersects = touchesOrIntersects,
|
||||
distance = distance
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.warn("Не удалось проверить непрерывность геометрий маршрутов при placement: {}", e.message)
|
||||
GeometryMergeAssessment(mergeable = false, geometryChecked = false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun unionContoursIfPolygon(
|
||||
baseContour: String,
|
||||
contourToAdd: String,
|
||||
satelliteId: Long,
|
||||
candidateSurvey: SurveyMode,
|
||||
existingSurvey: SurveyMode
|
||||
): String? {
|
||||
return try {
|
||||
val reader = WKTReader()
|
||||
val unionedGeometry = reader.read(baseContour).union(reader.read(contourToAdd))
|
||||
if (unionedGeometry !is Polygon) {
|
||||
logger.warn(
|
||||
"Merge skipped because union produced non-polygon geometry: satelliteId={}, geometryType={}, candidateStart={}, candidateEnd={}, existingStart={}, existingEnd={}, candidateRoll={}, existingRoll={}, candidateSource={}, existingSource={}",
|
||||
satelliteId,
|
||||
unionedGeometry.geometryType,
|
||||
candidateSurvey.time,
|
||||
candidateSurvey.timeStop,
|
||||
existingSurvey.time,
|
||||
existingSurvey.timeStop,
|
||||
candidateSurvey.roll,
|
||||
existingSurvey.roll,
|
||||
candidateSurvey.source,
|
||||
existingSurvey.source
|
||||
)
|
||||
return null
|
||||
}
|
||||
WKTWriter().write(unionedGeometry)
|
||||
} catch (e: Exception) {
|
||||
logger.warn("Не удалось объединить контуры маршрутов при placement: {}", e.message)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun logInvalidGeometryIfNeeded(
|
||||
satelliteId: Long,
|
||||
geometryRole: String,
|
||||
geometry: Geometry
|
||||
) {
|
||||
if (geometry.isValid) {
|
||||
return
|
||||
}
|
||||
logger.warn(
|
||||
"Merge geometry is invalid during placement: satelliteId={}, geometryRole={}, geometryType={}",
|
||||
satelliteId,
|
||||
geometryRole,
|
||||
geometry.geometryType
|
||||
)
|
||||
}
|
||||
|
||||
private fun logMergeSkippedWithoutSufficientGeometry(
|
||||
satelliteId: Long,
|
||||
candidateSurvey: SurveyMode,
|
||||
existingSurvey: SurveyMode,
|
||||
intersectionArea: Double,
|
||||
distance: Double?,
|
||||
overlapsInTime: Boolean
|
||||
) {
|
||||
if (overlapsInTime) {
|
||||
logger.warn(
|
||||
"Merge skipped because contours lack sufficient areal intersection despite temporal overlap: satelliteId={}, intersectionArea={}, epsilon={}, candidateStart={}, candidateEnd={}, existingStart={}, existingEnd={}, candidateRoll={}, existingRoll={}, candidateSource={}, existingSource={}, candidateCellNums={}, existingCellNums={}, candidateBookedSlotIds={}, existingBookedSlotIds={}",
|
||||
satelliteId,
|
||||
intersectionArea,
|
||||
GEOMETRY_INTERSECTION_AREA_EPSILON,
|
||||
candidateSurvey.time,
|
||||
candidateSurvey.timeStop,
|
||||
existingSurvey.time,
|
||||
existingSurvey.timeStop,
|
||||
candidateSurvey.roll,
|
||||
existingSurvey.roll,
|
||||
candidateSurvey.source,
|
||||
existingSurvey.source,
|
||||
normalizeCellNums(candidateSurvey).toList().sorted(),
|
||||
normalizeCellNums(existingSurvey).toList().sorted(),
|
||||
candidateSurvey.bookedSlotIds,
|
||||
existingSurvey.bookedSlotIds
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
"Merge skipped because contours are not continuous enough despite mergeable time window: satelliteId={}, intersectionArea={}, areaEpsilon={}, distance={}, distanceEpsilon={}, maxTimeGapSeconds={}, candidateStart={}, candidateEnd={}, existingStart={}, existingEnd={}, candidateRoll={}, existingRoll={}, candidateSource={}, existingSource={}, candidateCellNums={}, existingCellNums={}, candidateBookedSlotIds={}, existingBookedSlotIds={}",
|
||||
satelliteId,
|
||||
intersectionArea,
|
||||
GEOMETRY_INTERSECTION_AREA_EPSILON,
|
||||
distance,
|
||||
GEOMETRY_DISTANCE_EPSILON,
|
||||
MERGE_MAX_TIME_GAP_SECONDS,
|
||||
candidateSurvey.time,
|
||||
candidateSurvey.timeStop,
|
||||
existingSurvey.time,
|
||||
existingSurvey.timeStop,
|
||||
candidateSurvey.roll,
|
||||
existingSurvey.roll,
|
||||
candidateSurvey.source,
|
||||
existingSurvey.source,
|
||||
normalizeCellNums(candidateSurvey).toList().sorted(),
|
||||
normalizeCellNums(existingSurvey).toList().sorted(),
|
||||
candidateSurvey.bookedSlotIds,
|
||||
existingSurvey.bookedSlotIds
|
||||
)
|
||||
}
|
||||
|
||||
private fun SurveyMode.copyWith(
|
||||
time: LocalDateTime = this.time,
|
||||
timeStop: LocalDateTime = this.timeStop,
|
||||
duration: Double = this.duration,
|
||||
revolution: Long = this.revolution,
|
||||
contourWKT: String = this.contourWKT,
|
||||
cellNum: Long = this.cellNum,
|
||||
source: SatelliteModeSource = this.source,
|
||||
bookedSlotIds: List<Long> = this.bookedSlotIds,
|
||||
cellNums: List<Long> = this.cellNums
|
||||
) = SurveyMode(
|
||||
revolution = revolution,
|
||||
time = time,
|
||||
timeStop = timeStop,
|
||||
roll = roll,
|
||||
latitude = latitude,
|
||||
longitude = longitude,
|
||||
duration = duration,
|
||||
contourWKT = contourWKT,
|
||||
cellNum = cellNum,
|
||||
source = source,
|
||||
bookedSlotIds = bookedSlotIds,
|
||||
cellNums = cellNums
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
spring:
|
||||
application:
|
||||
name: pcp-complex-mission-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}
|
||||
|
||||
---
|
||||
# Fallback for direct bootRun when the external config server does not provide service datasource settings.
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: local
|
||||
datasource:
|
||||
driver-class-name: org.postgresql.Driver
|
||||
url: ${PCP_COMPLEX_MISSION_DATASOURCE_URL:jdbc:postgresql://${PCP_POSTGRES_HOST:localhost}:${PCP_POSTGRES_PORT:5432}/pcp_satellites}
|
||||
username: ${PCP_COMPLEX_MISSION_DATASOURCE_USERNAME:postgres}
|
||||
password: ${PCP_COMPLEX_MISSION_DATASOURCE_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
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: dev
|
||||
datasource:
|
||||
driver-class-name: org.postgresql.Driver
|
||||
url: ${PCP_COMPLEX_MISSION_DATASOURCE_URL:jdbc:postgresql://192.168.100.160:35400/pcp_satellites}
|
||||
username: ${PCP_COMPLEX_MISSION_DATASOURCE_USERNAME:postgres}
|
||||
password: ${PCP_COMPLEX_MISSION_DATASOURCE_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
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
CREATE TABLE IF NOT EXISTS satellite_modes (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
satellite_id BIGINT NOT NULL,
|
||||
source VARCHAR(20) NOT NULL,
|
||||
type VARCHAR(20) NOT NULL,
|
||||
start_time TIMESTAMP NOT NULL,
|
||||
end_time TIMESTAMP NOT NULL,
|
||||
revolution BIGINT NOT NULL,
|
||||
lat DOUBLE PRECISION NOT NULL,
|
||||
longitude DOUBLE PRECISION NOT NULL,
|
||||
duration DOUBLE PRECISION NOT NULL,
|
||||
contour_wkt TEXT,
|
||||
roll DOUBLE PRECISION NOT NULL,
|
||||
cell_num BIGINT NOT NULL DEFAULT -1,
|
||||
CONSTRAINT chk_satellite_modes_source CHECK (source IN ('SLOTS', 'COMPLAN', 'MIXED')),
|
||||
CONSTRAINT chk_satellite_modes_type CHECK (type IN ('SURVEY'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_satellite_modes_satellite_id
|
||||
ON satellite_modes(satellite_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_satellite_modes_start_time
|
||||
ON satellite_modes(start_time);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_satellite_modes_end_time
|
||||
ON satellite_modes(end_time);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_satellite_modes_source
|
||||
ON satellite_modes(source);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_satellite_modes_satellite_interval
|
||||
ON satellite_modes(satellite_id, start_time, end_time);
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
CREATE TABLE IF NOT EXISTS satellite_mode_booked_slot (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
mode_id BIGINT NOT NULL REFERENCES satellite_modes(id) ON DELETE CASCADE,
|
||||
booked_slot_id BIGINT NOT NULL,
|
||||
CONSTRAINT uq_satellite_mode_booked_slot_mode_booked_slot UNIQUE(mode_id, booked_slot_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_satellite_mode_booked_slot_mode_id
|
||||
ON satellite_mode_booked_slot(mode_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_satellite_mode_booked_slot_booked_slot_id
|
||||
ON satellite_mode_booked_slot(booked_slot_id);
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE satellite_modes
|
||||
DROP CONSTRAINT IF EXISTS chk_satellite_modes_source;
|
||||
|
||||
ALTER TABLE satellite_modes
|
||||
ADD CONSTRAINT chk_satellite_modes_source
|
||||
CHECK (source IN ('SLOTS', 'COMPLAN', 'MIXED'));
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
CREATE TABLE IF NOT EXISTS satellite_mode_cell (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
mode_id BIGINT NOT NULL REFERENCES satellite_modes(id) ON DELETE CASCADE,
|
||||
cell_num BIGINT NOT NULL,
|
||||
CONSTRAINT uq_satellite_mode_cell_mode_cell UNIQUE(mode_id, cell_num)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_satellite_mode_cell_mode_id
|
||||
ON satellite_mode_cell(mode_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_satellite_mode_cell_cell_num
|
||||
ON satellite_mode_cell(cell_num);
|
||||
|
||||
INSERT INTO satellite_mode_cell (mode_id, cell_num)
|
||||
SELECT id, cell_num
|
||||
FROM satellite_modes
|
||||
WHERE cell_num > 0
|
||||
ON CONFLICT DO NOTHING;
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
CREATE TABLE IF NOT EXISTS complex_plan_run (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
status VARCHAR(20) NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
started_at TIMESTAMP,
|
||||
finished_at TIMESTAMP,
|
||||
duration_ms BIGINT,
|
||||
interval_start TIMESTAMP NOT NULL,
|
||||
interval_end TIMESTAMP NOT NULL,
|
||||
requested_by VARCHAR(255),
|
||||
error_message TEXT,
|
||||
calculated_modes_count INTEGER NOT NULL DEFAULT 0,
|
||||
booked_modes_count INTEGER NOT NULL DEFAULT 0,
|
||||
complan_modes_count INTEGER NOT NULL DEFAULT 0,
|
||||
mixed_modes_count INTEGER NOT NULL DEFAULT 0,
|
||||
affected_satellites_count INTEGER NOT NULL DEFAULT 0,
|
||||
processed_cells_count INTEGER NOT NULL DEFAULT 0,
|
||||
booked_slot_links_count INTEGER NOT NULL DEFAULT 0,
|
||||
request_payload TEXT,
|
||||
CONSTRAINT chk_complex_plan_run_status CHECK (status IN ('CREATED', 'RUNNING', 'COMPLETED', 'FAILED'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_complex_plan_run_status
|
||||
ON complex_plan_run(status);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_complex_plan_run_created_at
|
||||
ON complex_plan_run(created_at);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_complex_plan_run_interval_start
|
||||
ON complex_plan_run(interval_start);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_complex_plan_run_interval_end
|
||||
ON complex_plan_run(interval_end);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS complex_plan_run_satellite (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
run_id BIGINT NOT NULL REFERENCES complex_plan_run(id) ON DELETE CASCADE,
|
||||
satellite_id BIGINT NOT NULL,
|
||||
CONSTRAINT uq_complex_plan_run_satellite_run_satellite UNIQUE (run_id, satellite_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_complex_plan_run_satellite_run_id
|
||||
ON complex_plan_run_satellite(run_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_complex_plan_run_satellite_satellite_id
|
||||
ON complex_plan_run_satellite(satellite_id);
|
||||
|
||||
ALTER TABLE satellite_modes
|
||||
ADD COLUMN IF NOT EXISTS complex_plan_run_id BIGINT REFERENCES complex_plan_run(id) ON DELETE SET NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_satellite_modes_complex_plan_run_id
|
||||
ON satellite_modes(complex_plan_run_id);
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE satellite_modes
|
||||
DROP CONSTRAINT IF EXISTS chk_satellite_modes_source;
|
||||
|
||||
ALTER TABLE satellite_modes
|
||||
ADD CONSTRAINT chk_satellite_modes_source
|
||||
CHECK (source IN ('SLOTS', 'COMPLAN', 'MIXED', 'MANUAL'));
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
CREATE TABLE IF NOT EXISTS satellite_mode_snapshots (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
satellite_id BIGINT NOT NULL,
|
||||
interval_start TIMESTAMP NOT NULL,
|
||||
interval_end TIMESTAMP NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
is_active BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
complex_plan_run_id BIGINT REFERENCES complex_plan_run(id) ON DELETE SET NULL,
|
||||
comment TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_satellite_mode_snapshots_satellite_id
|
||||
ON satellite_mode_snapshots(satellite_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_satellite_mode_snapshots_interval_start
|
||||
ON satellite_mode_snapshots(interval_start);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_satellite_mode_snapshots_interval_end
|
||||
ON satellite_mode_snapshots(interval_end);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_satellite_mode_snapshots_is_active
|
||||
ON satellite_mode_snapshots(is_active);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_satellite_mode_snapshots_run_id
|
||||
ON satellite_mode_snapshots(complex_plan_run_id);
|
||||
|
||||
ALTER TABLE satellite_modes
|
||||
ADD COLUMN IF NOT EXISTS snapshot_id BIGINT REFERENCES satellite_mode_snapshots(id) ON DELETE CASCADE;
|
||||
|
||||
INSERT INTO satellite_mode_snapshots (
|
||||
satellite_id,
|
||||
interval_start,
|
||||
interval_end,
|
||||
created_at,
|
||||
is_active,
|
||||
complex_plan_run_id,
|
||||
comment
|
||||
)
|
||||
SELECT
|
||||
sm.satellite_id,
|
||||
COALESCE(cpr.interval_start, MIN(sm.start_time)),
|
||||
COALESCE(cpr.interval_end, MAX(sm.end_time)),
|
||||
COALESCE(cpr.created_at, CURRENT_TIMESTAMP),
|
||||
TRUE,
|
||||
sm.complex_plan_run_id,
|
||||
CASE
|
||||
WHEN sm.complex_plan_run_id IS NULL THEN 'legacy-backfill-no-run'
|
||||
ELSE 'legacy-backfill-from-run'
|
||||
END
|
||||
FROM satellite_modes sm
|
||||
LEFT JOIN complex_plan_run cpr ON cpr.id = sm.complex_plan_run_id
|
||||
WHERE sm.snapshot_id IS NULL
|
||||
GROUP BY
|
||||
sm.satellite_id,
|
||||
sm.complex_plan_run_id,
|
||||
cpr.interval_start,
|
||||
cpr.interval_end,
|
||||
cpr.created_at;
|
||||
|
||||
UPDATE satellite_modes sm
|
||||
SET snapshot_id = snapshots.id
|
||||
FROM satellite_mode_snapshots snapshots
|
||||
WHERE sm.snapshot_id IS NULL
|
||||
AND snapshots.satellite_id = sm.satellite_id
|
||||
AND (
|
||||
(sm.complex_plan_run_id IS NOT NULL AND snapshots.complex_plan_run_id = sm.complex_plan_run_id)
|
||||
OR
|
||||
(sm.complex_plan_run_id IS NULL AND snapshots.complex_plan_run_id IS NULL)
|
||||
);
|
||||
|
||||
WITH ranked_snapshots AS (
|
||||
SELECT
|
||||
id,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY satellite_id, interval_start, interval_end
|
||||
ORDER BY created_at DESC, id DESC
|
||||
) AS rn
|
||||
FROM satellite_mode_snapshots
|
||||
)
|
||||
UPDATE satellite_mode_snapshots snapshots
|
||||
SET is_active = CASE WHEN ranked_snapshots.rn = 1 THEN TRUE ELSE FALSE END
|
||||
FROM ranked_snapshots
|
||||
WHERE ranked_snapshots.id = snapshots.id;
|
||||
|
||||
ALTER TABLE satellite_modes
|
||||
ALTER COLUMN snapshot_id SET NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_satellite_modes_snapshot_id
|
||||
ON satellite_modes(snapshot_id);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_satellite_mode_snapshots_active_per_interval
|
||||
ON satellite_mode_snapshots(satellite_id, interval_start, interval_end)
|
||||
WHERE is_active = TRUE;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE complex_plan_run
|
||||
ADD COLUMN IF NOT EXISTS total_requests_area DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS covered_area DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS covered_area_percent DOUBLE PRECISION NOT NULL DEFAULT 0;
|
||||
Reference in New Issue
Block a user