Init
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
FROM bellsoft/liberica-openjre-alpine:21.0.5
|
||||
|
||||
ENV JAVA_OPTS=""
|
||||
|
||||
ADD ./build/libs/*.jar /app.jar
|
||||
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app.jar"]
|
||||
@@ -0,0 +1,57 @@
|
||||
group = "space.nstart.pcp"
|
||||
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
kotlin("plugin.spring")
|
||||
id("org.springframework.boot")
|
||||
id("io.spring.dependency-management")
|
||||
}
|
||||
|
||||
|
||||
version = "1.0.0"
|
||||
description = "Комплексный план. Динамические слоты"
|
||||
|
||||
|
||||
kotlin {
|
||||
jvmToolchain((property("versions.java") as String).toInt())
|
||||
compilerOptions {
|
||||
freeCompilerArgs.addAll("-Xjsr305=strict")
|
||||
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.fromTarget(property("versions.java") as String))
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":libs:pcp-types-lib"))
|
||||
implementation(project(":libs:ballistics-lib"))
|
||||
|
||||
implementation("${property("dep.spring.actuator")}")
|
||||
implementation("org.springframework.boot:spring-boot-starter")
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
implementation("org.springframework.boot:spring-boot-starter-validation")
|
||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||
implementation("org.springframework.boot:spring-boot-starter-flyway")
|
||||
implementation("org.flywaydb:flyway-database-postgresql")
|
||||
implementation("org.springframework.kafka:spring-kafka")
|
||||
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:${property("versions.open-api")}")
|
||||
implementation("org.springframework.cloud:spring-cloud-starter-config")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||
implementation("org.locationtech.jts:jts-core:1.19.0")
|
||||
|
||||
runtimeOnly("org.postgresql:postgresql")
|
||||
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
}
|
||||
|
||||
dependencyManagement {
|
||||
imports {
|
||||
mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("versions.spring.cloud")}")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<Test> {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package space.nstart.pcp.complan
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
|
||||
|
||||
@SpringBootApplication
|
||||
class ComPlan
|
||||
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<space.nstart.pcp.complan.ComPlan>(*args)
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
package space.nstart.pcp.complan
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag
|
||||
import org.springframework.http.HttpStatus
|
||||
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.ResponseStatus
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
import space.nstart.pcp.complan.apl.BasicPlanCalculator
|
||||
import space.nstart.pcp.complan.service.DynamicPlanRunService
|
||||
import space.nstart.pcp.complan.types.ComplexPlanCalculationRequestDTO
|
||||
import space.nstart.pcp.complan.types.ComplexPlanCalculationResultDTO
|
||||
import space.nstart.pcp.complan.types.DynamicPlanIntervalDTO
|
||||
import space.nstart.pcp.complan.types.DynamicPlanRouteDTO
|
||||
import space.nstart.pcp.complan.types.DynamicPlanRunDTO
|
||||
import space.nstart.pcp.complan.types.DynamicPlanRunStatus
|
||||
import space.nstart.pcp.complan.types.GreedyMathDebugRequestDTO
|
||||
import space.nstart.pcp.complan.types.GreedyMathDebugResultDTO
|
||||
import space.nstart.pcp.complan.types.ObservationParametersDebugRequestDTO
|
||||
import space.nstart.pcp.complan.types.ObservationParametersDebugResultDTO
|
||||
import space.nstart.pcp.complan.types.TestCalcMarsDebugRequestDTO
|
||||
import space.nstart.pcp.complan.types.TestCalcMarsDebugResultDTO
|
||||
import java.nio.file.NoSuchFileException
|
||||
import java.util.UUID
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/v1/main")
|
||||
@Tag(name = "Расчет комплексного плана", description = "API расчета комплексного плана")
|
||||
class ComPlanController(
|
||||
private val dynamicPlanRunService: DynamicPlanRunService,
|
||||
private val basicPlanCalculator: BasicPlanCalculator
|
||||
) {
|
||||
|
||||
@PostMapping("/calcPlan")
|
||||
@ResponseStatus(HttpStatus.ACCEPTED)
|
||||
fun calcPlan(
|
||||
@RequestBody request: ComplexPlanCalculationRequestDTO
|
||||
): DynamicPlanRunDTO = dynamicPlanRunService.startCalculation(request)
|
||||
|
||||
@GetMapping("/calcPlan/runs")
|
||||
fun getCalculations(
|
||||
@RequestParam(required = false) status: DynamicPlanRunStatus?,
|
||||
@RequestParam(defaultValue = "50") limit: Int,
|
||||
@RequestParam(defaultValue = "0") offset: Int
|
||||
): List<DynamicPlanRunDTO> = dynamicPlanRunService.listRuns(status, limit, offset)
|
||||
|
||||
@GetMapping("/calcPlan/{runId}")
|
||||
fun getCalculation(
|
||||
@PathVariable runId: UUID
|
||||
): DynamicPlanRunDTO = dynamicPlanRunService.getRun(runId)
|
||||
|
||||
@GetMapping("/calcPlan/{runId}/result")
|
||||
fun getCalculationResult(
|
||||
@PathVariable runId: UUID
|
||||
): ComplexPlanCalculationResultDTO = dynamicPlanRunService.getResult(runId)
|
||||
|
||||
@GetMapping("/calcPlan/{runId}/routes")
|
||||
fun getCalculationRoutes(
|
||||
@PathVariable runId: UUID,
|
||||
@RequestParam(defaultValue = "1000") limit: Int,
|
||||
@RequestParam(defaultValue = "0") offset: Int
|
||||
): List<DynamicPlanRouteDTO> = dynamicPlanRunService.getRoutes(runId, limit, offset)
|
||||
|
||||
@GetMapping("/calcPlan/{runId}/intervals")
|
||||
fun getCalculationIntervals(
|
||||
@PathVariable runId: UUID,
|
||||
@RequestParam(defaultValue = "1000") limit: Int,
|
||||
@RequestParam(defaultValue = "0") offset: Int
|
||||
): List<DynamicPlanIntervalDTO> = dynamicPlanRunService.getIntervals(runId, limit, offset)
|
||||
|
||||
@PostMapping("/calcPlan/debug/observation-parameters")
|
||||
fun debugObservationParameters(
|
||||
@RequestBody request: ObservationParametersDebugRequestDTO
|
||||
): ObservationParametersDebugResultDTO {
|
||||
if (request.calculationEnd <= request.calculationStart) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "calculationEnd must be after calculationStart")
|
||||
}
|
||||
if (request.satelliteIds.isEmpty()) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "satelliteIds must not be empty")
|
||||
}
|
||||
if (request.chunkSize != null && request.chunkSize < 1) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "chunkSize must be greater than or equal to 1")
|
||||
}
|
||||
if (request.parallelism != null && request.parallelism < 1) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "parallelism must be greater than or equal to 1")
|
||||
}
|
||||
return basicPlanCalculator.calculateObservationParametersDebug(request)
|
||||
}
|
||||
|
||||
@PostMapping("/calcPlan/debug/greedy-math")
|
||||
fun debugGreedyMath(
|
||||
@RequestBody request: GreedyMathDebugRequestDTO
|
||||
): GreedyMathDebugResultDTO {
|
||||
if (request.calculationEnd <= request.calculationStart) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "calculationEnd must be after calculationStart")
|
||||
}
|
||||
if (request.satelliteIds.isEmpty()) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "satelliteIds must not be empty")
|
||||
}
|
||||
return try {
|
||||
basicPlanCalculator.calculateGreedyMathDebug(request)
|
||||
} catch (error: NoSuchFileException) {
|
||||
throw ResponseStatusException(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"observation parameters file not found: ${error.file}",
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/calcPlan/debug/testcalcMars")
|
||||
fun testCalcMars(
|
||||
@RequestBody request: TestCalcMarsDebugRequestDTO
|
||||
): TestCalcMarsDebugResultDTO {
|
||||
if (request.calculationEnd <= request.calculationStart) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "calculationEnd must be after calculationStart")
|
||||
}
|
||||
if (request.satelliteIds.isEmpty()) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "satelliteIds must not be empty")
|
||||
}
|
||||
return try {
|
||||
basicPlanCalculator.calculateTestCalcMarsDebug(request)
|
||||
} catch (error: NoSuchFileException) {
|
||||
throw ResponseStatusException(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"observation parameters file not found: ${error.file}",
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package space.nstart.pcp.complan
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.SerializationFeature
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
||||
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
||||
|
||||
@Configuration
|
||||
class JacksonConfig {
|
||||
|
||||
@Bean
|
||||
fun objectMapper(): ObjectMapper {
|
||||
val mapper = ObjectMapper()
|
||||
|
||||
// Настраиваем формат для LocalDateTime
|
||||
// val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.zzz")
|
||||
|
||||
// module.addSerializer<LocalDateTime?>(
|
||||
// LocalDateTime::class.java,
|
||||
// LocalDateTimeSerializer(formatter)
|
||||
// )
|
||||
// module.addDeserializer<LocalDateTime?>(
|
||||
// LocalDateTime::class.java,
|
||||
// LocalDateTimeDeserializer(formatter)
|
||||
// )
|
||||
mapper.registerModule(JavaTimeModule())
|
||||
.registerKotlinModule()
|
||||
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
|
||||
|
||||
return mapper
|
||||
}
|
||||
}
|
||||
+1985
File diff suppressed because one or more lines are too long
+36
@@ -0,0 +1,36 @@
|
||||
package space.nstart.pcp.complan.apl
|
||||
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.atan
|
||||
import kotlin.math.sqrt
|
||||
|
||||
class CaptureAngleCalculator {
|
||||
|
||||
fun calculateCaptureAngle(routeWidthKm: Double, points: List<OrbPointDTO>): Double? {
|
||||
if (routeWidthKm <= 0.0 || points.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val averageHeightMeters = points
|
||||
.map { it.orbitRadiusMeters() - EARTH_RADIUS_METERS }
|
||||
.filter { it > 0.0 }
|
||||
.average()
|
||||
|
||||
if (averageHeightMeters.isNaN() || averageHeightMeters <= 0.0) {
|
||||
return null
|
||||
}
|
||||
|
||||
val halfWidthMeters = routeWidthKm * METERS_IN_KILOMETER / 2.0
|
||||
return atan(halfWidthMeters / averageHeightMeters).toDegrees()
|
||||
}
|
||||
|
||||
private fun OrbPointDTO.orbitRadiusMeters(): Double = sqrt(x * x + y * y + z * z)
|
||||
|
||||
private fun Double.toDegrees(): Double = this * 180.0 / PI
|
||||
|
||||
private companion object {
|
||||
const val EARTH_RADIUS_METERS = 6_378_136.0
|
||||
const val METERS_IN_KILOMETER = 1_000.0
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package space.nstart.pcp.complan.apl
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.stereotype.Service
|
||||
import space.nstart.pcp.complan.types.GeoJson
|
||||
import space.nstart.pcp.complan.types.GeoJsonItem
|
||||
|
||||
private val DEFAULT_GEO_JSON_LAYER_OUTPUT_DIR =
|
||||
"${System.getProperty("java.io.tmpdir")}/pcp-dynamic-plan-service/geo-layers"
|
||||
|
||||
@Service
|
||||
class GeoJsonLayerWriter(
|
||||
@param:Value("\${complex-plan.geo-layers.enabled:false}")
|
||||
private val enabled: Boolean = false,
|
||||
|
||||
@param:Value("\${complex-plan.geo-layers.output-dir:\${java.io.tmpdir}/pcp-dynamic-plan-service/geo-layers}")
|
||||
private val outputDir: String = DEFAULT_GEO_JSON_LAYER_OUTPUT_DIR
|
||||
) {
|
||||
fun write(geoms: List<GeoJsonItem>, fileName: String, radToDeg: Boolean = false) {
|
||||
if (!enabled) {
|
||||
return
|
||||
}
|
||||
|
||||
GeoJson.createGeoJson(geoms, fileName, outputDir, radToDeg)
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package space.nstart.pcp.complan.apl
|
||||
|
||||
import org.locationtech.jts.geom.Geometry
|
||||
import org.locationtech.jts.operation.union.UnaryUnionOp
|
||||
|
||||
class IntervalCoverageChecker {
|
||||
|
||||
fun findUncoveredGeometry(contour: Geometry, intervals: List<Geometry>): Geometry {
|
||||
if (intervals.isEmpty()) {
|
||||
return contour.copy()
|
||||
}
|
||||
|
||||
val intervalsUnion = UnaryUnionOp(intervals).union()
|
||||
return contour.difference(intervalsUnion)
|
||||
}
|
||||
}
|
||||
Executable
+306
@@ -0,0 +1,306 @@
|
||||
package space.nstart.pcp.complan.apl
|
||||
|
||||
import org.locationtech.jts.geom.*
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import java.lang.Double.max
|
||||
import java.lang.Double.min
|
||||
import kotlin.collections.get
|
||||
import kotlin.compareTo
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.cos
|
||||
|
||||
/**
|
||||
* Класс построения интервалов съемки
|
||||
* Под интервалами съемки полагаются марш
|
||||
*/
|
||||
@Service
|
||||
class IntervalsBuilder(private val writeLog : Boolean = false) {
|
||||
/** Логирование*/
|
||||
private val log = LoggerFactory.getLogger(this::class.java)
|
||||
/** Фабрика geometry*/
|
||||
private val geomFactory = GeometryFactory()
|
||||
|
||||
private val erthEcvator = 111.3213777 * (180 / PI)
|
||||
|
||||
/**
|
||||
* Создание линии параллельной заданной подвинутой на delX
|
||||
* @param delX дельта перемещения по оси Х
|
||||
* @param line эталонная линия
|
||||
* @return Возвращает линию параллельную эталонной подвинутой на значение delX
|
||||
*/
|
||||
private fun createParallelLine(
|
||||
line: LineString,
|
||||
delX: Double
|
||||
): LineString {
|
||||
val newCoordinates = line.coordinates.map {
|
||||
Coordinate(it.x + delX, it.y)
|
||||
}.toTypedArray()
|
||||
|
||||
return geomFactory.createLineString(newCoordinates)
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание полигона параллельного заданному подвинутому на delX
|
||||
* @param delX дельта перемещения по оси Х
|
||||
* @param pol эталонная полигон
|
||||
* @return Возвращает полигон параллельный эталонному подвинутому на значение delX
|
||||
*/
|
||||
private fun createParallelPol(
|
||||
pol: Polygon,
|
||||
delX: Double
|
||||
): Polygon {
|
||||
val newCoordinates = pol.coordinates.map {
|
||||
Coordinate(it.x + delX, it.y)
|
||||
}.toTypedArray()
|
||||
|
||||
return geomFactory.createPolygon(newCoordinates)
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение описывающего многоугольника
|
||||
* @param geom контур геометрии
|
||||
* @return Возвращает описывающий многоугольник [org.locationtech.jts.geom.Envelope]
|
||||
*/
|
||||
private fun getBoxInternal(geom: Geometry) = geom.envelopeInternal
|
||||
|
||||
/**
|
||||
* Возвращает расстояние в км на определенной широте
|
||||
* @param distanceKm дистанция в км
|
||||
* @param lat широта
|
||||
* @return Возвращает значение в радианах эквивалентное дистанции в километрах на данной широте
|
||||
*/
|
||||
private fun getCaptureDistanceInKm(distanceKm: Double, lat: Double) = (distanceKm / (erthEcvator * cos(lat / 180 * PI ))) * 180 / PI //.toRad()
|
||||
//для градусов, но мы считаем в радианах
|
||||
// private fun getCaptureDistanceInKm(distanceKm: Double, lat: Double) = (distanceKm / (erthecvator * cos(lat))).toRad()
|
||||
|
||||
/**
|
||||
* Возвращает одно значение из двух, которое по модулю ближе к 0
|
||||
* @param a ервое значение
|
||||
* @param b второе значение
|
||||
* @return Возвращает значение по модулю близкое к 0
|
||||
*/
|
||||
private fun closerToZero(a: Double, b: Double) = if(abs(a) < abs(b)) a else b
|
||||
|
||||
/**
|
||||
* Передвижение маршрута ближе к контуру
|
||||
* Точность до 1/10 ширины маршрута
|
||||
* @param line линия
|
||||
* @param contour контур
|
||||
* @param offset ширина маршрута
|
||||
* @return Возвращает маршрут отступающий от контура не более чем на 0,1 ширины маршрута
|
||||
*/
|
||||
private fun moveBorderMarToContour(
|
||||
line: LineString,
|
||||
contour: Polygon,
|
||||
offset: Double
|
||||
): Polygon {
|
||||
var line = line
|
||||
var step = offset
|
||||
|
||||
val leftLine = line
|
||||
val rightLine = createParallelLine(line, -1 * step) //3
|
||||
var pol = geomFactory.createPolygon(createMar(rightLine, leftLine))
|
||||
|
||||
while (true) {
|
||||
if (!pol.intersects(contour)) {
|
||||
// if(i > 150) throw IllegalArgumentException("Бесконечный цикл нахождения граничных лииний")
|
||||
pol = createParallelPol(pol, step)
|
||||
} else {
|
||||
pol = createParallelPol(pol, -step)
|
||||
if (step == offset / 10)
|
||||
break
|
||||
step = step / 10
|
||||
}
|
||||
}
|
||||
return pol
|
||||
}
|
||||
|
||||
/**
|
||||
* Нахождение ширины региона
|
||||
* @param borderLines граничные линии для контура
|
||||
* @return Возвращает ширину региона между его граничными линиями
|
||||
*/
|
||||
private fun getRegionWidth(borderpols: Pair<Polygon, Polygon>, offset: Double): Double {
|
||||
val firstEnvelope = borderpols.first.envelopeInternal
|
||||
val secondEnvelope = borderpols.second.envelopeInternal
|
||||
|
||||
return (secondEnvelope.maxX - firstEnvelope.maxX) - 1*offset //3
|
||||
}
|
||||
|
||||
/**
|
||||
* Нахождение граничных полигонов
|
||||
* @param flightLine линия полета
|
||||
* @param contour контур региона
|
||||
* @param internalBox описывающий многоугольник
|
||||
* @param offset ширина маршрута
|
||||
* @return Возвращает пару полигонов в порядке: **ЛЕВЫЙ**, **ПРАВЫЙ**
|
||||
*/
|
||||
private fun findBorderPols(
|
||||
flightLine: LineString,
|
||||
contour: Polygon,
|
||||
internalBox: Envelope,
|
||||
offset: Double
|
||||
): Pair<Polygon, Polygon> {
|
||||
val xUp = LineShifter().getPointOnLineY(flightLine, internalBox.maxY) ?: throw IllegalStateException("Ошибка нахождения граничных точек")
|
||||
val xDown = LineShifter()
|
||||
.getPointOnLineY(flightLine, internalBox.minY) ?: throw IllegalStateException("Ошибка нахождения граничных точек")
|
||||
|
||||
val minXValue = min(min(internalBox.minX - xUp.x, internalBox.maxX - xUp.x), min(internalBox.minX - xDown.x, internalBox.maxX - xDown.x))
|
||||
val maxXValue = max(max(internalBox.minX - xUp.x, internalBox.maxX - xUp.x), max(internalBox.minX - xDown.x, internalBox.maxX - xDown.x))
|
||||
|
||||
val leftLine = LineShifter().lineShift(flightLine, minXValue) ?: throw IllegalStateException("Ошибка нахождения граничных точек")
|
||||
val rightLine = LineShifter().lineShift(flightLine, maxXValue) ?: throw IllegalStateException("Ошибка нахождения граничных точек")
|
||||
//двигаем линии ближе к контуру региона
|
||||
//первая линия
|
||||
val firstPol = moveBorderMarToContour(leftLine, contour, offset)
|
||||
//последняя линия
|
||||
val lastPol = moveBorderMarToContour(rightLine, contour, -offset)
|
||||
|
||||
if (writeLog) { log.info("Граничные точки найдены") }
|
||||
|
||||
return Pair(firstPol, lastPol)
|
||||
}
|
||||
|
||||
/**
|
||||
* Построение интервалов
|
||||
* @param mars список маршрутов
|
||||
* @param contour контур региона
|
||||
* @return Возвращает список интервалов
|
||||
*/
|
||||
private fun buildIntervals(
|
||||
mars: List<Geometry>,
|
||||
contour: Polygon
|
||||
): MutableList<Polygon> {
|
||||
var intervals = mutableListOf<Polygon>()
|
||||
for (mar in mars) {
|
||||
val interval = contour.intersection(mar)
|
||||
if(interval is Polygon) {
|
||||
intervals.add(interval)
|
||||
}
|
||||
else if(interval is MultiPolygon) {
|
||||
for (i in 0 until interval.numGeometries) {
|
||||
intervals.add(interval.getGeometryN(i) as Polygon)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return intervals
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание маршрута
|
||||
* @param leftLine левая граничная линия маршрута
|
||||
* @param rightLine правая граничная линия маршрута
|
||||
* @return Возвращает маршрут заключенный между граничными линиями
|
||||
*/
|
||||
private fun createMar(
|
||||
rightLine: LineString,
|
||||
leftLine: LineString
|
||||
): Array<Coordinate>{
|
||||
val coordinates = mutableListOf<Coordinate>()
|
||||
|
||||
for (j in rightLine.coordinates)
|
||||
coordinates.add(Coordinate(j.x, j.y))
|
||||
|
||||
for (j in leftLine.coordinates.size - 1 downTo 0)
|
||||
coordinates.add(Coordinate(leftLine.coordinates[j].x, leftLine.coordinates[j].y))
|
||||
|
||||
coordinates.add(Coordinate(rightLine.coordinates[0].x, rightLine.coordinates[0].y))
|
||||
return coordinates.toTypedArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* Построение маршрутов
|
||||
* @param width ширина региона
|
||||
* @param lines граничные линии первого маршрута
|
||||
* @param offset ширина маршрута
|
||||
* @param overlap перекрытие маршрутов
|
||||
* @return Возвращает список маршрутов для покрытия региона
|
||||
*/
|
||||
private fun buildMars(
|
||||
width: Double,
|
||||
firstPol: Polygon,
|
||||
offset: Double,
|
||||
overlap: Double
|
||||
): List<Geometry> {
|
||||
val offsetWithOverlap = offset - overlap
|
||||
//расчет минимального количества маршрутов для накрытия полигона
|
||||
var marCount = ((width - offset) / offsetWithOverlap) + 1
|
||||
val intMarCount = ceil(marCount)
|
||||
|
||||
//расчет дополнительного перекрытия
|
||||
val deltaOverlapMars = ((intMarCount * offsetWithOverlap - width) / (intMarCount - 1))//((intMarCount - marCount) / (intMarCount - 1)) * offset
|
||||
|
||||
val marOffset = offsetWithOverlap - deltaOverlapMars // if(deltaOverlapMars > overlap) (offset - deltaOverlapMars) else (offset - overlap)
|
||||
|
||||
var mars = mutableListOf<Geometry>()
|
||||
var marPol = firstPol
|
||||
for (i in 0 until intMarCount.toInt()) {
|
||||
if(i == 0){
|
||||
marPol = createParallelPol(firstPol, offset)
|
||||
}
|
||||
else {
|
||||
val step = marOffset
|
||||
marPol = createParallelPol(marPol, step)
|
||||
}
|
||||
|
||||
mars.add(marPol)
|
||||
}
|
||||
if (writeLog) { log.info("Модель накрытия района выполнена") }
|
||||
return mars
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение интервалов съемки
|
||||
* @param contour контур региона
|
||||
* @param flightLine линия полета
|
||||
* @param offsetKm ширина маршрута в км
|
||||
* @param overlapKm значение минимального перекрытия в км
|
||||
* @return Возвращает список интервалов для заданных параметров
|
||||
*/
|
||||
fun getIntervals(
|
||||
contour: Polygon,
|
||||
flightLine: LineString,
|
||||
offsetKm: Double,
|
||||
overlapKm: Double
|
||||
): List<Polygon> {
|
||||
//TODO(Исправить определение граничных точек района.
|
||||
// При построении интервалов некоторая часть района(граничная слева/справа) пропадает.)
|
||||
//расчет описывающего прямоугольника
|
||||
val internalBox = getBoxInternal(contour)
|
||||
|
||||
if (internalBox.maxY > 80.0 || internalBox.minY < -80.0) {
|
||||
return emptyList()
|
||||
//TODO("Такой район пока что не обрабатывается, нужно определить способ построения интервалов для таких районов")
|
||||
}
|
||||
|
||||
//получаем минимальную широту
|
||||
val minY = closerToZero(internalBox.maxY, internalBox.minY)
|
||||
|
||||
//находим перекрытие и ширину маршрута учитывая широту
|
||||
val offset = getCaptureDistanceInKm(offsetKm, minY)
|
||||
val overlap = getCaptureDistanceInKm(overlapKm, minY)
|
||||
|
||||
//если ширина контура не больше чем ширина маршрута - возвращаем контур как интервал
|
||||
if((internalBox.maxX - internalBox.minX) < offset) {
|
||||
if (writeLog) { log.info("Контур района менее контура маршрута") }
|
||||
return listOf(contour)
|
||||
}
|
||||
|
||||
try {
|
||||
//ищем граничные линии для контура
|
||||
val borderPols = findBorderPols(flightLine, contour, internalBox, offset)
|
||||
|
||||
//строим маршруты
|
||||
val mars = buildMars(getRegionWidth(borderPols, offset), borderPols.first, offset, overlap)
|
||||
|
||||
return buildIntervals(mars, contour)
|
||||
}catch (e: Exception){
|
||||
log.info(e.toString())
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package space.nstart.pcp.complan.apl
|
||||
|
||||
import org.locationtech.jts.geom.Coordinate
|
||||
import org.locationtech.jts.geom.CoordinateFilter
|
||||
import org.locationtech.jts.geom.LineSegment
|
||||
import org.locationtech.jts.geom.LineString
|
||||
import kotlin.collections.get
|
||||
|
||||
/**
|
||||
* Класс для "сдвига" линии на заданную координату по X, не меняя направление сегментов
|
||||
* Поиск значений координат на заданном отрезке по X и Y
|
||||
* */
|
||||
class LineShifter {
|
||||
|
||||
/**
|
||||
* Сдвигает линию на заданное значение X
|
||||
* Если линия состоит менее чем из 2-х точек, возвращает null
|
||||
* */
|
||||
fun lineShift(line : LineString, xValue : Double) : LineString? {
|
||||
if (line.coordinates.size < 2) { return null }
|
||||
|
||||
val copy = line.copy() as LineString
|
||||
|
||||
// Применяем фильтр, который конвертирует радианы → градусы
|
||||
copy.apply(CoordinateFilter { coord ->
|
||||
coord.x = coord.x + xValue // долгота
|
||||
coord.y = coord.y // широта
|
||||
})
|
||||
|
||||
return copy
|
||||
}
|
||||
|
||||
/**
|
||||
* Находит первую точку на полилинии, лежащую на заданной координате Y
|
||||
* Возвращает null, если значение Y лежит вне проекции ее на отрезок
|
||||
* Например отрезок LineSegment(Coordinate(x=0.0,y=0.0), Coordinate(x=10.0,y=5.0))
|
||||
* y должен принадлежать [0.0, 5.0], иначе null
|
||||
* */
|
||||
fun getPointOnLineY(line : LineString, yValue : Double) : Coordinate? {
|
||||
if (line.coordinates.size < 2) { return null }
|
||||
for (i in 1..<line.coordinates.count()) {
|
||||
val lineSegment = LineSegment(line.coordinates[i-1], line.coordinates[i])
|
||||
val needPoint = getPointOnLineY(lineSegment, yValue)
|
||||
if (needPoint != null) {
|
||||
return needPoint
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
/**
|
||||
* Находит точку на отрезке, лежащую на заданной координате Y
|
||||
* Возвращает null, если значение Y лежит вне проекции ее на отрезок
|
||||
* Например отрезок LineSegment(Coordinate(x=0.0,y=0.0), Coordinate(x=10.0,y=5.0))
|
||||
* y должен принадлежать [0.0, 5.0], иначе null
|
||||
* */
|
||||
private fun getPointOnLineY(line : LineSegment, yValue : Double) : Coordinate? {
|
||||
// Случай: отрезок горизонтальный
|
||||
if (line.p0.y == line.p1.y) {
|
||||
return if (yValue == line.p0.y) {
|
||||
// Можно вернуть среднюю X или любую в диапазоне
|
||||
Coordinate((line.p0.x + line.p1.x) / 2.0, yValue)
|
||||
} else {
|
||||
null // нет пересечения
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем, лежит ли targetY между y1 и y2
|
||||
val minY = minOf(line.p0.y, line.p1.y)
|
||||
val maxY = maxOf(line.p0.y, line.p1.y)
|
||||
|
||||
if (yValue !in minY..maxY) {
|
||||
return null // Y вне отрезка
|
||||
}
|
||||
|
||||
// Линейная интерполяция
|
||||
val t = (yValue - line.p0.y) / (line.p1.y - line.p0.y)
|
||||
return Coordinate(line.p0.x + t * (line.p1.x - line.p0.x), yValue)
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package space.nstart.pcp.complan.apl
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.stereotype.Service
|
||||
import space.nstart.pcp.complan.types.ObservationParametersDebugFileDTO
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.util.UUID
|
||||
|
||||
private val DEFAULT_OBSERVATION_PARAMETERS_OUTPUT_DIR =
|
||||
"/home/dsolovyov/ws/264/pcp/services/pcp-dynamic-plan-service/obsParams"
|
||||
|
||||
@Service
|
||||
class ObservationParametersDebugFileWriter(
|
||||
private val objectMapper: ObjectMapper,
|
||||
|
||||
@param:Value("\${complex-plan.observation-parameters.debug-output-dir:/home/dsolovyov/ws/264/pcp/services/pcp-dynamic-plan-service/obsParams}")
|
||||
private val outputDir: String = DEFAULT_OBSERVATION_PARAMETERS_OUTPUT_DIR
|
||||
) {
|
||||
fun write(artifact: ObservationParametersDebugFileDTO): Path {
|
||||
val directory = Path.of(outputDir).toAbsolutePath().normalize()
|
||||
Files.createDirectories(directory)
|
||||
|
||||
val file = filePathFor(artifact.requestId)
|
||||
objectMapper.writerWithDefaultPrettyPrinter().writeValue(file.toFile(), artifact)
|
||||
return file
|
||||
}
|
||||
|
||||
fun read(file: Path): ObservationParametersDebugFileDTO =
|
||||
objectMapper.readValue(file.toFile())
|
||||
|
||||
fun readByRequestId(requestId: UUID): ObservationParametersDebugFileDTO =
|
||||
read(filePathFor(requestId))
|
||||
|
||||
fun filePathFor(requestId: UUID): Path {
|
||||
val directory = Path.of(outputDir).toAbsolutePath().normalize()
|
||||
return directory.resolve("observation-parameters-$requestId.json")
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
package space.nstart.pcp.complan.apl
|
||||
|
||||
import ballistics.flightLine.PointOnEarthCalculator
|
||||
import ballistics.types.EarthType
|
||||
import ballistics.types.Orientation
|
||||
import ballistics.types.OrbitalPoint
|
||||
import ballistics.types.THBLPoint
|
||||
import ballistics.types.WorkCSType
|
||||
import ballistics.utils.fromDateTime
|
||||
import ballistics.utils.math.Vector3D
|
||||
import org.locationtech.jts.geom.Coordinate
|
||||
import org.locationtech.jts.geom.GeometryFactory
|
||||
import org.locationtech.jts.io.WKTWriter
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
|
||||
import kotlin.math.PI
|
||||
|
||||
@Service
|
||||
class SurveyContourBuilder {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||
private val geometryFactory = GeometryFactory()
|
||||
private val wktWriter = WKTWriter()
|
||||
|
||||
fun build(points: List<OrbPointDTO>, roll: Double, capture: Double): String? {
|
||||
val orderedPoints = points.sortedBy { it.time }
|
||||
if (orderedPoints.size < 2) {
|
||||
logger.warn("Контур маршрута не построен: недостаточно точек орбиты, points={}", orderedPoints.size)
|
||||
return null
|
||||
}
|
||||
|
||||
return try {
|
||||
val pointOnEarthCalculator = PointOnEarthCalculator(EarthType.PZ90d02, WorkCSType.WCSOrbit)
|
||||
val rightCoordinates = ArrayList<Coordinate>(orderedPoints.size)
|
||||
val leftCoordinates = ArrayList<Coordinate>(orderedPoints.size)
|
||||
|
||||
orderedPoints.map { it.toOrbitalPoint() }.forEach { point ->
|
||||
val rightPoint = viewParams(pointOnEarthCalculator, point, roll + capture)
|
||||
?: THBLPoint(0.0, 0.0, 0.0, 0.0)
|
||||
rightCoordinates += Coordinate(rightPoint.long.toDegrees(), rightPoint.lat.toDegrees(), 0.0)
|
||||
|
||||
val leftPoint = viewParams(pointOnEarthCalculator, point, roll - capture)
|
||||
?: THBLPoint(0.0, 0.0, 0.0, 0.0)
|
||||
leftCoordinates += Coordinate(leftPoint.long.toDegrees(), leftPoint.lat.toDegrees(), 0.0)
|
||||
}
|
||||
|
||||
val shellCoordinates = ArrayList<Coordinate>(rightCoordinates.size + leftCoordinates.size + 1)
|
||||
shellCoordinates.addAll(rightCoordinates)
|
||||
for (index in leftCoordinates.indices.reversed()) {
|
||||
shellCoordinates += leftCoordinates[index]
|
||||
}
|
||||
if (shellCoordinates.isNotEmpty()) {
|
||||
shellCoordinates += Coordinate(shellCoordinates.first().x, shellCoordinates.first().y, 0.0)
|
||||
}
|
||||
|
||||
val distinctVertices = shellCoordinates
|
||||
.dropLast(1)
|
||||
.map { it.x to it.y }
|
||||
.distinct()
|
||||
if (shellCoordinates.size < 4 || distinctVertices.size < 3) {
|
||||
logger.warn("Контур маршрута не построен: недостаточно уникальных точек полигона")
|
||||
return null
|
||||
}
|
||||
|
||||
val polygon = geometryFactory.createPolygon(shellCoordinates.toTypedArray())
|
||||
wktWriter.write(polygon)
|
||||
} catch (error: Exception) {
|
||||
logger.warn("Контур маршрута не построен: {}", error.message)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun viewParams(
|
||||
pointOnEarthCalculator: PointOnEarthCalculator,
|
||||
point: OrbitalPoint,
|
||||
gamma: Double
|
||||
): THBLPoint? = pointOnEarthCalculator.pointOnEarth(point, Orientation(0.0, gamma.toRadians(), 0.0))
|
||||
|
||||
private fun OrbPointDTO.toOrbitalPoint() = OrbitalPoint(
|
||||
t = fromDateTime(time),
|
||||
vit = revolution.toInt(),
|
||||
r = Vector3D(x, y, z),
|
||||
v = Vector3D(vx, vy, vz)
|
||||
)
|
||||
|
||||
private fun Double.toDegrees(): Double = this * 180.0 / PI
|
||||
|
||||
private fun Double.toRadians(): Double = this * PI / 180.0
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package space.nstart.pcp.complan.config
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@Configuration
|
||||
class DynamicPlanExecutorConfig {
|
||||
|
||||
@Bean(destroyMethod = "shutdown")
|
||||
fun dynamicPlanExecutor(): ExecutorService =
|
||||
Executors.newSingleThreadExecutor { task ->
|
||||
Thread(task, "dynamic-plan-calculation").apply {
|
||||
isDaemon = false
|
||||
}
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package space.nstart.pcp.complan.config
|
||||
|
||||
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()) }
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package space.nstart.pcp.complan.db.entity
|
||||
|
||||
import jakarta.persistence.Entity
|
||||
import jakarta.persistence.GeneratedValue
|
||||
import jakarta.persistence.GenerationType
|
||||
import jakarta.persistence.Id
|
||||
import jakarta.persistence.Column
|
||||
import jakarta.persistence.Table
|
||||
|
||||
@Table(name = "aplsettings")
|
||||
@Entity
|
||||
class AplSettingsEntity (
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "id")
|
||||
var id : Int? = null,
|
||||
|
||||
@Column(name = "settings")
|
||||
var settings : String
|
||||
) {
|
||||
constructor() : this(settings = "")
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package space.nstart.pcp.complan.db.entity
|
||||
|
||||
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.JoinColumn
|
||||
import jakarta.persistence.ManyToOne
|
||||
import jakarta.persistence.Table
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
|
||||
import java.util.UUID
|
||||
|
||||
@Table(name = "dynamic_plan_interval")
|
||||
@Entity
|
||||
class DynamicPlanIntervalEntity(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "id")
|
||||
var id: Long? = null,
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "run_id", nullable = false)
|
||||
var run: DynamicPlanRunEntity? = null,
|
||||
|
||||
@Column(name = "request_id", nullable = false)
|
||||
var requestId: UUID? = null,
|
||||
|
||||
@Column(name = "interval_id", nullable = false)
|
||||
var intervalId: UUID? = null,
|
||||
|
||||
@Column(name = "region_id", nullable = false)
|
||||
var regionId: UUID? = null,
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "rev_sign", nullable = false)
|
||||
var revSign: RevolutionSign = RevolutionSign.ASC,
|
||||
|
||||
@Column(name = "contour_wkt", nullable = false, columnDefinition = "TEXT")
|
||||
var contourWkt: String = "",
|
||||
|
||||
@Column(name = "observation_parameters_count", nullable = false)
|
||||
var observationParametersCount: Int = 0,
|
||||
|
||||
@Column(name = "interval_order", nullable = false)
|
||||
var intervalOrder: Int = 0
|
||||
) {
|
||||
constructor() : this(id = null)
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package space.nstart.pcp.complan.db.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 java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
|
||||
@Table(name = "dynamic_plan_route")
|
||||
@Entity
|
||||
class DynamicPlanRouteEntity(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "id")
|
||||
var id: Long? = null,
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "run_id", nullable = false)
|
||||
var run: DynamicPlanRunEntity? = null,
|
||||
|
||||
@Column(name = "request_id", nullable = false)
|
||||
var requestId: UUID? = null,
|
||||
|
||||
@Column(name = "satellite_id", nullable = false)
|
||||
var satelliteId: Long = 0,
|
||||
|
||||
@Column(name = "start_time", nullable = false)
|
||||
var startTime: LocalDateTime = LocalDateTime.now(),
|
||||
|
||||
@Column(name = "end_time", nullable = false)
|
||||
var endTime: LocalDateTime = LocalDateTime.now(),
|
||||
|
||||
@Column(name = "duration", nullable = false)
|
||||
var duration: Double = 0.0,
|
||||
|
||||
@Column(name = "revolution", nullable = false)
|
||||
var revolution: Long = 0,
|
||||
|
||||
@Column(name = "roll", nullable = false)
|
||||
var roll: Double = 0.0,
|
||||
|
||||
@Column(name = "contour_wkt", nullable = false, columnDefinition = "TEXT")
|
||||
var contourWkt: String = "",
|
||||
|
||||
@Column(name = "route_order", nullable = false)
|
||||
var routeOrder: Int = 0
|
||||
) {
|
||||
constructor() : this(id = null)
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package space.nstart.pcp.complan.db.entity
|
||||
|
||||
import jakarta.persistence.Column
|
||||
import jakarta.persistence.Entity
|
||||
import jakarta.persistence.EnumType
|
||||
import jakarta.persistence.Enumerated
|
||||
import jakarta.persistence.Id
|
||||
import jakarta.persistence.Table
|
||||
import space.nstart.pcp.complan.types.DynamicPlanCalculationMode
|
||||
import space.nstart.pcp.complan.types.DynamicPlanRevolutionMode
|
||||
import space.nstart.pcp.complan.types.DynamicPlanRunStatus
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
|
||||
@Table(name = "dynamic_plan_run")
|
||||
@Entity
|
||||
class DynamicPlanRunEntity(
|
||||
@Id
|
||||
@Column(name = "id", nullable = false)
|
||||
var id: UUID? = null,
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "status", nullable = false)
|
||||
var status: DynamicPlanRunStatus = DynamicPlanRunStatus.PENDING,
|
||||
|
||||
@Column(name = "request_id", nullable = false)
|
||||
var requestId: UUID? = null,
|
||||
|
||||
@Column(name = "satellite_ids", nullable = false)
|
||||
var satelliteIds: String = "[]",
|
||||
|
||||
@Column(name = "calculation_start", nullable = false)
|
||||
var calculationStart: LocalDateTime? = null,
|
||||
|
||||
@Column(name = "calculation_end", nullable = false)
|
||||
var calculationEnd: LocalDateTime? = null,
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "calculation_mode", nullable = false)
|
||||
var calculationMode: DynamicPlanCalculationMode = DynamicPlanCalculationMode.FULL,
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "revolution_mode", nullable = false)
|
||||
var revolutionMode: DynamicPlanRevolutionMode = DynamicPlanRevolutionMode.BOTH,
|
||||
|
||||
@Column(name = "filter_covered_routes", nullable = false)
|
||||
var filterCoveredRoutes: Boolean = false,
|
||||
|
||||
@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 = "result_status")
|
||||
var resultStatus: String? = null,
|
||||
|
||||
@Column(name = "routes_count")
|
||||
var routesCount: Int? = null,
|
||||
|
||||
@Column(name = "result_json", columnDefinition = "TEXT")
|
||||
var resultJson: String? = null,
|
||||
|
||||
@Column(name = "error_message", columnDefinition = "TEXT")
|
||||
var errorMessage: String? = null
|
||||
) {
|
||||
constructor() : this(id = null)
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package space.nstart.pcp.complan.db.repository
|
||||
|
||||
import space.nstart.pcp.complan.db.entity.AplSettingsEntity
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
|
||||
@Repository
|
||||
@Transactional
|
||||
interface AplSettingsRepository: JpaRepository<space.nstart.pcp.complan.db.entity.AplSettingsEntity, Long> {
|
||||
fun findFirstByOrderByIdDesc() : space.nstart.pcp.complan.db.entity.AplSettingsEntity?
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package space.nstart.pcp.complan.db.repository
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
import org.springframework.data.repository.query.Param
|
||||
import org.springframework.stereotype.Repository
|
||||
import space.nstart.pcp.complan.db.entity.DynamicPlanIntervalEntity
|
||||
import java.util.UUID
|
||||
|
||||
@Repository
|
||||
interface DynamicPlanIntervalRepository : JpaRepository<DynamicPlanIntervalEntity, Long> {
|
||||
@Query(
|
||||
value = """
|
||||
select *
|
||||
from dynamic_plan_interval
|
||||
where run_id = :runId
|
||||
order by interval_order asc, id asc
|
||||
limit :limit offset :offset
|
||||
""",
|
||||
nativeQuery = true
|
||||
)
|
||||
fun findIntervalsByRunId(
|
||||
@Param("runId") runId: UUID,
|
||||
@Param("limit") limit: Int,
|
||||
@Param("offset") offset: Int
|
||||
): List<DynamicPlanIntervalEntity>
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package space.nstart.pcp.complan.db.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 org.springframework.stereotype.Repository
|
||||
import space.nstart.pcp.complan.db.entity.DynamicPlanRouteEntity
|
||||
import java.util.UUID
|
||||
|
||||
@Repository
|
||||
interface DynamicPlanRouteRepository : JpaRepository<DynamicPlanRouteEntity, Long> {
|
||||
fun countBySatelliteId(satelliteId: Long): Long
|
||||
|
||||
@Modifying
|
||||
@Query("delete from DynamicPlanRouteEntity route where route.satelliteId = :satelliteId")
|
||||
fun deleteAllBySatelliteId(@Param("satelliteId") satelliteId: Long): Int
|
||||
|
||||
@Query(
|
||||
value = """
|
||||
select *
|
||||
from dynamic_plan_route
|
||||
where run_id = :runId
|
||||
order by start_time asc, id asc
|
||||
limit :limit offset :offset
|
||||
""",
|
||||
nativeQuery = true
|
||||
)
|
||||
fun findRoutesByRunId(
|
||||
@Param("runId") runId: UUID,
|
||||
@Param("limit") limit: Int,
|
||||
@Param("offset") offset: Int
|
||||
): List<DynamicPlanRouteEntity>
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package space.nstart.pcp.complan.db.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 org.springframework.stereotype.Repository
|
||||
import space.nstart.pcp.complan.db.entity.DynamicPlanRunEntity
|
||||
import space.nstart.pcp.complan.types.DynamicPlanRunStatus
|
||||
import java.util.UUID
|
||||
|
||||
@Repository
|
||||
interface DynamicPlanRunRepository : JpaRepository<DynamicPlanRunEntity, UUID> {
|
||||
|
||||
@Query(
|
||||
value = """
|
||||
select count(*)
|
||||
from dynamic_plan_run run
|
||||
where exists (
|
||||
select 1
|
||||
from jsonb_array_elements_text(run.satellite_ids::jsonb) satellite_id(value)
|
||||
where satellite_id.value::bigint = :satelliteId
|
||||
)
|
||||
""",
|
||||
nativeQuery = true
|
||||
)
|
||||
fun countBySatelliteId(@Param("satelliteId") satelliteId: Long): Long
|
||||
|
||||
@Modifying
|
||||
@Query(
|
||||
value = """
|
||||
delete from dynamic_plan_run run
|
||||
where exists (
|
||||
select 1
|
||||
from jsonb_array_elements_text(run.satellite_ids::jsonb) satellite_id(value)
|
||||
where satellite_id.value::bigint = :satelliteId
|
||||
)
|
||||
""",
|
||||
nativeQuery = true
|
||||
)
|
||||
fun deleteAllBySatelliteId(@Param("satelliteId") satelliteId: Long): Int
|
||||
|
||||
@Query(
|
||||
value = """
|
||||
select *
|
||||
from dynamic_plan_run
|
||||
order by created_at desc
|
||||
limit :limit offset :offset
|
||||
""",
|
||||
nativeQuery = true
|
||||
)
|
||||
fun findRuns(
|
||||
@Param("limit") limit: Int,
|
||||
@Param("offset") offset: Int
|
||||
): List<DynamicPlanRunEntity>
|
||||
|
||||
@Query(
|
||||
value = """
|
||||
select *
|
||||
from dynamic_plan_run
|
||||
where status = :status
|
||||
order by created_at desc
|
||||
limit :limit offset :offset
|
||||
""",
|
||||
nativeQuery = true
|
||||
)
|
||||
fun findRunsByStatus(
|
||||
@Param("status") status: String,
|
||||
@Param("limit") limit: Int,
|
||||
@Param("offset") offset: Int
|
||||
): List<DynamicPlanRunEntity>
|
||||
|
||||
fun findAllByStatusIn(statuses: Collection<DynamicPlanRunStatus>): List<DynamicPlanRunEntity>
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package space.nstart.pcp.complan.service
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
import org.springframework.http.HttpStatus
|
||||
import space.nstart.pcp.complan.db.repository.AplSettingsRepository
|
||||
import space.nstart.pcp.complan.settings.AplSettings
|
||||
import space.nstart.pcp.complan.settings.SettingsDTO
|
||||
|
||||
@Service
|
||||
class AplSettingsLoader(
|
||||
private val aplSetRep: AplSettingsRepository,
|
||||
private val aplSettings: AplSettings,
|
||||
private val objectMapper: ObjectMapper
|
||||
) {
|
||||
private val log = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun loadLatestSettings() {
|
||||
val settingsEntity = aplSetRep.findFirstByOrderByIdDesc()
|
||||
if (settingsEntity == null) {
|
||||
log.info("APL settings were not found in DB, current in-memory settings will be used")
|
||||
return
|
||||
}
|
||||
|
||||
aplSettings.settings = try {
|
||||
objectMapper.readValue(settingsEntity.settings, SettingsDTO::class.java)
|
||||
} catch (error: Exception) {
|
||||
throw ResponseStatusException(
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
"Failed to read APL settings from DB record id=${settingsEntity.id}",
|
||||
error
|
||||
)
|
||||
}
|
||||
log.info("APL settings loaded from DB record id={}", settingsEntity.id)
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
package space.nstart.pcp.complan.service
|
||||
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.core.ParameterizedTypeReference
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.web.client.RestClient
|
||||
import org.springframework.web.util.UriComponentsBuilder
|
||||
import space.nstart.pcp.complan.types.ObjViewRequestDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ExactTimePositionRequestDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.FlightLineDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ObjDTO as BallisticsObjDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ObjViewRequestDTO as BallisticsObjViewRequestDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteOrbitAvailabilityDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamDTO
|
||||
import java.time.LocalDateTime
|
||||
|
||||
/**Получение данных от сервиса баллистики*/
|
||||
@Service
|
||||
class BallTaskServce {
|
||||
|
||||
|
||||
@Value("\${complex-plan.ballistics-service-url}")
|
||||
private lateinit var ballisticsServiceUrl: String
|
||||
|
||||
fun getOrbitAvailability(noradId : Long): SatelliteOrbitAvailabilityDTO =
|
||||
RestClient.create()
|
||||
.get()
|
||||
.uri(buildBallisticsUri("/api/satellites/orbit/availability/$noradId"))
|
||||
.retrieve()
|
||||
.body(SatelliteOrbitAvailabilityDTO::class.java)
|
||||
?: throw IllegalStateException("Пустой ответ баллистики для доступности ПДЦМ КА $noradId")
|
||||
|
||||
fun getFlightLine(noradId: Long, timeStart : LocalDateTime, timeStop : LocalDateTime): List<FlightLineDTO> =
|
||||
RestClient.create()
|
||||
.get()
|
||||
.uri(
|
||||
UriComponentsBuilder
|
||||
.fromUriString(ballisticsServiceUrl)
|
||||
.path("/api/satellites/$noradId/flight-line")
|
||||
.queryParam("time_start", timeStart)
|
||||
.queryParam("time_stop", timeStop)
|
||||
.toUriString()
|
||||
)
|
||||
.retrieve()
|
||||
.body(FLIGHT_LINE_LIST_TYPE)
|
||||
.orEmpty()
|
||||
|
||||
fun getExactTime(noradId: Long, request: ExactTimePositionRequestDTO): List<OrbPointDTO> =
|
||||
RestClient.create()
|
||||
.post()
|
||||
.uri(buildBallisticsUri("/api/satellites/$noradId/extract-time"))
|
||||
.body(request)
|
||||
.retrieve()
|
||||
.body(ORB_POINT_LIST_TYPE)
|
||||
.orEmpty()
|
||||
|
||||
fun getMplForObjects(objects : ObjViewRequestDTO): List<SquareViewParamDTO> {
|
||||
val satellites = objects.satellites?.toList().orEmpty()
|
||||
val timeStart = objects.timeStart ?: throw IllegalArgumentException("timeStart must be specified for MPL request")
|
||||
val timeStop = objects.timeStop ?: throw IllegalArgumentException("timeStop must be specified for MPL request")
|
||||
|
||||
return objects.objects.flatMap { obj ->
|
||||
RestClient.create()
|
||||
.post()
|
||||
.uri(buildBallisticsUri("/api/obj-view/mpl-square"))
|
||||
.body(
|
||||
BallisticsObjViewRequestDTO(
|
||||
satellites = satellites,
|
||||
timeStart = timeStart,
|
||||
timeStop = timeStop,
|
||||
obj = BallisticsObjDTO(
|
||||
id = obj.id.toString(),
|
||||
position = obj.position,
|
||||
contourWKT = obj.contourWKT
|
||||
),
|
||||
sunAngleMin = objects.sunAngleMin
|
||||
)
|
||||
)
|
||||
.retrieve()
|
||||
.body(SQUARE_VIEW_PARAM_LIST_TYPE)
|
||||
.orEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildBallisticsUri(path: String): String =
|
||||
UriComponentsBuilder
|
||||
.fromUriString(ballisticsServiceUrl)
|
||||
.path(path)
|
||||
.toUriString()
|
||||
|
||||
private companion object {
|
||||
val FLIGHT_LINE_LIST_TYPE = object : ParameterizedTypeReference<List<FlightLineDTO>>() {}
|
||||
val ORB_POINT_LIST_TYPE = object : ParameterizedTypeReference<List<OrbPointDTO>>() {}
|
||||
val SQUARE_VIEW_PARAM_LIST_TYPE = object : ParameterizedTypeReference<List<SquareViewParamDTO>>() {}
|
||||
}
|
||||
}
|
||||
+324
@@ -0,0 +1,324 @@
|
||||
package space.nstart.pcp.complan.service
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.support.TransactionTemplate
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
import space.nstart.pcp.complan.apl.BasicPlanCalculator
|
||||
import space.nstart.pcp.complan.db.entity.DynamicPlanIntervalEntity
|
||||
import space.nstart.pcp.complan.db.entity.DynamicPlanRouteEntity
|
||||
import space.nstart.pcp.complan.db.entity.DynamicPlanRunEntity
|
||||
import space.nstart.pcp.complan.db.repository.DynamicPlanIntervalRepository
|
||||
import space.nstart.pcp.complan.db.repository.DynamicPlanRouteRepository
|
||||
import space.nstart.pcp.complan.db.repository.DynamicPlanRunRepository
|
||||
import space.nstart.pcp.complan.types.ComplexPlanCalculationOutput
|
||||
import space.nstart.pcp.complan.types.ComplexPlanCalculationRequestDTO
|
||||
import space.nstart.pcp.complan.types.ComplexPlanCalculationResultDTO
|
||||
import space.nstart.pcp.complan.types.DynamicPlanIntervalDTO
|
||||
import space.nstart.pcp.complan.types.DynamicPlanRouteDTO
|
||||
import space.nstart.pcp.complan.types.DynamicPlanRunDTO
|
||||
import space.nstart.pcp.complan.types.DynamicPlanRunStatus
|
||||
import space.nstart.pcp.complan.types.IntervalItemDTO
|
||||
import space.nstart.pcp.complan.types.SurveyContour
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ExecutorService
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
@Service
|
||||
class DynamicPlanRunService(
|
||||
private val runRepository: DynamicPlanRunRepository,
|
||||
private val routeRepository: DynamicPlanRouteRepository,
|
||||
private val intervalRepository: DynamicPlanIntervalRepository,
|
||||
private val basePlan: BasicPlanCalculator,
|
||||
private val aplSettingsLoader: AplSettingsLoader,
|
||||
private val objectMapper: ObjectMapper,
|
||||
private val transactionTemplate: TransactionTemplate,
|
||||
private val dynamicPlanExecutor: ExecutorService
|
||||
) {
|
||||
private val log = LoggerFactory.getLogger(this::class.java)
|
||||
private val satelliteIdsType = object : TypeReference<List<Long>>() {}
|
||||
|
||||
fun startCalculation(request: ComplexPlanCalculationRequestDTO): DynamicPlanRunDTO {
|
||||
validateRequest(request)
|
||||
|
||||
val runId = UUID.randomUUID()
|
||||
val createdRun = transactionTemplate.execute {
|
||||
runRepository.save(
|
||||
DynamicPlanRunEntity(
|
||||
id = runId,
|
||||
status = DynamicPlanRunStatus.PENDING,
|
||||
requestId = request.requestId,
|
||||
satelliteIds = objectMapper.writeValueAsString(request.satelliteIds.distinct()),
|
||||
calculationStart = request.calculationStart,
|
||||
calculationEnd = request.calculationEnd,
|
||||
calculationMode = request.calculationMode,
|
||||
revolutionMode = request.revolutionMode,
|
||||
filterCoveredRoutes = request.filterCoveredRoutes,
|
||||
createdAt = LocalDateTime.now()
|
||||
)
|
||||
)
|
||||
} ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to create calculation run")
|
||||
|
||||
dynamicPlanExecutor.submit {
|
||||
executeCalculation(runId)
|
||||
}
|
||||
|
||||
return createdRun.toDto()
|
||||
}
|
||||
|
||||
fun getRun(runId: UUID): DynamicPlanRunDTO =
|
||||
findRun(runId).toDto()
|
||||
|
||||
fun listRuns(status: DynamicPlanRunStatus?, limit: Int, offset: Int): List<DynamicPlanRunDTO> {
|
||||
validatePage(limit, offset)
|
||||
val runs = when (status) {
|
||||
null -> runRepository.findRuns(limit, offset)
|
||||
else -> runRepository.findRunsByStatus(status.name, limit, offset)
|
||||
}
|
||||
return runs.map { run -> run.toDto() }
|
||||
}
|
||||
|
||||
fun getResult(runId: UUID): ComplexPlanCalculationResultDTO {
|
||||
val run = findRun(runId)
|
||||
if (run.status != DynamicPlanRunStatus.COMPLETED) {
|
||||
throw ResponseStatusException(HttpStatus.CONFLICT, "Calculation $runId is not completed")
|
||||
}
|
||||
|
||||
val resultJson = run.resultJson
|
||||
?: throw ResponseStatusException(HttpStatus.CONFLICT, "Calculation $runId has no result")
|
||||
|
||||
val resultNode = objectMapper.readTree(resultJson)
|
||||
if (resultNode is ObjectNode && !resultNode.has("calculationMode")) {
|
||||
resultNode.put("calculationMode", run.calculationMode.name)
|
||||
}
|
||||
if (resultNode is ObjectNode && !resultNode.has("revolutionMode")) {
|
||||
resultNode.put("revolutionMode", run.revolutionMode.name)
|
||||
}
|
||||
if (resultNode is ObjectNode && !resultNode.has("filterCoveredRoutes")) {
|
||||
resultNode.put("filterCoveredRoutes", run.filterCoveredRoutes)
|
||||
}
|
||||
|
||||
return objectMapper.treeToValue(resultNode, ComplexPlanCalculationResultDTO::class.java)
|
||||
}
|
||||
|
||||
fun getRoutes(runId: UUID, limit: Int, offset: Int): List<DynamicPlanRouteDTO> {
|
||||
validatePage(limit, offset)
|
||||
val run = findRun(runId)
|
||||
if (run.status != DynamicPlanRunStatus.COMPLETED) {
|
||||
throw ResponseStatusException(HttpStatus.CONFLICT, "Calculation $runId is not completed")
|
||||
}
|
||||
return routeRepository.findRoutesByRunId(runId, limit, offset)
|
||||
.map { route -> route.toDto() }
|
||||
}
|
||||
|
||||
fun getIntervals(runId: UUID, limit: Int, offset: Int): List<DynamicPlanIntervalDTO> {
|
||||
validatePage(limit, offset)
|
||||
val run = findRun(runId)
|
||||
if (run.status != DynamicPlanRunStatus.COMPLETED) {
|
||||
throw ResponseStatusException(HttpStatus.CONFLICT, "Calculation $runId is not completed")
|
||||
}
|
||||
return intervalRepository.findIntervalsByRunId(runId, limit, offset)
|
||||
.map { interval -> interval.toDto() }
|
||||
}
|
||||
|
||||
fun recoverInterruptedRuns() {
|
||||
val recoveredCount = transactionTemplate.execute {
|
||||
val runs = runRepository.findAllByStatusIn(
|
||||
listOf(DynamicPlanRunStatus.PENDING, DynamicPlanRunStatus.RUNNING)
|
||||
)
|
||||
if (runs.isEmpty()) {
|
||||
return@execute 0
|
||||
}
|
||||
|
||||
val finishedAt = LocalDateTime.now()
|
||||
runs.forEach { run ->
|
||||
run.status = DynamicPlanRunStatus.FAILED
|
||||
run.finishedAt = finishedAt
|
||||
run.errorMessage = "Calculation was interrupted by service restart"
|
||||
}
|
||||
runRepository.saveAll(runs)
|
||||
runs.size
|
||||
} ?: 0
|
||||
|
||||
if (recoveredCount > 0) {
|
||||
log.warn("Recovered {} interrupted dynamic plan runs after startup", recoveredCount)
|
||||
}
|
||||
}
|
||||
|
||||
private fun executeCalculation(runId: UUID) {
|
||||
val request = markRunningAndBuildRequest(runId)
|
||||
if (request == null) {
|
||||
log.warn("Dynamic plan run {} was not found before execution", runId)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
aplSettingsLoader.loadLatestSettings()
|
||||
val output = basePlan.calculate(request)
|
||||
markCompleted(runId, output)
|
||||
} catch (error: Exception) {
|
||||
log.error("Dynamic plan run {} failed", runId, error)
|
||||
markFailed(runId, error)
|
||||
}
|
||||
}
|
||||
|
||||
private fun markRunningAndBuildRequest(runId: UUID): ComplexPlanCalculationRequestDTO? =
|
||||
transactionTemplate.execute {
|
||||
val run = runRepository.findById(runId).orElse(null) ?: return@execute null
|
||||
run.status = DynamicPlanRunStatus.RUNNING
|
||||
run.startedAt = LocalDateTime.now()
|
||||
run.errorMessage = null
|
||||
runRepository.save(run)
|
||||
run.toRequest()
|
||||
}
|
||||
|
||||
private fun markCompleted(runId: UUID, output: ComplexPlanCalculationOutput) {
|
||||
transactionTemplate.execute {
|
||||
val run = runRepository.findById(runId).orElseThrow()
|
||||
val result = output.result
|
||||
run.status = DynamicPlanRunStatus.COMPLETED
|
||||
run.finishedAt = LocalDateTime.now()
|
||||
run.durationMs = result.durationMs
|
||||
run.resultStatus = result.status.name
|
||||
run.routesCount = result.routesCount
|
||||
run.resultJson = objectMapper.writeValueAsString(result)
|
||||
run.errorMessage = null
|
||||
intervalRepository.saveAll(
|
||||
output.intervals.mapIndexed { index, interval -> interval.toEntity(run, result.requestId, index) }
|
||||
)
|
||||
routeRepository.saveAll(output.routes.mapIndexed { index, route -> route.toEntity(run, result.requestId, index) })
|
||||
runRepository.save(run)
|
||||
}
|
||||
}
|
||||
|
||||
private fun markFailed(runId: UUID, error: Exception) {
|
||||
transactionTemplate.execute {
|
||||
val run = runRepository.findById(runId).orElseThrow()
|
||||
run.status = DynamicPlanRunStatus.FAILED
|
||||
run.finishedAt = LocalDateTime.now()
|
||||
run.errorMessage = error.message ?: error::class.java.simpleName
|
||||
runRepository.save(run)
|
||||
}
|
||||
}
|
||||
|
||||
private fun findRun(runId: UUID): DynamicPlanRunEntity =
|
||||
runRepository.findById(runId)
|
||||
.orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Calculation $runId was not found") }
|
||||
|
||||
private fun validateRequest(request: ComplexPlanCalculationRequestDTO) {
|
||||
if (request.calculationEnd <= request.calculationStart) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "calculationEnd must be after calculationStart")
|
||||
}
|
||||
if (request.satelliteIds.isEmpty()) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "satelliteIds must not be empty")
|
||||
}
|
||||
}
|
||||
|
||||
private fun validatePage(limit: Int, offset: Int) {
|
||||
if (limit !in 1..10_000) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "limit must be between 1 and 10000")
|
||||
}
|
||||
if (offset < 0) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "offset must be greater than or equal to 0")
|
||||
}
|
||||
}
|
||||
|
||||
private fun DynamicPlanRunEntity.toRequest(): ComplexPlanCalculationRequestDTO =
|
||||
ComplexPlanCalculationRequestDTO(
|
||||
requestId = requestId ?: throw IllegalStateException("Run $id has no requestId"),
|
||||
satelliteIds = readSatelliteIds(satelliteIds),
|
||||
calculationStart = calculationStart ?: throw IllegalStateException("Run $id has no calculationStart"),
|
||||
calculationEnd = calculationEnd ?: throw IllegalStateException("Run $id has no calculationEnd"),
|
||||
calculationMode = calculationMode,
|
||||
revolutionMode = revolutionMode,
|
||||
filterCoveredRoutes = filterCoveredRoutes
|
||||
)
|
||||
|
||||
private fun DynamicPlanRunEntity.toDto(): DynamicPlanRunDTO =
|
||||
DynamicPlanRunDTO(
|
||||
runId = id ?: throw IllegalStateException("Run has no id"),
|
||||
status = status,
|
||||
requestId = requestId ?: throw IllegalStateException("Run $id has no requestId"),
|
||||
satelliteIds = readSatelliteIds(satelliteIds),
|
||||
calculationStart = calculationStart ?: throw IllegalStateException("Run $id has no calculationStart"),
|
||||
calculationEnd = calculationEnd ?: throw IllegalStateException("Run $id has no calculationEnd"),
|
||||
calculationMode = calculationMode,
|
||||
revolutionMode = revolutionMode,
|
||||
filterCoveredRoutes = filterCoveredRoutes,
|
||||
createdAt = createdAt,
|
||||
startedAt = startedAt,
|
||||
finishedAt = finishedAt,
|
||||
durationMs = durationMs,
|
||||
resultStatus = resultStatus,
|
||||
routesCount = routesCount,
|
||||
errorMessage = errorMessage
|
||||
)
|
||||
|
||||
private fun readSatelliteIds(value: String): List<Long> =
|
||||
objectMapper.readValue(value, satelliteIdsType)
|
||||
|
||||
private fun IntervalItemDTO.toEntity(
|
||||
run: DynamicPlanRunEntity,
|
||||
requestId: UUID,
|
||||
intervalOrder: Int
|
||||
): DynamicPlanIntervalEntity =
|
||||
DynamicPlanIntervalEntity(
|
||||
run = run,
|
||||
requestId = requestId,
|
||||
intervalId = id,
|
||||
regionId = regionId,
|
||||
revSign = revSign,
|
||||
contourWkt = geometry,
|
||||
observationParametersCount = mpl.size,
|
||||
intervalOrder = intervalOrder
|
||||
)
|
||||
|
||||
private fun SurveyContour.toEntity(
|
||||
run: DynamicPlanRunEntity,
|
||||
requestId: UUID,
|
||||
routeOrder: Int
|
||||
): DynamicPlanRouteEntity =
|
||||
DynamicPlanRouteEntity(
|
||||
run = run,
|
||||
requestId = requestId,
|
||||
satelliteId = mpl.noradId,
|
||||
startTime = time,
|
||||
endTime = time.plusNanos((duration * 1_000_000_000.0).roundToLong().coerceAtLeast(1L)),
|
||||
duration = duration,
|
||||
revolution = rev,
|
||||
roll = gamma,
|
||||
contourWkt = geometry,
|
||||
routeOrder = routeOrder
|
||||
)
|
||||
|
||||
private fun DynamicPlanIntervalEntity.toDto(): DynamicPlanIntervalDTO =
|
||||
DynamicPlanIntervalDTO(
|
||||
id = id ?: throw IllegalStateException("Interval has no id"),
|
||||
runId = run?.id ?: throw IllegalStateException("Interval $id has no run id"),
|
||||
requestId = requestId ?: throw IllegalStateException("Interval $id has no request id"),
|
||||
intervalId = intervalId ?: throw IllegalStateException("Interval $id has no interval id"),
|
||||
regionId = regionId ?: throw IllegalStateException("Interval $id has no region id"),
|
||||
revSign = revSign,
|
||||
contourWkt = contourWkt,
|
||||
observationParametersCount = observationParametersCount
|
||||
)
|
||||
|
||||
private fun DynamicPlanRouteEntity.toDto(): DynamicPlanRouteDTO =
|
||||
DynamicPlanRouteDTO(
|
||||
id = id ?: throw IllegalStateException("Route has no id"),
|
||||
runId = run?.id ?: throw IllegalStateException("Route $id has no run id"),
|
||||
requestId = requestId ?: throw IllegalStateException("Route $id has no request id"),
|
||||
satelliteId = satelliteId,
|
||||
startTime = startTime,
|
||||
endTime = endTime,
|
||||
duration = duration,
|
||||
revolution = revolution,
|
||||
roll = roll,
|
||||
contourWkt = contourWkt
|
||||
)
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package space.nstart.pcp.complan.service
|
||||
|
||||
import org.springframework.boot.ApplicationArguments
|
||||
import org.springframework.boot.ApplicationRunner
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class DynamicPlanRunStartupRecovery(
|
||||
private val dynamicPlanRunService: DynamicPlanRunService
|
||||
) : ApplicationRunner {
|
||||
|
||||
override fun run(args: ApplicationArguments) {
|
||||
dynamicPlanRunService.recoverInterruptedRuns()
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package space.nstart.pcp.complan.service
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.web.client.HttpClientErrorException
|
||||
import org.springframework.web.client.RestClient
|
||||
import org.springframework.web.util.UriComponentsBuilder
|
||||
import space.nstart.pcp.complan.types.RequestItemDTO
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.UUID
|
||||
|
||||
/** Получение заявок из pcp-request-service /v1 API. */
|
||||
@Service
|
||||
class RequestServiceClient {
|
||||
@Value("\${complex-plan.request-service-url}")
|
||||
private lateinit var requestServiceUrl: String
|
||||
|
||||
@Value("\${complex-plan.request-selection-path:/v1/requests}")
|
||||
private lateinit var requestSelectionPath: String
|
||||
|
||||
fun getRequest(requestId: UUID): RequestItemDTO? =
|
||||
try {
|
||||
RestClient.create()
|
||||
.get()
|
||||
.uri(
|
||||
UriComponentsBuilder
|
||||
.fromUriString(requestServiceUrl)
|
||||
.path(requestSelectionPath)
|
||||
.path("/{requestId}")
|
||||
.buildAndExpand(requestId)
|
||||
.toUriString()
|
||||
)
|
||||
.retrieve()
|
||||
.body(RequestResponseDto::class.java)
|
||||
?.toRequestItemDto()
|
||||
} catch (_: HttpClientErrorException.NotFound) {
|
||||
null
|
||||
}
|
||||
|
||||
private fun RequestResponseDto.toRequestItemDto(): RequestItemDTO =
|
||||
RequestItemDTO(
|
||||
id = id,
|
||||
name = name,
|
||||
geometry = geometry,
|
||||
intervalBegin = beginDateTime.toLocalDateTime(),
|
||||
intervalEnd = endDateTime.toLocalDateTime(),
|
||||
resolution = resolution(),
|
||||
importance = importance,
|
||||
)
|
||||
|
||||
private fun RequestResponseDto.resolution(): Double =
|
||||
optics?.resolution
|
||||
?: rsa?.resolution
|
||||
?: throw IllegalStateException("Request $id has no survey resolution")
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class RequestResponseDto(
|
||||
val id: UUID,
|
||||
val name: String,
|
||||
val status: String = "",
|
||||
val surveyType: String = "",
|
||||
val geometry: String,
|
||||
val importance: Double,
|
||||
val beginDateTime: OffsetDateTime,
|
||||
val endDateTime: OffsetDateTime,
|
||||
val kpp: List<Int> = emptyList(),
|
||||
val highPriorityTransmit: Boolean = false,
|
||||
val optics: SurveyParamsDto? = null,
|
||||
val rsa: SurveyParamsDto? = null,
|
||||
val coverage: CoverageStateDto = CoverageStateDto(),
|
||||
val createdAt: OffsetDateTime? = null,
|
||||
val updatedAt: OffsetDateTime? = null,
|
||||
val deletedAt: OffsetDateTime? = null,
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class SurveyParamsDto(
|
||||
val resolution: Double,
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class CoverageStateDto(
|
||||
val requiredPercent: Double? = null,
|
||||
val currentPercent: Double = 0.0,
|
||||
)
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package space.nstart.pcp.complan.service
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.core.ParameterizedTypeReference
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.web.client.RestClient
|
||||
import org.springframework.web.util.UriComponentsBuilder
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteSummaryDTO
|
||||
|
||||
/** Получение данных КА из satellite-catalog-service. */
|
||||
@Service
|
||||
class SatelliteCatalogServiceClient {
|
||||
@Value("\${complex-plan.satellite-catalog-service-url}")
|
||||
private lateinit var satelliteCatalogServiceUrl: String
|
||||
|
||||
fun getSatellites(): List<SatelliteSummaryDTO> =
|
||||
RestClient.create()
|
||||
.get()
|
||||
.uri(buildSatelliteCatalogUri("/api/satellites"))
|
||||
.retrieve()
|
||||
.body(SATELLITE_SUMMARY_LIST_TYPE)
|
||||
.orEmpty()
|
||||
|
||||
private fun buildSatelliteCatalogUri(path: String): String =
|
||||
UriComponentsBuilder
|
||||
.fromUriString(satelliteCatalogServiceUrl)
|
||||
.path(path)
|
||||
.toUriString()
|
||||
|
||||
private companion object {
|
||||
val SATELLITE_SUMMARY_LIST_TYPE = object : ParameterizedTypeReference<List<SatelliteSummaryDTO>>() {}
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package space.nstart.pcp.complan.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-dynamic-plan-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-dynamic-plan-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-dynamic-plan-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-dynamic-plan-service", exception)
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
|
||||
private fun SatelliteDeletedEventDTO.deleteIdentifiers(): List<Long> =
|
||||
listOfNotNull(satelliteId, noradId).distinct()
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package space.nstart.pcp.complan.service
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import space.nstart.pcp.complan.db.repository.DynamicPlanRouteRepository
|
||||
import space.nstart.pcp.complan.db.repository.DynamicPlanRunRepository
|
||||
|
||||
@Service
|
||||
class SatelliteDeletedService(
|
||||
private val runRepository: DynamicPlanRunRepository,
|
||||
private val routeRepository: DynamicPlanRouteRepository
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
@Transactional
|
||||
fun deleteSatelliteData(satelliteId: Long): SatelliteDynamicPlanDeletionSummary {
|
||||
val runsBeforeDelete = runRepository.countBySatelliteId(satelliteId)
|
||||
val routesBeforeDelete = routeRepository.countBySatelliteId(satelliteId)
|
||||
|
||||
val runsDeleted = runRepository.deleteAllBySatelliteId(satelliteId)
|
||||
val orphanRoutesDeleted = routeRepository.deleteAllBySatelliteId(satelliteId)
|
||||
val summary = SatelliteDynamicPlanDeletionSummary(
|
||||
satelliteId = satelliteId,
|
||||
runsDeleted = runsDeleted,
|
||||
routesDeletedByRunCascade = routesBeforeDelete - orphanRoutesDeleted,
|
||||
orphanRoutesDeleted = orphanRoutesDeleted
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Deleted dynamic plan data for satellite: satelliteId={}, runsDeleted={}, runsBeforeDelete={}, routesDeletedByRunCascade={}, orphanRoutesDeleted={}, routesBeforeDelete={}",
|
||||
summary.satelliteId,
|
||||
summary.runsDeleted,
|
||||
runsBeforeDelete,
|
||||
summary.routesDeletedByRunCascade,
|
||||
summary.orphanRoutesDeleted,
|
||||
routesBeforeDelete
|
||||
)
|
||||
return summary
|
||||
}
|
||||
}
|
||||
|
||||
data class SatelliteDynamicPlanDeletionSummary(
|
||||
val satelliteId: Long,
|
||||
val runsDeleted: Int,
|
||||
val routesDeletedByRunCascade: Long,
|
||||
val orphanRoutesDeleted: Int
|
||||
)
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package space.nstart.pcp.complan.settings
|
||||
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
/** Класс хранения настроек автомата планирования */
|
||||
class AplSettings() {
|
||||
/** Типовые настройки работы автомата планирования */
|
||||
var settings = _root_ide_package_.space.nstart.pcp.complan.settings.SettingsDTO()
|
||||
}
|
||||
|
||||
/* Типовой набор настроек
|
||||
{
|
||||
"daysCalc": 1,
|
||||
"kaList": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
],
|
||||
"marBeginCorrectionTime": 1,
|
||||
"marEndCorrectionTime": 1,
|
||||
"marWidth": 30,
|
||||
"marOverlap": 1,
|
||||
"marMaxLength": 60,
|
||||
"marMinLength": 5,
|
||||
"maxRevTime": 300,
|
||||
"maxDayTime": 2100,
|
||||
"contourStepBuild": 1,
|
||||
"filterByRegionUUID": true,
|
||||
"regionList": ["831abc95-b3c2-4aec-96d7-7023e5256042"],
|
||||
"filterByGamma": true,
|
||||
"gammaValue": 1
|
||||
}
|
||||
Пример записи в БД
|
||||
"{""daysCalc"":10,""kaList"":[1,2,3],""marBeginCorrectionTime"":1,""marEndCorrectionTime"":1,""marWidth"":30.0,""marOverlap"":1.0,""marMaxLength"":60.0,""marMinLength"":5.0,""maxRevTime"":300.0,""maxDayTime"":2100.0,""contourStepBuild"":1.0,""filterByRegionUUID"":true,""regionList"":[""3fa85f64-5717-4562-b3fc-111111111111""],""filterByGamma"":false,""gammaValue"":6.0,""filterByRevolution"":true,""revolutionType"":0}"
|
||||
|
||||
* */
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package space.nstart.pcp.complan.settings
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import java.util.UUID
|
||||
|
||||
@Schema(description = "Типовые настройки работы автомата планирования")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonSerialize
|
||||
data class SettingsDTO(
|
||||
|
||||
@get:JsonProperty("daysCalc")
|
||||
@param:JsonProperty("daysCalc")
|
||||
val daysCalc: Int = 1,
|
||||
|
||||
@get:JsonProperty("kaList")
|
||||
@param:JsonProperty("kaList")
|
||||
val kaList: Set<Int> = setOf(1,2,3),
|
||||
|
||||
@get:JsonProperty("marBeginCorrectionTime")
|
||||
@param:JsonProperty("marBeginCorrectionTime")
|
||||
val marBeginCorrectionTime: Long = 1,
|
||||
|
||||
@get:JsonProperty("marEndCorrectionTime")
|
||||
@param:JsonProperty("marEndCorrectionTime")
|
||||
val marEndCorrectionTime: Long = 1,
|
||||
|
||||
@get:JsonProperty("marWidth")
|
||||
@param:JsonProperty("marWidth")
|
||||
val marWidth : Double = 30.0,
|
||||
|
||||
@get:JsonProperty("marOverlap")
|
||||
@param:JsonProperty("marOverlap")
|
||||
val marOverlap : Double = 1.0,
|
||||
|
||||
@get:JsonProperty("marMaxLength")
|
||||
@param:JsonProperty("marMaxLength")
|
||||
val marMaxLength : Double = 60.0,
|
||||
|
||||
@get:JsonProperty("marMinLength")
|
||||
@param:JsonProperty("marMinLength")
|
||||
val marMinLength : Double = 5.0,
|
||||
|
||||
@get:JsonProperty("maxRevTime")
|
||||
@param:JsonProperty("maxRevTime")
|
||||
val maxRevTime : Double = 300.0,
|
||||
|
||||
@get:JsonProperty("maxDayTime")
|
||||
@param:JsonProperty("maxDayTime")
|
||||
val maxDayTime : Double = 2100.0,
|
||||
|
||||
@get:JsonProperty("contourStepBuild")
|
||||
@param:JsonProperty("contourStepBuild")
|
||||
val contourStepBuild : Double = 1.0,
|
||||
|
||||
@get:JsonProperty("filterByRegionUUID")
|
||||
@param:JsonProperty("filterByRegionUUID")
|
||||
val filterByRegionUUID : Boolean = false,
|
||||
|
||||
@get:JsonProperty("regionList")
|
||||
@param:JsonProperty("regionList")
|
||||
val regionList : Set<UUID> = setOf(),
|
||||
|
||||
|
||||
@get:JsonProperty("filterByGamma")
|
||||
@param:JsonProperty("filterByGamma")
|
||||
val filterByGamma : Boolean = false,
|
||||
|
||||
@get:JsonProperty("gammaValue")
|
||||
@param:JsonProperty("gammaValue")
|
||||
val gammaValue : Double = 40.0,
|
||||
|
||||
@get:JsonProperty("filterByRevolution")
|
||||
@param:JsonProperty("filterByRevolution")
|
||||
val filterByRevolution : Boolean = false,
|
||||
|
||||
@get:JsonProperty("revolutionType")
|
||||
@param:JsonProperty("revolutionType")
|
||||
val revolutionType : Int = 0,
|
||||
|
||||
)
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package space.nstart.pcp.complan.spacecraft
|
||||
|
||||
/**
|
||||
* Ограничения работы одного космического аппарата при построении расписания включений.
|
||||
*
|
||||
* Сейчас модель содержит только временные лимиты, которые использует greedy-планировщик:
|
||||
* суммарное время работы на витке, суммарное время работы за сутки и технологический разрыв
|
||||
* между двумя включениями одного аппарата. Класс вынесен рядом со SpaceCraft, чтобы будущие
|
||||
* ограничения аппарата добавлялись к доменной модели КА, а не разрастались внутри алгоритма планирования.
|
||||
*/
|
||||
data class SCConstraints(
|
||||
val maxRevolutionDurationSeconds: Long = 300,
|
||||
val maxDailyDurationSeconds: Long = 2100,
|
||||
val routeGapSeconds: Long = 60
|
||||
)
|
||||
Executable
+144
@@ -0,0 +1,144 @@
|
||||
package space.nstart.pcp.complan.spacecraft
|
||||
|
||||
import org.locationtech.jts.geom.Coordinate
|
||||
import org.locationtech.jts.geom.GeometryFactory
|
||||
import org.locationtech.jts.geom.LineString
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.FlightLineDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
|
||||
import java.time.LocalDateTime
|
||||
|
||||
/**
|
||||
* Класс космического аппарата**
|
||||
*/
|
||||
class SpaceCraft (
|
||||
val id : Long,
|
||||
val constraints: SCConstraints = SCConstraints()
|
||||
) {
|
||||
val flightLine : MutableList<FlightLineDTO> = mutableListOf()
|
||||
|
||||
/**Время начала планирования аппарата*/
|
||||
var timeStart: LocalDateTime = LocalDateTime.now()
|
||||
/**Время конца планирования аппарата*/
|
||||
var timeStop : LocalDateTime = LocalDateTime.now()
|
||||
/**
|
||||
* Создание линий полета восходящего и нисходящего витка
|
||||
* @param geomFactory фабрика геометрии
|
||||
* @param fLC координаты линии
|
||||
* @return
|
||||
* @return Возвращает геометрию линии полученной из массива координат
|
||||
*/
|
||||
fun createLineString(
|
||||
geomFactory: GeometryFactory,
|
||||
fLC: MutableList<Coordinate>,
|
||||
): LineString {
|
||||
require(fLC.size >= 2) { "Для построения трассы требуется минимум две точки" }
|
||||
|
||||
val coordinates = fLC.toTypedArray()
|
||||
val minY = coordinates.minOf { it.y }
|
||||
val maxY = coordinates.maxOf { it.y }
|
||||
|
||||
val startIndex1 = coordinates.indexOfFirst { it.y == minY }
|
||||
val endIndex1 = coordinates.indexOfFirst { it.y == maxY }
|
||||
|
||||
val array = if (startIndex1 < endIndex1)
|
||||
coordinates.sliceArray(startIndex1..endIndex1).toMutableList()
|
||||
else
|
||||
coordinates.sliceArray(endIndex1..startIndex1).toMutableList()
|
||||
|
||||
val geom = geomFactory.createLineString(array.toTypedArray())
|
||||
|
||||
return geom
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение линии полета в формате LineString
|
||||
* @return Возвращает часть линии полета от минимального занчения координаты Y до максимального. Параметры: **первый - НИСХОДЯЩИЙ, второй - ВОСХОДЯЩИЙ**
|
||||
* TODO Переписать структуру хранения FlightLine, неудобно держать в памяти первый или второй, нужны признаки
|
||||
*/
|
||||
fun createFlightLine(): Pair<LineString, LineString> {
|
||||
val geomFactory = GeometryFactory()
|
||||
val passes = getFlightLinePasses()
|
||||
val fLC1 = lineCorrection(selectPass(passes, RevolutionSign.DESC).coordinates).toMutableList()
|
||||
val fLC2 = lineCorrection(selectPass(passes, RevolutionSign.ASC).coordinates).toMutableList()
|
||||
|
||||
return Pair(createLineString(geomFactory, fLC1), createLineString(geomFactory, fLC2))
|
||||
}
|
||||
|
||||
private fun getFlightLinePasses(): List<FlightLinePass> {
|
||||
if (flightLine.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val passes = mutableListOf<FlightLinePass>()
|
||||
var currentSign = flightLine.first().revSign
|
||||
var currentCoordinates = mutableListOf<Coordinate>()
|
||||
|
||||
flightLine.forEach { point ->
|
||||
if (point.revSign != currentSign) {
|
||||
addPass(passes, currentSign, currentCoordinates)
|
||||
currentSign = point.revSign
|
||||
currentCoordinates = mutableListOf()
|
||||
}
|
||||
currentCoordinates.add(Coordinate(point.long, point.lat))
|
||||
}
|
||||
addPass(passes, currentSign, currentCoordinates)
|
||||
|
||||
return passes
|
||||
}
|
||||
|
||||
private fun addPass(
|
||||
passes: MutableList<FlightLinePass>,
|
||||
revSign: RevolutionSign,
|
||||
coordinates: List<Coordinate>
|
||||
) {
|
||||
if (coordinates.size >= 2) {
|
||||
passes.add(FlightLinePass(revSign, coordinates.map { it.copy() }))
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectPass(passes: List<FlightLinePass>, revSign: RevolutionSign): FlightLinePass {
|
||||
val sameDirectionPasses = passes.filter { it.revSign == revSign }
|
||||
return sameDirectionPasses.firstOrNull { it.isFullPass() }
|
||||
?: sameDirectionPasses.maxByOrNull { it.latitudeRange }
|
||||
?: throw IllegalStateException("Не найдена трасса полета для направления $revSign")
|
||||
}
|
||||
|
||||
fun lineCorrection(tmpLine : List<Coordinate>) : List<Coordinate> {
|
||||
if (tmpLine.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val line = mutableListOf<Coordinate>()
|
||||
|
||||
line.add(tmpLine.first().copy())
|
||||
for (i in 1..<tmpLine.count()) {
|
||||
val coord = tmpLine[i].copy()
|
||||
while (coord.x - line.last().x > HALF_WORLD_DEGREES) {
|
||||
coord.x -= FULL_WORLD_DEGREES
|
||||
}
|
||||
while (coord.x - line.last().x < -HALF_WORLD_DEGREES) {
|
||||
coord.x += FULL_WORLD_DEGREES
|
||||
}
|
||||
line.add(coord.copy())
|
||||
}
|
||||
return line
|
||||
}
|
||||
|
||||
private data class FlightLinePass(
|
||||
val revSign: RevolutionSign,
|
||||
val coordinates: List<Coordinate>
|
||||
) {
|
||||
val minLat = coordinates.minOf { it.y }
|
||||
val maxLat = coordinates.maxOf { it.y }
|
||||
val latitudeRange = maxLat - minLat
|
||||
|
||||
fun isFullPass(): Boolean = minLat <= FULL_PASS_MIN_LAT && maxLat >= FULL_PASS_MAX_LAT
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val FULL_PASS_MIN_LAT = -80.0
|
||||
const val FULL_PASS_MAX_LAT = 80.0
|
||||
const val HALF_WORLD_DEGREES = 180.0
|
||||
const val FULL_WORLD_DEGREES = 360.0
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import org.locationtech.jts.geom.Geometry
|
||||
import org.locationtech.jts.io.WKTReader
|
||||
import java.time.LocalDateTime
|
||||
import java.util.*
|
||||
|
||||
@Schema(description = "Объект каталога для планирования")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonSerialize
|
||||
data class AplObjItem(
|
||||
|
||||
@get:JsonProperty("name")
|
||||
@param:JsonProperty("name")
|
||||
val name: String,
|
||||
|
||||
@get:JsonProperty("geometry")
|
||||
@param:JsonProperty("geometry")
|
||||
val geometry: Geometry,
|
||||
|
||||
@get:JsonProperty("id")
|
||||
@param:JsonProperty("id")
|
||||
val id : UUID,
|
||||
|
||||
@get:JsonProperty("intevals")
|
||||
@param:JsonProperty("intevals")
|
||||
@JsonIgnore
|
||||
val intevals : MutableList<IntervalItemDTO>,
|
||||
|
||||
@get:JsonProperty("krenMin")
|
||||
@param:JsonProperty("krenMin")
|
||||
val krenMin : Double,
|
||||
|
||||
@get:JsonProperty("krenMax")
|
||||
@param:JsonProperty("krenMax")
|
||||
val krenMax : Double,
|
||||
|
||||
@get:JsonProperty("intervalBegin")
|
||||
@param:JsonProperty("intervalBegin")
|
||||
val intervalBegin : LocalDateTime? = null,
|
||||
|
||||
@get:JsonProperty("intervalEnd")
|
||||
@param:JsonProperty("intervalEnd")
|
||||
val intervalEnd : LocalDateTime? = null,
|
||||
|
||||
@get:JsonProperty("lastChange")
|
||||
@param:JsonProperty("lastChange")
|
||||
/** Если значение данного параметра null, значит задание новое */
|
||||
val lastChange : LocalDateTime? = null,
|
||||
) {
|
||||
constructor(obj: RequestItemDTO) : this(
|
||||
name = obj.name,
|
||||
geometry = WKTReader().read(obj.geometry),
|
||||
id = obj.id,
|
||||
intevals = mutableListOf(),
|
||||
krenMin = -45.0,
|
||||
krenMax = 45.0
|
||||
)
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
data class ComplexPlanCalculationOutput(
|
||||
val result: ComplexPlanCalculationResultDTO,
|
||||
val routes: List<SurveyContour>,
|
||||
val intervals: List<IntervalItemDTO> = emptyList()
|
||||
)
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
|
||||
@Schema(description = "Запрос расчета модели комплексного плана для одной заявки")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class ComplexPlanCalculationRequestDTO(
|
||||
@get:JsonProperty("requestId")
|
||||
@param:JsonProperty("requestId")
|
||||
val requestId: UUID,
|
||||
|
||||
@get:JsonProperty("satelliteIds")
|
||||
@param:JsonProperty("satelliteIds")
|
||||
val satelliteIds: List<Long>,
|
||||
|
||||
@get:JsonProperty("calculationStart")
|
||||
@param:JsonProperty("calculationStart")
|
||||
val calculationStart: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("calculationEnd")
|
||||
@param:JsonProperty("calculationEnd")
|
||||
val calculationEnd: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("calculationMode")
|
||||
@param:JsonProperty("calculationMode")
|
||||
val calculationMode: DynamicPlanCalculationMode = DynamicPlanCalculationMode.FULL,
|
||||
|
||||
@get:JsonProperty("revolutionMode")
|
||||
@param:JsonProperty("revolutionMode")
|
||||
val revolutionMode: DynamicPlanRevolutionMode = DynamicPlanRevolutionMode.BOTH,
|
||||
|
||||
@get:JsonProperty("filterCoveredRoutes")
|
||||
@param:JsonProperty("filterCoveredRoutes")
|
||||
val filterCoveredRoutes: Boolean = false
|
||||
)
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
|
||||
enum class ComplexPlanCalculationStatus {
|
||||
COMPLETED,
|
||||
REQUEST_NOT_FOUND,
|
||||
NO_AVAILABLE_SATELLITES
|
||||
}
|
||||
|
||||
@Schema(description = "Результат запуска расчета модели комплексного плана")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class ComplexPlanCalculationResultDTO(
|
||||
@get:JsonProperty("status")
|
||||
@param:JsonProperty("status")
|
||||
val status: ComplexPlanCalculationStatus,
|
||||
|
||||
@get:JsonProperty("requestId")
|
||||
@param:JsonProperty("requestId")
|
||||
val requestId: UUID,
|
||||
|
||||
@get:JsonProperty("satelliteIds")
|
||||
@param:JsonProperty("satelliteIds")
|
||||
val satelliteIds: List<Long>,
|
||||
|
||||
@get:JsonProperty("missingSatelliteIds")
|
||||
@param:JsonProperty("missingSatelliteIds")
|
||||
val missingSatelliteIds: List<Long>,
|
||||
|
||||
@get:JsonProperty("calculationStart")
|
||||
@param:JsonProperty("calculationStart")
|
||||
val calculationStart: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("calculationEnd")
|
||||
@param:JsonProperty("calculationEnd")
|
||||
val calculationEnd: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("calculationMode")
|
||||
@param:JsonProperty("calculationMode")
|
||||
val calculationMode: DynamicPlanCalculationMode = DynamicPlanCalculationMode.FULL,
|
||||
|
||||
@get:JsonProperty("revolutionMode")
|
||||
@param:JsonProperty("revolutionMode")
|
||||
val revolutionMode: DynamicPlanRevolutionMode = DynamicPlanRevolutionMode.BOTH,
|
||||
|
||||
@get:JsonProperty("filterCoveredRoutes")
|
||||
@param:JsonProperty("filterCoveredRoutes")
|
||||
val filterCoveredRoutes: Boolean = false,
|
||||
|
||||
@get:JsonProperty("routesCount")
|
||||
@param:JsonProperty("routesCount")
|
||||
val routesCount: Int,
|
||||
|
||||
@get:JsonProperty("lastRouteStart")
|
||||
@param:JsonProperty("lastRouteStart")
|
||||
val lastRouteStart: LocalDateTime? = null,
|
||||
|
||||
@get:JsonProperty("lastRouteEnd")
|
||||
@param:JsonProperty("lastRouteEnd")
|
||||
val lastRouteEnd: LocalDateTime? = null,
|
||||
|
||||
@get:JsonProperty("coveredAreaPercent")
|
||||
@param:JsonProperty("coveredAreaPercent")
|
||||
val coveredAreaPercent: Double? = null,
|
||||
|
||||
@get:JsonProperty("durationMs")
|
||||
@param:JsonProperty("durationMs")
|
||||
val durationMs: Long
|
||||
)
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Schema(description = "Задание на расчет контура")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonSerialize
|
||||
data class ContourRequestDTO(
|
||||
@get:JsonProperty("satelliteId")
|
||||
@param:JsonProperty("satelliteId")
|
||||
val satelliteId : Long,
|
||||
|
||||
@get:JsonProperty("time")
|
||||
@param:JsonProperty("time")
|
||||
val time : LocalDateTime,
|
||||
|
||||
@get:JsonProperty("gamma")
|
||||
@param:JsonProperty("gamma")
|
||||
val gamma : Double,
|
||||
|
||||
@get:JsonProperty("duration")
|
||||
@param:JsonProperty("duration")
|
||||
val duration : Double,
|
||||
|
||||
@get:JsonProperty("routeWidthKm")
|
||||
@param:JsonProperty("routeWidthKm")
|
||||
val routeWidthKm : Double,
|
||||
|
||||
@get:JsonProperty("step")
|
||||
@param:JsonProperty("step")
|
||||
val step : Double = 1.0
|
||||
)
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
enum class DynamicPlanCalculationMode {
|
||||
FULL,
|
||||
GREEDY
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
|
||||
import java.util.UUID
|
||||
|
||||
@Schema(description = "Интервал расчета динамического плана")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class DynamicPlanIntervalDTO(
|
||||
@get:JsonProperty("id")
|
||||
@param:JsonProperty("id")
|
||||
val id: Long,
|
||||
|
||||
@get:JsonProperty("runId")
|
||||
@param:JsonProperty("runId")
|
||||
val runId: UUID,
|
||||
|
||||
@get:JsonProperty("requestId")
|
||||
@param:JsonProperty("requestId")
|
||||
val requestId: UUID,
|
||||
|
||||
@get:JsonProperty("intervalId")
|
||||
@param:JsonProperty("intervalId")
|
||||
val intervalId: UUID,
|
||||
|
||||
@get:JsonProperty("regionId")
|
||||
@param:JsonProperty("regionId")
|
||||
val regionId: UUID,
|
||||
|
||||
@get:JsonProperty("revSign")
|
||||
@param:JsonProperty("revSign")
|
||||
val revSign: RevolutionSign,
|
||||
|
||||
@get:JsonProperty("contourWkt")
|
||||
@param:JsonProperty("contourWkt")
|
||||
val contourWkt: String,
|
||||
|
||||
@get:JsonProperty("observationParametersCount")
|
||||
@param:JsonProperty("observationParametersCount")
|
||||
val observationParametersCount: Int
|
||||
)
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
enum class DynamicPlanRevolutionMode {
|
||||
BOTH,
|
||||
ASC,
|
||||
DESC
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
|
||||
@Schema(description = "Маршрут расчета динамического плана")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class DynamicPlanRouteDTO(
|
||||
@get:JsonProperty("id")
|
||||
@param:JsonProperty("id")
|
||||
val id: Long,
|
||||
|
||||
@get:JsonProperty("runId")
|
||||
@param:JsonProperty("runId")
|
||||
val runId: UUID,
|
||||
|
||||
@get:JsonProperty("requestId")
|
||||
@param:JsonProperty("requestId")
|
||||
val requestId: UUID,
|
||||
|
||||
@get:JsonProperty("satelliteId")
|
||||
@param:JsonProperty("satelliteId")
|
||||
val satelliteId: Long,
|
||||
|
||||
@get:JsonProperty("startTime")
|
||||
@param:JsonProperty("startTime")
|
||||
val startTime: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("endTime")
|
||||
@param:JsonProperty("endTime")
|
||||
val endTime: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("duration")
|
||||
@param:JsonProperty("duration")
|
||||
val duration: Double,
|
||||
|
||||
@get:JsonProperty("revolution")
|
||||
@param:JsonProperty("revolution")
|
||||
val revolution: Long,
|
||||
|
||||
@get:JsonProperty("roll")
|
||||
@param:JsonProperty("roll")
|
||||
val roll: Double,
|
||||
|
||||
@get:JsonProperty("contourWkt")
|
||||
@param:JsonProperty("contourWkt")
|
||||
val contourWkt: String
|
||||
)
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
|
||||
enum class DynamicPlanRunStatus {
|
||||
PENDING,
|
||||
RUNNING,
|
||||
COMPLETED,
|
||||
FAILED
|
||||
}
|
||||
|
||||
@Schema(description = "Состояние запуска расчета динамического плана")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class DynamicPlanRunDTO(
|
||||
@get:JsonProperty("runId")
|
||||
@param:JsonProperty("runId")
|
||||
val runId: UUID,
|
||||
|
||||
@get:JsonProperty("status")
|
||||
@param:JsonProperty("status")
|
||||
val status: DynamicPlanRunStatus,
|
||||
|
||||
@get:JsonProperty("requestId")
|
||||
@param:JsonProperty("requestId")
|
||||
val requestId: UUID,
|
||||
|
||||
@get:JsonProperty("satelliteIds")
|
||||
@param:JsonProperty("satelliteIds")
|
||||
val satelliteIds: List<Long>,
|
||||
|
||||
@get:JsonProperty("calculationStart")
|
||||
@param:JsonProperty("calculationStart")
|
||||
val calculationStart: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("calculationEnd")
|
||||
@param:JsonProperty("calculationEnd")
|
||||
val calculationEnd: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("calculationMode")
|
||||
@param:JsonProperty("calculationMode")
|
||||
val calculationMode: DynamicPlanCalculationMode,
|
||||
|
||||
@get:JsonProperty("revolutionMode")
|
||||
@param:JsonProperty("revolutionMode")
|
||||
val revolutionMode: DynamicPlanRevolutionMode,
|
||||
|
||||
@get:JsonProperty("filterCoveredRoutes")
|
||||
@param:JsonProperty("filterCoveredRoutes")
|
||||
val filterCoveredRoutes: Boolean,
|
||||
|
||||
@get:JsonProperty("createdAt")
|
||||
@param:JsonProperty("createdAt")
|
||||
val createdAt: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("startedAt")
|
||||
@param:JsonProperty("startedAt")
|
||||
val startedAt: LocalDateTime?,
|
||||
|
||||
@get:JsonProperty("finishedAt")
|
||||
@param:JsonProperty("finishedAt")
|
||||
val finishedAt: LocalDateTime?,
|
||||
|
||||
@get:JsonProperty("durationMs")
|
||||
@param:JsonProperty("durationMs")
|
||||
val durationMs: Long?,
|
||||
|
||||
@get:JsonProperty("resultStatus")
|
||||
@param:JsonProperty("resultStatus")
|
||||
val resultStatus: String?,
|
||||
|
||||
@get:JsonProperty("routesCount")
|
||||
@param:JsonProperty("routesCount")
|
||||
val routesCount: Int?,
|
||||
|
||||
@get:JsonProperty("errorMessage")
|
||||
@param:JsonProperty("errorMessage")
|
||||
val errorMessage: String?
|
||||
)
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import org.locationtech.jts.geom.MultiPolygon
|
||||
import org.locationtech.jts.geom.LineString
|
||||
import org.locationtech.jts.geom.Geometry
|
||||
import org.locationtech.jts.geom.LinearRing
|
||||
import org.locationtech.jts.geom.Polygon
|
||||
import org.locationtech.jts.geom.Point
|
||||
import java.io.File
|
||||
import kotlin.collections.forEach
|
||||
import kotlin.math.PI
|
||||
|
||||
|
||||
class GeoJson {
|
||||
|
||||
data class GeometryGeoJson(val type: String, val coordinates: Any)
|
||||
data class Feature(val name: String, val bbox: List<Double>? = null, val type: String, val geometry: GeometryGeoJson, val properties: Any)
|
||||
data class FeatureCollection(val type: String, val features: List<Feature>)
|
||||
|
||||
private fun multipolygonToGeojsonCoord(multipolygon: MultiPolygon, radToDeg : Boolean = false): List<List<List<Array<Double>>>> {
|
||||
return (0 until multipolygon.numGeometries).map{i->
|
||||
val polygon = multipolygon.getGeometryN(i) as Polygon
|
||||
polygonToGeojsonCoord(polygon, radToDeg)
|
||||
}
|
||||
}
|
||||
|
||||
private fun polygonToGeojsonCoord(polygon: Polygon, radToDeg : Boolean = false): List<List<Array<Double>>> {
|
||||
val coordinates = mutableListOf<List<Array<Double>>>()
|
||||
|
||||
coordinates.add(linearRingToCoord(polygon.exteriorRing, radToDeg))
|
||||
|
||||
(0 until polygon.numInteriorRing).forEach { i ->
|
||||
coordinates.add(linearRingToCoord(polygon.getInteriorRingN(i), radToDeg))
|
||||
}
|
||||
|
||||
return coordinates
|
||||
}
|
||||
|
||||
private fun linearRingToCoord(ring: LinearRing, radToDeg : Boolean = false): List<Array<Double>> {
|
||||
if (radToDeg) {
|
||||
return ring.coordinates.map{ coordinate ->
|
||||
listOf(coordinate.x * 180 / PI, coordinate.y * 180 / PI).toTypedArray()
|
||||
}
|
||||
} else {
|
||||
return ring.coordinates.map{ coordinate ->
|
||||
listOf(coordinate.x, coordinate.y).toTypedArray()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun getBBox(geometry: Geometry): List<Double>{
|
||||
val bbox = geometry.envelopeInternal
|
||||
return listOf(
|
||||
bbox.minX,
|
||||
bbox.maxY,
|
||||
bbox.maxX,
|
||||
bbox.minY,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Конвертация из Geometry в формат geoJSON
|
||||
* @param geometry исходная геометрия
|
||||
* @return Возвращает конвертированную геометрию для записи в формате geoGSON
|
||||
*/
|
||||
fun convertToGeoJson(geometry: Geometry): GeometryGeoJson {
|
||||
return when (geometry) {
|
||||
is Point -> GeometryGeoJson(type = "Point", coordinates = listOf( geometry.x, geometry.y ))
|
||||
is LineString -> GeometryGeoJson(type = "LineString", coordinates = geometry.coordinates.map{arrayOf( it.x, it.y )})
|
||||
is Polygon -> GeometryGeoJson(type = "Polygon", coordinates = polygonToGeojsonCoord(geometry))
|
||||
is MultiPolygon -> GeometryGeoJson(type = "MultiPolygon", coordinates = multipolygonToGeojsonCoord(geometry))
|
||||
else -> throw IllegalArgumentException("Unknown geometry type: $geometry")
|
||||
}
|
||||
}
|
||||
fun convertToGeoJsonRadToDeg(geometry: Geometry): GeometryGeoJson {
|
||||
return when (geometry) {
|
||||
is Point -> GeometryGeoJson(type = "Point", coordinates = listOf( radToDeg(geometry.x), (geometry.y) ))
|
||||
is LineString -> GeometryGeoJson(type = "LineString", coordinates = geometry.coordinates.map{arrayOf( radToDeg(it.x), radToDeg(it.y) )})
|
||||
is Polygon -> GeometryGeoJson(type = "Polygon", coordinates = polygonToGeojsonCoord(geometry, radToDeg = true))
|
||||
is MultiPolygon -> GeometryGeoJson(type = "MultiPolygon", coordinates = multipolygonToGeojsonCoord(geometry, radToDeg = true))
|
||||
else -> throw IllegalArgumentException("Unknown geometry type: $geometry")
|
||||
}
|
||||
}
|
||||
|
||||
fun radToDeg(value : Double): Double {
|
||||
return value * 180 / PI
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun createFeatureCollection(
|
||||
geoms: List<GeoJsonItem>,
|
||||
fileName: String,
|
||||
radToDeg: Boolean = false
|
||||
): FeatureCollection {
|
||||
val gj = GeoJson()
|
||||
val features = geoms.map { geom ->
|
||||
Feature(
|
||||
name = fileName,
|
||||
bbox = gj.getBBox(geom.geometry),
|
||||
type = "Feature",
|
||||
geometry = if (radToDeg) {
|
||||
gj.convertToGeoJsonRadToDeg(geom.geometry)
|
||||
} else {
|
||||
gj.convertToGeoJson(geom.geometry)
|
||||
},
|
||||
properties = geom.properties
|
||||
)
|
||||
}
|
||||
return FeatureCollection(type = "FeatureCollection", features = features)
|
||||
}
|
||||
|
||||
/**
|
||||
* Создать geoJSON.
|
||||
* Файлы geoJson хранятся в папке ресурсов рядом с картой в папке geoJson.
|
||||
* Названия **AprioryScans, Catalog, FactScans, Region** соответственно.
|
||||
* @param geom массив элементов [GeoJsonItem], содержащий геометрию и свойства слоя при необходимости
|
||||
* @param outputPath целевой путь сохранения файла
|
||||
*/
|
||||
fun createGeoJson(geoms: List<GeoJsonItem>, fileName: String, dirToSave: String, radToDeg: Boolean = false) {
|
||||
val objectMapper = ObjectMapper()
|
||||
val geoJsonString = objectMapper.writerWithDefaultPrettyPrinter()
|
||||
.writeValueAsString(createFeatureCollection(geoms, fileName, radToDeg))
|
||||
|
||||
val dataDir = File(dirToSave)
|
||||
if(!dataDir.exists()){
|
||||
dataDir.mkdirs()
|
||||
}
|
||||
|
||||
val path = "$fileName.json"
|
||||
val file = File(dataDir, path)
|
||||
file.writeText(geoJsonString)
|
||||
}
|
||||
}
|
||||
}
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
import org.locationtech.jts.geom.Geometry
|
||||
import org.locationtech.jts.geom.LineString
|
||||
import org.locationtech.jts.geom.Polygon
|
||||
import org.locationtech.jts.io.WKTReader
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamDTO
|
||||
|
||||
|
||||
data class GeoJsonItem(
|
||||
val geometry: Geometry,
|
||||
val properties: Map<String, Any>,
|
||||
){
|
||||
companion object {
|
||||
// /**
|
||||
// * Создание слоя интервалов с атрибутивной информацией
|
||||
// * @param
|
||||
// * @return
|
||||
// */
|
||||
// fun createIntervalsLay(ka: KaPass): List<GeoJsonItem> {
|
||||
// val rez = mutableListOf<GeoJsonItem>()
|
||||
// for (view in ka.views) {
|
||||
// var unionGeom: Geometry? = view.matPlan.interval.geometry//.contour[0].geometry
|
||||
//
|
||||
// if (unionGeom == null) continue
|
||||
// val resItem = GeoJsonItem(
|
||||
// geometry = unionGeom,//, //.geometryToDeg(),
|
||||
// properties = mapOf(
|
||||
// "Аппарат" to "${ka.ka.kaId}",
|
||||
// "Время нач." to view.matPlan.tMinLDT.toString(),//.toDateString(),
|
||||
// "Время кон." to view.matPlan.tMaxLDT.toString(),//.toDateString(),
|
||||
// "Виток" to view.matPlan.vit,
|
||||
// "Регион" to "${view.matPlan.interval.obj.reqRegionId}",
|
||||
// )
|
||||
// )
|
||||
// rez.add(resItem)
|
||||
// }
|
||||
//
|
||||
// return rez
|
||||
// }
|
||||
|
||||
/**
|
||||
* Создание слоя интервалов с атрибутивной информацией
|
||||
* @return Возвращает заполненный [GeoJsonItem] с геометрией контура и атрибутивной информацией
|
||||
*/
|
||||
fun createIntervalsLay(interval: List<IntervalItemDTO>): List<GeoJsonItem> {
|
||||
val rez = mutableListOf<GeoJsonItem>()
|
||||
for(interval in interval) {
|
||||
|
||||
if(WKTReader().read(interval.geometry).getGeometryN(0).coordinates.first() != WKTReader().read(interval.geometry).getGeometryN(0).coordinates.last())
|
||||
continue
|
||||
|
||||
val resItem = GeoJsonItem(
|
||||
geometry = WKTReader().read(interval.geometry),
|
||||
properties = mapOf(
|
||||
// "№" to "${interval.num}",
|
||||
// "Region" to "${interval.regionId}",
|
||||
// "Interval" to "${interval.id}",
|
||||
"mpl" to getMplStringForLay(interval.mpl)
|
||||
// "mpl" to interval.mpl.map { "[kaid ${it.noradId}, rev ${it.revolutionBegin}, time ${it.timeBegin}]" },
|
||||
// "mpl" to "${interval.mpl}",
|
||||
)
|
||||
)
|
||||
rez.add(resItem)
|
||||
}
|
||||
return rez
|
||||
}
|
||||
|
||||
/**
|
||||
* Формирование атрибутивной информации по матрице планирования
|
||||
* @return Возвращает строку с перечнем прохождений КА в минималистическом формате
|
||||
* КА[[виток время][..]..]
|
||||
*/
|
||||
fun getMplStringForLay(mpl : List<SquareViewParamDTO>) : String {
|
||||
val kaList = mutableSetOf<Long>()//.addAll(mpl.map { it.noradId })
|
||||
|
||||
mpl.sortedBy { it.timeBegin }
|
||||
|
||||
mpl.forEach {
|
||||
kaList.add(it.noradId)
|
||||
}
|
||||
|
||||
val res = StringBuilder()
|
||||
|
||||
val kaListTmp = kaList.sortedBy { it }
|
||||
|
||||
kaListTmp.forEach { kaId ->
|
||||
val str = mpl.filter { it.noradId == kaId }.sortedBy { it.timeBegin }.map { "[rev ${it.revolutionBegin}, time ${it.timeBegin}, gamma ${(it.gammaMin + it.gammaMax)/2}, gammaMin ${it.gammaMin}, gammaMax ${it.gammaMax}]" }
|
||||
|
||||
res.append("noradId:$kaId")
|
||||
res.append("[")
|
||||
str.forEach { res.append(it) }
|
||||
res.append("]")
|
||||
}
|
||||
|
||||
return res.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание слоя маршрутов с атрибутивной информацией
|
||||
* @return Возвращает заполненный [GeoJsonItem] с геометрией контура и атрибутивной информацией
|
||||
*/
|
||||
fun createSurveyLay(surveys: List<SurveyContour>): List<GeoJsonItem> {
|
||||
val rez = mutableListOf<GeoJsonItem>()
|
||||
for(survey in surveys) {
|
||||
|
||||
val geometry = WKTReader().read(survey.geometry)
|
||||
|
||||
if (geometry !is Polygon) { continue }
|
||||
|
||||
if(geometry.getGeometryN(0).coordinates.first() != geometry.getGeometryN(0).coordinates.last())
|
||||
continue
|
||||
|
||||
val resItem = GeoJsonItem(
|
||||
geometry = geometry,
|
||||
properties = mapOf(
|
||||
"noradId" to survey.mpl.noradId,
|
||||
"rev" to survey.rev,
|
||||
"gamma" to survey.gamma,
|
||||
"time" to survey.time.toString(),
|
||||
"duration" to survey.duration,
|
||||
// "mpl" to survey.mpl.toString(),
|
||||
)
|
||||
)
|
||||
rez.add(resItem)
|
||||
}
|
||||
return rez
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание слоя с атрибутивной информацией
|
||||
* @return Возвращает заполненный [GeoJsonItem] с геометрией контура и атрибутивной информацией
|
||||
*/
|
||||
fun createAnyLay(pols: List<Polygon>): MutableList<GeoJsonItem> {
|
||||
val rez = mutableListOf<GeoJsonItem>()
|
||||
for (pol in pols) {
|
||||
val resItem = GeoJsonItem(
|
||||
geometry = pol,//,
|
||||
properties = mapOf()
|
||||
)
|
||||
rez.add(resItem)
|
||||
}
|
||||
return rez
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание слоя с атрибутивной информацией
|
||||
* @return Возвращает заполненный [GeoJsonItem] с геометрией контура и атрибутивной информацией
|
||||
*/
|
||||
fun createRequestLay(requests: List<RequestItemDTO>): List<GeoJsonItem> {
|
||||
val rez = mutableListOf<GeoJsonItem>()
|
||||
for (item in requests) {
|
||||
val resItem = GeoJsonItem(
|
||||
geometry = WKTReader().read(item.geometry),//,
|
||||
properties = mapOf(
|
||||
"UUID" to "${item.id}",
|
||||
"Name" to "${item.name}",
|
||||
"Interval_begin" to "${item.intervalBegin}",
|
||||
"Interval_end" to "${item.intervalEnd}",
|
||||
"Importance" to "${item.importance}",
|
||||
"Priority" to "${item.priority}",
|
||||
"Resulution" to "${item.resolution}",
|
||||
)
|
||||
)
|
||||
rez.add(resItem)
|
||||
}
|
||||
return rez
|
||||
}
|
||||
/**
|
||||
* Создание слоя с атрибутивной информацией
|
||||
* @return Возвращает заполненный [GeoJsonItem] с геометрией контура и атрибутивной информацией
|
||||
*/
|
||||
fun createAplCatalogLay(aplCatalog: List<AplObjItem>): List<GeoJsonItem> {
|
||||
val rez = mutableListOf<GeoJsonItem>()
|
||||
for (item in aplCatalog) {
|
||||
val resItem = GeoJsonItem(
|
||||
geometry = item.geometry,
|
||||
properties = mapOf(
|
||||
"UUID" to "${item.id}",
|
||||
"Name" to "${item.name}",
|
||||
// "Interval_begin" to "${item.intervalBegin}",
|
||||
// "Interval_end" to "${item.intervalEnd}",
|
||||
// "Importance" to "${item.importance}",
|
||||
// "Priority" to "${item.priority}",
|
||||
// "Resulution" to "${item.resolution}",
|
||||
)
|
||||
)
|
||||
rez.add(resItem)
|
||||
}
|
||||
return rez
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание слой для отображения из LineString
|
||||
* @return Возвращает заполненный [GeoJsonItem] с геометрией контура и атрибутивной информацией
|
||||
*/
|
||||
fun createLineStringLay(lines: List<LineString>): MutableList<GeoJsonItem> {
|
||||
val rez = mutableListOf<GeoJsonItem>()
|
||||
for (item in lines) {
|
||||
val resItem = GeoJsonItem(
|
||||
geometry = item,//,
|
||||
properties = mapOf()
|
||||
)
|
||||
rez.add(resItem)
|
||||
}
|
||||
return rez
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
|
||||
@Schema(description = "Debug-запрос расчета greedy-математики по сохраненным параметрам наблюдения")
|
||||
data class GreedyMathDebugRequestDTO(
|
||||
@get:JsonProperty("requestId")
|
||||
@param:JsonProperty("requestId")
|
||||
val requestId: UUID,
|
||||
|
||||
@get:JsonProperty("satelliteIds")
|
||||
@param:JsonProperty("satelliteIds")
|
||||
val satelliteIds: List<Long>,
|
||||
|
||||
@get:JsonProperty("calculationStart")
|
||||
@param:JsonProperty("calculationStart")
|
||||
val calculationStart: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("calculationEnd")
|
||||
@param:JsonProperty("calculationEnd")
|
||||
val calculationEnd: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("revolutionMode")
|
||||
@param:JsonProperty("revolutionMode")
|
||||
val revolutionMode: DynamicPlanRevolutionMode = DynamicPlanRevolutionMode.BOTH,
|
||||
|
||||
@get:JsonProperty("filterCoveredRoutes")
|
||||
@param:JsonProperty("filterCoveredRoutes")
|
||||
val filterCoveredRoutes: Boolean = false
|
||||
)
|
||||
|
||||
@Schema(description = "Выбранное greedy-включение без геометрии маршрута")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class GreedyMathCandidateDTO(
|
||||
@get:JsonProperty("intervalId")
|
||||
@param:JsonProperty("intervalId")
|
||||
val intervalId: UUID,
|
||||
|
||||
@get:JsonProperty("regionId")
|
||||
@param:JsonProperty("regionId")
|
||||
val regionId: UUID,
|
||||
|
||||
@get:JsonProperty("satelliteId")
|
||||
@param:JsonProperty("satelliteId")
|
||||
val satelliteId: Long,
|
||||
|
||||
@get:JsonProperty("revolution")
|
||||
@param:JsonProperty("revolution")
|
||||
val revolution: Long,
|
||||
|
||||
@get:JsonProperty("day")
|
||||
@param:JsonProperty("day")
|
||||
val day: LocalDate,
|
||||
|
||||
@get:JsonProperty("timeBegin")
|
||||
@param:JsonProperty("timeBegin")
|
||||
val timeBegin: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("timeEnd")
|
||||
@param:JsonProperty("timeEnd")
|
||||
val timeEnd: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("durationSeconds")
|
||||
@param:JsonProperty("durationSeconds")
|
||||
val durationSeconds: Long,
|
||||
|
||||
@get:JsonProperty("gamma")
|
||||
@param:JsonProperty("gamma")
|
||||
val gamma: Double
|
||||
)
|
||||
|
||||
@Schema(description = "Debug-результат greedy-математики по сохраненным параметрам наблюдения")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class GreedyMathDebugResultDTO(
|
||||
@get:JsonProperty("requestId")
|
||||
@param:JsonProperty("requestId")
|
||||
val requestId: UUID,
|
||||
|
||||
@get:JsonProperty("satelliteIds")
|
||||
@param:JsonProperty("satelliteIds")
|
||||
val satelliteIds: List<Long>,
|
||||
|
||||
@get:JsonProperty("missingSatelliteIds")
|
||||
@param:JsonProperty("missingSatelliteIds")
|
||||
val missingSatelliteIds: List<Long>,
|
||||
|
||||
@get:JsonProperty("calculationStart")
|
||||
@param:JsonProperty("calculationStart")
|
||||
val calculationStart: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("calculationEnd")
|
||||
@param:JsonProperty("calculationEnd")
|
||||
val calculationEnd: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("status")
|
||||
@param:JsonProperty("status")
|
||||
val status: ComplexPlanCalculationStatus,
|
||||
|
||||
@get:JsonProperty("revolutionMode")
|
||||
@param:JsonProperty("revolutionMode")
|
||||
val revolutionMode: DynamicPlanRevolutionMode,
|
||||
|
||||
@get:JsonProperty("observationParametersFilePath")
|
||||
@param:JsonProperty("observationParametersFilePath")
|
||||
val observationParametersFilePath: String? = null,
|
||||
|
||||
@get:JsonProperty("intervalsCount")
|
||||
@param:JsonProperty("intervalsCount")
|
||||
val intervalsCount: Int,
|
||||
|
||||
@get:JsonProperty("observationParametersCount")
|
||||
@param:JsonProperty("observationParametersCount")
|
||||
val observationParametersCount: Int,
|
||||
|
||||
@get:JsonProperty("selectedCandidatesCount")
|
||||
@param:JsonProperty("selectedCandidatesCount")
|
||||
val selectedCandidatesCount: Int,
|
||||
|
||||
@get:JsonProperty("failuresCount")
|
||||
@param:JsonProperty("failuresCount")
|
||||
val failuresCount: Int,
|
||||
|
||||
@get:JsonProperty("selectedCandidates")
|
||||
@param:JsonProperty("selectedCandidates")
|
||||
val selectedCandidates: List<GreedyMathCandidateDTO>,
|
||||
|
||||
@get:JsonProperty("filterCoveredRoutes")
|
||||
@param:JsonProperty("filterCoveredRoutes")
|
||||
val filterCoveredRoutes: Boolean,
|
||||
|
||||
@get:JsonProperty("routesBeforeUsefulAreaFilterCount")
|
||||
@param:JsonProperty("routesBeforeUsefulAreaFilterCount")
|
||||
val routesBeforeUsefulAreaFilterCount: Int,
|
||||
|
||||
@get:JsonProperty("routesAfterUsefulAreaFilterCount")
|
||||
@param:JsonProperty("routesAfterUsefulAreaFilterCount")
|
||||
val routesAfterUsefulAreaFilterCount: Int,
|
||||
|
||||
@get:JsonProperty("routeBuildFailuresCount")
|
||||
@param:JsonProperty("routeBuildFailuresCount")
|
||||
val routeBuildFailuresCount: Int,
|
||||
|
||||
@get:JsonProperty("greedySelectionMs")
|
||||
@param:JsonProperty("greedySelectionMs")
|
||||
val greedySelectionMs: Long,
|
||||
|
||||
@get:JsonProperty("routeBuildMs")
|
||||
@param:JsonProperty("routeBuildMs")
|
||||
val routeBuildMs: Long,
|
||||
|
||||
@get:JsonProperty("usefulAreaAnalysisMs")
|
||||
@param:JsonProperty("usefulAreaAnalysisMs")
|
||||
val usefulAreaAnalysisMs: Long,
|
||||
|
||||
@get:JsonProperty("totalMs")
|
||||
@param:JsonProperty("totalMs")
|
||||
val totalMs: Long
|
||||
)
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamDTO
|
||||
import java.util.*
|
||||
|
||||
@Schema(description = "Интервал")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonSerialize
|
||||
data class IntervalItemDTO(
|
||||
@get:JsonProperty("id")
|
||||
@param:JsonProperty("id")
|
||||
val id : UUID = UUID.randomUUID(),
|
||||
|
||||
@get:JsonProperty("regionId")
|
||||
@param:JsonProperty("regionId")
|
||||
val regionId : UUID,
|
||||
|
||||
@get:JsonProperty("revSign")
|
||||
@param:JsonProperty("revSign")
|
||||
val revSign : RevolutionSign,
|
||||
|
||||
@get:JsonProperty("geometry")
|
||||
@param:JsonProperty("geometry")
|
||||
val geometry: String,
|
||||
|
||||
@get:JsonProperty("mpl")
|
||||
@param:JsonProperty("mpl")
|
||||
val mpl: MutableList<SquareViewParamDTO> = mutableListOf()
|
||||
)
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.PositionDTO
|
||||
import java.util.UUID
|
||||
|
||||
@Schema(description = "Модель данных объекта для расчета наблюдения")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonSerialize
|
||||
data class ObjDTO(
|
||||
@get:JsonProperty("id")
|
||||
@param:JsonProperty("id")
|
||||
val id: UUID,
|
||||
|
||||
@get:JsonProperty("position")
|
||||
@param:JsonProperty("position")
|
||||
val position: PositionDTO? = null,
|
||||
|
||||
@get:JsonProperty("contourWKT")
|
||||
@param:JsonProperty("contourWKT")
|
||||
val contourWKT: String? = null
|
||||
)
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Schema(description = "Запрос расчета параметров наблюдения для набора объектов")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonSerialize
|
||||
data class ObjViewRequestDTO(
|
||||
@get:JsonProperty("sunAngleMin")
|
||||
@param:JsonProperty("sunAngleMin")
|
||||
val sunAngleMin: Double? = null,
|
||||
|
||||
@get:JsonProperty("objects")
|
||||
@param:JsonProperty("objects")
|
||||
val objects: List<ObjDTO>,
|
||||
|
||||
@get:JsonProperty("satellites")
|
||||
@param:JsonProperty("satellites")
|
||||
val satellites: Iterable<Long>? = null,
|
||||
|
||||
@get:JsonProperty("timeStart")
|
||||
@param:JsonProperty("timeStart")
|
||||
val timeStart: LocalDateTime? = null,
|
||||
|
||||
@get:JsonProperty("timeStop")
|
||||
@param:JsonProperty("timeStop")
|
||||
val timeStop: LocalDateTime? = null
|
||||
)
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
|
||||
@Schema(description = "Debug-запрос построения интервалов и расчета параметров наблюдения")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class ObservationParametersDebugRequestDTO(
|
||||
@get:JsonProperty("requestId")
|
||||
@param:JsonProperty("requestId")
|
||||
val requestId: UUID,
|
||||
|
||||
@get:JsonProperty("satelliteIds")
|
||||
@param:JsonProperty("satelliteIds")
|
||||
val satelliteIds: List<Long>,
|
||||
|
||||
@get:JsonProperty("calculationStart")
|
||||
@param:JsonProperty("calculationStart")
|
||||
val calculationStart: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("calculationEnd")
|
||||
@param:JsonProperty("calculationEnd")
|
||||
val calculationEnd: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("parallelBallisticsCalculation")
|
||||
@param:JsonProperty("parallelBallisticsCalculation")
|
||||
val parallelBallisticsCalculation: Boolean = false,
|
||||
|
||||
@get:JsonProperty("chunkSize")
|
||||
@param:JsonProperty("chunkSize")
|
||||
val chunkSize: Int? = null,
|
||||
|
||||
@get:JsonProperty("parallelism")
|
||||
@param:JsonProperty("parallelism")
|
||||
val parallelism: Int? = null,
|
||||
|
||||
@get:JsonProperty("saveResultToFile")
|
||||
@param:JsonProperty("saveResultToFile")
|
||||
val saveResultToFile: Boolean = false
|
||||
)
|
||||
|
||||
@Schema(description = "Debug-результат построения интервалов и расчета параметров наблюдения")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class ObservationParametersDebugResultDTO(
|
||||
@get:JsonProperty("requestId")
|
||||
@param:JsonProperty("requestId")
|
||||
val requestId: UUID,
|
||||
|
||||
@get:JsonProperty("satelliteIds")
|
||||
@param:JsonProperty("satelliteIds")
|
||||
val satelliteIds: List<Long>,
|
||||
|
||||
@get:JsonProperty("missingSatelliteIds")
|
||||
@param:JsonProperty("missingSatelliteIds")
|
||||
val missingSatelliteIds: List<Long>,
|
||||
|
||||
@get:JsonProperty("calculationStart")
|
||||
@param:JsonProperty("calculationStart")
|
||||
val calculationStart: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("calculationEnd")
|
||||
@param:JsonProperty("calculationEnd")
|
||||
val calculationEnd: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("status")
|
||||
@param:JsonProperty("status")
|
||||
val status: ComplexPlanCalculationStatus,
|
||||
|
||||
@get:JsonProperty("parallelBallisticsCalculation")
|
||||
@param:JsonProperty("parallelBallisticsCalculation")
|
||||
val parallelBallisticsCalculation: Boolean,
|
||||
|
||||
@get:JsonProperty("intervalsCount")
|
||||
@param:JsonProperty("intervalsCount")
|
||||
val intervalsCount: Int,
|
||||
|
||||
@get:JsonProperty("observationParametersCount")
|
||||
@param:JsonProperty("observationParametersCount")
|
||||
val observationParametersCount: Int,
|
||||
|
||||
@get:JsonProperty("chunksCount")
|
||||
@param:JsonProperty("chunksCount")
|
||||
val chunksCount: Int,
|
||||
|
||||
@get:JsonProperty("intervalsMs")
|
||||
@param:JsonProperty("intervalsMs")
|
||||
val intervalsMs: Long,
|
||||
|
||||
@get:JsonProperty("observationParametersMs")
|
||||
@param:JsonProperty("observationParametersMs")
|
||||
val observationParametersMs: Long,
|
||||
|
||||
@get:JsonProperty("totalMs")
|
||||
@param:JsonProperty("totalMs")
|
||||
val totalMs: Long,
|
||||
|
||||
@get:JsonProperty("savedToFile")
|
||||
@param:JsonProperty("savedToFile")
|
||||
val savedToFile: Boolean = false,
|
||||
|
||||
@get:JsonProperty("resultFilePath")
|
||||
@param:JsonProperty("resultFilePath")
|
||||
val resultFilePath: String? = null
|
||||
)
|
||||
|
||||
@Schema(description = "Файловый артефакт debug-расчета интервалов и параметров наблюдения")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class ObservationParametersDebugFileDTO(
|
||||
@get:JsonProperty("schemaVersion")
|
||||
@param:JsonProperty("schemaVersion")
|
||||
val schemaVersion: Int = 1,
|
||||
|
||||
@get:JsonProperty("createdAt")
|
||||
@param:JsonProperty("createdAt")
|
||||
val createdAt: Instant,
|
||||
|
||||
@get:JsonProperty("requestId")
|
||||
@param:JsonProperty("requestId")
|
||||
val requestId: UUID,
|
||||
|
||||
@get:JsonProperty("satelliteIds")
|
||||
@param:JsonProperty("satelliteIds")
|
||||
val satelliteIds: List<Long>,
|
||||
|
||||
@get:JsonProperty("missingSatelliteIds")
|
||||
@param:JsonProperty("missingSatelliteIds")
|
||||
val missingSatelliteIds: List<Long>,
|
||||
|
||||
@get:JsonProperty("calculationStart")
|
||||
@param:JsonProperty("calculationStart")
|
||||
val calculationStart: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("calculationEnd")
|
||||
@param:JsonProperty("calculationEnd")
|
||||
val calculationEnd: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("parallelBallisticsCalculation")
|
||||
@param:JsonProperty("parallelBallisticsCalculation")
|
||||
val parallelBallisticsCalculation: Boolean,
|
||||
|
||||
@get:JsonProperty("chunkSize")
|
||||
@param:JsonProperty("chunkSize")
|
||||
val chunkSize: Int?,
|
||||
|
||||
@get:JsonProperty("parallelism")
|
||||
@param:JsonProperty("parallelism")
|
||||
val parallelism: Int?,
|
||||
|
||||
@get:JsonProperty("intervalsCount")
|
||||
@param:JsonProperty("intervalsCount")
|
||||
val intervalsCount: Int,
|
||||
|
||||
@get:JsonProperty("observationParametersCount")
|
||||
@param:JsonProperty("observationParametersCount")
|
||||
val observationParametersCount: Int,
|
||||
|
||||
@get:JsonProperty("intervals")
|
||||
@param:JsonProperty("intervals")
|
||||
val intervals: List<IntervalItemDTO>
|
||||
)
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonAlias
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
|
||||
@Schema(description = "Заявка для расчета комплексного плана")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonSerialize
|
||||
data class RequestItemDTO(
|
||||
@get:JsonProperty("id")
|
||||
@param:JsonProperty("id")
|
||||
val id : UUID = UUID.randomUUID(),
|
||||
|
||||
@get:JsonProperty("name")
|
||||
@param:JsonProperty("name")
|
||||
val name : String,
|
||||
|
||||
@get:JsonProperty("originalGeometry")
|
||||
@param:JsonProperty("originalGeometry")
|
||||
@param:JsonAlias("geometry", "remainingGeometry")
|
||||
val geometry : String,
|
||||
|
||||
@get:JsonProperty("intervalBegin")
|
||||
@param:JsonProperty("intervalBegin")
|
||||
val intervalBegin : LocalDateTime,
|
||||
|
||||
@get:JsonProperty("intervalEnd")
|
||||
@param:JsonProperty("intervalEnd")
|
||||
val intervalEnd : LocalDateTime,
|
||||
|
||||
@get:JsonProperty("resolution")
|
||||
@param:JsonProperty("resolution")
|
||||
val resolution : Double,
|
||||
|
||||
@get:JsonProperty("priority")
|
||||
@param:JsonProperty("priority")
|
||||
val priority : Int? = null,
|
||||
|
||||
@get:JsonProperty("importance")
|
||||
@param:JsonProperty("importance")
|
||||
val importance : Double,
|
||||
|
||||
/**Исходная площадь района.
|
||||
* Используется для расчета важности задания */
|
||||
@get:JsonProperty("baseArea")
|
||||
@param:JsonProperty("baseArea")
|
||||
var baseArea : Double? = null,
|
||||
|
||||
/**Текущая площадь района.
|
||||
* Используется для расчета важности задания */
|
||||
@get:JsonProperty("currentArea")
|
||||
@param:JsonProperty("currentArea")
|
||||
var currentArea : Double? = null,
|
||||
|
||||
/**Дата время последнего наблюдения.
|
||||
* Используется для расчета важности задания */
|
||||
@get:JsonProperty("dateTimeLastObservation")
|
||||
@param:JsonProperty("dateTimeLastObservation")
|
||||
var dateTimeLastObservation : LocalDateTime? = null,
|
||||
|
||||
@get:JsonProperty("appType")
|
||||
@param:JsonProperty("appType")
|
||||
val appType : Int = 0,
|
||||
|
||||
@get:JsonProperty("intervals")
|
||||
@param:JsonProperty("intervals")
|
||||
val intervals : MutableList<IntervalItemDTO>? = null,
|
||||
)
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamDTO
|
||||
import java.time.LocalDateTime
|
||||
|
||||
data class SurveyContour(
|
||||
val mpl : SquareViewParamDTO,
|
||||
val time : LocalDateTime,
|
||||
val duration : Double,
|
||||
val rev : Long,
|
||||
val gamma : Double,
|
||||
val geometry : String,
|
||||
)
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
|
||||
@Schema(description = "Debug-запрос построения маршрутов по всем параметрам видимости из файла")
|
||||
data class TestCalcMarsDebugRequestDTO(
|
||||
@get:JsonProperty("requestId")
|
||||
@param:JsonProperty("requestId")
|
||||
val requestId: UUID,
|
||||
|
||||
@get:JsonProperty("satelliteIds")
|
||||
@param:JsonProperty("satelliteIds")
|
||||
val satelliteIds: List<Long>,
|
||||
|
||||
@get:JsonProperty("calculationStart")
|
||||
@param:JsonProperty("calculationStart")
|
||||
val calculationStart: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("calculationEnd")
|
||||
@param:JsonProperty("calculationEnd")
|
||||
val calculationEnd: LocalDateTime
|
||||
)
|
||||
|
||||
@Schema(description = "Debug-результат построения маршрутов по всем параметрам видимости из файла")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class TestCalcMarsDebugResultDTO(
|
||||
@get:JsonProperty("requestId")
|
||||
@param:JsonProperty("requestId")
|
||||
val requestId: UUID,
|
||||
|
||||
@get:JsonProperty("satelliteIds")
|
||||
@param:JsonProperty("satelliteIds")
|
||||
val satelliteIds: List<Long>,
|
||||
|
||||
@get:JsonProperty("missingSatelliteIds")
|
||||
@param:JsonProperty("missingSatelliteIds")
|
||||
val missingSatelliteIds: List<Long>,
|
||||
|
||||
@get:JsonProperty("calculationStart")
|
||||
@param:JsonProperty("calculationStart")
|
||||
val calculationStart: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("calculationEnd")
|
||||
@param:JsonProperty("calculationEnd")
|
||||
val calculationEnd: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("status")
|
||||
@param:JsonProperty("status")
|
||||
val status: ComplexPlanCalculationStatus,
|
||||
|
||||
@get:JsonProperty("observationParametersFilePath")
|
||||
@param:JsonProperty("observationParametersFilePath")
|
||||
val observationParametersFilePath: String? = null,
|
||||
|
||||
@get:JsonProperty("intervalsCount")
|
||||
@param:JsonProperty("intervalsCount")
|
||||
val intervalsCount: Int,
|
||||
|
||||
@get:JsonProperty("observationParametersCount")
|
||||
@param:JsonProperty("observationParametersCount")
|
||||
val observationParametersCount: Int,
|
||||
|
||||
@get:JsonProperty("routeBuildAttemptsCount")
|
||||
@param:JsonProperty("routeBuildAttemptsCount")
|
||||
val routeBuildAttemptsCount: Int,
|
||||
|
||||
@get:JsonProperty("routesCount")
|
||||
@param:JsonProperty("routesCount")
|
||||
val routesCount: Int,
|
||||
|
||||
@get:JsonProperty("routeBuildFailuresCount")
|
||||
@param:JsonProperty("routeBuildFailuresCount")
|
||||
val routeBuildFailuresCount: Int,
|
||||
|
||||
@get:JsonProperty("routeBuildMs")
|
||||
@param:JsonProperty("routeBuildMs")
|
||||
val routeBuildMs: Long,
|
||||
|
||||
@get:JsonProperty("totalMs")
|
||||
@param:JsonProperty("totalMs")
|
||||
val totalMs: Long
|
||||
)
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package space.nstart.pcp.complan.utils.extensions
|
||||
|
||||
import org.locationtech.jts.geom.CoordinateFilter
|
||||
import org.locationtech.jts.geom.Geometry
|
||||
import kotlin.math.PI
|
||||
|
||||
|
||||
fun Geometry.radiansToDegrees(): Geometry {
|
||||
// Создаём копию, чтобы не мутировать оригинал
|
||||
val copy = this.copy() as Geometry
|
||||
|
||||
// Применяем фильтр, который конвертирует радианы → градусы
|
||||
copy.apply(CoordinateFilter { coord ->
|
||||
coord.x = coord.x * 180.0 / PI // долгота
|
||||
coord.y = coord.y * 180.0 / PI // широта
|
||||
})
|
||||
|
||||
return copy
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package space.nstart.pcp.complan.utils.extensions
|
||||
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
|
||||
|
||||
fun RevolutionSign.toInt() : Int {
|
||||
return when (this) {
|
||||
RevolutionSign.ASC -> 0
|
||||
RevolutionSign.DESC -> 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
spring:
|
||||
application:
|
||||
name: pcp-dynamic-plan-service
|
||||
profiles:
|
||||
default: local
|
||||
config:
|
||||
import: "configserver:"
|
||||
cloud:
|
||||
config:
|
||||
uri: ${CONFIG_SERVER_URI:http://192.168.100.160:38888}
|
||||
fail-fast: ${CONFIG_SERVER_FAIL_FAST:true}
|
||||
profile: ${SPRING_CLOUD_CONFIG_PROFILE:${SPRING_PROFILES_ACTIVE:${spring.profiles.default}}}
|
||||
label: ${SPRING_CLOUD_CONFIG_LABEL:master}
|
||||
# codec:
|
||||
# max-in-memory-size: 16MB
|
||||
datasource:
|
||||
driver-class-name: org.postgresql.Driver
|
||||
url: jdbc:postgresql://192.168.60.68:5432/complan
|
||||
username: postgres
|
||||
password: password
|
||||
jpa:
|
||||
database-platform: org.hibernate.dialect.PostgreSQLDialect
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
show-sql: false
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: false
|
||||
jdbc:
|
||||
batch_size: 40
|
||||
order_inserts: true
|
||||
order_updates: true
|
||||
|
||||
feign:
|
||||
client:
|
||||
config:
|
||||
default:
|
||||
connectTimeout: 5000
|
||||
readTimeout: 30000
|
||||
loggerLevel: basic
|
||||
|
||||
flyway:
|
||||
enabled: true
|
||||
locations: classpath:db/migration/pcp-complex-plan
|
||||
baseline-on-migrate: true
|
||||
ignore-migration-patterns: "*:missing"
|
||||
baseline-version: 0
|
||||
validate-on-migrate: true
|
||||
repair-on-migrate: true
|
||||
clean-disabled: true
|
||||
out-of-order: true
|
||||
schemas: public
|
||||
|
||||
logging:
|
||||
level:
|
||||
org.flywaydb: DEBUG
|
||||
org.hibernate.SQL: DEBUG
|
||||
org.hibernate.type.descriptor.sql.BasicBinder: TRACE
|
||||
org.springframework.jdbc.core: TRACE
|
||||
|
||||
springdoc:
|
||||
swagger-ui:
|
||||
enabled: true
|
||||
layout: BaseLayout
|
||||
path: /swagger/ui
|
||||
api-docs:
|
||||
enabled: true
|
||||
path: /v3/api-docs
|
||||
|
||||
complex-plan:
|
||||
observation-parameters:
|
||||
parallel-enabled: ${COMPLEX_PLAN_OBSERVATION_PARAMETERS_PARALLEL_ENABLED:true}
|
||||
chunk-size: ${COMPLEX_PLAN_OBSERVATION_PARAMETERS_CHUNK_SIZE:20}
|
||||
parallelism: ${COMPLEX_PLAN_OBSERVATION_PARAMETERS_PARALLELISM:8}
|
||||
debug-output-dir: ${COMPLEX_PLAN_OBSERVATION_PARAMETERS_DEBUG_OUTPUT_DIR:${java.io.tmpdir}/pcp-dynamic-plan-service/obsParams}
|
||||
geo-layers:
|
||||
enabled: false
|
||||
output-dir: ${java.io.tmpdir}/pcp-dynamic-plan-service/geo-layers
|
||||
@@ -0,0 +1 @@
|
||||
SELECT 'Database initialization complete' as status;
|
||||
@@ -0,0 +1,4 @@
|
||||
CREATE TABLE IF NOT EXISTS aplsettings (
|
||||
id SERIAL PRIMARY KEY, --серийный ключ
|
||||
settings TEXT --настройки автомата в виде JSON
|
||||
);
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
CREATE TABLE IF NOT EXISTS aplsettings (
|
||||
id SERIAL PRIMARY KEY, --серийный ключ
|
||||
settings TEXT --настройки автомата в виде JSON
|
||||
);
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
create table if not exists dynamic_plan_run
|
||||
(
|
||||
id uuid primary key,
|
||||
status varchar(32) not null,
|
||||
request_id uuid not null,
|
||||
satellite_ids text not null,
|
||||
calculation_start timestamp not null,
|
||||
calculation_end timestamp not null,
|
||||
created_at timestamp not null,
|
||||
started_at timestamp,
|
||||
finished_at timestamp,
|
||||
duration_ms bigint,
|
||||
result_status varchar(64),
|
||||
routes_count integer,
|
||||
result_json text,
|
||||
error_message text
|
||||
);
|
||||
|
||||
create index if not exists idx_dynamic_plan_run_status on dynamic_plan_run(status);
|
||||
create index if not exists idx_dynamic_plan_run_request_id on dynamic_plan_run(request_id);
|
||||
|
||||
create table if not exists dynamic_plan_route
|
||||
(
|
||||
id bigserial primary key,
|
||||
run_id uuid not null references dynamic_plan_run(id) on delete cascade,
|
||||
request_id uuid not null,
|
||||
satellite_id bigint not null,
|
||||
start_time timestamp not null,
|
||||
end_time timestamp not null,
|
||||
duration double precision not null,
|
||||
revolution bigint not null,
|
||||
roll double precision not null,
|
||||
contour_wkt text not null,
|
||||
route_order integer not null
|
||||
);
|
||||
|
||||
create index if not exists idx_dynamic_plan_route_run_id on dynamic_plan_route(run_id);
|
||||
create index if not exists idx_dynamic_plan_route_request_id on dynamic_plan_route(request_id);
|
||||
create index if not exists idx_dynamic_plan_route_satellite_id on dynamic_plan_route(satellite_id);
|
||||
create index if not exists idx_dynamic_plan_route_start_time on dynamic_plan_route(start_time);
|
||||
create index if not exists idx_dynamic_plan_route_run_start on dynamic_plan_route(run_id, start_time);
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
alter table dynamic_plan_run
|
||||
add column if not exists calculation_mode varchar(32) not null default 'FULL';
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
alter table dynamic_plan_run
|
||||
add column if not exists revolution_mode varchar(32) not null default 'BOTH';
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
alter table dynamic_plan_run
|
||||
add column if not exists filter_covered_routes boolean not null default false;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
alter table dynamic_plan_run
|
||||
add column if not exists min_useful_route_area_km2 double precision not null default 0.0;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
create table if not exists dynamic_plan_interval
|
||||
(
|
||||
id bigserial primary key,
|
||||
run_id uuid not null references dynamic_plan_run(id) on delete cascade,
|
||||
request_id uuid not null,
|
||||
interval_id uuid not null,
|
||||
region_id uuid not null,
|
||||
rev_sign varchar(16) not null,
|
||||
contour_wkt text not null,
|
||||
observation_parameters_count integer not null,
|
||||
interval_order integer not null
|
||||
);
|
||||
|
||||
create index if not exists idx_dynamic_plan_interval_run_id on dynamic_plan_interval(run_id);
|
||||
create index if not exists idx_dynamic_plan_interval_request_id on dynamic_plan_interval(request_id);
|
||||
create index if not exists idx_dynamic_plan_interval_region_id on dynamic_plan_interval(region_id);
|
||||
create index if not exists idx_dynamic_plan_interval_run_order on dynamic_plan_interval(run_id, interval_order);
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
package space.nstart.pcp.complan
|
||||
|
||||
import org.springframework.web.bind.annotation.DeleteMapping
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PatchMapping
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.PutMapping
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class ComPlanControllerTest {
|
||||
|
||||
@Test
|
||||
fun `exposes calculation run endpoints`() {
|
||||
val mappedMethods = ComPlanController::class.java.declaredMethods
|
||||
.filter { method ->
|
||||
listOf(
|
||||
GetMapping::class.java,
|
||||
PostMapping::class.java,
|
||||
PutMapping::class.java,
|
||||
PatchMapping::class.java,
|
||||
DeleteMapping::class.java,
|
||||
RequestMapping::class.java
|
||||
).any { annotation -> method.isAnnotationPresent(annotation) }
|
||||
}
|
||||
.map { it.name }
|
||||
.sorted()
|
||||
|
||||
assertEquals(
|
||||
listOf(
|
||||
"calcPlan",
|
||||
"debugGreedyMath",
|
||||
"debugObservationParameters",
|
||||
"getCalculation",
|
||||
"getCalculationIntervals",
|
||||
"getCalculationResult",
|
||||
"getCalculationRoutes",
|
||||
"getCalculations",
|
||||
"testCalcMars"
|
||||
),
|
||||
mappedMethods
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calcPlan is exposed as POST endpoint`() {
|
||||
val calcPlanMethod = ComPlanController::class.java.getDeclaredMethod(
|
||||
"calcPlan",
|
||||
space.nstart.pcp.complan.types.ComplexPlanCalculationRequestDTO::class.java
|
||||
)
|
||||
|
||||
assertNotNull(calcPlanMethod.getAnnotation(PostMapping::class.java))
|
||||
assertNull(calcPlanMethod.getAnnotation(GetMapping::class.java))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `observation parameters debug endpoint is exposed as POST endpoint`() {
|
||||
val debugMethod = ComPlanController::class.java.getDeclaredMethod(
|
||||
"debugObservationParameters",
|
||||
space.nstart.pcp.complan.types.ObservationParametersDebugRequestDTO::class.java
|
||||
)
|
||||
|
||||
assertNotNull(debugMethod.getAnnotation(PostMapping::class.java))
|
||||
assertNull(debugMethod.getAnnotation(GetMapping::class.java))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `greedy math debug endpoint is exposed as POST endpoint`() {
|
||||
val debugMethod = ComPlanController::class.java.getDeclaredMethod(
|
||||
"debugGreedyMath",
|
||||
space.nstart.pcp.complan.types.GreedyMathDebugRequestDTO::class.java
|
||||
)
|
||||
|
||||
assertNotNull(debugMethod.getAnnotation(PostMapping::class.java))
|
||||
assertNull(debugMethod.getAnnotation(GetMapping::class.java))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test calc mars debug endpoint is exposed as POST endpoint`() {
|
||||
val debugMethod = ComPlanController::class.java.getDeclaredMethod(
|
||||
"testCalcMars",
|
||||
space.nstart.pcp.complan.types.TestCalcMarsDebugRequestDTO::class.java
|
||||
)
|
||||
|
||||
assertNotNull(debugMethod.getAnnotation(PostMapping::class.java))
|
||||
assertNull(debugMethod.getAnnotation(GetMapping::class.java))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculation status and result are exposed as GET endpoints`() {
|
||||
val statusMethod = ComPlanController::class.java.getDeclaredMethod(
|
||||
"getCalculation",
|
||||
java.util.UUID::class.java
|
||||
)
|
||||
val resultMethod = ComPlanController::class.java.getDeclaredMethod(
|
||||
"getCalculationResult",
|
||||
java.util.UUID::class.java
|
||||
)
|
||||
val routesMethod = ComPlanController::class.java.getDeclaredMethod(
|
||||
"getCalculationRoutes",
|
||||
java.util.UUID::class.java,
|
||||
Int::class.java,
|
||||
Int::class.java
|
||||
)
|
||||
val intervalsMethod = ComPlanController::class.java.getDeclaredMethod(
|
||||
"getCalculationIntervals",
|
||||
java.util.UUID::class.java,
|
||||
Int::class.java,
|
||||
Int::class.java
|
||||
)
|
||||
val runsMethod = ComPlanController::class.java.getDeclaredMethod(
|
||||
"getCalculations",
|
||||
space.nstart.pcp.complan.types.DynamicPlanRunStatus::class.java,
|
||||
Int::class.java,
|
||||
Int::class.java
|
||||
)
|
||||
|
||||
assertNotNull(statusMethod.getAnnotation(GetMapping::class.java))
|
||||
assertNotNull(resultMethod.getAnnotation(GetMapping::class.java))
|
||||
assertNotNull(routesMethod.getAnnotation(GetMapping::class.java))
|
||||
assertNotNull(intervalsMethod.getAnnotation(GetMapping::class.java))
|
||||
assertNotNull(runsMethod.getAnnotation(GetMapping::class.java))
|
||||
assertNull(statusMethod.getAnnotation(PostMapping::class.java))
|
||||
assertNull(resultMethod.getAnnotation(PostMapping::class.java))
|
||||
assertNull(routesMethod.getAnnotation(PostMapping::class.java))
|
||||
assertNull(intervalsMethod.getAnnotation(PostMapping::class.java))
|
||||
assertNull(runsMethod.getAnnotation(PostMapping::class.java))
|
||||
}
|
||||
}
|
||||
+457
@@ -0,0 +1,457 @@
|
||||
package space.nstart.pcp.complan.apl
|
||||
|
||||
import org.locationtech.jts.io.WKTReader
|
||||
import space.nstart.pcp.complan.settings.SettingsDTO
|
||||
import space.nstart.pcp.complan.spacecraft.SCConstraints
|
||||
import space.nstart.pcp.complan.spacecraft.SpaceCraft
|
||||
import space.nstart.pcp.complan.types.DynamicPlanRevolutionMode
|
||||
import space.nstart.pcp.complan.types.IntervalItemDTO
|
||||
import space.nstart.pcp.complan.types.SurveyContour
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamDTO
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class BasicPlanCalculatorTest {
|
||||
|
||||
@Test
|
||||
fun `selects only satellites present in catalog`() {
|
||||
val result = selectSatellitesForCalculation(
|
||||
requestedSatelliteIds = listOf(56756L, 62138L, 99999L),
|
||||
catalogSatelliteIds = listOf(56756L, 62138L)
|
||||
)
|
||||
|
||||
assertEquals(listOf(56756L, 62138L), result.existingIds)
|
||||
assertEquals(listOf(99999L), result.missingIds)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `keeps requested order and removes duplicates`() {
|
||||
val result = selectSatellitesForCalculation(
|
||||
requestedSatelliteIds = listOf(62138L, 56756L, 62138L),
|
||||
catalogSatelliteIds = listOf(56756L, 62138L)
|
||||
)
|
||||
|
||||
assertEquals(listOf(62138L, 56756L), result.existingIds)
|
||||
assertEquals(emptyList(), result.missingIds)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns all requested satellites as missing when catalog has none of them`() {
|
||||
val result = selectSatellitesForCalculation(
|
||||
requestedSatelliteIds = listOf(1L, 2L, 3L),
|
||||
catalogSatelliteIds = listOf(56756L, 62138L)
|
||||
)
|
||||
|
||||
assertEquals(emptyList(), result.existingIds)
|
||||
assertEquals(listOf(1L, 2L, 3L), result.missingIds)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `route build uses all available MPL items sorted by gamma`() {
|
||||
val first = mplItem(noradId = 1L, gammaMin = -12.0)
|
||||
val second = mplItem(noradId = 2L, gammaMin = 4.0)
|
||||
val third = mplItem(noradId = 3L, gammaMin = -8.0)
|
||||
|
||||
val result = selectMplItemsForRouteBuild(
|
||||
mplItems = listOf(first, second, third),
|
||||
settings = SettingsDTO()
|
||||
)
|
||||
|
||||
assertEquals(listOf(second, third, first), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `route build keeps existing gamma and revolution filters`() {
|
||||
val ascInGamma = mplItem(noradId = 1L, gammaMin = 3.0, gammaMax = 5.0, revSignBegin = RevolutionSign.ASC)
|
||||
val descInGamma = mplItem(noradId = 2L, gammaMin = 2.0, gammaMax = 4.0, revSignBegin = RevolutionSign.DESC)
|
||||
val ascOutOfGamma = mplItem(noradId = 3L, gammaMin = 20.0, gammaMax = 24.0, revSignBegin = RevolutionSign.ASC)
|
||||
|
||||
val result = selectMplItemsForRouteBuild(
|
||||
mplItems = listOf(ascOutOfGamma, descInGamma, ascInGamma),
|
||||
settings = SettingsDTO(
|
||||
filterByGamma = true,
|
||||
gammaValue = 10.0,
|
||||
filterByRevolution = true,
|
||||
revolutionType = 0
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(listOf(ascInGamma), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `greedy route selection builds one route per interval`() {
|
||||
val interval = interval(
|
||||
mplItem(noradId = 1L, gammaMin = 2.0),
|
||||
mplItem(noradId = 2L, gammaMin = 1.0)
|
||||
)
|
||||
|
||||
val result = selectGreedyRouteCandidates(
|
||||
intervals = listOf(interval),
|
||||
settings = SettingsDTO()
|
||||
)
|
||||
|
||||
assertEquals(1, result.selectedCandidates.size)
|
||||
assertEquals(interval.id, result.selectedCandidates.single().intervalId)
|
||||
assertEquals(2L, result.selectedCandidates.single().satelliteId)
|
||||
assertEquals(emptyList(), result.failures)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `greedy route selection prioritizes lower roll angle over earlier time`() {
|
||||
val earlierHighRollInterval = interval(
|
||||
mplItem(
|
||||
noradId = 1L,
|
||||
gammaMin = 12.0,
|
||||
timeBegin = LocalDateTime.of(2026, 5, 1, 0, 0),
|
||||
timeEnd = LocalDateTime.of(2026, 5, 1, 0, 1)
|
||||
)
|
||||
)
|
||||
val laterLowRollInterval = interval(
|
||||
mplItem(
|
||||
noradId = 2L,
|
||||
gammaMin = 1.0,
|
||||
timeBegin = LocalDateTime.of(2026, 5, 1, 0, 10),
|
||||
timeEnd = LocalDateTime.of(2026, 5, 1, 0, 11)
|
||||
)
|
||||
)
|
||||
|
||||
val result = selectGreedyRouteCandidates(
|
||||
intervals = listOf(earlierHighRollInterval, laterLowRollInterval),
|
||||
settings = SettingsDTO()
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
listOf(laterLowRollInterval.id, earlierHighRollInterval.id),
|
||||
result.selectedCandidates.map { it.intervalId }
|
||||
)
|
||||
assertEquals(emptyList(), result.failures)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `greedy route selection filters intervals by revolution mode`() {
|
||||
val ascInterval = interval(
|
||||
mplItem(noradId = 1L, gammaMin = 1.0, revSignBegin = RevolutionSign.ASC),
|
||||
revSign = RevolutionSign.ASC
|
||||
)
|
||||
val descInterval = interval(
|
||||
mplItem(noradId = 2L, gammaMin = 2.0, revSignBegin = RevolutionSign.DESC),
|
||||
revSign = RevolutionSign.DESC
|
||||
)
|
||||
|
||||
val result = selectGreedyRouteCandidates(
|
||||
intervals = listOf(ascInterval, descInterval),
|
||||
settings = SettingsDTO(),
|
||||
revolutionMode = DynamicPlanRevolutionMode.DESC
|
||||
)
|
||||
|
||||
assertEquals(listOf(descInterval.id), result.selectedCandidates.map { it.intervalId })
|
||||
assertEquals(emptyList(), result.failures)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `interval build uses only requested revolution signs`() {
|
||||
assertEquals(listOf(RevolutionSign.ASC), revolutionSignsForIntervals(DynamicPlanRevolutionMode.ASC))
|
||||
assertEquals(listOf(RevolutionSign.DESC), revolutionSignsForIntervals(DynamicPlanRevolutionMode.DESC))
|
||||
assertEquals(
|
||||
listOf(RevolutionSign.DESC, RevolutionSign.ASC),
|
||||
revolutionSignsForIntervals(DynamicPlanRevolutionMode.BOTH)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `greedy route selection avoids simultaneous routes for same satellite`() {
|
||||
val firstInterval = interval(
|
||||
mplItem(
|
||||
noradId = 1L,
|
||||
gammaMin = 1.0,
|
||||
timeBegin = LocalDateTime.of(2026, 5, 1, 0, 0),
|
||||
timeEnd = LocalDateTime.of(2026, 5, 1, 0, 1)
|
||||
)
|
||||
)
|
||||
val secondInterval = interval(
|
||||
mplItem(
|
||||
noradId = 1L,
|
||||
gammaMin = 1.0,
|
||||
timeBegin = LocalDateTime.of(2026, 5, 1, 0, 1, 30),
|
||||
timeEnd = LocalDateTime.of(2026, 5, 1, 0, 2)
|
||||
),
|
||||
mplItem(
|
||||
noradId = 2L,
|
||||
gammaMin = 2.0,
|
||||
timeBegin = LocalDateTime.of(2026, 5, 1, 0, 1, 30),
|
||||
timeEnd = LocalDateTime.of(2026, 5, 1, 0, 2)
|
||||
)
|
||||
)
|
||||
|
||||
val result = selectGreedyRouteCandidates(
|
||||
intervals = listOf(secondInterval, firstInterval),
|
||||
settings = SettingsDTO()
|
||||
)
|
||||
|
||||
assertEquals(listOf(1L, 2L), result.selectedCandidates.map { it.satelliteId })
|
||||
assertEquals(emptyList(), result.failures)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `greedy route selection reports revolution limit failures`() {
|
||||
val firstInterval = interval(
|
||||
mplItem(
|
||||
noradId = 1L,
|
||||
gammaMin = 1.0,
|
||||
revolutionBegin = 10,
|
||||
timeBegin = LocalDateTime.of(2026, 5, 1, 0, 0),
|
||||
timeEnd = LocalDateTime.of(2026, 5, 1, 0, 4, 10)
|
||||
)
|
||||
)
|
||||
val secondInterval = interval(
|
||||
mplItem(
|
||||
noradId = 1L,
|
||||
gammaMin = 1.0,
|
||||
revolutionBegin = 10,
|
||||
timeBegin = LocalDateTime.of(2026, 5, 1, 0, 6),
|
||||
timeEnd = LocalDateTime.of(2026, 5, 1, 0, 8)
|
||||
)
|
||||
)
|
||||
|
||||
val result = selectGreedyRouteCandidates(
|
||||
intervals = listOf(firstInterval, secondInterval),
|
||||
settings = SettingsDTO()
|
||||
)
|
||||
|
||||
assertEquals(1, result.selectedCandidates.size)
|
||||
assertEquals(GreedyIntervalFailureReason.REVOLUTION_LIMIT_EXCEEDED, result.failures.single().reason)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `greedy route selection reports daily limit failures`() {
|
||||
val firstInterval = interval(
|
||||
mplItem(
|
||||
noradId = 1L,
|
||||
gammaMin = 1.0,
|
||||
revolutionBegin = 10,
|
||||
timeBegin = LocalDateTime.of(2026, 5, 1, 0, 0),
|
||||
timeEnd = LocalDateTime.of(2026, 5, 1, 0, 4, 50)
|
||||
)
|
||||
)
|
||||
val secondInterval = interval(
|
||||
mplItem(
|
||||
noradId = 1L,
|
||||
gammaMin = 1.0,
|
||||
revolutionBegin = 11,
|
||||
timeBegin = LocalDateTime.of(2026, 5, 1, 0, 6),
|
||||
timeEnd = LocalDateTime.of(2026, 5, 1, 0, 8)
|
||||
)
|
||||
)
|
||||
|
||||
val result = selectGreedyRouteCandidates(
|
||||
intervals = listOf(firstInterval, secondInterval),
|
||||
settings = SettingsDTO(),
|
||||
spacecraft = listOf(
|
||||
SpaceCraft(
|
||||
id = 1L,
|
||||
constraints = SCConstraints(maxDailyDurationSeconds = 300)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(1, result.selectedCandidates.size)
|
||||
assertEquals(GreedyIntervalFailureReason.DAILY_LIMIT_EXCEEDED, result.failures.single().reason)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `remaining request geometry filter keeps route when it still covers uncovered request territory`() {
|
||||
val requestGeometry = geometry("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))")
|
||||
val earlierRoute = route(
|
||||
noradId = 1L,
|
||||
time = LocalDateTime.of(2026, 5, 1, 0, 0),
|
||||
geometry = "POLYGON ((-1 0, -1 1, 0.5 1, 0.5 0, -1 0))"
|
||||
)
|
||||
val laterRoute = route(
|
||||
noradId = 2L,
|
||||
time = LocalDateTime.of(2026, 5, 1, 0, 10),
|
||||
geometry = "POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))"
|
||||
)
|
||||
|
||||
val result = filterRoutesByRemainingRequestGeometry(listOf(earlierRoute, laterRoute), requestGeometry)
|
||||
|
||||
assertEquals(listOf(earlierRoute, laterRoute), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `remaining request geometry filter removes route after earlier route covers request territory`() {
|
||||
val requestGeometry = geometry("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))")
|
||||
val earlierRoute = route(
|
||||
noradId = 1L,
|
||||
time = LocalDateTime.of(2026, 5, 1, 0, 0),
|
||||
geometry = "POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))"
|
||||
)
|
||||
val laterRoute = route(
|
||||
noradId = 2L,
|
||||
time = LocalDateTime.of(2026, 5, 1, 0, 10),
|
||||
geometry = "POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))"
|
||||
)
|
||||
|
||||
val result = filterRoutesByRemainingRequestGeometry(listOf(earlierRoute, laterRoute), requestGeometry)
|
||||
|
||||
assertEquals(listOf(earlierRoute), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `remaining request geometry filter keeps route with uncovered territory`() {
|
||||
val requestGeometry = geometry("POLYGON ((0 0, 0 2, 2 2, 2 0, 0 0))")
|
||||
val earlierRoute = route(
|
||||
noradId = 1L,
|
||||
time = LocalDateTime.of(2026, 5, 1, 0, 0),
|
||||
geometry = "POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))"
|
||||
)
|
||||
val laterRoute = route(
|
||||
noradId = 2L,
|
||||
time = LocalDateTime.of(2026, 5, 1, 0, 10),
|
||||
geometry = "POLYGON ((1 0, 1 1, 2 1, 2 0, 1 0))"
|
||||
)
|
||||
|
||||
val result = filterRoutesByRemainingRequestGeometry(listOf(earlierRoute, laterRoute), requestGeometry)
|
||||
|
||||
assertEquals(listOf(earlierRoute, laterRoute), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `remaining request geometry filter keeps only routes intersecting uncovered territory`() {
|
||||
val requestGeometry = geometry("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))")
|
||||
val earlyLeftRoute = route(
|
||||
noradId = 1L,
|
||||
time = LocalDateTime.of(2026, 5, 1, 0, 0),
|
||||
geometry = "POLYGON ((0 0, 0 1, 0.5 1, 0.5 0, 0 0))"
|
||||
)
|
||||
val duplicateLeftRoute = route(
|
||||
noradId = 2L,
|
||||
time = LocalDateTime.of(2026, 5, 1, 0, 10),
|
||||
geometry = "POLYGON ((0 0, 0 1, 0.5 1, 0.5 0, 0 0))"
|
||||
)
|
||||
val laterRightRoute = route(
|
||||
noradId = 3L,
|
||||
time = LocalDateTime.of(2026, 5, 1, 0, 20),
|
||||
geometry = "POLYGON ((0.5 0, 0.5 1, 1 1, 1 0, 0.5 0))"
|
||||
)
|
||||
|
||||
val result = filterRoutesByRemainingRequestGeometry(
|
||||
routes = listOf(laterRightRoute, duplicateLeftRoute, earlyLeftRoute),
|
||||
requestGeometry = requestGeometry
|
||||
)
|
||||
|
||||
assertEquals(listOf(earlyLeftRoute, laterRightRoute), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `route coverage summary returns last route and covered area percent`() {
|
||||
val requestGeometry = geometry("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))")
|
||||
val earlierRoute = route(
|
||||
noradId = 1L,
|
||||
time = LocalDateTime.of(2026, 5, 1, 0, 0),
|
||||
geometry = "POLYGON ((0 0, 0 1, 0.5 1, 0.5 0, 0 0))"
|
||||
)
|
||||
val laterRoute = route(
|
||||
noradId = 2L,
|
||||
time = LocalDateTime.of(2026, 5, 10, 12, 0),
|
||||
geometry = "POLYGON ((0.5 0, 0.5 1, 1 1, 1 0, 0.5 0))"
|
||||
)
|
||||
|
||||
val result = summarizeRouteCoverage(listOf(earlierRoute, laterRoute), requestGeometry)
|
||||
|
||||
assertEquals(laterRoute.time, result.lastRouteStart)
|
||||
assertEquals(laterRoute.time.plusSeconds(60), result.lastRouteEnd)
|
||||
assertEquals(100.0, result.coveredAreaPercent ?: -1.0, 0.0001)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `greedy math file filter keeps only selected satellites inside requested interval`() {
|
||||
val calculationStart = LocalDateTime.of(2026, 5, 1, 10, 0)
|
||||
val calculationEnd = LocalDateTime.of(2026, 5, 1, 11, 0)
|
||||
val validMpl = mplItem(
|
||||
noradId = 101L,
|
||||
gammaMin = 1.0,
|
||||
timeBegin = LocalDateTime.of(2026, 5, 1, 10, 5),
|
||||
timeEnd = LocalDateTime.of(2026, 5, 1, 10, 10)
|
||||
)
|
||||
val otherSatelliteMpl = mplItem(
|
||||
noradId = 202L,
|
||||
gammaMin = 1.0,
|
||||
timeBegin = LocalDateTime.of(2026, 5, 1, 10, 5),
|
||||
timeEnd = LocalDateTime.of(2026, 5, 1, 10, 10)
|
||||
)
|
||||
val startsBeforeIntervalMpl = mplItem(
|
||||
noradId = 101L,
|
||||
gammaMin = 1.0,
|
||||
timeBegin = LocalDateTime.of(2026, 5, 1, 9, 59),
|
||||
timeEnd = LocalDateTime.of(2026, 5, 1, 10, 10)
|
||||
)
|
||||
val endsAfterIntervalMpl = mplItem(
|
||||
noradId = 101L,
|
||||
gammaMin = 1.0,
|
||||
timeBegin = LocalDateTime.of(2026, 5, 1, 10, 50),
|
||||
timeEnd = LocalDateTime.of(2026, 5, 1, 11, 1)
|
||||
)
|
||||
val interval = interval(validMpl, otherSatelliteMpl, startsBeforeIntervalMpl, endsAfterIntervalMpl)
|
||||
|
||||
val result = filterIntervalsForGreedyMathFile(
|
||||
intervals = listOf(interval),
|
||||
selectedSatelliteIds = listOf(101L),
|
||||
calculationStart = calculationStart,
|
||||
calculationEnd = calculationEnd
|
||||
)
|
||||
|
||||
assertEquals(listOf(validMpl), result.single().mpl)
|
||||
}
|
||||
|
||||
private fun mplItem(
|
||||
noradId: Long,
|
||||
gammaMin: Double,
|
||||
gammaMax: Double = gammaMin,
|
||||
revSignBegin: RevolutionSign = RevolutionSign.ASC,
|
||||
revolutionBegin: Long = 1,
|
||||
timeBegin: LocalDateTime = LocalDateTime.of(2026, 5, 1, 0, 0),
|
||||
timeEnd: LocalDateTime = LocalDateTime.of(2026, 5, 1, 0, 1)
|
||||
) = SquareViewParamDTO(
|
||||
noradId = noradId,
|
||||
objectId = noradId.toString(),
|
||||
revolutionBegin = revolutionBegin,
|
||||
timeBegin = timeBegin,
|
||||
revSignBegin = revSignBegin,
|
||||
revolutionEnd = revolutionBegin,
|
||||
timeEnd = timeEnd,
|
||||
revSignEnd = revSignBegin,
|
||||
gammaMin = gammaMin,
|
||||
gammaMax = gammaMax
|
||||
)
|
||||
|
||||
private fun interval(
|
||||
vararg mplItems: SquareViewParamDTO,
|
||||
revSign: RevolutionSign = RevolutionSign.ASC
|
||||
) =
|
||||
IntervalItemDTO(
|
||||
id = UUID.randomUUID(),
|
||||
regionId = UUID.randomUUID(),
|
||||
revSign = revSign,
|
||||
geometry = "POLYGON EMPTY",
|
||||
mpl = mplItems.toMutableList()
|
||||
)
|
||||
|
||||
private fun route(
|
||||
noradId: Long,
|
||||
time: LocalDateTime,
|
||||
geometry: String
|
||||
) = SurveyContour(
|
||||
mpl = SquareViewParamDTO(noradId = noradId),
|
||||
time = time,
|
||||
duration = 60.0,
|
||||
rev = 1L,
|
||||
gamma = 0.0,
|
||||
geometry = geometry
|
||||
)
|
||||
|
||||
private fun geometry(value: String) = WKTReader().read(value)
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package space.nstart.pcp.complan.apl
|
||||
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class CaptureAngleCalculatorTest {
|
||||
|
||||
private val calculator = CaptureAngleCalculator()
|
||||
|
||||
@Test
|
||||
fun `calculates capture angle from route width and orbital height`() {
|
||||
val angle = calculator.calculateCaptureAngle(
|
||||
routeWidthKm = 30.0,
|
||||
points = listOf(orbPoint(radiusMeters = 6_378_136.0 + 516_000.0))
|
||||
)
|
||||
|
||||
assertEquals(1.665, angle!!, absoluteTolerance = 0.001)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns null for empty points`() {
|
||||
assertNull(calculator.calculateCaptureAngle(routeWidthKm = 30.0, points = emptyList()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns null for non positive route width`() {
|
||||
assertNull(calculator.calculateCaptureAngle(routeWidthKm = 0.0, points = listOf(orbPoint())))
|
||||
}
|
||||
|
||||
private fun orbPoint(radiusMeters: Double = 6_894_136.0) = OrbPointDTO(
|
||||
time = LocalDateTime.of(2026, 4, 30, 0, 0),
|
||||
revolution = 1L,
|
||||
x = radiusMeters,
|
||||
y = 0.0,
|
||||
z = 0.0,
|
||||
vx = 0.0,
|
||||
vy = 0.0,
|
||||
vz = 0.0
|
||||
)
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package space.nstart.pcp.complan.apl
|
||||
|
||||
import org.junit.jupiter.api.io.TempDir
|
||||
import org.locationtech.jts.geom.Coordinate
|
||||
import org.locationtech.jts.geom.GeometryFactory
|
||||
import space.nstart.pcp.complan.types.GeoJsonItem
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class GeoJsonLayerWriterTest {
|
||||
|
||||
@Test
|
||||
fun `does not create layer file when layer writing is disabled`(@TempDir tempDir: Path) {
|
||||
val layerDir = tempDir.resolve("layers")
|
||||
val writer = GeoJsonLayerWriter(enabled = false, outputDir = layerDir.toString())
|
||||
|
||||
writer.write(listOf(testGeoJsonItem()), "Requests")
|
||||
|
||||
assertFalse(Files.exists(layerDir))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `creates layer file in configured directory when layer writing is enabled`(@TempDir tempDir: Path) {
|
||||
val layerDir = tempDir.resolve("layers")
|
||||
val writer = GeoJsonLayerWriter(enabled = true, outputDir = layerDir.toString())
|
||||
|
||||
writer.write(listOf(testGeoJsonItem()), "Requests")
|
||||
|
||||
assertTrue(Files.exists(layerDir.resolve("Requests.json")))
|
||||
}
|
||||
|
||||
private fun testGeoJsonItem(): GeoJsonItem =
|
||||
GeoJsonItem(
|
||||
geometry = GeometryFactory().createPoint(Coordinate(37.0, 55.0)),
|
||||
properties = emptyMap<String, Any>()
|
||||
)
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package space.nstart.pcp.complan.apl
|
||||
|
||||
import org.locationtech.jts.io.WKTReader
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class IntervalCoverageCheckerTest {
|
||||
|
||||
private val reader = WKTReader()
|
||||
private val checker = IntervalCoverageChecker()
|
||||
|
||||
@Test
|
||||
fun `returns empty geometry when intervals fully cover contour`() {
|
||||
val contour = reader.read("POLYGON ((0 0, 2 0, 2 1, 0 1, 0 0))")
|
||||
val intervals = listOf(
|
||||
reader.read("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"),
|
||||
reader.read("POLYGON ((1 0, 2 0, 2 1, 1 1, 1 0))")
|
||||
)
|
||||
|
||||
val uncovered = checker.findUncoveredGeometry(contour, intervals)
|
||||
|
||||
assertTrue(uncovered.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns uncovered part when intervals leave gap in contour`() {
|
||||
val contour = reader.read("POLYGON ((0 0, 2 0, 2 1, 0 1, 0 0))")
|
||||
val intervals = listOf(
|
||||
reader.read("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))")
|
||||
)
|
||||
|
||||
val uncovered = checker.findUncoveredGeometry(contour, intervals)
|
||||
|
||||
assertEquals(1.0, uncovered.area)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns full contour when intervals are empty`() {
|
||||
val contour = reader.read("POLYGON ((0 0, 2 0, 2 1, 0 1, 0 0))")
|
||||
|
||||
val uncovered = checker.findUncoveredGeometry(contour, emptyList())
|
||||
|
||||
assertEquals(contour.area, uncovered.area)
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package space.nstart.pcp.complan.apl
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import org.junit.jupiter.api.io.TempDir
|
||||
import space.nstart.pcp.complan.JacksonConfig
|
||||
import space.nstart.pcp.complan.types.IntervalItemDTO
|
||||
import space.nstart.pcp.complan.types.ObservationParametersDebugFileDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamDTO
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class ObservationParametersDebugFileWriterTest {
|
||||
|
||||
private val objectMapper: ObjectMapper = JacksonConfig().objectMapper()
|
||||
|
||||
@Test
|
||||
fun `writes observation parameters artifact with intervals and mpl`(@TempDir tempDir: Path) {
|
||||
val requestId = UUID.fromString("11111111-1111-1111-1111-111111111111")
|
||||
val intervalId = UUID.fromString("22222222-2222-2222-2222-222222222222")
|
||||
val writer = ObservationParametersDebugFileWriter(
|
||||
objectMapper = objectMapper,
|
||||
outputDir = tempDir.resolve("observation-parameters").toString()
|
||||
)
|
||||
val artifact = ObservationParametersDebugFileDTO(
|
||||
createdAt = Instant.parse("2026-05-05T10:15:30Z"),
|
||||
requestId = requestId,
|
||||
satelliteIds = listOf(1001L),
|
||||
missingSatelliteIds = emptyList(),
|
||||
calculationStart = LocalDateTime.parse("2026-05-05T00:00:00"),
|
||||
calculationEnd = LocalDateTime.parse("2026-05-06T00:00:00"),
|
||||
parallelBallisticsCalculation = true,
|
||||
chunkSize = 10,
|
||||
parallelism = 2,
|
||||
intervalsCount = 1,
|
||||
observationParametersCount = 1,
|
||||
intervals = listOf(
|
||||
IntervalItemDTO(
|
||||
id = intervalId,
|
||||
regionId = UUID.fromString("33333333-3333-3333-3333-333333333333"),
|
||||
revSign = RevolutionSign.ASC,
|
||||
geometry = "POLYGON ((0 0, 1 0, 1 1, 0 0))",
|
||||
mpl = mutableListOf(
|
||||
SquareViewParamDTO(
|
||||
noradId = 1001L,
|
||||
objectId = intervalId.toString(),
|
||||
revolutionBegin = 42L,
|
||||
timeBegin = LocalDateTime.parse("2026-05-05T01:00:00"),
|
||||
revolutionEnd = 42L,
|
||||
timeEnd = LocalDateTime.parse("2026-05-05T01:01:00")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val file = writer.write(artifact)
|
||||
|
||||
assertTrue(Files.exists(file))
|
||||
assertEquals("observation-parameters-$requestId.json", file.fileName.toString())
|
||||
assertEquals(file, writer.filePathFor(requestId))
|
||||
val saved = writer.readByRequestId(requestId)
|
||||
assertEquals(1, saved.schemaVersion)
|
||||
assertEquals(intervalId, saved.intervals.single().id)
|
||||
assertEquals(1001L, saved.intervals.single().mpl.single().noradId)
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
package space.nstart.pcp.complan.apl
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.locationtech.jts.geom.Polygon
|
||||
import org.locationtech.jts.io.WKTReader
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class SurveyContourBuilderTest {
|
||||
|
||||
private val builder = SurveyContourBuilder()
|
||||
|
||||
@Test
|
||||
fun `build returns closed polygon contour for orbital points`() {
|
||||
val contour = builder.build(
|
||||
points = sampleOrbitalPoints(),
|
||||
roll = 5.0,
|
||||
capture = 1.5
|
||||
)
|
||||
|
||||
assertNotNull(contour)
|
||||
val geometry = WKTReader().read(contour)
|
||||
assertTrue(geometry is Polygon)
|
||||
assertTrue(geometry.isValid)
|
||||
assertEquals(geometry.coordinates.first(), geometry.coordinates.last())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `build returns null when orbital points are not enough`() {
|
||||
val contour = builder.build(
|
||||
points = sampleOrbitalPoints().take(1),
|
||||
roll = 5.0,
|
||||
capture = 1.5
|
||||
)
|
||||
|
||||
assertNull(contour)
|
||||
}
|
||||
|
||||
private fun sampleOrbitalPoints() = listOf(
|
||||
orbPoint(
|
||||
time = LocalDateTime.of(2026, 4, 14, 10, 0),
|
||||
revolution = 42L,
|
||||
x = -6603039.949,
|
||||
y = -1870023.148,
|
||||
z = 0.0,
|
||||
vx = -401.529,
|
||||
vy = 1413.431,
|
||||
vz = 7547.696
|
||||
),
|
||||
orbPoint(
|
||||
time = LocalDateTime.of(2026, 4, 14, 10, 1),
|
||||
revolution = 42L,
|
||||
x = -6595000.0,
|
||||
y = -1860000.0,
|
||||
z = 452000.0,
|
||||
vx = -420.0,
|
||||
vy = 1390.0,
|
||||
vz = 7540.0
|
||||
)
|
||||
)
|
||||
|
||||
private fun orbPoint(
|
||||
time: LocalDateTime,
|
||||
revolution: Long,
|
||||
x: Double,
|
||||
y: Double,
|
||||
z: Double,
|
||||
vx: Double,
|
||||
vy: Double,
|
||||
vz: Double
|
||||
) = OrbPointDTO(
|
||||
time = time,
|
||||
revolution = revolution,
|
||||
vx = vx,
|
||||
vy = vy,
|
||||
vz = vz,
|
||||
x = x,
|
||||
y = y,
|
||||
z = z
|
||||
)
|
||||
}
|
||||
+439
@@ -0,0 +1,439 @@
|
||||
package space.nstart.pcp.complan.service
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.ArgumentMatchers
|
||||
import org.mockito.Mockito.doReturn
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.verify
|
||||
import org.springframework.transaction.PlatformTransactionManager
|
||||
import org.springframework.transaction.TransactionDefinition
|
||||
import org.springframework.transaction.TransactionStatus
|
||||
import org.springframework.transaction.support.SimpleTransactionStatus
|
||||
import org.springframework.transaction.support.TransactionTemplate
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
import space.nstart.pcp.complan.apl.BasicPlanCalculator
|
||||
import space.nstart.pcp.complan.db.entity.DynamicPlanIntervalEntity
|
||||
import space.nstart.pcp.complan.db.entity.DynamicPlanRouteEntity
|
||||
import space.nstart.pcp.complan.db.entity.DynamicPlanRunEntity
|
||||
import space.nstart.pcp.complan.db.repository.DynamicPlanIntervalRepository
|
||||
import space.nstart.pcp.complan.db.repository.DynamicPlanRouteRepository
|
||||
import space.nstart.pcp.complan.db.repository.DynamicPlanRunRepository
|
||||
import space.nstart.pcp.complan.types.ComplexPlanCalculationOutput
|
||||
import space.nstart.pcp.complan.types.ComplexPlanCalculationRequestDTO
|
||||
import space.nstart.pcp.complan.types.ComplexPlanCalculationResultDTO
|
||||
import space.nstart.pcp.complan.types.ComplexPlanCalculationStatus
|
||||
import space.nstart.pcp.complan.types.DynamicPlanCalculationMode
|
||||
import space.nstart.pcp.complan.types.DynamicPlanRevolutionMode
|
||||
import space.nstart.pcp.complan.types.DynamicPlanRunStatus
|
||||
import space.nstart.pcp.complan.types.IntervalItemDTO
|
||||
import space.nstart.pcp.complan.types.SurveyContour
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamDTO
|
||||
import java.time.LocalDateTime
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.AbstractExecutorService
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class DynamicPlanRunServiceTest {
|
||||
|
||||
private val requestId = UUID.fromString("11111111-1111-4111-8111-111111111111")
|
||||
private val request = ComplexPlanCalculationRequestDTO(
|
||||
requestId = requestId,
|
||||
satelliteIds = listOf(101L, 202L),
|
||||
calculationStart = LocalDateTime.of(2026, 4, 8, 10, 0),
|
||||
calculationEnd = LocalDateTime.of(2026, 4, 8, 12, 0)
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `startCalculation creates pending run and queues background work`() {
|
||||
val fixture = fixture()
|
||||
|
||||
val run = fixture.service.startCalculation(request)
|
||||
|
||||
assertEquals(DynamicPlanRunStatus.PENDING, run.status)
|
||||
assertEquals(requestId, run.requestId)
|
||||
assertEquals(listOf(101L, 202L), run.satelliteIds)
|
||||
assertEquals(DynamicPlanCalculationMode.FULL, run.calculationMode)
|
||||
assertEquals(DynamicPlanRevolutionMode.BOTH, run.revolutionMode)
|
||||
assertEquals(false, run.filterCoveredRoutes)
|
||||
assertEquals(1, fixture.executor.queuedTasksCount())
|
||||
assertNotNull(fixture.store[run.runId])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `queued calculation stores completed result`() {
|
||||
val fixture = fixture()
|
||||
val result = ComplexPlanCalculationResultDTO(
|
||||
status = ComplexPlanCalculationStatus.COMPLETED,
|
||||
requestId = requestId,
|
||||
satelliteIds = listOf(101L, 202L),
|
||||
missingSatelliteIds = emptyList(),
|
||||
calculationStart = request.calculationStart,
|
||||
calculationEnd = request.calculationEnd,
|
||||
routesCount = 1,
|
||||
durationMs = 5000
|
||||
)
|
||||
val route = SurveyContour(
|
||||
mpl = SquareViewParamDTO(noradId = 101L),
|
||||
time = LocalDateTime.of(2026, 4, 8, 10, 10),
|
||||
duration = 30.0,
|
||||
rev = 55L,
|
||||
gamma = 11.5,
|
||||
geometry = "POLYGON ((0 0, 0 1, 1 1, 0 0))"
|
||||
)
|
||||
val interval = IntervalItemDTO(
|
||||
id = UUID.fromString("33333333-3333-4333-8333-333333333333"),
|
||||
regionId = UUID.fromString("44444444-4444-4444-8444-444444444444"),
|
||||
revSign = RevolutionSign.ASC,
|
||||
geometry = "POLYGON ((0 0, 0 2, 2 2, 0 0))",
|
||||
mpl = mutableListOf(SquareViewParamDTO(noradId = 101L))
|
||||
)
|
||||
doReturn(ComplexPlanCalculationOutput(result = result, routes = listOf(route), intervals = listOf(interval)))
|
||||
.`when`(fixture.basePlan).calculate(request)
|
||||
|
||||
val run = fixture.service.startCalculation(request)
|
||||
fixture.executor.runNext()
|
||||
|
||||
val storedRun = fixture.service.getRun(run.runId)
|
||||
val storedResult = fixture.service.getResult(run.runId)
|
||||
val storedRoutes = fixture.service.getRoutes(run.runId, limit = 1000, offset = 0)
|
||||
val storedIntervals = fixture.service.getIntervals(run.runId, limit = 1000, offset = 0)
|
||||
|
||||
assertEquals(DynamicPlanRunStatus.COMPLETED, storedRun.status)
|
||||
assertEquals("COMPLETED", storedRun.resultStatus)
|
||||
assertEquals(1, storedRun.routesCount)
|
||||
assertEquals(5000, storedRun.durationMs)
|
||||
assertNull(storedRun.errorMessage)
|
||||
assertEquals(result, storedResult)
|
||||
assertEquals(1, storedRoutes.size)
|
||||
assertEquals(101L, storedRoutes[0].satelliteId)
|
||||
assertEquals("POLYGON ((0 0, 0 1, 1 1, 0 0))", storedRoutes[0].contourWkt)
|
||||
assertEquals(1, storedIntervals.size)
|
||||
assertEquals(interval.id, storedIntervals[0].intervalId)
|
||||
assertEquals(RevolutionSign.ASC, storedIntervals[0].revSign)
|
||||
assertEquals(1, storedIntervals[0].observationParametersCount)
|
||||
verify(fixture.aplSettingsLoader).loadLatestSettings()
|
||||
verify(fixture.basePlan).calculate(request)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `queued calculation keeps selected calculation mode`() {
|
||||
val fixture = fixture()
|
||||
val greedyRequest = request.copy(
|
||||
calculationMode = DynamicPlanCalculationMode.GREEDY,
|
||||
revolutionMode = DynamicPlanRevolutionMode.DESC,
|
||||
filterCoveredRoutes = true
|
||||
)
|
||||
val result = ComplexPlanCalculationResultDTO(
|
||||
status = ComplexPlanCalculationStatus.COMPLETED,
|
||||
requestId = requestId,
|
||||
satelliteIds = listOf(101L, 202L),
|
||||
missingSatelliteIds = emptyList(),
|
||||
calculationStart = request.calculationStart,
|
||||
calculationEnd = request.calculationEnd,
|
||||
calculationMode = DynamicPlanCalculationMode.GREEDY,
|
||||
revolutionMode = DynamicPlanRevolutionMode.DESC,
|
||||
filterCoveredRoutes = true,
|
||||
routesCount = 0,
|
||||
durationMs = 1000
|
||||
)
|
||||
doReturn(ComplexPlanCalculationOutput(result = result, routes = emptyList())).`when`(fixture.basePlan).calculate(greedyRequest)
|
||||
|
||||
val run = fixture.service.startCalculation(greedyRequest)
|
||||
fixture.executor.runNext()
|
||||
|
||||
val storedRun = fixture.service.getRun(run.runId)
|
||||
val storedResult = fixture.service.getResult(run.runId)
|
||||
|
||||
assertEquals(DynamicPlanCalculationMode.GREEDY, storedRun.calculationMode)
|
||||
assertEquals(DynamicPlanRevolutionMode.DESC, storedRun.revolutionMode)
|
||||
assertEquals(true, storedRun.filterCoveredRoutes)
|
||||
assertEquals(DynamicPlanCalculationMode.GREEDY, storedResult.calculationMode)
|
||||
assertEquals(DynamicPlanRevolutionMode.DESC, storedResult.revolutionMode)
|
||||
assertEquals(true, storedResult.filterCoveredRoutes)
|
||||
verify(fixture.basePlan).calculate(greedyRequest)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getResult reads legacy result json without calculation mode`() {
|
||||
val fixture = fixture()
|
||||
val runId = UUID.fromString("55555555-5555-4555-8555-555555555555")
|
||||
fixture.store[runId] = runEntity(
|
||||
id = runId,
|
||||
status = DynamicPlanRunStatus.COMPLETED
|
||||
).apply {
|
||||
resultJson = """
|
||||
{
|
||||
"status": "COMPLETED",
|
||||
"requestId": "$requestId",
|
||||
"satelliteIds": [101, 202],
|
||||
"missingSatelliteIds": [],
|
||||
"calculationStart": "2026-04-08T10:00:00",
|
||||
"calculationEnd": "2026-04-08T12:00:00",
|
||||
"routesCount": 1,
|
||||
"durationMs": 5000
|
||||
}
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
val result = fixture.service.getResult(runId)
|
||||
|
||||
assertEquals(DynamicPlanCalculationMode.FULL, result.calculationMode)
|
||||
assertEquals(DynamicPlanRevolutionMode.BOTH, result.revolutionMode)
|
||||
assertEquals(false, result.filterCoveredRoutes)
|
||||
assertEquals(1, result.routesCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getResult rejects unfinished run`() {
|
||||
val fixture = fixture()
|
||||
val run = fixture.service.startCalculation(request)
|
||||
|
||||
assertFailsWith<ResponseStatusException> {
|
||||
fixture.service.getResult(run.runId)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `listRuns returns latest runs and supports status filter`() {
|
||||
val fixture = fixture()
|
||||
val olderRun = runEntity(
|
||||
id = UUID.fromString("22222222-2222-4222-8222-222222222222"),
|
||||
status = DynamicPlanRunStatus.COMPLETED,
|
||||
createdAt = LocalDateTime.of(2026, 4, 8, 9, 0)
|
||||
)
|
||||
val newerRun = runEntity(
|
||||
id = UUID.fromString("33333333-3333-4333-8333-333333333333"),
|
||||
status = DynamicPlanRunStatus.FAILED,
|
||||
createdAt = LocalDateTime.of(2026, 4, 8, 10, 0)
|
||||
)
|
||||
fixture.store[olderRun.id!!] = olderRun
|
||||
fixture.store[newerRun.id!!] = newerRun
|
||||
|
||||
val allRuns = fixture.service.listRuns(status = null, limit = 10, offset = 0)
|
||||
val failedRuns = fixture.service.listRuns(status = DynamicPlanRunStatus.FAILED, limit = 10, offset = 0)
|
||||
|
||||
assertEquals(listOf(newerRun.id, olderRun.id), allRuns.map { run -> run.runId })
|
||||
assertEquals(listOf(newerRun.id), failedRuns.map { run -> run.runId })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `recoverInterruptedRuns marks pending and running runs as failed`() {
|
||||
val fixture = fixture()
|
||||
val pendingRun = runEntity(
|
||||
id = UUID.fromString("22222222-2222-4222-8222-222222222222"),
|
||||
status = DynamicPlanRunStatus.PENDING
|
||||
)
|
||||
val runningRun = runEntity(
|
||||
id = UUID.fromString("33333333-3333-4333-8333-333333333333"),
|
||||
status = DynamicPlanRunStatus.RUNNING
|
||||
)
|
||||
val completedRun = runEntity(
|
||||
id = UUID.fromString("44444444-4444-4444-8444-444444444444"),
|
||||
status = DynamicPlanRunStatus.COMPLETED
|
||||
)
|
||||
fixture.store[pendingRun.id!!] = pendingRun
|
||||
fixture.store[runningRun.id!!] = runningRun
|
||||
fixture.store[completedRun.id!!] = completedRun
|
||||
|
||||
fixture.service.recoverInterruptedRuns()
|
||||
|
||||
assertEquals(DynamicPlanRunStatus.FAILED, pendingRun.status)
|
||||
assertEquals(DynamicPlanRunStatus.FAILED, runningRun.status)
|
||||
assertEquals(DynamicPlanRunStatus.COMPLETED, completedRun.status)
|
||||
assertEquals("Calculation was interrupted by service restart", pendingRun.errorMessage)
|
||||
assertEquals("Calculation was interrupted by service restart", runningRun.errorMessage)
|
||||
assertNotNull(pendingRun.finishedAt)
|
||||
assertNotNull(runningRun.finishedAt)
|
||||
assertNull(completedRun.finishedAt)
|
||||
}
|
||||
|
||||
private fun fixture(): Fixture {
|
||||
val store = linkedMapOf<UUID, DynamicPlanRunEntity>()
|
||||
val routeStore = mutableListOf<DynamicPlanRouteEntity>()
|
||||
val intervalStore = mutableListOf<DynamicPlanIntervalEntity>()
|
||||
val repository = mock(DynamicPlanRunRepository::class.java)
|
||||
val routeRepository = mock(DynamicPlanRouteRepository::class.java)
|
||||
val intervalRepository = mock(DynamicPlanIntervalRepository::class.java)
|
||||
org.mockito.Mockito.`when`(
|
||||
repository.save(ArgumentMatchers.any(DynamicPlanRunEntity::class.java) ?: DynamicPlanRunEntity(id = UUID.randomUUID()))
|
||||
).thenAnswer { invocation ->
|
||||
val entity = invocation.arguments[0] as DynamicPlanRunEntity
|
||||
store[entity.id!!] = entity
|
||||
entity
|
||||
}
|
||||
org.mockito.Mockito.`when`(
|
||||
repository.findById(ArgumentMatchers.any(UUID::class.java) ?: UUID.randomUUID())
|
||||
).thenAnswer { invocation ->
|
||||
Optional.ofNullable(store[invocation.arguments[0] as UUID])
|
||||
}
|
||||
org.mockito.Mockito.`when`(
|
||||
repository.saveAll(ArgumentMatchers.anyIterable<DynamicPlanRunEntity>() ?: emptyList())
|
||||
).thenAnswer { invocation ->
|
||||
val entities = invocation.arguments[0] as Iterable<DynamicPlanRunEntity>
|
||||
entities.map { entity ->
|
||||
store[entity.id!!] = entity
|
||||
entity
|
||||
}
|
||||
}
|
||||
org.mockito.Mockito.`when`(
|
||||
repository.findRuns(ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt())
|
||||
).thenAnswer { invocation ->
|
||||
val limit = invocation.arguments[0] as Int
|
||||
val offset = invocation.arguments[1] as Int
|
||||
store.values
|
||||
.sortedByDescending { run -> run.createdAt }
|
||||
.drop(offset)
|
||||
.take(limit)
|
||||
}
|
||||
org.mockito.Mockito.`when`(
|
||||
repository.findRunsByStatus(
|
||||
ArgumentMatchers.anyString(),
|
||||
ArgumentMatchers.anyInt(),
|
||||
ArgumentMatchers.anyInt()
|
||||
)
|
||||
).thenAnswer { invocation ->
|
||||
val status = DynamicPlanRunStatus.valueOf(invocation.arguments[0] as String)
|
||||
val limit = invocation.arguments[1] as Int
|
||||
val offset = invocation.arguments[2] as Int
|
||||
store.values
|
||||
.filter { run -> run.status == status }
|
||||
.sortedByDescending { run -> run.createdAt }
|
||||
.drop(offset)
|
||||
.take(limit)
|
||||
}
|
||||
org.mockito.Mockito.`when`(
|
||||
repository.findAllByStatusIn(ArgumentMatchers.anyCollection<DynamicPlanRunStatus>() ?: emptyList())
|
||||
).thenAnswer { invocation ->
|
||||
val statuses = invocation.arguments[0] as Collection<DynamicPlanRunStatus>
|
||||
store.values.filter { run -> run.status in statuses }
|
||||
}
|
||||
org.mockito.Mockito.`when`(
|
||||
routeRepository.saveAll(ArgumentMatchers.anyList<DynamicPlanRouteEntity>() ?: emptyList())
|
||||
).thenAnswer { invocation ->
|
||||
val entities = invocation.arguments[0] as Iterable<DynamicPlanRouteEntity>
|
||||
entities.map { entity ->
|
||||
entity.id = (routeStore.size + 1).toLong()
|
||||
routeStore.add(entity)
|
||||
entity
|
||||
}
|
||||
}
|
||||
org.mockito.Mockito.`when`(
|
||||
intervalRepository.saveAll(ArgumentMatchers.anyList<DynamicPlanIntervalEntity>() ?: emptyList())
|
||||
).thenAnswer { invocation ->
|
||||
val entities = invocation.arguments[0] as Iterable<DynamicPlanIntervalEntity>
|
||||
entities.map { entity ->
|
||||
entity.id = (intervalStore.size + 1).toLong()
|
||||
intervalStore.add(entity)
|
||||
entity
|
||||
}
|
||||
}
|
||||
org.mockito.Mockito.`when`(
|
||||
routeRepository.findRoutesByRunId(
|
||||
ArgumentMatchers.any(UUID::class.java) ?: UUID.randomUUID(),
|
||||
ArgumentMatchers.anyInt(),
|
||||
ArgumentMatchers.anyInt()
|
||||
)
|
||||
)
|
||||
.thenAnswer { invocation ->
|
||||
val runId = invocation.arguments[0] as UUID
|
||||
routeStore.filter { route -> route.run?.id == runId }.sortedWith(compareBy({ it.startTime }, { it.id }))
|
||||
}
|
||||
org.mockito.Mockito.`when`(
|
||||
intervalRepository.findIntervalsByRunId(
|
||||
ArgumentMatchers.any(UUID::class.java) ?: UUID.randomUUID(),
|
||||
ArgumentMatchers.anyInt(),
|
||||
ArgumentMatchers.anyInt()
|
||||
)
|
||||
)
|
||||
.thenAnswer { invocation ->
|
||||
val runId = invocation.arguments[0] as UUID
|
||||
intervalStore.filter { interval -> interval.run?.id == runId }.sortedWith(compareBy({ it.intervalOrder }, { it.id }))
|
||||
}
|
||||
|
||||
val basePlan = mock(BasicPlanCalculator::class.java)
|
||||
val aplSettingsLoader = mock(AplSettingsLoader::class.java)
|
||||
val executor = QueueingExecutorService()
|
||||
val service = DynamicPlanRunService(
|
||||
runRepository = repository,
|
||||
routeRepository = routeRepository,
|
||||
intervalRepository = intervalRepository,
|
||||
basePlan = basePlan,
|
||||
aplSettingsLoader = aplSettingsLoader,
|
||||
objectMapper = ObjectMapper().findAndRegisterModules(),
|
||||
transactionTemplate = TransactionTemplate(NoopTransactionManager()),
|
||||
dynamicPlanExecutor = executor
|
||||
)
|
||||
|
||||
return Fixture(service, basePlan, aplSettingsLoader, executor, store)
|
||||
}
|
||||
|
||||
private fun runEntity(
|
||||
id: UUID,
|
||||
status: DynamicPlanRunStatus,
|
||||
createdAt: LocalDateTime = LocalDateTime.of(2026, 4, 8, 10, 0)
|
||||
): DynamicPlanRunEntity =
|
||||
DynamicPlanRunEntity(
|
||||
id = id,
|
||||
status = status,
|
||||
requestId = requestId,
|
||||
satelliteIds = """[101,202]""",
|
||||
calculationStart = request.calculationStart,
|
||||
calculationEnd = request.calculationEnd,
|
||||
createdAt = createdAt
|
||||
)
|
||||
|
||||
private data class Fixture(
|
||||
val service: DynamicPlanRunService,
|
||||
val basePlan: BasicPlanCalculator,
|
||||
val aplSettingsLoader: AplSettingsLoader,
|
||||
val executor: QueueingExecutorService,
|
||||
val store: MutableMap<UUID, DynamicPlanRunEntity>
|
||||
)
|
||||
|
||||
private class QueueingExecutorService : AbstractExecutorService() {
|
||||
private val tasks = ArrayDeque<Runnable>()
|
||||
private var shutdown = false
|
||||
|
||||
override fun execute(command: Runnable) {
|
||||
tasks.add(command)
|
||||
}
|
||||
|
||||
fun queuedTasksCount(): Int = tasks.size
|
||||
|
||||
fun runNext() {
|
||||
tasks.removeFirst().run()
|
||||
}
|
||||
|
||||
override fun shutdown() {
|
||||
shutdown = true
|
||||
}
|
||||
|
||||
override fun shutdownNow(): MutableList<Runnable> {
|
||||
shutdown = true
|
||||
val queued = tasks.toMutableList()
|
||||
tasks.clear()
|
||||
return queued
|
||||
}
|
||||
|
||||
override fun isShutdown(): Boolean = shutdown
|
||||
|
||||
override fun isTerminated(): Boolean = shutdown && tasks.isEmpty()
|
||||
|
||||
override fun awaitTermination(timeout: Long, unit: TimeUnit): Boolean = isTerminated
|
||||
}
|
||||
|
||||
private class NoopTransactionManager : PlatformTransactionManager {
|
||||
override fun getTransaction(definition: TransactionDefinition?): TransactionStatus = SimpleTransactionStatus()
|
||||
|
||||
override fun commit(status: TransactionStatus) = Unit
|
||||
|
||||
override fun rollback(status: TransactionStatus) = Unit
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package space.nstart.pcp.complan.service
|
||||
|
||||
import com.sun.net.httpserver.HttpServer
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.test.util.ReflectionTestUtils
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.URI
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class RequestServiceClientTest {
|
||||
|
||||
private var server: HttpServer? = null
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
server?.stop(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getRequest calls v1 request endpoint and maps response to request item`() {
|
||||
val requestId = UUID.fromString("00000000-0000-0000-0000-000000000001")
|
||||
val requestedUris = mutableListOf<URI>()
|
||||
server = serverWithRequestResponse(requestedUris, requestId)
|
||||
|
||||
val request = requestServiceClient().getRequest(requestId)!!
|
||||
|
||||
assertEquals("/v1/requests/$requestId", requestedUris.single().path)
|
||||
assertFalse(requestedUris.any { uri -> uri.path.startsWith("/api/v1/requests") })
|
||||
assertEquals(requestId, request.id)
|
||||
assertEquals("Dynamic plan request", request.name)
|
||||
assertEquals("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", request.geometry)
|
||||
assertEquals(LocalDateTime.of(2026, 1, 2, 3, 4, 5), request.intervalBegin)
|
||||
assertEquals(LocalDateTime.of(2026, 1, 3, 4, 5, 6), request.intervalEnd)
|
||||
assertEquals(2.5, request.resolution)
|
||||
assertEquals(7.5, request.importance)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getRequest returns null on not found like legacy client`() {
|
||||
val requestId = UUID.fromString("00000000-0000-0000-0000-000000000404")
|
||||
val requestedUris = mutableListOf<URI>()
|
||||
server = HttpServer.create(InetSocketAddress(0), 0).apply {
|
||||
createContext("/v1/requests") { exchange ->
|
||||
requestedUris += exchange.requestURI
|
||||
val responseBody = """{"code":"REQUEST_NOT_FOUND"}"""
|
||||
exchange.responseHeaders.add("Content-Type", "application/json")
|
||||
exchange.sendResponseHeaders(404, responseBody.toByteArray().size.toLong())
|
||||
exchange.responseBody.use { it.write(responseBody.toByteArray()) }
|
||||
}
|
||||
start()
|
||||
}
|
||||
|
||||
val request = requestServiceClient().getRequest(requestId)
|
||||
|
||||
assertNull(request)
|
||||
assertEquals("/v1/requests/$requestId", requestedUris.single().path)
|
||||
assertFalse(requestedUris.any { uri -> uri.path.startsWith("/api/v1/requests") })
|
||||
}
|
||||
|
||||
private fun serverWithRequestResponse(
|
||||
requestedUris: MutableList<URI>,
|
||||
requestId: UUID,
|
||||
): HttpServer =
|
||||
HttpServer.create(InetSocketAddress(0), 0).apply {
|
||||
createContext("/v1/requests") { exchange ->
|
||||
requestedUris += exchange.requestURI
|
||||
val responseBody = """
|
||||
{
|
||||
"id": "$requestId",
|
||||
"name": "Dynamic plan request",
|
||||
"status": "ACTIVE",
|
||||
"surveyType": "OPTICS",
|
||||
"geometry": "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
|
||||
"importance": 7.5,
|
||||
"beginDateTime": "2026-01-02T03:04:05Z",
|
||||
"endDateTime": "2026-01-03T04:05:06Z",
|
||||
"kpp": [1],
|
||||
"highPriorityTransmit": false,
|
||||
"optics": {
|
||||
"resultType": "PANCHROMATIC",
|
||||
"resolution": 2.5,
|
||||
"sunAngleMin": 10.0,
|
||||
"sunAngleMax": 90.0,
|
||||
"clouds": 100.0
|
||||
},
|
||||
"rsa": null,
|
||||
"coverage": {"currentPercent": 0.0},
|
||||
"createdAt": "2026-01-01T00:00:00Z",
|
||||
"updatedAt": "2026-01-01T00:00:00Z",
|
||||
"deletedAt": null
|
||||
}
|
||||
""".trimIndent()
|
||||
exchange.responseHeaders.add("Content-Type", "application/json")
|
||||
exchange.sendResponseHeaders(200, responseBody.toByteArray().size.toLong())
|
||||
exchange.responseBody.use { it.write(responseBody.toByteArray()) }
|
||||
}
|
||||
start()
|
||||
}
|
||||
|
||||
private fun requestServiceClient(): RequestServiceClient {
|
||||
val client = RequestServiceClient()
|
||||
ReflectionTestUtils.setField(client, "requestServiceUrl", "http://localhost:${server!!.address.port}")
|
||||
ReflectionTestUtils.setField(client, "requestSelectionPath", "/v1/requests")
|
||||
return client
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package space.nstart.pcp.complan.service
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.`when`
|
||||
import space.nstart.pcp.complan.db.repository.DynamicPlanRouteRepository
|
||||
import space.nstart.pcp.complan.db.repository.DynamicPlanRunRepository
|
||||
|
||||
class SatelliteDeletedServiceTest {
|
||||
|
||||
@Test
|
||||
fun `deleteSatelliteData removes dynamic plan runs and satellite routes`() {
|
||||
val runRepository = mock(DynamicPlanRunRepository::class.java)
|
||||
val routeRepository = mock(DynamicPlanRouteRepository::class.java)
|
||||
val service = SatelliteDeletedService(runRepository, routeRepository)
|
||||
|
||||
`when`(runRepository.countBySatelliteId(56756L)).thenReturn(2L)
|
||||
`when`(routeRepository.countBySatelliteId(56756L)).thenReturn(5L)
|
||||
`when`(runRepository.deleteAllBySatelliteId(56756L)).thenReturn(2)
|
||||
`when`(routeRepository.deleteAllBySatelliteId(56756L)).thenReturn(1)
|
||||
|
||||
val summary = service.deleteSatelliteData(56756L)
|
||||
|
||||
assertEquals(56756L, summary.satelliteId)
|
||||
assertEquals(2, summary.runsDeleted)
|
||||
assertEquals(4L, summary.routesDeletedByRunCascade)
|
||||
assertEquals(1, summary.orphanRoutesDeleted)
|
||||
verify(runRepository).deleteAllBySatelliteId(56756L)
|
||||
verify(routeRepository).deleteAllBySatelliteId(56756L)
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package space.nstart.pcp.complan.spacecraft
|
||||
|
||||
import org.locationtech.jts.geom.Coordinate
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.FlightLineDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class SpaceCraftTest {
|
||||
|
||||
@Test
|
||||
fun `spacecraft has default planning constraints`() {
|
||||
val spacecraft = SpaceCraft(56756)
|
||||
|
||||
assertEquals(SCConstraints(), spacecraft.constraints)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create flight line selects full passes split by revolution`() {
|
||||
val spacecraft = SpaceCraft(56756)
|
||||
spacecraft.flightLine.addAll(
|
||||
listOf(
|
||||
point(RevolutionSign.ASC, 1, -70.0, 150.0),
|
||||
point(RevolutionSign.ASC, 1, 82.0, 20.0),
|
||||
point(RevolutionSign.DESC, 1, 82.0, 20.0),
|
||||
point(RevolutionSign.DESC, 1, 30.0, 5.0),
|
||||
point(RevolutionSign.DESC, 2, -82.0, 350.0),
|
||||
point(RevolutionSign.ASC, 2, -82.0, 350.0),
|
||||
point(RevolutionSign.ASC, 2, 30.0, 20.0),
|
||||
point(RevolutionSign.ASC, 2, 82.0, 50.0),
|
||||
)
|
||||
)
|
||||
|
||||
val (desc, asc) = spacecraft.createFlightLine()
|
||||
|
||||
assertTrue(desc.coordinates.minOf { it.y } <= -80.0)
|
||||
assertTrue(desc.coordinates.maxOf { it.y } >= 80.0)
|
||||
assertTrue(asc.coordinates.minOf { it.y } <= -80.0)
|
||||
assertTrue(asc.coordinates.maxOf { it.y } >= 80.0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `line correction unwraps longitude jumps in both directions`() {
|
||||
val spacecraft = SpaceCraft(56756)
|
||||
|
||||
val corrected = spacecraft.lineCorrection(
|
||||
listOf(
|
||||
Coordinate(350.0, 0.0),
|
||||
Coordinate(10.0, 1.0),
|
||||
Coordinate(340.0, 2.0),
|
||||
)
|
||||
)
|
||||
|
||||
assertTrue(corrected[1].x > 350.0)
|
||||
assertTrue(corrected[2].x < corrected[1].x)
|
||||
assertTrue(kotlin.math.abs(corrected[1].x - corrected[0].x) < 180.0)
|
||||
assertTrue(kotlin.math.abs(corrected[2].x - corrected[1].x) < 180.0)
|
||||
}
|
||||
|
||||
private fun point(
|
||||
revSign: RevolutionSign,
|
||||
revolution: Long,
|
||||
lat: Double,
|
||||
long: Double
|
||||
): FlightLineDTO =
|
||||
FlightLineDTO(
|
||||
time = LocalDateTime.of(2026, 4, 30, 0, 0),
|
||||
revolution = revolution,
|
||||
lat = lat,
|
||||
long = long,
|
||||
revSign = revSign
|
||||
)
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import space.nstart.pcp.complan.JacksonConfig
|
||||
import java.util.UUID
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class RequestItemDTOTest {
|
||||
|
||||
private val objectMapper = JacksonConfig().objectMapper()
|
||||
|
||||
@Test
|
||||
fun `request item supports nullable priority and intervals from request service`() {
|
||||
val requestId = UUID.fromString("39ef7800-d20c-4aae-8041-00208c4551b2")
|
||||
val body = """
|
||||
{
|
||||
"id": "$requestId",
|
||||
"name": "Гибралтар",
|
||||
"geometry": "POLYGON ((-5.909782 35.691644, -4.797418 35.691644, -4.797418 36.589956, -5.909782 36.589956, -5.909782 35.691644))",
|
||||
"intervalBegin": "2000-01-01T00:00:00",
|
||||
"intervalEnd": "2100-01-01T00:00:00",
|
||||
"resolution": 1.0,
|
||||
"priority": null,
|
||||
"importance": 0.5,
|
||||
"appType": 0,
|
||||
"intervals": null
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val result = objectMapper.readValue(body, RequestItemDTO::class.java)
|
||||
|
||||
assertEquals(requestId, result.id)
|
||||
assertEquals("Гибралтар", result.name)
|
||||
assertNull(result.priority)
|
||||
assertNull(result.intervals)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user