This commit is contained in:
Дмитрий Соловьев
2026-05-25 14:23:52 +03:00
parent b3a6012ebb
commit d48ddd2657
1066 changed files with 104601 additions and 3 deletions
@@ -0,0 +1,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)
}
@@ -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
)
}
}
}
@@ -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
}
}
File diff suppressed because one or more lines are too long
@@ -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
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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
}
}
}
@@ -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)
}
}
@@ -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")
}
}
@@ -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
}
@@ -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
}
}
}
@@ -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()) }
}
@@ -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 = "")
}
@@ -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)
}
@@ -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)
}
@@ -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)
}
@@ -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?
}
@@ -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>
}
@@ -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>
}
@@ -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>
}
@@ -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)
}
}
@@ -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>>() {}
}
}
@@ -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
)
}
@@ -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()
}
}
@@ -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,
)
@@ -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>>() {}
}
}
@@ -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()
}
@@ -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
)
@@ -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}"
* */
@@ -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,
)
@@ -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
)
@@ -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
}
}
@@ -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
)
}
@@ -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()
)
@@ -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
)
@@ -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
)
@@ -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
)
@@ -0,0 +1,6 @@
package space.nstart.pcp.complan.types
enum class DynamicPlanCalculationMode {
FULL,
GREEDY
}
@@ -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
)
@@ -0,0 +1,7 @@
package space.nstart.pcp.complan.types
enum class DynamicPlanRevolutionMode {
BOTH,
ASC,
DESC
}
@@ -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
)
@@ -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?
)
@@ -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)
}
}
}
@@ -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
}
}
}
@@ -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
)
@@ -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()
)
@@ -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
)
@@ -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
)
@@ -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>
)
@@ -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,
)
@@ -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,
)
@@ -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
)
@@ -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
}
@@ -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
);
@@ -0,0 +1,4 @@
CREATE TABLE IF NOT EXISTS aplsettings (
id SERIAL PRIMARY KEY, --серийный ключ
settings TEXT --настройки автомата в виде JSON
);
@@ -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);
@@ -0,0 +1,2 @@
alter table dynamic_plan_run
add column if not exists calculation_mode varchar(32) not null default 'FULL';
@@ -0,0 +1,2 @@
alter table dynamic_plan_run
add column if not exists revolution_mode varchar(32) not null default 'BOTH';
@@ -0,0 +1,2 @@
alter table dynamic_plan_run
add column if not exists filter_covered_routes boolean not null default false;
@@ -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;
@@ -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);