Init
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
package space.nstart.pcp.complan
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
|
||||
|
||||
@SpringBootApplication
|
||||
class ComPlan
|
||||
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<space.nstart.pcp.complan.ComPlan>(*args)
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
package space.nstart.pcp.complan
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import org.springframework.web.bind.annotation.ResponseStatus
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
import space.nstart.pcp.complan.apl.BasicPlanCalculator
|
||||
import space.nstart.pcp.complan.service.DynamicPlanRunService
|
||||
import space.nstart.pcp.complan.types.ComplexPlanCalculationRequestDTO
|
||||
import space.nstart.pcp.complan.types.ComplexPlanCalculationResultDTO
|
||||
import space.nstart.pcp.complan.types.DynamicPlanIntervalDTO
|
||||
import space.nstart.pcp.complan.types.DynamicPlanRouteDTO
|
||||
import space.nstart.pcp.complan.types.DynamicPlanRunDTO
|
||||
import space.nstart.pcp.complan.types.DynamicPlanRunStatus
|
||||
import space.nstart.pcp.complan.types.GreedyMathDebugRequestDTO
|
||||
import space.nstart.pcp.complan.types.GreedyMathDebugResultDTO
|
||||
import space.nstart.pcp.complan.types.ObservationParametersDebugRequestDTO
|
||||
import space.nstart.pcp.complan.types.ObservationParametersDebugResultDTO
|
||||
import space.nstart.pcp.complan.types.TestCalcMarsDebugRequestDTO
|
||||
import space.nstart.pcp.complan.types.TestCalcMarsDebugResultDTO
|
||||
import java.nio.file.NoSuchFileException
|
||||
import java.util.UUID
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/v1/main")
|
||||
@Tag(name = "Расчет комплексного плана", description = "API расчета комплексного плана")
|
||||
class ComPlanController(
|
||||
private val dynamicPlanRunService: DynamicPlanRunService,
|
||||
private val basicPlanCalculator: BasicPlanCalculator
|
||||
) {
|
||||
|
||||
@PostMapping("/calcPlan")
|
||||
@ResponseStatus(HttpStatus.ACCEPTED)
|
||||
fun calcPlan(
|
||||
@RequestBody request: ComplexPlanCalculationRequestDTO
|
||||
): DynamicPlanRunDTO = dynamicPlanRunService.startCalculation(request)
|
||||
|
||||
@GetMapping("/calcPlan/runs")
|
||||
fun getCalculations(
|
||||
@RequestParam(required = false) status: DynamicPlanRunStatus?,
|
||||
@RequestParam(defaultValue = "50") limit: Int,
|
||||
@RequestParam(defaultValue = "0") offset: Int
|
||||
): List<DynamicPlanRunDTO> = dynamicPlanRunService.listRuns(status, limit, offset)
|
||||
|
||||
@GetMapping("/calcPlan/{runId}")
|
||||
fun getCalculation(
|
||||
@PathVariable runId: UUID
|
||||
): DynamicPlanRunDTO = dynamicPlanRunService.getRun(runId)
|
||||
|
||||
@GetMapping("/calcPlan/{runId}/result")
|
||||
fun getCalculationResult(
|
||||
@PathVariable runId: UUID
|
||||
): ComplexPlanCalculationResultDTO = dynamicPlanRunService.getResult(runId)
|
||||
|
||||
@GetMapping("/calcPlan/{runId}/routes")
|
||||
fun getCalculationRoutes(
|
||||
@PathVariable runId: UUID,
|
||||
@RequestParam(defaultValue = "1000") limit: Int,
|
||||
@RequestParam(defaultValue = "0") offset: Int
|
||||
): List<DynamicPlanRouteDTO> = dynamicPlanRunService.getRoutes(runId, limit, offset)
|
||||
|
||||
@GetMapping("/calcPlan/{runId}/intervals")
|
||||
fun getCalculationIntervals(
|
||||
@PathVariable runId: UUID,
|
||||
@RequestParam(defaultValue = "1000") limit: Int,
|
||||
@RequestParam(defaultValue = "0") offset: Int
|
||||
): List<DynamicPlanIntervalDTO> = dynamicPlanRunService.getIntervals(runId, limit, offset)
|
||||
|
||||
@PostMapping("/calcPlan/debug/observation-parameters")
|
||||
fun debugObservationParameters(
|
||||
@RequestBody request: ObservationParametersDebugRequestDTO
|
||||
): ObservationParametersDebugResultDTO {
|
||||
if (request.calculationEnd <= request.calculationStart) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "calculationEnd must be after calculationStart")
|
||||
}
|
||||
if (request.satelliteIds.isEmpty()) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "satelliteIds must not be empty")
|
||||
}
|
||||
if (request.chunkSize != null && request.chunkSize < 1) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "chunkSize must be greater than or equal to 1")
|
||||
}
|
||||
if (request.parallelism != null && request.parallelism < 1) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "parallelism must be greater than or equal to 1")
|
||||
}
|
||||
return basicPlanCalculator.calculateObservationParametersDebug(request)
|
||||
}
|
||||
|
||||
@PostMapping("/calcPlan/debug/greedy-math")
|
||||
fun debugGreedyMath(
|
||||
@RequestBody request: GreedyMathDebugRequestDTO
|
||||
): GreedyMathDebugResultDTO {
|
||||
if (request.calculationEnd <= request.calculationStart) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "calculationEnd must be after calculationStart")
|
||||
}
|
||||
if (request.satelliteIds.isEmpty()) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "satelliteIds must not be empty")
|
||||
}
|
||||
return try {
|
||||
basicPlanCalculator.calculateGreedyMathDebug(request)
|
||||
} catch (error: NoSuchFileException) {
|
||||
throw ResponseStatusException(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"observation parameters file not found: ${error.file}",
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/calcPlan/debug/testcalcMars")
|
||||
fun testCalcMars(
|
||||
@RequestBody request: TestCalcMarsDebugRequestDTO
|
||||
): TestCalcMarsDebugResultDTO {
|
||||
if (request.calculationEnd <= request.calculationStart) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "calculationEnd must be after calculationStart")
|
||||
}
|
||||
if (request.satelliteIds.isEmpty()) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "satelliteIds must not be empty")
|
||||
}
|
||||
return try {
|
||||
basicPlanCalculator.calculateTestCalcMarsDebug(request)
|
||||
} catch (error: NoSuchFileException) {
|
||||
throw ResponseStatusException(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"observation parameters file not found: ${error.file}",
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package space.nstart.pcp.complan
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.SerializationFeature
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
||||
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
||||
|
||||
@Configuration
|
||||
class JacksonConfig {
|
||||
|
||||
@Bean
|
||||
fun objectMapper(): ObjectMapper {
|
||||
val mapper = ObjectMapper()
|
||||
|
||||
// Настраиваем формат для LocalDateTime
|
||||
// val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.zzz")
|
||||
|
||||
// module.addSerializer<LocalDateTime?>(
|
||||
// LocalDateTime::class.java,
|
||||
// LocalDateTimeSerializer(formatter)
|
||||
// )
|
||||
// module.addDeserializer<LocalDateTime?>(
|
||||
// LocalDateTime::class.java,
|
||||
// LocalDateTimeDeserializer(formatter)
|
||||
// )
|
||||
mapper.registerModule(JavaTimeModule())
|
||||
.registerKotlinModule()
|
||||
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
|
||||
|
||||
return mapper
|
||||
}
|
||||
}
|
||||
+1985
File diff suppressed because one or more lines are too long
+36
@@ -0,0 +1,36 @@
|
||||
package space.nstart.pcp.complan.apl
|
||||
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.atan
|
||||
import kotlin.math.sqrt
|
||||
|
||||
class CaptureAngleCalculator {
|
||||
|
||||
fun calculateCaptureAngle(routeWidthKm: Double, points: List<OrbPointDTO>): Double? {
|
||||
if (routeWidthKm <= 0.0 || points.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val averageHeightMeters = points
|
||||
.map { it.orbitRadiusMeters() - EARTH_RADIUS_METERS }
|
||||
.filter { it > 0.0 }
|
||||
.average()
|
||||
|
||||
if (averageHeightMeters.isNaN() || averageHeightMeters <= 0.0) {
|
||||
return null
|
||||
}
|
||||
|
||||
val halfWidthMeters = routeWidthKm * METERS_IN_KILOMETER / 2.0
|
||||
return atan(halfWidthMeters / averageHeightMeters).toDegrees()
|
||||
}
|
||||
|
||||
private fun OrbPointDTO.orbitRadiusMeters(): Double = sqrt(x * x + y * y + z * z)
|
||||
|
||||
private fun Double.toDegrees(): Double = this * 180.0 / PI
|
||||
|
||||
private companion object {
|
||||
const val EARTH_RADIUS_METERS = 6_378_136.0
|
||||
const val METERS_IN_KILOMETER = 1_000.0
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package space.nstart.pcp.complan.apl
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.stereotype.Service
|
||||
import space.nstart.pcp.complan.types.GeoJson
|
||||
import space.nstart.pcp.complan.types.GeoJsonItem
|
||||
|
||||
private val DEFAULT_GEO_JSON_LAYER_OUTPUT_DIR =
|
||||
"${System.getProperty("java.io.tmpdir")}/pcp-dynamic-plan-service/geo-layers"
|
||||
|
||||
@Service
|
||||
class GeoJsonLayerWriter(
|
||||
@param:Value("\${complex-plan.geo-layers.enabled:false}")
|
||||
private val enabled: Boolean = false,
|
||||
|
||||
@param:Value("\${complex-plan.geo-layers.output-dir:\${java.io.tmpdir}/pcp-dynamic-plan-service/geo-layers}")
|
||||
private val outputDir: String = DEFAULT_GEO_JSON_LAYER_OUTPUT_DIR
|
||||
) {
|
||||
fun write(geoms: List<GeoJsonItem>, fileName: String, radToDeg: Boolean = false) {
|
||||
if (!enabled) {
|
||||
return
|
||||
}
|
||||
|
||||
GeoJson.createGeoJson(geoms, fileName, outputDir, radToDeg)
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package space.nstart.pcp.complan.apl
|
||||
|
||||
import org.locationtech.jts.geom.Geometry
|
||||
import org.locationtech.jts.operation.union.UnaryUnionOp
|
||||
|
||||
class IntervalCoverageChecker {
|
||||
|
||||
fun findUncoveredGeometry(contour: Geometry, intervals: List<Geometry>): Geometry {
|
||||
if (intervals.isEmpty()) {
|
||||
return contour.copy()
|
||||
}
|
||||
|
||||
val intervalsUnion = UnaryUnionOp(intervals).union()
|
||||
return contour.difference(intervalsUnion)
|
||||
}
|
||||
}
|
||||
Executable
+306
@@ -0,0 +1,306 @@
|
||||
package space.nstart.pcp.complan.apl
|
||||
|
||||
import org.locationtech.jts.geom.*
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import java.lang.Double.max
|
||||
import java.lang.Double.min
|
||||
import kotlin.collections.get
|
||||
import kotlin.compareTo
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.cos
|
||||
|
||||
/**
|
||||
* Класс построения интервалов съемки
|
||||
* Под интервалами съемки полагаются марш
|
||||
*/
|
||||
@Service
|
||||
class IntervalsBuilder(private val writeLog : Boolean = false) {
|
||||
/** Логирование*/
|
||||
private val log = LoggerFactory.getLogger(this::class.java)
|
||||
/** Фабрика geometry*/
|
||||
private val geomFactory = GeometryFactory()
|
||||
|
||||
private val erthEcvator = 111.3213777 * (180 / PI)
|
||||
|
||||
/**
|
||||
* Создание линии параллельной заданной подвинутой на delX
|
||||
* @param delX дельта перемещения по оси Х
|
||||
* @param line эталонная линия
|
||||
* @return Возвращает линию параллельную эталонной подвинутой на значение delX
|
||||
*/
|
||||
private fun createParallelLine(
|
||||
line: LineString,
|
||||
delX: Double
|
||||
): LineString {
|
||||
val newCoordinates = line.coordinates.map {
|
||||
Coordinate(it.x + delX, it.y)
|
||||
}.toTypedArray()
|
||||
|
||||
return geomFactory.createLineString(newCoordinates)
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание полигона параллельного заданному подвинутому на delX
|
||||
* @param delX дельта перемещения по оси Х
|
||||
* @param pol эталонная полигон
|
||||
* @return Возвращает полигон параллельный эталонному подвинутому на значение delX
|
||||
*/
|
||||
private fun createParallelPol(
|
||||
pol: Polygon,
|
||||
delX: Double
|
||||
): Polygon {
|
||||
val newCoordinates = pol.coordinates.map {
|
||||
Coordinate(it.x + delX, it.y)
|
||||
}.toTypedArray()
|
||||
|
||||
return geomFactory.createPolygon(newCoordinates)
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение описывающего многоугольника
|
||||
* @param geom контур геометрии
|
||||
* @return Возвращает описывающий многоугольник [org.locationtech.jts.geom.Envelope]
|
||||
*/
|
||||
private fun getBoxInternal(geom: Geometry) = geom.envelopeInternal
|
||||
|
||||
/**
|
||||
* Возвращает расстояние в км на определенной широте
|
||||
* @param distanceKm дистанция в км
|
||||
* @param lat широта
|
||||
* @return Возвращает значение в радианах эквивалентное дистанции в километрах на данной широте
|
||||
*/
|
||||
private fun getCaptureDistanceInKm(distanceKm: Double, lat: Double) = (distanceKm / (erthEcvator * cos(lat / 180 * PI ))) * 180 / PI //.toRad()
|
||||
//для градусов, но мы считаем в радианах
|
||||
// private fun getCaptureDistanceInKm(distanceKm: Double, lat: Double) = (distanceKm / (erthecvator * cos(lat))).toRad()
|
||||
|
||||
/**
|
||||
* Возвращает одно значение из двух, которое по модулю ближе к 0
|
||||
* @param a ервое значение
|
||||
* @param b второе значение
|
||||
* @return Возвращает значение по модулю близкое к 0
|
||||
*/
|
||||
private fun closerToZero(a: Double, b: Double) = if(abs(a) < abs(b)) a else b
|
||||
|
||||
/**
|
||||
* Передвижение маршрута ближе к контуру
|
||||
* Точность до 1/10 ширины маршрута
|
||||
* @param line линия
|
||||
* @param contour контур
|
||||
* @param offset ширина маршрута
|
||||
* @return Возвращает маршрут отступающий от контура не более чем на 0,1 ширины маршрута
|
||||
*/
|
||||
private fun moveBorderMarToContour(
|
||||
line: LineString,
|
||||
contour: Polygon,
|
||||
offset: Double
|
||||
): Polygon {
|
||||
var line = line
|
||||
var step = offset
|
||||
|
||||
val leftLine = line
|
||||
val rightLine = createParallelLine(line, -1 * step) //3
|
||||
var pol = geomFactory.createPolygon(createMar(rightLine, leftLine))
|
||||
|
||||
while (true) {
|
||||
if (!pol.intersects(contour)) {
|
||||
// if(i > 150) throw IllegalArgumentException("Бесконечный цикл нахождения граничных лииний")
|
||||
pol = createParallelPol(pol, step)
|
||||
} else {
|
||||
pol = createParallelPol(pol, -step)
|
||||
if (step == offset / 10)
|
||||
break
|
||||
step = step / 10
|
||||
}
|
||||
}
|
||||
return pol
|
||||
}
|
||||
|
||||
/**
|
||||
* Нахождение ширины региона
|
||||
* @param borderLines граничные линии для контура
|
||||
* @return Возвращает ширину региона между его граничными линиями
|
||||
*/
|
||||
private fun getRegionWidth(borderpols: Pair<Polygon, Polygon>, offset: Double): Double {
|
||||
val firstEnvelope = borderpols.first.envelopeInternal
|
||||
val secondEnvelope = borderpols.second.envelopeInternal
|
||||
|
||||
return (secondEnvelope.maxX - firstEnvelope.maxX) - 1*offset //3
|
||||
}
|
||||
|
||||
/**
|
||||
* Нахождение граничных полигонов
|
||||
* @param flightLine линия полета
|
||||
* @param contour контур региона
|
||||
* @param internalBox описывающий многоугольник
|
||||
* @param offset ширина маршрута
|
||||
* @return Возвращает пару полигонов в порядке: **ЛЕВЫЙ**, **ПРАВЫЙ**
|
||||
*/
|
||||
private fun findBorderPols(
|
||||
flightLine: LineString,
|
||||
contour: Polygon,
|
||||
internalBox: Envelope,
|
||||
offset: Double
|
||||
): Pair<Polygon, Polygon> {
|
||||
val xUp = LineShifter().getPointOnLineY(flightLine, internalBox.maxY) ?: throw IllegalStateException("Ошибка нахождения граничных точек")
|
||||
val xDown = LineShifter()
|
||||
.getPointOnLineY(flightLine, internalBox.minY) ?: throw IllegalStateException("Ошибка нахождения граничных точек")
|
||||
|
||||
val minXValue = min(min(internalBox.minX - xUp.x, internalBox.maxX - xUp.x), min(internalBox.minX - xDown.x, internalBox.maxX - xDown.x))
|
||||
val maxXValue = max(max(internalBox.minX - xUp.x, internalBox.maxX - xUp.x), max(internalBox.minX - xDown.x, internalBox.maxX - xDown.x))
|
||||
|
||||
val leftLine = LineShifter().lineShift(flightLine, minXValue) ?: throw IllegalStateException("Ошибка нахождения граничных точек")
|
||||
val rightLine = LineShifter().lineShift(flightLine, maxXValue) ?: throw IllegalStateException("Ошибка нахождения граничных точек")
|
||||
//двигаем линии ближе к контуру региона
|
||||
//первая линия
|
||||
val firstPol = moveBorderMarToContour(leftLine, contour, offset)
|
||||
//последняя линия
|
||||
val lastPol = moveBorderMarToContour(rightLine, contour, -offset)
|
||||
|
||||
if (writeLog) { log.info("Граничные точки найдены") }
|
||||
|
||||
return Pair(firstPol, lastPol)
|
||||
}
|
||||
|
||||
/**
|
||||
* Построение интервалов
|
||||
* @param mars список маршрутов
|
||||
* @param contour контур региона
|
||||
* @return Возвращает список интервалов
|
||||
*/
|
||||
private fun buildIntervals(
|
||||
mars: List<Geometry>,
|
||||
contour: Polygon
|
||||
): MutableList<Polygon> {
|
||||
var intervals = mutableListOf<Polygon>()
|
||||
for (mar in mars) {
|
||||
val interval = contour.intersection(mar)
|
||||
if(interval is Polygon) {
|
||||
intervals.add(interval)
|
||||
}
|
||||
else if(interval is MultiPolygon) {
|
||||
for (i in 0 until interval.numGeometries) {
|
||||
intervals.add(interval.getGeometryN(i) as Polygon)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return intervals
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание маршрута
|
||||
* @param leftLine левая граничная линия маршрута
|
||||
* @param rightLine правая граничная линия маршрута
|
||||
* @return Возвращает маршрут заключенный между граничными линиями
|
||||
*/
|
||||
private fun createMar(
|
||||
rightLine: LineString,
|
||||
leftLine: LineString
|
||||
): Array<Coordinate>{
|
||||
val coordinates = mutableListOf<Coordinate>()
|
||||
|
||||
for (j in rightLine.coordinates)
|
||||
coordinates.add(Coordinate(j.x, j.y))
|
||||
|
||||
for (j in leftLine.coordinates.size - 1 downTo 0)
|
||||
coordinates.add(Coordinate(leftLine.coordinates[j].x, leftLine.coordinates[j].y))
|
||||
|
||||
coordinates.add(Coordinate(rightLine.coordinates[0].x, rightLine.coordinates[0].y))
|
||||
return coordinates.toTypedArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* Построение маршрутов
|
||||
* @param width ширина региона
|
||||
* @param lines граничные линии первого маршрута
|
||||
* @param offset ширина маршрута
|
||||
* @param overlap перекрытие маршрутов
|
||||
* @return Возвращает список маршрутов для покрытия региона
|
||||
*/
|
||||
private fun buildMars(
|
||||
width: Double,
|
||||
firstPol: Polygon,
|
||||
offset: Double,
|
||||
overlap: Double
|
||||
): List<Geometry> {
|
||||
val offsetWithOverlap = offset - overlap
|
||||
//расчет минимального количества маршрутов для накрытия полигона
|
||||
var marCount = ((width - offset) / offsetWithOverlap) + 1
|
||||
val intMarCount = ceil(marCount)
|
||||
|
||||
//расчет дополнительного перекрытия
|
||||
val deltaOverlapMars = ((intMarCount * offsetWithOverlap - width) / (intMarCount - 1))//((intMarCount - marCount) / (intMarCount - 1)) * offset
|
||||
|
||||
val marOffset = offsetWithOverlap - deltaOverlapMars // if(deltaOverlapMars > overlap) (offset - deltaOverlapMars) else (offset - overlap)
|
||||
|
||||
var mars = mutableListOf<Geometry>()
|
||||
var marPol = firstPol
|
||||
for (i in 0 until intMarCount.toInt()) {
|
||||
if(i == 0){
|
||||
marPol = createParallelPol(firstPol, offset)
|
||||
}
|
||||
else {
|
||||
val step = marOffset
|
||||
marPol = createParallelPol(marPol, step)
|
||||
}
|
||||
|
||||
mars.add(marPol)
|
||||
}
|
||||
if (writeLog) { log.info("Модель накрытия района выполнена") }
|
||||
return mars
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение интервалов съемки
|
||||
* @param contour контур региона
|
||||
* @param flightLine линия полета
|
||||
* @param offsetKm ширина маршрута в км
|
||||
* @param overlapKm значение минимального перекрытия в км
|
||||
* @return Возвращает список интервалов для заданных параметров
|
||||
*/
|
||||
fun getIntervals(
|
||||
contour: Polygon,
|
||||
flightLine: LineString,
|
||||
offsetKm: Double,
|
||||
overlapKm: Double
|
||||
): List<Polygon> {
|
||||
//TODO(Исправить определение граничных точек района.
|
||||
// При построении интервалов некоторая часть района(граничная слева/справа) пропадает.)
|
||||
//расчет описывающего прямоугольника
|
||||
val internalBox = getBoxInternal(contour)
|
||||
|
||||
if (internalBox.maxY > 80.0 || internalBox.minY < -80.0) {
|
||||
return emptyList()
|
||||
//TODO("Такой район пока что не обрабатывается, нужно определить способ построения интервалов для таких районов")
|
||||
}
|
||||
|
||||
//получаем минимальную широту
|
||||
val minY = closerToZero(internalBox.maxY, internalBox.minY)
|
||||
|
||||
//находим перекрытие и ширину маршрута учитывая широту
|
||||
val offset = getCaptureDistanceInKm(offsetKm, minY)
|
||||
val overlap = getCaptureDistanceInKm(overlapKm, minY)
|
||||
|
||||
//если ширина контура не больше чем ширина маршрута - возвращаем контур как интервал
|
||||
if((internalBox.maxX - internalBox.minX) < offset) {
|
||||
if (writeLog) { log.info("Контур района менее контура маршрута") }
|
||||
return listOf(contour)
|
||||
}
|
||||
|
||||
try {
|
||||
//ищем граничные линии для контура
|
||||
val borderPols = findBorderPols(flightLine, contour, internalBox, offset)
|
||||
|
||||
//строим маршруты
|
||||
val mars = buildMars(getRegionWidth(borderPols, offset), borderPols.first, offset, overlap)
|
||||
|
||||
return buildIntervals(mars, contour)
|
||||
}catch (e: Exception){
|
||||
log.info(e.toString())
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package space.nstart.pcp.complan.apl
|
||||
|
||||
import org.locationtech.jts.geom.Coordinate
|
||||
import org.locationtech.jts.geom.CoordinateFilter
|
||||
import org.locationtech.jts.geom.LineSegment
|
||||
import org.locationtech.jts.geom.LineString
|
||||
import kotlin.collections.get
|
||||
|
||||
/**
|
||||
* Класс для "сдвига" линии на заданную координату по X, не меняя направление сегментов
|
||||
* Поиск значений координат на заданном отрезке по X и Y
|
||||
* */
|
||||
class LineShifter {
|
||||
|
||||
/**
|
||||
* Сдвигает линию на заданное значение X
|
||||
* Если линия состоит менее чем из 2-х точек, возвращает null
|
||||
* */
|
||||
fun lineShift(line : LineString, xValue : Double) : LineString? {
|
||||
if (line.coordinates.size < 2) { return null }
|
||||
|
||||
val copy = line.copy() as LineString
|
||||
|
||||
// Применяем фильтр, который конвертирует радианы → градусы
|
||||
copy.apply(CoordinateFilter { coord ->
|
||||
coord.x = coord.x + xValue // долгота
|
||||
coord.y = coord.y // широта
|
||||
})
|
||||
|
||||
return copy
|
||||
}
|
||||
|
||||
/**
|
||||
* Находит первую точку на полилинии, лежащую на заданной координате Y
|
||||
* Возвращает null, если значение Y лежит вне проекции ее на отрезок
|
||||
* Например отрезок LineSegment(Coordinate(x=0.0,y=0.0), Coordinate(x=10.0,y=5.0))
|
||||
* y должен принадлежать [0.0, 5.0], иначе null
|
||||
* */
|
||||
fun getPointOnLineY(line : LineString, yValue : Double) : Coordinate? {
|
||||
if (line.coordinates.size < 2) { return null }
|
||||
for (i in 1..<line.coordinates.count()) {
|
||||
val lineSegment = LineSegment(line.coordinates[i-1], line.coordinates[i])
|
||||
val needPoint = getPointOnLineY(lineSegment, yValue)
|
||||
if (needPoint != null) {
|
||||
return needPoint
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
/**
|
||||
* Находит точку на отрезке, лежащую на заданной координате Y
|
||||
* Возвращает null, если значение Y лежит вне проекции ее на отрезок
|
||||
* Например отрезок LineSegment(Coordinate(x=0.0,y=0.0), Coordinate(x=10.0,y=5.0))
|
||||
* y должен принадлежать [0.0, 5.0], иначе null
|
||||
* */
|
||||
private fun getPointOnLineY(line : LineSegment, yValue : Double) : Coordinate? {
|
||||
// Случай: отрезок горизонтальный
|
||||
if (line.p0.y == line.p1.y) {
|
||||
return if (yValue == line.p0.y) {
|
||||
// Можно вернуть среднюю X или любую в диапазоне
|
||||
Coordinate((line.p0.x + line.p1.x) / 2.0, yValue)
|
||||
} else {
|
||||
null // нет пересечения
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем, лежит ли targetY между y1 и y2
|
||||
val minY = minOf(line.p0.y, line.p1.y)
|
||||
val maxY = maxOf(line.p0.y, line.p1.y)
|
||||
|
||||
if (yValue !in minY..maxY) {
|
||||
return null // Y вне отрезка
|
||||
}
|
||||
|
||||
// Линейная интерполяция
|
||||
val t = (yValue - line.p0.y) / (line.p1.y - line.p0.y)
|
||||
return Coordinate(line.p0.x + t * (line.p1.x - line.p0.x), yValue)
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package space.nstart.pcp.complan.apl
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.stereotype.Service
|
||||
import space.nstart.pcp.complan.types.ObservationParametersDebugFileDTO
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.util.UUID
|
||||
|
||||
private val DEFAULT_OBSERVATION_PARAMETERS_OUTPUT_DIR =
|
||||
"/home/dsolovyov/ws/264/pcp/services/pcp-dynamic-plan-service/obsParams"
|
||||
|
||||
@Service
|
||||
class ObservationParametersDebugFileWriter(
|
||||
private val objectMapper: ObjectMapper,
|
||||
|
||||
@param:Value("\${complex-plan.observation-parameters.debug-output-dir:/home/dsolovyov/ws/264/pcp/services/pcp-dynamic-plan-service/obsParams}")
|
||||
private val outputDir: String = DEFAULT_OBSERVATION_PARAMETERS_OUTPUT_DIR
|
||||
) {
|
||||
fun write(artifact: ObservationParametersDebugFileDTO): Path {
|
||||
val directory = Path.of(outputDir).toAbsolutePath().normalize()
|
||||
Files.createDirectories(directory)
|
||||
|
||||
val file = filePathFor(artifact.requestId)
|
||||
objectMapper.writerWithDefaultPrettyPrinter().writeValue(file.toFile(), artifact)
|
||||
return file
|
||||
}
|
||||
|
||||
fun read(file: Path): ObservationParametersDebugFileDTO =
|
||||
objectMapper.readValue(file.toFile())
|
||||
|
||||
fun readByRequestId(requestId: UUID): ObservationParametersDebugFileDTO =
|
||||
read(filePathFor(requestId))
|
||||
|
||||
fun filePathFor(requestId: UUID): Path {
|
||||
val directory = Path.of(outputDir).toAbsolutePath().normalize()
|
||||
return directory.resolve("observation-parameters-$requestId.json")
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
package space.nstart.pcp.complan.apl
|
||||
|
||||
import ballistics.flightLine.PointOnEarthCalculator
|
||||
import ballistics.types.EarthType
|
||||
import ballistics.types.Orientation
|
||||
import ballistics.types.OrbitalPoint
|
||||
import ballistics.types.THBLPoint
|
||||
import ballistics.types.WorkCSType
|
||||
import ballistics.utils.fromDateTime
|
||||
import ballistics.utils.math.Vector3D
|
||||
import org.locationtech.jts.geom.Coordinate
|
||||
import org.locationtech.jts.geom.GeometryFactory
|
||||
import org.locationtech.jts.io.WKTWriter
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
|
||||
import kotlin.math.PI
|
||||
|
||||
@Service
|
||||
class SurveyContourBuilder {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||
private val geometryFactory = GeometryFactory()
|
||||
private val wktWriter = WKTWriter()
|
||||
|
||||
fun build(points: List<OrbPointDTO>, roll: Double, capture: Double): String? {
|
||||
val orderedPoints = points.sortedBy { it.time }
|
||||
if (orderedPoints.size < 2) {
|
||||
logger.warn("Контур маршрута не построен: недостаточно точек орбиты, points={}", orderedPoints.size)
|
||||
return null
|
||||
}
|
||||
|
||||
return try {
|
||||
val pointOnEarthCalculator = PointOnEarthCalculator(EarthType.PZ90d02, WorkCSType.WCSOrbit)
|
||||
val rightCoordinates = ArrayList<Coordinate>(orderedPoints.size)
|
||||
val leftCoordinates = ArrayList<Coordinate>(orderedPoints.size)
|
||||
|
||||
orderedPoints.map { it.toOrbitalPoint() }.forEach { point ->
|
||||
val rightPoint = viewParams(pointOnEarthCalculator, point, roll + capture)
|
||||
?: THBLPoint(0.0, 0.0, 0.0, 0.0)
|
||||
rightCoordinates += Coordinate(rightPoint.long.toDegrees(), rightPoint.lat.toDegrees(), 0.0)
|
||||
|
||||
val leftPoint = viewParams(pointOnEarthCalculator, point, roll - capture)
|
||||
?: THBLPoint(0.0, 0.0, 0.0, 0.0)
|
||||
leftCoordinates += Coordinate(leftPoint.long.toDegrees(), leftPoint.lat.toDegrees(), 0.0)
|
||||
}
|
||||
|
||||
val shellCoordinates = ArrayList<Coordinate>(rightCoordinates.size + leftCoordinates.size + 1)
|
||||
shellCoordinates.addAll(rightCoordinates)
|
||||
for (index in leftCoordinates.indices.reversed()) {
|
||||
shellCoordinates += leftCoordinates[index]
|
||||
}
|
||||
if (shellCoordinates.isNotEmpty()) {
|
||||
shellCoordinates += Coordinate(shellCoordinates.first().x, shellCoordinates.first().y, 0.0)
|
||||
}
|
||||
|
||||
val distinctVertices = shellCoordinates
|
||||
.dropLast(1)
|
||||
.map { it.x to it.y }
|
||||
.distinct()
|
||||
if (shellCoordinates.size < 4 || distinctVertices.size < 3) {
|
||||
logger.warn("Контур маршрута не построен: недостаточно уникальных точек полигона")
|
||||
return null
|
||||
}
|
||||
|
||||
val polygon = geometryFactory.createPolygon(shellCoordinates.toTypedArray())
|
||||
wktWriter.write(polygon)
|
||||
} catch (error: Exception) {
|
||||
logger.warn("Контур маршрута не построен: {}", error.message)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun viewParams(
|
||||
pointOnEarthCalculator: PointOnEarthCalculator,
|
||||
point: OrbitalPoint,
|
||||
gamma: Double
|
||||
): THBLPoint? = pointOnEarthCalculator.pointOnEarth(point, Orientation(0.0, gamma.toRadians(), 0.0))
|
||||
|
||||
private fun OrbPointDTO.toOrbitalPoint() = OrbitalPoint(
|
||||
t = fromDateTime(time),
|
||||
vit = revolution.toInt(),
|
||||
r = Vector3D(x, y, z),
|
||||
v = Vector3D(vx, vy, vz)
|
||||
)
|
||||
|
||||
private fun Double.toDegrees(): Double = this * 180.0 / PI
|
||||
|
||||
private fun Double.toRadians(): Double = this * PI / 180.0
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package space.nstart.pcp.complan.config
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@Configuration
|
||||
class DynamicPlanExecutorConfig {
|
||||
|
||||
@Bean(destroyMethod = "shutdown")
|
||||
fun dynamicPlanExecutor(): ExecutorService =
|
||||
Executors.newSingleThreadExecutor { task ->
|
||||
Thread(task, "dynamic-plan-calculation").apply {
|
||||
isDaemon = false
|
||||
}
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package space.nstart.pcp.complan.config
|
||||
|
||||
import org.apache.kafka.clients.consumer.ConsumerConfig
|
||||
import org.apache.kafka.common.serialization.StringDeserializer
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory
|
||||
import org.springframework.kafka.core.ConsumerFactory
|
||||
import org.springframework.kafka.core.DefaultKafkaConsumerFactory
|
||||
|
||||
@ConditionalOnProperty(name = ["spring.kafka.bootstrap-servers"], matchIfMissing = false)
|
||||
@Configuration
|
||||
class KafkaConfig {
|
||||
|
||||
@Value("\${spring.kafka.bootstrap-servers}")
|
||||
private lateinit var bootstrapServers: String
|
||||
|
||||
@Value("\${spring.kafka.consumer.group-id}")
|
||||
private lateinit var groupId: String
|
||||
|
||||
@Bean
|
||||
fun consumerFactory(): ConsumerFactory<String, String> {
|
||||
val props = hashMapOf<String, Any>()
|
||||
props[ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG] = bootstrapServers
|
||||
props[ConsumerConfig.GROUP_ID_CONFIG] = groupId
|
||||
props[ConsumerConfig.AUTO_OFFSET_RESET_CONFIG] = "latest"
|
||||
props[ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG] = false
|
||||
props[ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG] = StringDeserializer::class.java
|
||||
props[ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG] = StringDeserializer::class.java
|
||||
|
||||
return DefaultKafkaConsumerFactory(props)
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun kafkaListenerContainerFactory(): ConcurrentKafkaListenerContainerFactory<String, String> =
|
||||
ConcurrentKafkaListenerContainerFactory<String, String>()
|
||||
.apply { setConsumerFactory(consumerFactory()) }
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package space.nstart.pcp.complan.db.entity
|
||||
|
||||
import jakarta.persistence.Entity
|
||||
import jakarta.persistence.GeneratedValue
|
||||
import jakarta.persistence.GenerationType
|
||||
import jakarta.persistence.Id
|
||||
import jakarta.persistence.Column
|
||||
import jakarta.persistence.Table
|
||||
|
||||
@Table(name = "aplsettings")
|
||||
@Entity
|
||||
class AplSettingsEntity (
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "id")
|
||||
var id : Int? = null,
|
||||
|
||||
@Column(name = "settings")
|
||||
var settings : String
|
||||
) {
|
||||
constructor() : this(settings = "")
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package space.nstart.pcp.complan.db.entity
|
||||
|
||||
import jakarta.persistence.Column
|
||||
import jakarta.persistence.Entity
|
||||
import jakarta.persistence.EnumType
|
||||
import jakarta.persistence.Enumerated
|
||||
import jakarta.persistence.FetchType
|
||||
import jakarta.persistence.GeneratedValue
|
||||
import jakarta.persistence.GenerationType
|
||||
import jakarta.persistence.Id
|
||||
import jakarta.persistence.JoinColumn
|
||||
import jakarta.persistence.ManyToOne
|
||||
import jakarta.persistence.Table
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
|
||||
import java.util.UUID
|
||||
|
||||
@Table(name = "dynamic_plan_interval")
|
||||
@Entity
|
||||
class DynamicPlanIntervalEntity(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "id")
|
||||
var id: Long? = null,
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "run_id", nullable = false)
|
||||
var run: DynamicPlanRunEntity? = null,
|
||||
|
||||
@Column(name = "request_id", nullable = false)
|
||||
var requestId: UUID? = null,
|
||||
|
||||
@Column(name = "interval_id", nullable = false)
|
||||
var intervalId: UUID? = null,
|
||||
|
||||
@Column(name = "region_id", nullable = false)
|
||||
var regionId: UUID? = null,
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "rev_sign", nullable = false)
|
||||
var revSign: RevolutionSign = RevolutionSign.ASC,
|
||||
|
||||
@Column(name = "contour_wkt", nullable = false, columnDefinition = "TEXT")
|
||||
var contourWkt: String = "",
|
||||
|
||||
@Column(name = "observation_parameters_count", nullable = false)
|
||||
var observationParametersCount: Int = 0,
|
||||
|
||||
@Column(name = "interval_order", nullable = false)
|
||||
var intervalOrder: Int = 0
|
||||
) {
|
||||
constructor() : this(id = null)
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package space.nstart.pcp.complan.db.entity
|
||||
|
||||
import jakarta.persistence.Column
|
||||
import jakarta.persistence.Entity
|
||||
import jakarta.persistence.FetchType
|
||||
import jakarta.persistence.GeneratedValue
|
||||
import jakarta.persistence.GenerationType
|
||||
import jakarta.persistence.Id
|
||||
import jakarta.persistence.JoinColumn
|
||||
import jakarta.persistence.ManyToOne
|
||||
import jakarta.persistence.Table
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
|
||||
@Table(name = "dynamic_plan_route")
|
||||
@Entity
|
||||
class DynamicPlanRouteEntity(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "id")
|
||||
var id: Long? = null,
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "run_id", nullable = false)
|
||||
var run: DynamicPlanRunEntity? = null,
|
||||
|
||||
@Column(name = "request_id", nullable = false)
|
||||
var requestId: UUID? = null,
|
||||
|
||||
@Column(name = "satellite_id", nullable = false)
|
||||
var satelliteId: Long = 0,
|
||||
|
||||
@Column(name = "start_time", nullable = false)
|
||||
var startTime: LocalDateTime = LocalDateTime.now(),
|
||||
|
||||
@Column(name = "end_time", nullable = false)
|
||||
var endTime: LocalDateTime = LocalDateTime.now(),
|
||||
|
||||
@Column(name = "duration", nullable = false)
|
||||
var duration: Double = 0.0,
|
||||
|
||||
@Column(name = "revolution", nullable = false)
|
||||
var revolution: Long = 0,
|
||||
|
||||
@Column(name = "roll", nullable = false)
|
||||
var roll: Double = 0.0,
|
||||
|
||||
@Column(name = "contour_wkt", nullable = false, columnDefinition = "TEXT")
|
||||
var contourWkt: String = "",
|
||||
|
||||
@Column(name = "route_order", nullable = false)
|
||||
var routeOrder: Int = 0
|
||||
) {
|
||||
constructor() : this(id = null)
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package space.nstart.pcp.complan.db.entity
|
||||
|
||||
import jakarta.persistence.Column
|
||||
import jakarta.persistence.Entity
|
||||
import jakarta.persistence.EnumType
|
||||
import jakarta.persistence.Enumerated
|
||||
import jakarta.persistence.Id
|
||||
import jakarta.persistence.Table
|
||||
import space.nstart.pcp.complan.types.DynamicPlanCalculationMode
|
||||
import space.nstart.pcp.complan.types.DynamicPlanRevolutionMode
|
||||
import space.nstart.pcp.complan.types.DynamicPlanRunStatus
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
|
||||
@Table(name = "dynamic_plan_run")
|
||||
@Entity
|
||||
class DynamicPlanRunEntity(
|
||||
@Id
|
||||
@Column(name = "id", nullable = false)
|
||||
var id: UUID? = null,
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "status", nullable = false)
|
||||
var status: DynamicPlanRunStatus = DynamicPlanRunStatus.PENDING,
|
||||
|
||||
@Column(name = "request_id", nullable = false)
|
||||
var requestId: UUID? = null,
|
||||
|
||||
@Column(name = "satellite_ids", nullable = false)
|
||||
var satelliteIds: String = "[]",
|
||||
|
||||
@Column(name = "calculation_start", nullable = false)
|
||||
var calculationStart: LocalDateTime? = null,
|
||||
|
||||
@Column(name = "calculation_end", nullable = false)
|
||||
var calculationEnd: LocalDateTime? = null,
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "calculation_mode", nullable = false)
|
||||
var calculationMode: DynamicPlanCalculationMode = DynamicPlanCalculationMode.FULL,
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "revolution_mode", nullable = false)
|
||||
var revolutionMode: DynamicPlanRevolutionMode = DynamicPlanRevolutionMode.BOTH,
|
||||
|
||||
@Column(name = "filter_covered_routes", nullable = false)
|
||||
var filterCoveredRoutes: Boolean = false,
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
var createdAt: LocalDateTime = LocalDateTime.now(),
|
||||
|
||||
@Column(name = "started_at")
|
||||
var startedAt: LocalDateTime? = null,
|
||||
|
||||
@Column(name = "finished_at")
|
||||
var finishedAt: LocalDateTime? = null,
|
||||
|
||||
@Column(name = "duration_ms")
|
||||
var durationMs: Long? = null,
|
||||
|
||||
@Column(name = "result_status")
|
||||
var resultStatus: String? = null,
|
||||
|
||||
@Column(name = "routes_count")
|
||||
var routesCount: Int? = null,
|
||||
|
||||
@Column(name = "result_json", columnDefinition = "TEXT")
|
||||
var resultJson: String? = null,
|
||||
|
||||
@Column(name = "error_message", columnDefinition = "TEXT")
|
||||
var errorMessage: String? = null
|
||||
) {
|
||||
constructor() : this(id = null)
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package space.nstart.pcp.complan.db.repository
|
||||
|
||||
import space.nstart.pcp.complan.db.entity.AplSettingsEntity
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
|
||||
@Repository
|
||||
@Transactional
|
||||
interface AplSettingsRepository: JpaRepository<space.nstart.pcp.complan.db.entity.AplSettingsEntity, Long> {
|
||||
fun findFirstByOrderByIdDesc() : space.nstart.pcp.complan.db.entity.AplSettingsEntity?
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package space.nstart.pcp.complan.db.repository
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
import org.springframework.data.repository.query.Param
|
||||
import org.springframework.stereotype.Repository
|
||||
import space.nstart.pcp.complan.db.entity.DynamicPlanIntervalEntity
|
||||
import java.util.UUID
|
||||
|
||||
@Repository
|
||||
interface DynamicPlanIntervalRepository : JpaRepository<DynamicPlanIntervalEntity, Long> {
|
||||
@Query(
|
||||
value = """
|
||||
select *
|
||||
from dynamic_plan_interval
|
||||
where run_id = :runId
|
||||
order by interval_order asc, id asc
|
||||
limit :limit offset :offset
|
||||
""",
|
||||
nativeQuery = true
|
||||
)
|
||||
fun findIntervalsByRunId(
|
||||
@Param("runId") runId: UUID,
|
||||
@Param("limit") limit: Int,
|
||||
@Param("offset") offset: Int
|
||||
): List<DynamicPlanIntervalEntity>
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package space.nstart.pcp.complan.db.repository
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.Modifying
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
import org.springframework.data.repository.query.Param
|
||||
import org.springframework.stereotype.Repository
|
||||
import space.nstart.pcp.complan.db.entity.DynamicPlanRouteEntity
|
||||
import java.util.UUID
|
||||
|
||||
@Repository
|
||||
interface DynamicPlanRouteRepository : JpaRepository<DynamicPlanRouteEntity, Long> {
|
||||
fun countBySatelliteId(satelliteId: Long): Long
|
||||
|
||||
@Modifying
|
||||
@Query("delete from DynamicPlanRouteEntity route where route.satelliteId = :satelliteId")
|
||||
fun deleteAllBySatelliteId(@Param("satelliteId") satelliteId: Long): Int
|
||||
|
||||
@Query(
|
||||
value = """
|
||||
select *
|
||||
from dynamic_plan_route
|
||||
where run_id = :runId
|
||||
order by start_time asc, id asc
|
||||
limit :limit offset :offset
|
||||
""",
|
||||
nativeQuery = true
|
||||
)
|
||||
fun findRoutesByRunId(
|
||||
@Param("runId") runId: UUID,
|
||||
@Param("limit") limit: Int,
|
||||
@Param("offset") offset: Int
|
||||
): List<DynamicPlanRouteEntity>
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package space.nstart.pcp.complan.db.repository
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.Modifying
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
import org.springframework.data.repository.query.Param
|
||||
import org.springframework.stereotype.Repository
|
||||
import space.nstart.pcp.complan.db.entity.DynamicPlanRunEntity
|
||||
import space.nstart.pcp.complan.types.DynamicPlanRunStatus
|
||||
import java.util.UUID
|
||||
|
||||
@Repository
|
||||
interface DynamicPlanRunRepository : JpaRepository<DynamicPlanRunEntity, UUID> {
|
||||
|
||||
@Query(
|
||||
value = """
|
||||
select count(*)
|
||||
from dynamic_plan_run run
|
||||
where exists (
|
||||
select 1
|
||||
from jsonb_array_elements_text(run.satellite_ids::jsonb) satellite_id(value)
|
||||
where satellite_id.value::bigint = :satelliteId
|
||||
)
|
||||
""",
|
||||
nativeQuery = true
|
||||
)
|
||||
fun countBySatelliteId(@Param("satelliteId") satelliteId: Long): Long
|
||||
|
||||
@Modifying
|
||||
@Query(
|
||||
value = """
|
||||
delete from dynamic_plan_run run
|
||||
where exists (
|
||||
select 1
|
||||
from jsonb_array_elements_text(run.satellite_ids::jsonb) satellite_id(value)
|
||||
where satellite_id.value::bigint = :satelliteId
|
||||
)
|
||||
""",
|
||||
nativeQuery = true
|
||||
)
|
||||
fun deleteAllBySatelliteId(@Param("satelliteId") satelliteId: Long): Int
|
||||
|
||||
@Query(
|
||||
value = """
|
||||
select *
|
||||
from dynamic_plan_run
|
||||
order by created_at desc
|
||||
limit :limit offset :offset
|
||||
""",
|
||||
nativeQuery = true
|
||||
)
|
||||
fun findRuns(
|
||||
@Param("limit") limit: Int,
|
||||
@Param("offset") offset: Int
|
||||
): List<DynamicPlanRunEntity>
|
||||
|
||||
@Query(
|
||||
value = """
|
||||
select *
|
||||
from dynamic_plan_run
|
||||
where status = :status
|
||||
order by created_at desc
|
||||
limit :limit offset :offset
|
||||
""",
|
||||
nativeQuery = true
|
||||
)
|
||||
fun findRunsByStatus(
|
||||
@Param("status") status: String,
|
||||
@Param("limit") limit: Int,
|
||||
@Param("offset") offset: Int
|
||||
): List<DynamicPlanRunEntity>
|
||||
|
||||
fun findAllByStatusIn(statuses: Collection<DynamicPlanRunStatus>): List<DynamicPlanRunEntity>
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package space.nstart.pcp.complan.service
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
import org.springframework.http.HttpStatus
|
||||
import space.nstart.pcp.complan.db.repository.AplSettingsRepository
|
||||
import space.nstart.pcp.complan.settings.AplSettings
|
||||
import space.nstart.pcp.complan.settings.SettingsDTO
|
||||
|
||||
@Service
|
||||
class AplSettingsLoader(
|
||||
private val aplSetRep: AplSettingsRepository,
|
||||
private val aplSettings: AplSettings,
|
||||
private val objectMapper: ObjectMapper
|
||||
) {
|
||||
private val log = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun loadLatestSettings() {
|
||||
val settingsEntity = aplSetRep.findFirstByOrderByIdDesc()
|
||||
if (settingsEntity == null) {
|
||||
log.info("APL settings were not found in DB, current in-memory settings will be used")
|
||||
return
|
||||
}
|
||||
|
||||
aplSettings.settings = try {
|
||||
objectMapper.readValue(settingsEntity.settings, SettingsDTO::class.java)
|
||||
} catch (error: Exception) {
|
||||
throw ResponseStatusException(
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
"Failed to read APL settings from DB record id=${settingsEntity.id}",
|
||||
error
|
||||
)
|
||||
}
|
||||
log.info("APL settings loaded from DB record id={}", settingsEntity.id)
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
package space.nstart.pcp.complan.service
|
||||
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.core.ParameterizedTypeReference
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.web.client.RestClient
|
||||
import org.springframework.web.util.UriComponentsBuilder
|
||||
import space.nstart.pcp.complan.types.ObjViewRequestDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ExactTimePositionRequestDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.FlightLineDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ObjDTO as BallisticsObjDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ObjViewRequestDTO as BallisticsObjViewRequestDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteOrbitAvailabilityDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamDTO
|
||||
import java.time.LocalDateTime
|
||||
|
||||
/**Получение данных от сервиса баллистики*/
|
||||
@Service
|
||||
class BallTaskServce {
|
||||
|
||||
|
||||
@Value("\${complex-plan.ballistics-service-url}")
|
||||
private lateinit var ballisticsServiceUrl: String
|
||||
|
||||
fun getOrbitAvailability(noradId : Long): SatelliteOrbitAvailabilityDTO =
|
||||
RestClient.create()
|
||||
.get()
|
||||
.uri(buildBallisticsUri("/api/satellites/orbit/availability/$noradId"))
|
||||
.retrieve()
|
||||
.body(SatelliteOrbitAvailabilityDTO::class.java)
|
||||
?: throw IllegalStateException("Пустой ответ баллистики для доступности ПДЦМ КА $noradId")
|
||||
|
||||
fun getFlightLine(noradId: Long, timeStart : LocalDateTime, timeStop : LocalDateTime): List<FlightLineDTO> =
|
||||
RestClient.create()
|
||||
.get()
|
||||
.uri(
|
||||
UriComponentsBuilder
|
||||
.fromUriString(ballisticsServiceUrl)
|
||||
.path("/api/satellites/$noradId/flight-line")
|
||||
.queryParam("time_start", timeStart)
|
||||
.queryParam("time_stop", timeStop)
|
||||
.toUriString()
|
||||
)
|
||||
.retrieve()
|
||||
.body(FLIGHT_LINE_LIST_TYPE)
|
||||
.orEmpty()
|
||||
|
||||
fun getExactTime(noradId: Long, request: ExactTimePositionRequestDTO): List<OrbPointDTO> =
|
||||
RestClient.create()
|
||||
.post()
|
||||
.uri(buildBallisticsUri("/api/satellites/$noradId/extract-time"))
|
||||
.body(request)
|
||||
.retrieve()
|
||||
.body(ORB_POINT_LIST_TYPE)
|
||||
.orEmpty()
|
||||
|
||||
fun getMplForObjects(objects : ObjViewRequestDTO): List<SquareViewParamDTO> {
|
||||
val satellites = objects.satellites?.toList().orEmpty()
|
||||
val timeStart = objects.timeStart ?: throw IllegalArgumentException("timeStart must be specified for MPL request")
|
||||
val timeStop = objects.timeStop ?: throw IllegalArgumentException("timeStop must be specified for MPL request")
|
||||
|
||||
return objects.objects.flatMap { obj ->
|
||||
RestClient.create()
|
||||
.post()
|
||||
.uri(buildBallisticsUri("/api/obj-view/mpl-square"))
|
||||
.body(
|
||||
BallisticsObjViewRequestDTO(
|
||||
satellites = satellites,
|
||||
timeStart = timeStart,
|
||||
timeStop = timeStop,
|
||||
obj = BallisticsObjDTO(
|
||||
id = obj.id.toString(),
|
||||
position = obj.position,
|
||||
contourWKT = obj.contourWKT
|
||||
),
|
||||
sunAngleMin = objects.sunAngleMin
|
||||
)
|
||||
)
|
||||
.retrieve()
|
||||
.body(SQUARE_VIEW_PARAM_LIST_TYPE)
|
||||
.orEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildBallisticsUri(path: String): String =
|
||||
UriComponentsBuilder
|
||||
.fromUriString(ballisticsServiceUrl)
|
||||
.path(path)
|
||||
.toUriString()
|
||||
|
||||
private companion object {
|
||||
val FLIGHT_LINE_LIST_TYPE = object : ParameterizedTypeReference<List<FlightLineDTO>>() {}
|
||||
val ORB_POINT_LIST_TYPE = object : ParameterizedTypeReference<List<OrbPointDTO>>() {}
|
||||
val SQUARE_VIEW_PARAM_LIST_TYPE = object : ParameterizedTypeReference<List<SquareViewParamDTO>>() {}
|
||||
}
|
||||
}
|
||||
+324
@@ -0,0 +1,324 @@
|
||||
package space.nstart.pcp.complan.service
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.support.TransactionTemplate
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
import space.nstart.pcp.complan.apl.BasicPlanCalculator
|
||||
import space.nstart.pcp.complan.db.entity.DynamicPlanIntervalEntity
|
||||
import space.nstart.pcp.complan.db.entity.DynamicPlanRouteEntity
|
||||
import space.nstart.pcp.complan.db.entity.DynamicPlanRunEntity
|
||||
import space.nstart.pcp.complan.db.repository.DynamicPlanIntervalRepository
|
||||
import space.nstart.pcp.complan.db.repository.DynamicPlanRouteRepository
|
||||
import space.nstart.pcp.complan.db.repository.DynamicPlanRunRepository
|
||||
import space.nstart.pcp.complan.types.ComplexPlanCalculationOutput
|
||||
import space.nstart.pcp.complan.types.ComplexPlanCalculationRequestDTO
|
||||
import space.nstart.pcp.complan.types.ComplexPlanCalculationResultDTO
|
||||
import space.nstart.pcp.complan.types.DynamicPlanIntervalDTO
|
||||
import space.nstart.pcp.complan.types.DynamicPlanRouteDTO
|
||||
import space.nstart.pcp.complan.types.DynamicPlanRunDTO
|
||||
import space.nstart.pcp.complan.types.DynamicPlanRunStatus
|
||||
import space.nstart.pcp.complan.types.IntervalItemDTO
|
||||
import space.nstart.pcp.complan.types.SurveyContour
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ExecutorService
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
@Service
|
||||
class DynamicPlanRunService(
|
||||
private val runRepository: DynamicPlanRunRepository,
|
||||
private val routeRepository: DynamicPlanRouteRepository,
|
||||
private val intervalRepository: DynamicPlanIntervalRepository,
|
||||
private val basePlan: BasicPlanCalculator,
|
||||
private val aplSettingsLoader: AplSettingsLoader,
|
||||
private val objectMapper: ObjectMapper,
|
||||
private val transactionTemplate: TransactionTemplate,
|
||||
private val dynamicPlanExecutor: ExecutorService
|
||||
) {
|
||||
private val log = LoggerFactory.getLogger(this::class.java)
|
||||
private val satelliteIdsType = object : TypeReference<List<Long>>() {}
|
||||
|
||||
fun startCalculation(request: ComplexPlanCalculationRequestDTO): DynamicPlanRunDTO {
|
||||
validateRequest(request)
|
||||
|
||||
val runId = UUID.randomUUID()
|
||||
val createdRun = transactionTemplate.execute {
|
||||
runRepository.save(
|
||||
DynamicPlanRunEntity(
|
||||
id = runId,
|
||||
status = DynamicPlanRunStatus.PENDING,
|
||||
requestId = request.requestId,
|
||||
satelliteIds = objectMapper.writeValueAsString(request.satelliteIds.distinct()),
|
||||
calculationStart = request.calculationStart,
|
||||
calculationEnd = request.calculationEnd,
|
||||
calculationMode = request.calculationMode,
|
||||
revolutionMode = request.revolutionMode,
|
||||
filterCoveredRoutes = request.filterCoveredRoutes,
|
||||
createdAt = LocalDateTime.now()
|
||||
)
|
||||
)
|
||||
} ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to create calculation run")
|
||||
|
||||
dynamicPlanExecutor.submit {
|
||||
executeCalculation(runId)
|
||||
}
|
||||
|
||||
return createdRun.toDto()
|
||||
}
|
||||
|
||||
fun getRun(runId: UUID): DynamicPlanRunDTO =
|
||||
findRun(runId).toDto()
|
||||
|
||||
fun listRuns(status: DynamicPlanRunStatus?, limit: Int, offset: Int): List<DynamicPlanRunDTO> {
|
||||
validatePage(limit, offset)
|
||||
val runs = when (status) {
|
||||
null -> runRepository.findRuns(limit, offset)
|
||||
else -> runRepository.findRunsByStatus(status.name, limit, offset)
|
||||
}
|
||||
return runs.map { run -> run.toDto() }
|
||||
}
|
||||
|
||||
fun getResult(runId: UUID): ComplexPlanCalculationResultDTO {
|
||||
val run = findRun(runId)
|
||||
if (run.status != DynamicPlanRunStatus.COMPLETED) {
|
||||
throw ResponseStatusException(HttpStatus.CONFLICT, "Calculation $runId is not completed")
|
||||
}
|
||||
|
||||
val resultJson = run.resultJson
|
||||
?: throw ResponseStatusException(HttpStatus.CONFLICT, "Calculation $runId has no result")
|
||||
|
||||
val resultNode = objectMapper.readTree(resultJson)
|
||||
if (resultNode is ObjectNode && !resultNode.has("calculationMode")) {
|
||||
resultNode.put("calculationMode", run.calculationMode.name)
|
||||
}
|
||||
if (resultNode is ObjectNode && !resultNode.has("revolutionMode")) {
|
||||
resultNode.put("revolutionMode", run.revolutionMode.name)
|
||||
}
|
||||
if (resultNode is ObjectNode && !resultNode.has("filterCoveredRoutes")) {
|
||||
resultNode.put("filterCoveredRoutes", run.filterCoveredRoutes)
|
||||
}
|
||||
|
||||
return objectMapper.treeToValue(resultNode, ComplexPlanCalculationResultDTO::class.java)
|
||||
}
|
||||
|
||||
fun getRoutes(runId: UUID, limit: Int, offset: Int): List<DynamicPlanRouteDTO> {
|
||||
validatePage(limit, offset)
|
||||
val run = findRun(runId)
|
||||
if (run.status != DynamicPlanRunStatus.COMPLETED) {
|
||||
throw ResponseStatusException(HttpStatus.CONFLICT, "Calculation $runId is not completed")
|
||||
}
|
||||
return routeRepository.findRoutesByRunId(runId, limit, offset)
|
||||
.map { route -> route.toDto() }
|
||||
}
|
||||
|
||||
fun getIntervals(runId: UUID, limit: Int, offset: Int): List<DynamicPlanIntervalDTO> {
|
||||
validatePage(limit, offset)
|
||||
val run = findRun(runId)
|
||||
if (run.status != DynamicPlanRunStatus.COMPLETED) {
|
||||
throw ResponseStatusException(HttpStatus.CONFLICT, "Calculation $runId is not completed")
|
||||
}
|
||||
return intervalRepository.findIntervalsByRunId(runId, limit, offset)
|
||||
.map { interval -> interval.toDto() }
|
||||
}
|
||||
|
||||
fun recoverInterruptedRuns() {
|
||||
val recoveredCount = transactionTemplate.execute {
|
||||
val runs = runRepository.findAllByStatusIn(
|
||||
listOf(DynamicPlanRunStatus.PENDING, DynamicPlanRunStatus.RUNNING)
|
||||
)
|
||||
if (runs.isEmpty()) {
|
||||
return@execute 0
|
||||
}
|
||||
|
||||
val finishedAt = LocalDateTime.now()
|
||||
runs.forEach { run ->
|
||||
run.status = DynamicPlanRunStatus.FAILED
|
||||
run.finishedAt = finishedAt
|
||||
run.errorMessage = "Calculation was interrupted by service restart"
|
||||
}
|
||||
runRepository.saveAll(runs)
|
||||
runs.size
|
||||
} ?: 0
|
||||
|
||||
if (recoveredCount > 0) {
|
||||
log.warn("Recovered {} interrupted dynamic plan runs after startup", recoveredCount)
|
||||
}
|
||||
}
|
||||
|
||||
private fun executeCalculation(runId: UUID) {
|
||||
val request = markRunningAndBuildRequest(runId)
|
||||
if (request == null) {
|
||||
log.warn("Dynamic plan run {} was not found before execution", runId)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
aplSettingsLoader.loadLatestSettings()
|
||||
val output = basePlan.calculate(request)
|
||||
markCompleted(runId, output)
|
||||
} catch (error: Exception) {
|
||||
log.error("Dynamic plan run {} failed", runId, error)
|
||||
markFailed(runId, error)
|
||||
}
|
||||
}
|
||||
|
||||
private fun markRunningAndBuildRequest(runId: UUID): ComplexPlanCalculationRequestDTO? =
|
||||
transactionTemplate.execute {
|
||||
val run = runRepository.findById(runId).orElse(null) ?: return@execute null
|
||||
run.status = DynamicPlanRunStatus.RUNNING
|
||||
run.startedAt = LocalDateTime.now()
|
||||
run.errorMessage = null
|
||||
runRepository.save(run)
|
||||
run.toRequest()
|
||||
}
|
||||
|
||||
private fun markCompleted(runId: UUID, output: ComplexPlanCalculationOutput) {
|
||||
transactionTemplate.execute {
|
||||
val run = runRepository.findById(runId).orElseThrow()
|
||||
val result = output.result
|
||||
run.status = DynamicPlanRunStatus.COMPLETED
|
||||
run.finishedAt = LocalDateTime.now()
|
||||
run.durationMs = result.durationMs
|
||||
run.resultStatus = result.status.name
|
||||
run.routesCount = result.routesCount
|
||||
run.resultJson = objectMapper.writeValueAsString(result)
|
||||
run.errorMessage = null
|
||||
intervalRepository.saveAll(
|
||||
output.intervals.mapIndexed { index, interval -> interval.toEntity(run, result.requestId, index) }
|
||||
)
|
||||
routeRepository.saveAll(output.routes.mapIndexed { index, route -> route.toEntity(run, result.requestId, index) })
|
||||
runRepository.save(run)
|
||||
}
|
||||
}
|
||||
|
||||
private fun markFailed(runId: UUID, error: Exception) {
|
||||
transactionTemplate.execute {
|
||||
val run = runRepository.findById(runId).orElseThrow()
|
||||
run.status = DynamicPlanRunStatus.FAILED
|
||||
run.finishedAt = LocalDateTime.now()
|
||||
run.errorMessage = error.message ?: error::class.java.simpleName
|
||||
runRepository.save(run)
|
||||
}
|
||||
}
|
||||
|
||||
private fun findRun(runId: UUID): DynamicPlanRunEntity =
|
||||
runRepository.findById(runId)
|
||||
.orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Calculation $runId was not found") }
|
||||
|
||||
private fun validateRequest(request: ComplexPlanCalculationRequestDTO) {
|
||||
if (request.calculationEnd <= request.calculationStart) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "calculationEnd must be after calculationStart")
|
||||
}
|
||||
if (request.satelliteIds.isEmpty()) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "satelliteIds must not be empty")
|
||||
}
|
||||
}
|
||||
|
||||
private fun validatePage(limit: Int, offset: Int) {
|
||||
if (limit !in 1..10_000) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "limit must be between 1 and 10000")
|
||||
}
|
||||
if (offset < 0) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "offset must be greater than or equal to 0")
|
||||
}
|
||||
}
|
||||
|
||||
private fun DynamicPlanRunEntity.toRequest(): ComplexPlanCalculationRequestDTO =
|
||||
ComplexPlanCalculationRequestDTO(
|
||||
requestId = requestId ?: throw IllegalStateException("Run $id has no requestId"),
|
||||
satelliteIds = readSatelliteIds(satelliteIds),
|
||||
calculationStart = calculationStart ?: throw IllegalStateException("Run $id has no calculationStart"),
|
||||
calculationEnd = calculationEnd ?: throw IllegalStateException("Run $id has no calculationEnd"),
|
||||
calculationMode = calculationMode,
|
||||
revolutionMode = revolutionMode,
|
||||
filterCoveredRoutes = filterCoveredRoutes
|
||||
)
|
||||
|
||||
private fun DynamicPlanRunEntity.toDto(): DynamicPlanRunDTO =
|
||||
DynamicPlanRunDTO(
|
||||
runId = id ?: throw IllegalStateException("Run has no id"),
|
||||
status = status,
|
||||
requestId = requestId ?: throw IllegalStateException("Run $id has no requestId"),
|
||||
satelliteIds = readSatelliteIds(satelliteIds),
|
||||
calculationStart = calculationStart ?: throw IllegalStateException("Run $id has no calculationStart"),
|
||||
calculationEnd = calculationEnd ?: throw IllegalStateException("Run $id has no calculationEnd"),
|
||||
calculationMode = calculationMode,
|
||||
revolutionMode = revolutionMode,
|
||||
filterCoveredRoutes = filterCoveredRoutes,
|
||||
createdAt = createdAt,
|
||||
startedAt = startedAt,
|
||||
finishedAt = finishedAt,
|
||||
durationMs = durationMs,
|
||||
resultStatus = resultStatus,
|
||||
routesCount = routesCount,
|
||||
errorMessage = errorMessage
|
||||
)
|
||||
|
||||
private fun readSatelliteIds(value: String): List<Long> =
|
||||
objectMapper.readValue(value, satelliteIdsType)
|
||||
|
||||
private fun IntervalItemDTO.toEntity(
|
||||
run: DynamicPlanRunEntity,
|
||||
requestId: UUID,
|
||||
intervalOrder: Int
|
||||
): DynamicPlanIntervalEntity =
|
||||
DynamicPlanIntervalEntity(
|
||||
run = run,
|
||||
requestId = requestId,
|
||||
intervalId = id,
|
||||
regionId = regionId,
|
||||
revSign = revSign,
|
||||
contourWkt = geometry,
|
||||
observationParametersCount = mpl.size,
|
||||
intervalOrder = intervalOrder
|
||||
)
|
||||
|
||||
private fun SurveyContour.toEntity(
|
||||
run: DynamicPlanRunEntity,
|
||||
requestId: UUID,
|
||||
routeOrder: Int
|
||||
): DynamicPlanRouteEntity =
|
||||
DynamicPlanRouteEntity(
|
||||
run = run,
|
||||
requestId = requestId,
|
||||
satelliteId = mpl.noradId,
|
||||
startTime = time,
|
||||
endTime = time.plusNanos((duration * 1_000_000_000.0).roundToLong().coerceAtLeast(1L)),
|
||||
duration = duration,
|
||||
revolution = rev,
|
||||
roll = gamma,
|
||||
contourWkt = geometry,
|
||||
routeOrder = routeOrder
|
||||
)
|
||||
|
||||
private fun DynamicPlanIntervalEntity.toDto(): DynamicPlanIntervalDTO =
|
||||
DynamicPlanIntervalDTO(
|
||||
id = id ?: throw IllegalStateException("Interval has no id"),
|
||||
runId = run?.id ?: throw IllegalStateException("Interval $id has no run id"),
|
||||
requestId = requestId ?: throw IllegalStateException("Interval $id has no request id"),
|
||||
intervalId = intervalId ?: throw IllegalStateException("Interval $id has no interval id"),
|
||||
regionId = regionId ?: throw IllegalStateException("Interval $id has no region id"),
|
||||
revSign = revSign,
|
||||
contourWkt = contourWkt,
|
||||
observationParametersCount = observationParametersCount
|
||||
)
|
||||
|
||||
private fun DynamicPlanRouteEntity.toDto(): DynamicPlanRouteDTO =
|
||||
DynamicPlanRouteDTO(
|
||||
id = id ?: throw IllegalStateException("Route has no id"),
|
||||
runId = run?.id ?: throw IllegalStateException("Route $id has no run id"),
|
||||
requestId = requestId ?: throw IllegalStateException("Route $id has no request id"),
|
||||
satelliteId = satelliteId,
|
||||
startTime = startTime,
|
||||
endTime = endTime,
|
||||
duration = duration,
|
||||
revolution = revolution,
|
||||
roll = roll,
|
||||
contourWkt = contourWkt
|
||||
)
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package space.nstart.pcp.complan.service
|
||||
|
||||
import org.springframework.boot.ApplicationArguments
|
||||
import org.springframework.boot.ApplicationRunner
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class DynamicPlanRunStartupRecovery(
|
||||
private val dynamicPlanRunService: DynamicPlanRunService
|
||||
) : ApplicationRunner {
|
||||
|
||||
override fun run(args: ApplicationArguments) {
|
||||
dynamicPlanRunService.recoverInterruptedRuns()
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package space.nstart.pcp.complan.service
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.web.client.HttpClientErrorException
|
||||
import org.springframework.web.client.RestClient
|
||||
import org.springframework.web.util.UriComponentsBuilder
|
||||
import space.nstart.pcp.complan.types.RequestItemDTO
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.UUID
|
||||
|
||||
/** Получение заявок из pcp-request-service /v1 API. */
|
||||
@Service
|
||||
class RequestServiceClient {
|
||||
@Value("\${complex-plan.request-service-url}")
|
||||
private lateinit var requestServiceUrl: String
|
||||
|
||||
@Value("\${complex-plan.request-selection-path:/v1/requests}")
|
||||
private lateinit var requestSelectionPath: String
|
||||
|
||||
fun getRequest(requestId: UUID): RequestItemDTO? =
|
||||
try {
|
||||
RestClient.create()
|
||||
.get()
|
||||
.uri(
|
||||
UriComponentsBuilder
|
||||
.fromUriString(requestServiceUrl)
|
||||
.path(requestSelectionPath)
|
||||
.path("/{requestId}")
|
||||
.buildAndExpand(requestId)
|
||||
.toUriString()
|
||||
)
|
||||
.retrieve()
|
||||
.body(RequestResponseDto::class.java)
|
||||
?.toRequestItemDto()
|
||||
} catch (_: HttpClientErrorException.NotFound) {
|
||||
null
|
||||
}
|
||||
|
||||
private fun RequestResponseDto.toRequestItemDto(): RequestItemDTO =
|
||||
RequestItemDTO(
|
||||
id = id,
|
||||
name = name,
|
||||
geometry = geometry,
|
||||
intervalBegin = beginDateTime.toLocalDateTime(),
|
||||
intervalEnd = endDateTime.toLocalDateTime(),
|
||||
resolution = resolution(),
|
||||
importance = importance,
|
||||
)
|
||||
|
||||
private fun RequestResponseDto.resolution(): Double =
|
||||
optics?.resolution
|
||||
?: rsa?.resolution
|
||||
?: throw IllegalStateException("Request $id has no survey resolution")
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class RequestResponseDto(
|
||||
val id: UUID,
|
||||
val name: String,
|
||||
val status: String = "",
|
||||
val surveyType: String = "",
|
||||
val geometry: String,
|
||||
val importance: Double,
|
||||
val beginDateTime: OffsetDateTime,
|
||||
val endDateTime: OffsetDateTime,
|
||||
val kpp: List<Int> = emptyList(),
|
||||
val highPriorityTransmit: Boolean = false,
|
||||
val optics: SurveyParamsDto? = null,
|
||||
val rsa: SurveyParamsDto? = null,
|
||||
val coverage: CoverageStateDto = CoverageStateDto(),
|
||||
val createdAt: OffsetDateTime? = null,
|
||||
val updatedAt: OffsetDateTime? = null,
|
||||
val deletedAt: OffsetDateTime? = null,
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class SurveyParamsDto(
|
||||
val resolution: Double,
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class CoverageStateDto(
|
||||
val requiredPercent: Double? = null,
|
||||
val currentPercent: Double = 0.0,
|
||||
)
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package space.nstart.pcp.complan.service
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.core.ParameterizedTypeReference
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.web.client.RestClient
|
||||
import org.springframework.web.util.UriComponentsBuilder
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteSummaryDTO
|
||||
|
||||
/** Получение данных КА из satellite-catalog-service. */
|
||||
@Service
|
||||
class SatelliteCatalogServiceClient {
|
||||
@Value("\${complex-plan.satellite-catalog-service-url}")
|
||||
private lateinit var satelliteCatalogServiceUrl: String
|
||||
|
||||
fun getSatellites(): List<SatelliteSummaryDTO> =
|
||||
RestClient.create()
|
||||
.get()
|
||||
.uri(buildSatelliteCatalogUri("/api/satellites"))
|
||||
.retrieve()
|
||||
.body(SATELLITE_SUMMARY_LIST_TYPE)
|
||||
.orEmpty()
|
||||
|
||||
private fun buildSatelliteCatalogUri(path: String): String =
|
||||
UriComponentsBuilder
|
||||
.fromUriString(satelliteCatalogServiceUrl)
|
||||
.path(path)
|
||||
.toUriString()
|
||||
|
||||
private companion object {
|
||||
val SATELLITE_SUMMARY_LIST_TYPE = object : ParameterizedTypeReference<List<SatelliteSummaryDTO>>() {}
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package space.nstart.pcp.complan.service
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.ObjectProvider
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||
import org.springframework.kafka.annotation.KafkaListener
|
||||
import org.springframework.stereotype.Component
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteDeletedEventDTO
|
||||
import space.nstart.pcp.pcp_types_lib.message.KafkaMessage
|
||||
import space.nstart.pcp.pcp_types_lib.message.PcpKafkaEvent
|
||||
|
||||
private const val SATELLITE_DELETED_CONSUMER_GROUP = "pcp-dynamic-plan-service-satellite-deleted"
|
||||
|
||||
@ConditionalOnProperty(name = ["spring.kafka.bootstrap-servers"], matchIfMissing = false)
|
||||
@Component
|
||||
class SatelliteDeletedKafkaListener(
|
||||
objectMapperProvider: ObjectProvider<ObjectMapper>,
|
||||
private val satelliteDeletedService: SatelliteDeletedService
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||
private val objectMapper = objectMapperProvider.ifAvailable ?: jacksonObjectMapper().findAndRegisterModules()
|
||||
|
||||
@KafkaListener(
|
||||
topics = ["\${app.kafka.topics.satellites:pcp.satellites}"],
|
||||
groupId = SATELLITE_DELETED_CONSUMER_GROUP
|
||||
)
|
||||
fun consume(message: String) {
|
||||
try {
|
||||
val kafkaMessage = objectMapper.readValue(
|
||||
message,
|
||||
object : TypeReference<KafkaMessage<SatelliteDeletedEventDTO>>() {}
|
||||
)
|
||||
if (kafkaMessage.type != PcpKafkaEvent.SatelliteDeletedEvent) {
|
||||
logger.debug("Ignoring Kafka message with unsupported type {}", kafkaMessage.type)
|
||||
return
|
||||
}
|
||||
|
||||
val event = kafkaMessage.data
|
||||
logger.info(
|
||||
"Received SatelliteDeletedEvent in pcp-dynamic-plan-service: satelliteId={}, noradId={}, eventId={}, traceId={}, source={}, consumerGroup={}",
|
||||
event.satelliteId,
|
||||
event.noradId,
|
||||
kafkaMessage.id,
|
||||
kafkaMessage.traceId,
|
||||
kafkaMessage.source,
|
||||
SATELLITE_DELETED_CONSUMER_GROUP
|
||||
)
|
||||
event.deleteIdentifiers().forEach { satelliteId ->
|
||||
satelliteDeletedService.deleteSatelliteData(satelliteId)
|
||||
}
|
||||
logger.info(
|
||||
"SatelliteDeletedEvent processed in pcp-dynamic-plan-service: satelliteId={}, noradId={}, eventId={}, traceId={}, source={}, consumerGroup={}",
|
||||
event.satelliteId,
|
||||
event.noradId,
|
||||
kafkaMessage.id,
|
||||
kafkaMessage.traceId,
|
||||
kafkaMessage.source,
|
||||
SATELLITE_DELETED_CONSUMER_GROUP
|
||||
)
|
||||
} catch (exception: Exception) {
|
||||
logger.error("Failed to process SatelliteDeletedEvent in pcp-dynamic-plan-service", exception)
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
|
||||
private fun SatelliteDeletedEventDTO.deleteIdentifiers(): List<Long> =
|
||||
listOfNotNull(satelliteId, noradId).distinct()
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package space.nstart.pcp.complan.service
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import space.nstart.pcp.complan.db.repository.DynamicPlanRouteRepository
|
||||
import space.nstart.pcp.complan.db.repository.DynamicPlanRunRepository
|
||||
|
||||
@Service
|
||||
class SatelliteDeletedService(
|
||||
private val runRepository: DynamicPlanRunRepository,
|
||||
private val routeRepository: DynamicPlanRouteRepository
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
@Transactional
|
||||
fun deleteSatelliteData(satelliteId: Long): SatelliteDynamicPlanDeletionSummary {
|
||||
val runsBeforeDelete = runRepository.countBySatelliteId(satelliteId)
|
||||
val routesBeforeDelete = routeRepository.countBySatelliteId(satelliteId)
|
||||
|
||||
val runsDeleted = runRepository.deleteAllBySatelliteId(satelliteId)
|
||||
val orphanRoutesDeleted = routeRepository.deleteAllBySatelliteId(satelliteId)
|
||||
val summary = SatelliteDynamicPlanDeletionSummary(
|
||||
satelliteId = satelliteId,
|
||||
runsDeleted = runsDeleted,
|
||||
routesDeletedByRunCascade = routesBeforeDelete - orphanRoutesDeleted,
|
||||
orphanRoutesDeleted = orphanRoutesDeleted
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Deleted dynamic plan data for satellite: satelliteId={}, runsDeleted={}, runsBeforeDelete={}, routesDeletedByRunCascade={}, orphanRoutesDeleted={}, routesBeforeDelete={}",
|
||||
summary.satelliteId,
|
||||
summary.runsDeleted,
|
||||
runsBeforeDelete,
|
||||
summary.routesDeletedByRunCascade,
|
||||
summary.orphanRoutesDeleted,
|
||||
routesBeforeDelete
|
||||
)
|
||||
return summary
|
||||
}
|
||||
}
|
||||
|
||||
data class SatelliteDynamicPlanDeletionSummary(
|
||||
val satelliteId: Long,
|
||||
val runsDeleted: Int,
|
||||
val routesDeletedByRunCascade: Long,
|
||||
val orphanRoutesDeleted: Int
|
||||
)
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package space.nstart.pcp.complan.settings
|
||||
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
/** Класс хранения настроек автомата планирования */
|
||||
class AplSettings() {
|
||||
/** Типовые настройки работы автомата планирования */
|
||||
var settings = _root_ide_package_.space.nstart.pcp.complan.settings.SettingsDTO()
|
||||
}
|
||||
|
||||
/* Типовой набор настроек
|
||||
{
|
||||
"daysCalc": 1,
|
||||
"kaList": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
],
|
||||
"marBeginCorrectionTime": 1,
|
||||
"marEndCorrectionTime": 1,
|
||||
"marWidth": 30,
|
||||
"marOverlap": 1,
|
||||
"marMaxLength": 60,
|
||||
"marMinLength": 5,
|
||||
"maxRevTime": 300,
|
||||
"maxDayTime": 2100,
|
||||
"contourStepBuild": 1,
|
||||
"filterByRegionUUID": true,
|
||||
"regionList": ["831abc95-b3c2-4aec-96d7-7023e5256042"],
|
||||
"filterByGamma": true,
|
||||
"gammaValue": 1
|
||||
}
|
||||
Пример записи в БД
|
||||
"{""daysCalc"":10,""kaList"":[1,2,3],""marBeginCorrectionTime"":1,""marEndCorrectionTime"":1,""marWidth"":30.0,""marOverlap"":1.0,""marMaxLength"":60.0,""marMinLength"":5.0,""maxRevTime"":300.0,""maxDayTime"":2100.0,""contourStepBuild"":1.0,""filterByRegionUUID"":true,""regionList"":[""3fa85f64-5717-4562-b3fc-111111111111""],""filterByGamma"":false,""gammaValue"":6.0,""filterByRevolution"":true,""revolutionType"":0}"
|
||||
|
||||
* */
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package space.nstart.pcp.complan.settings
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import java.util.UUID
|
||||
|
||||
@Schema(description = "Типовые настройки работы автомата планирования")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonSerialize
|
||||
data class SettingsDTO(
|
||||
|
||||
@get:JsonProperty("daysCalc")
|
||||
@param:JsonProperty("daysCalc")
|
||||
val daysCalc: Int = 1,
|
||||
|
||||
@get:JsonProperty("kaList")
|
||||
@param:JsonProperty("kaList")
|
||||
val kaList: Set<Int> = setOf(1,2,3),
|
||||
|
||||
@get:JsonProperty("marBeginCorrectionTime")
|
||||
@param:JsonProperty("marBeginCorrectionTime")
|
||||
val marBeginCorrectionTime: Long = 1,
|
||||
|
||||
@get:JsonProperty("marEndCorrectionTime")
|
||||
@param:JsonProperty("marEndCorrectionTime")
|
||||
val marEndCorrectionTime: Long = 1,
|
||||
|
||||
@get:JsonProperty("marWidth")
|
||||
@param:JsonProperty("marWidth")
|
||||
val marWidth : Double = 30.0,
|
||||
|
||||
@get:JsonProperty("marOverlap")
|
||||
@param:JsonProperty("marOverlap")
|
||||
val marOverlap : Double = 1.0,
|
||||
|
||||
@get:JsonProperty("marMaxLength")
|
||||
@param:JsonProperty("marMaxLength")
|
||||
val marMaxLength : Double = 60.0,
|
||||
|
||||
@get:JsonProperty("marMinLength")
|
||||
@param:JsonProperty("marMinLength")
|
||||
val marMinLength : Double = 5.0,
|
||||
|
||||
@get:JsonProperty("maxRevTime")
|
||||
@param:JsonProperty("maxRevTime")
|
||||
val maxRevTime : Double = 300.0,
|
||||
|
||||
@get:JsonProperty("maxDayTime")
|
||||
@param:JsonProperty("maxDayTime")
|
||||
val maxDayTime : Double = 2100.0,
|
||||
|
||||
@get:JsonProperty("contourStepBuild")
|
||||
@param:JsonProperty("contourStepBuild")
|
||||
val contourStepBuild : Double = 1.0,
|
||||
|
||||
@get:JsonProperty("filterByRegionUUID")
|
||||
@param:JsonProperty("filterByRegionUUID")
|
||||
val filterByRegionUUID : Boolean = false,
|
||||
|
||||
@get:JsonProperty("regionList")
|
||||
@param:JsonProperty("regionList")
|
||||
val regionList : Set<UUID> = setOf(),
|
||||
|
||||
|
||||
@get:JsonProperty("filterByGamma")
|
||||
@param:JsonProperty("filterByGamma")
|
||||
val filterByGamma : Boolean = false,
|
||||
|
||||
@get:JsonProperty("gammaValue")
|
||||
@param:JsonProperty("gammaValue")
|
||||
val gammaValue : Double = 40.0,
|
||||
|
||||
@get:JsonProperty("filterByRevolution")
|
||||
@param:JsonProperty("filterByRevolution")
|
||||
val filterByRevolution : Boolean = false,
|
||||
|
||||
@get:JsonProperty("revolutionType")
|
||||
@param:JsonProperty("revolutionType")
|
||||
val revolutionType : Int = 0,
|
||||
|
||||
)
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package space.nstart.pcp.complan.spacecraft
|
||||
|
||||
/**
|
||||
* Ограничения работы одного космического аппарата при построении расписания включений.
|
||||
*
|
||||
* Сейчас модель содержит только временные лимиты, которые использует greedy-планировщик:
|
||||
* суммарное время работы на витке, суммарное время работы за сутки и технологический разрыв
|
||||
* между двумя включениями одного аппарата. Класс вынесен рядом со SpaceCraft, чтобы будущие
|
||||
* ограничения аппарата добавлялись к доменной модели КА, а не разрастались внутри алгоритма планирования.
|
||||
*/
|
||||
data class SCConstraints(
|
||||
val maxRevolutionDurationSeconds: Long = 300,
|
||||
val maxDailyDurationSeconds: Long = 2100,
|
||||
val routeGapSeconds: Long = 60
|
||||
)
|
||||
Executable
+144
@@ -0,0 +1,144 @@
|
||||
package space.nstart.pcp.complan.spacecraft
|
||||
|
||||
import org.locationtech.jts.geom.Coordinate
|
||||
import org.locationtech.jts.geom.GeometryFactory
|
||||
import org.locationtech.jts.geom.LineString
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.FlightLineDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
|
||||
import java.time.LocalDateTime
|
||||
|
||||
/**
|
||||
* Класс космического аппарата**
|
||||
*/
|
||||
class SpaceCraft (
|
||||
val id : Long,
|
||||
val constraints: SCConstraints = SCConstraints()
|
||||
) {
|
||||
val flightLine : MutableList<FlightLineDTO> = mutableListOf()
|
||||
|
||||
/**Время начала планирования аппарата*/
|
||||
var timeStart: LocalDateTime = LocalDateTime.now()
|
||||
/**Время конца планирования аппарата*/
|
||||
var timeStop : LocalDateTime = LocalDateTime.now()
|
||||
/**
|
||||
* Создание линий полета восходящего и нисходящего витка
|
||||
* @param geomFactory фабрика геометрии
|
||||
* @param fLC координаты линии
|
||||
* @return
|
||||
* @return Возвращает геометрию линии полученной из массива координат
|
||||
*/
|
||||
fun createLineString(
|
||||
geomFactory: GeometryFactory,
|
||||
fLC: MutableList<Coordinate>,
|
||||
): LineString {
|
||||
require(fLC.size >= 2) { "Для построения трассы требуется минимум две точки" }
|
||||
|
||||
val coordinates = fLC.toTypedArray()
|
||||
val minY = coordinates.minOf { it.y }
|
||||
val maxY = coordinates.maxOf { it.y }
|
||||
|
||||
val startIndex1 = coordinates.indexOfFirst { it.y == minY }
|
||||
val endIndex1 = coordinates.indexOfFirst { it.y == maxY }
|
||||
|
||||
val array = if (startIndex1 < endIndex1)
|
||||
coordinates.sliceArray(startIndex1..endIndex1).toMutableList()
|
||||
else
|
||||
coordinates.sliceArray(endIndex1..startIndex1).toMutableList()
|
||||
|
||||
val geom = geomFactory.createLineString(array.toTypedArray())
|
||||
|
||||
return geom
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение линии полета в формате LineString
|
||||
* @return Возвращает часть линии полета от минимального занчения координаты Y до максимального. Параметры: **первый - НИСХОДЯЩИЙ, второй - ВОСХОДЯЩИЙ**
|
||||
* TODO Переписать структуру хранения FlightLine, неудобно держать в памяти первый или второй, нужны признаки
|
||||
*/
|
||||
fun createFlightLine(): Pair<LineString, LineString> {
|
||||
val geomFactory = GeometryFactory()
|
||||
val passes = getFlightLinePasses()
|
||||
val fLC1 = lineCorrection(selectPass(passes, RevolutionSign.DESC).coordinates).toMutableList()
|
||||
val fLC2 = lineCorrection(selectPass(passes, RevolutionSign.ASC).coordinates).toMutableList()
|
||||
|
||||
return Pair(createLineString(geomFactory, fLC1), createLineString(geomFactory, fLC2))
|
||||
}
|
||||
|
||||
private fun getFlightLinePasses(): List<FlightLinePass> {
|
||||
if (flightLine.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val passes = mutableListOf<FlightLinePass>()
|
||||
var currentSign = flightLine.first().revSign
|
||||
var currentCoordinates = mutableListOf<Coordinate>()
|
||||
|
||||
flightLine.forEach { point ->
|
||||
if (point.revSign != currentSign) {
|
||||
addPass(passes, currentSign, currentCoordinates)
|
||||
currentSign = point.revSign
|
||||
currentCoordinates = mutableListOf()
|
||||
}
|
||||
currentCoordinates.add(Coordinate(point.long, point.lat))
|
||||
}
|
||||
addPass(passes, currentSign, currentCoordinates)
|
||||
|
||||
return passes
|
||||
}
|
||||
|
||||
private fun addPass(
|
||||
passes: MutableList<FlightLinePass>,
|
||||
revSign: RevolutionSign,
|
||||
coordinates: List<Coordinate>
|
||||
) {
|
||||
if (coordinates.size >= 2) {
|
||||
passes.add(FlightLinePass(revSign, coordinates.map { it.copy() }))
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectPass(passes: List<FlightLinePass>, revSign: RevolutionSign): FlightLinePass {
|
||||
val sameDirectionPasses = passes.filter { it.revSign == revSign }
|
||||
return sameDirectionPasses.firstOrNull { it.isFullPass() }
|
||||
?: sameDirectionPasses.maxByOrNull { it.latitudeRange }
|
||||
?: throw IllegalStateException("Не найдена трасса полета для направления $revSign")
|
||||
}
|
||||
|
||||
fun lineCorrection(tmpLine : List<Coordinate>) : List<Coordinate> {
|
||||
if (tmpLine.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val line = mutableListOf<Coordinate>()
|
||||
|
||||
line.add(tmpLine.first().copy())
|
||||
for (i in 1..<tmpLine.count()) {
|
||||
val coord = tmpLine[i].copy()
|
||||
while (coord.x - line.last().x > HALF_WORLD_DEGREES) {
|
||||
coord.x -= FULL_WORLD_DEGREES
|
||||
}
|
||||
while (coord.x - line.last().x < -HALF_WORLD_DEGREES) {
|
||||
coord.x += FULL_WORLD_DEGREES
|
||||
}
|
||||
line.add(coord.copy())
|
||||
}
|
||||
return line
|
||||
}
|
||||
|
||||
private data class FlightLinePass(
|
||||
val revSign: RevolutionSign,
|
||||
val coordinates: List<Coordinate>
|
||||
) {
|
||||
val minLat = coordinates.minOf { it.y }
|
||||
val maxLat = coordinates.maxOf { it.y }
|
||||
val latitudeRange = maxLat - minLat
|
||||
|
||||
fun isFullPass(): Boolean = minLat <= FULL_PASS_MIN_LAT && maxLat >= FULL_PASS_MAX_LAT
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val FULL_PASS_MIN_LAT = -80.0
|
||||
const val FULL_PASS_MAX_LAT = 80.0
|
||||
const val HALF_WORLD_DEGREES = 180.0
|
||||
const val FULL_WORLD_DEGREES = 360.0
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import org.locationtech.jts.geom.Geometry
|
||||
import org.locationtech.jts.io.WKTReader
|
||||
import java.time.LocalDateTime
|
||||
import java.util.*
|
||||
|
||||
@Schema(description = "Объект каталога для планирования")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonSerialize
|
||||
data class AplObjItem(
|
||||
|
||||
@get:JsonProperty("name")
|
||||
@param:JsonProperty("name")
|
||||
val name: String,
|
||||
|
||||
@get:JsonProperty("geometry")
|
||||
@param:JsonProperty("geometry")
|
||||
val geometry: Geometry,
|
||||
|
||||
@get:JsonProperty("id")
|
||||
@param:JsonProperty("id")
|
||||
val id : UUID,
|
||||
|
||||
@get:JsonProperty("intevals")
|
||||
@param:JsonProperty("intevals")
|
||||
@JsonIgnore
|
||||
val intevals : MutableList<IntervalItemDTO>,
|
||||
|
||||
@get:JsonProperty("krenMin")
|
||||
@param:JsonProperty("krenMin")
|
||||
val krenMin : Double,
|
||||
|
||||
@get:JsonProperty("krenMax")
|
||||
@param:JsonProperty("krenMax")
|
||||
val krenMax : Double,
|
||||
|
||||
@get:JsonProperty("intervalBegin")
|
||||
@param:JsonProperty("intervalBegin")
|
||||
val intervalBegin : LocalDateTime? = null,
|
||||
|
||||
@get:JsonProperty("intervalEnd")
|
||||
@param:JsonProperty("intervalEnd")
|
||||
val intervalEnd : LocalDateTime? = null,
|
||||
|
||||
@get:JsonProperty("lastChange")
|
||||
@param:JsonProperty("lastChange")
|
||||
/** Если значение данного параметра null, значит задание новое */
|
||||
val lastChange : LocalDateTime? = null,
|
||||
) {
|
||||
constructor(obj: RequestItemDTO) : this(
|
||||
name = obj.name,
|
||||
geometry = WKTReader().read(obj.geometry),
|
||||
id = obj.id,
|
||||
intevals = mutableListOf(),
|
||||
krenMin = -45.0,
|
||||
krenMax = 45.0
|
||||
)
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
data class ComplexPlanCalculationOutput(
|
||||
val result: ComplexPlanCalculationResultDTO,
|
||||
val routes: List<SurveyContour>,
|
||||
val intervals: List<IntervalItemDTO> = emptyList()
|
||||
)
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
|
||||
@Schema(description = "Запрос расчета модели комплексного плана для одной заявки")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class ComplexPlanCalculationRequestDTO(
|
||||
@get:JsonProperty("requestId")
|
||||
@param:JsonProperty("requestId")
|
||||
val requestId: UUID,
|
||||
|
||||
@get:JsonProperty("satelliteIds")
|
||||
@param:JsonProperty("satelliteIds")
|
||||
val satelliteIds: List<Long>,
|
||||
|
||||
@get:JsonProperty("calculationStart")
|
||||
@param:JsonProperty("calculationStart")
|
||||
val calculationStart: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("calculationEnd")
|
||||
@param:JsonProperty("calculationEnd")
|
||||
val calculationEnd: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("calculationMode")
|
||||
@param:JsonProperty("calculationMode")
|
||||
val calculationMode: DynamicPlanCalculationMode = DynamicPlanCalculationMode.FULL,
|
||||
|
||||
@get:JsonProperty("revolutionMode")
|
||||
@param:JsonProperty("revolutionMode")
|
||||
val revolutionMode: DynamicPlanRevolutionMode = DynamicPlanRevolutionMode.BOTH,
|
||||
|
||||
@get:JsonProperty("filterCoveredRoutes")
|
||||
@param:JsonProperty("filterCoveredRoutes")
|
||||
val filterCoveredRoutes: Boolean = false
|
||||
)
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
|
||||
enum class ComplexPlanCalculationStatus {
|
||||
COMPLETED,
|
||||
REQUEST_NOT_FOUND,
|
||||
NO_AVAILABLE_SATELLITES
|
||||
}
|
||||
|
||||
@Schema(description = "Результат запуска расчета модели комплексного плана")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class ComplexPlanCalculationResultDTO(
|
||||
@get:JsonProperty("status")
|
||||
@param:JsonProperty("status")
|
||||
val status: ComplexPlanCalculationStatus,
|
||||
|
||||
@get:JsonProperty("requestId")
|
||||
@param:JsonProperty("requestId")
|
||||
val requestId: UUID,
|
||||
|
||||
@get:JsonProperty("satelliteIds")
|
||||
@param:JsonProperty("satelliteIds")
|
||||
val satelliteIds: List<Long>,
|
||||
|
||||
@get:JsonProperty("missingSatelliteIds")
|
||||
@param:JsonProperty("missingSatelliteIds")
|
||||
val missingSatelliteIds: List<Long>,
|
||||
|
||||
@get:JsonProperty("calculationStart")
|
||||
@param:JsonProperty("calculationStart")
|
||||
val calculationStart: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("calculationEnd")
|
||||
@param:JsonProperty("calculationEnd")
|
||||
val calculationEnd: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("calculationMode")
|
||||
@param:JsonProperty("calculationMode")
|
||||
val calculationMode: DynamicPlanCalculationMode = DynamicPlanCalculationMode.FULL,
|
||||
|
||||
@get:JsonProperty("revolutionMode")
|
||||
@param:JsonProperty("revolutionMode")
|
||||
val revolutionMode: DynamicPlanRevolutionMode = DynamicPlanRevolutionMode.BOTH,
|
||||
|
||||
@get:JsonProperty("filterCoveredRoutes")
|
||||
@param:JsonProperty("filterCoveredRoutes")
|
||||
val filterCoveredRoutes: Boolean = false,
|
||||
|
||||
@get:JsonProperty("routesCount")
|
||||
@param:JsonProperty("routesCount")
|
||||
val routesCount: Int,
|
||||
|
||||
@get:JsonProperty("lastRouteStart")
|
||||
@param:JsonProperty("lastRouteStart")
|
||||
val lastRouteStart: LocalDateTime? = null,
|
||||
|
||||
@get:JsonProperty("lastRouteEnd")
|
||||
@param:JsonProperty("lastRouteEnd")
|
||||
val lastRouteEnd: LocalDateTime? = null,
|
||||
|
||||
@get:JsonProperty("coveredAreaPercent")
|
||||
@param:JsonProperty("coveredAreaPercent")
|
||||
val coveredAreaPercent: Double? = null,
|
||||
|
||||
@get:JsonProperty("durationMs")
|
||||
@param:JsonProperty("durationMs")
|
||||
val durationMs: Long
|
||||
)
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Schema(description = "Задание на расчет контура")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonSerialize
|
||||
data class ContourRequestDTO(
|
||||
@get:JsonProperty("satelliteId")
|
||||
@param:JsonProperty("satelliteId")
|
||||
val satelliteId : Long,
|
||||
|
||||
@get:JsonProperty("time")
|
||||
@param:JsonProperty("time")
|
||||
val time : LocalDateTime,
|
||||
|
||||
@get:JsonProperty("gamma")
|
||||
@param:JsonProperty("gamma")
|
||||
val gamma : Double,
|
||||
|
||||
@get:JsonProperty("duration")
|
||||
@param:JsonProperty("duration")
|
||||
val duration : Double,
|
||||
|
||||
@get:JsonProperty("routeWidthKm")
|
||||
@param:JsonProperty("routeWidthKm")
|
||||
val routeWidthKm : Double,
|
||||
|
||||
@get:JsonProperty("step")
|
||||
@param:JsonProperty("step")
|
||||
val step : Double = 1.0
|
||||
)
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
enum class DynamicPlanCalculationMode {
|
||||
FULL,
|
||||
GREEDY
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
|
||||
import java.util.UUID
|
||||
|
||||
@Schema(description = "Интервал расчета динамического плана")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class DynamicPlanIntervalDTO(
|
||||
@get:JsonProperty("id")
|
||||
@param:JsonProperty("id")
|
||||
val id: Long,
|
||||
|
||||
@get:JsonProperty("runId")
|
||||
@param:JsonProperty("runId")
|
||||
val runId: UUID,
|
||||
|
||||
@get:JsonProperty("requestId")
|
||||
@param:JsonProperty("requestId")
|
||||
val requestId: UUID,
|
||||
|
||||
@get:JsonProperty("intervalId")
|
||||
@param:JsonProperty("intervalId")
|
||||
val intervalId: UUID,
|
||||
|
||||
@get:JsonProperty("regionId")
|
||||
@param:JsonProperty("regionId")
|
||||
val regionId: UUID,
|
||||
|
||||
@get:JsonProperty("revSign")
|
||||
@param:JsonProperty("revSign")
|
||||
val revSign: RevolutionSign,
|
||||
|
||||
@get:JsonProperty("contourWkt")
|
||||
@param:JsonProperty("contourWkt")
|
||||
val contourWkt: String,
|
||||
|
||||
@get:JsonProperty("observationParametersCount")
|
||||
@param:JsonProperty("observationParametersCount")
|
||||
val observationParametersCount: Int
|
||||
)
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
enum class DynamicPlanRevolutionMode {
|
||||
BOTH,
|
||||
ASC,
|
||||
DESC
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
|
||||
@Schema(description = "Маршрут расчета динамического плана")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class DynamicPlanRouteDTO(
|
||||
@get:JsonProperty("id")
|
||||
@param:JsonProperty("id")
|
||||
val id: Long,
|
||||
|
||||
@get:JsonProperty("runId")
|
||||
@param:JsonProperty("runId")
|
||||
val runId: UUID,
|
||||
|
||||
@get:JsonProperty("requestId")
|
||||
@param:JsonProperty("requestId")
|
||||
val requestId: UUID,
|
||||
|
||||
@get:JsonProperty("satelliteId")
|
||||
@param:JsonProperty("satelliteId")
|
||||
val satelliteId: Long,
|
||||
|
||||
@get:JsonProperty("startTime")
|
||||
@param:JsonProperty("startTime")
|
||||
val startTime: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("endTime")
|
||||
@param:JsonProperty("endTime")
|
||||
val endTime: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("duration")
|
||||
@param:JsonProperty("duration")
|
||||
val duration: Double,
|
||||
|
||||
@get:JsonProperty("revolution")
|
||||
@param:JsonProperty("revolution")
|
||||
val revolution: Long,
|
||||
|
||||
@get:JsonProperty("roll")
|
||||
@param:JsonProperty("roll")
|
||||
val roll: Double,
|
||||
|
||||
@get:JsonProperty("contourWkt")
|
||||
@param:JsonProperty("contourWkt")
|
||||
val contourWkt: String
|
||||
)
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
|
||||
enum class DynamicPlanRunStatus {
|
||||
PENDING,
|
||||
RUNNING,
|
||||
COMPLETED,
|
||||
FAILED
|
||||
}
|
||||
|
||||
@Schema(description = "Состояние запуска расчета динамического плана")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class DynamicPlanRunDTO(
|
||||
@get:JsonProperty("runId")
|
||||
@param:JsonProperty("runId")
|
||||
val runId: UUID,
|
||||
|
||||
@get:JsonProperty("status")
|
||||
@param:JsonProperty("status")
|
||||
val status: DynamicPlanRunStatus,
|
||||
|
||||
@get:JsonProperty("requestId")
|
||||
@param:JsonProperty("requestId")
|
||||
val requestId: UUID,
|
||||
|
||||
@get:JsonProperty("satelliteIds")
|
||||
@param:JsonProperty("satelliteIds")
|
||||
val satelliteIds: List<Long>,
|
||||
|
||||
@get:JsonProperty("calculationStart")
|
||||
@param:JsonProperty("calculationStart")
|
||||
val calculationStart: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("calculationEnd")
|
||||
@param:JsonProperty("calculationEnd")
|
||||
val calculationEnd: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("calculationMode")
|
||||
@param:JsonProperty("calculationMode")
|
||||
val calculationMode: DynamicPlanCalculationMode,
|
||||
|
||||
@get:JsonProperty("revolutionMode")
|
||||
@param:JsonProperty("revolutionMode")
|
||||
val revolutionMode: DynamicPlanRevolutionMode,
|
||||
|
||||
@get:JsonProperty("filterCoveredRoutes")
|
||||
@param:JsonProperty("filterCoveredRoutes")
|
||||
val filterCoveredRoutes: Boolean,
|
||||
|
||||
@get:JsonProperty("createdAt")
|
||||
@param:JsonProperty("createdAt")
|
||||
val createdAt: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("startedAt")
|
||||
@param:JsonProperty("startedAt")
|
||||
val startedAt: LocalDateTime?,
|
||||
|
||||
@get:JsonProperty("finishedAt")
|
||||
@param:JsonProperty("finishedAt")
|
||||
val finishedAt: LocalDateTime?,
|
||||
|
||||
@get:JsonProperty("durationMs")
|
||||
@param:JsonProperty("durationMs")
|
||||
val durationMs: Long?,
|
||||
|
||||
@get:JsonProperty("resultStatus")
|
||||
@param:JsonProperty("resultStatus")
|
||||
val resultStatus: String?,
|
||||
|
||||
@get:JsonProperty("routesCount")
|
||||
@param:JsonProperty("routesCount")
|
||||
val routesCount: Int?,
|
||||
|
||||
@get:JsonProperty("errorMessage")
|
||||
@param:JsonProperty("errorMessage")
|
||||
val errorMessage: String?
|
||||
)
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import org.locationtech.jts.geom.MultiPolygon
|
||||
import org.locationtech.jts.geom.LineString
|
||||
import org.locationtech.jts.geom.Geometry
|
||||
import org.locationtech.jts.geom.LinearRing
|
||||
import org.locationtech.jts.geom.Polygon
|
||||
import org.locationtech.jts.geom.Point
|
||||
import java.io.File
|
||||
import kotlin.collections.forEach
|
||||
import kotlin.math.PI
|
||||
|
||||
|
||||
class GeoJson {
|
||||
|
||||
data class GeometryGeoJson(val type: String, val coordinates: Any)
|
||||
data class Feature(val name: String, val bbox: List<Double>? = null, val type: String, val geometry: GeometryGeoJson, val properties: Any)
|
||||
data class FeatureCollection(val type: String, val features: List<Feature>)
|
||||
|
||||
private fun multipolygonToGeojsonCoord(multipolygon: MultiPolygon, radToDeg : Boolean = false): List<List<List<Array<Double>>>> {
|
||||
return (0 until multipolygon.numGeometries).map{i->
|
||||
val polygon = multipolygon.getGeometryN(i) as Polygon
|
||||
polygonToGeojsonCoord(polygon, radToDeg)
|
||||
}
|
||||
}
|
||||
|
||||
private fun polygonToGeojsonCoord(polygon: Polygon, radToDeg : Boolean = false): List<List<Array<Double>>> {
|
||||
val coordinates = mutableListOf<List<Array<Double>>>()
|
||||
|
||||
coordinates.add(linearRingToCoord(polygon.exteriorRing, radToDeg))
|
||||
|
||||
(0 until polygon.numInteriorRing).forEach { i ->
|
||||
coordinates.add(linearRingToCoord(polygon.getInteriorRingN(i), radToDeg))
|
||||
}
|
||||
|
||||
return coordinates
|
||||
}
|
||||
|
||||
private fun linearRingToCoord(ring: LinearRing, radToDeg : Boolean = false): List<Array<Double>> {
|
||||
if (radToDeg) {
|
||||
return ring.coordinates.map{ coordinate ->
|
||||
listOf(coordinate.x * 180 / PI, coordinate.y * 180 / PI).toTypedArray()
|
||||
}
|
||||
} else {
|
||||
return ring.coordinates.map{ coordinate ->
|
||||
listOf(coordinate.x, coordinate.y).toTypedArray()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun getBBox(geometry: Geometry): List<Double>{
|
||||
val bbox = geometry.envelopeInternal
|
||||
return listOf(
|
||||
bbox.minX,
|
||||
bbox.maxY,
|
||||
bbox.maxX,
|
||||
bbox.minY,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Конвертация из Geometry в формат geoJSON
|
||||
* @param geometry исходная геометрия
|
||||
* @return Возвращает конвертированную геометрию для записи в формате geoGSON
|
||||
*/
|
||||
fun convertToGeoJson(geometry: Geometry): GeometryGeoJson {
|
||||
return when (geometry) {
|
||||
is Point -> GeometryGeoJson(type = "Point", coordinates = listOf( geometry.x, geometry.y ))
|
||||
is LineString -> GeometryGeoJson(type = "LineString", coordinates = geometry.coordinates.map{arrayOf( it.x, it.y )})
|
||||
is Polygon -> GeometryGeoJson(type = "Polygon", coordinates = polygonToGeojsonCoord(geometry))
|
||||
is MultiPolygon -> GeometryGeoJson(type = "MultiPolygon", coordinates = multipolygonToGeojsonCoord(geometry))
|
||||
else -> throw IllegalArgumentException("Unknown geometry type: $geometry")
|
||||
}
|
||||
}
|
||||
fun convertToGeoJsonRadToDeg(geometry: Geometry): GeometryGeoJson {
|
||||
return when (geometry) {
|
||||
is Point -> GeometryGeoJson(type = "Point", coordinates = listOf( radToDeg(geometry.x), (geometry.y) ))
|
||||
is LineString -> GeometryGeoJson(type = "LineString", coordinates = geometry.coordinates.map{arrayOf( radToDeg(it.x), radToDeg(it.y) )})
|
||||
is Polygon -> GeometryGeoJson(type = "Polygon", coordinates = polygonToGeojsonCoord(geometry, radToDeg = true))
|
||||
is MultiPolygon -> GeometryGeoJson(type = "MultiPolygon", coordinates = multipolygonToGeojsonCoord(geometry, radToDeg = true))
|
||||
else -> throw IllegalArgumentException("Unknown geometry type: $geometry")
|
||||
}
|
||||
}
|
||||
|
||||
fun radToDeg(value : Double): Double {
|
||||
return value * 180 / PI
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun createFeatureCollection(
|
||||
geoms: List<GeoJsonItem>,
|
||||
fileName: String,
|
||||
radToDeg: Boolean = false
|
||||
): FeatureCollection {
|
||||
val gj = GeoJson()
|
||||
val features = geoms.map { geom ->
|
||||
Feature(
|
||||
name = fileName,
|
||||
bbox = gj.getBBox(geom.geometry),
|
||||
type = "Feature",
|
||||
geometry = if (radToDeg) {
|
||||
gj.convertToGeoJsonRadToDeg(geom.geometry)
|
||||
} else {
|
||||
gj.convertToGeoJson(geom.geometry)
|
||||
},
|
||||
properties = geom.properties
|
||||
)
|
||||
}
|
||||
return FeatureCollection(type = "FeatureCollection", features = features)
|
||||
}
|
||||
|
||||
/**
|
||||
* Создать geoJSON.
|
||||
* Файлы geoJson хранятся в папке ресурсов рядом с картой в папке geoJson.
|
||||
* Названия **AprioryScans, Catalog, FactScans, Region** соответственно.
|
||||
* @param geom массив элементов [GeoJsonItem], содержащий геометрию и свойства слоя при необходимости
|
||||
* @param outputPath целевой путь сохранения файла
|
||||
*/
|
||||
fun createGeoJson(geoms: List<GeoJsonItem>, fileName: String, dirToSave: String, radToDeg: Boolean = false) {
|
||||
val objectMapper = ObjectMapper()
|
||||
val geoJsonString = objectMapper.writerWithDefaultPrettyPrinter()
|
||||
.writeValueAsString(createFeatureCollection(geoms, fileName, radToDeg))
|
||||
|
||||
val dataDir = File(dirToSave)
|
||||
if(!dataDir.exists()){
|
||||
dataDir.mkdirs()
|
||||
}
|
||||
|
||||
val path = "$fileName.json"
|
||||
val file = File(dataDir, path)
|
||||
file.writeText(geoJsonString)
|
||||
}
|
||||
}
|
||||
}
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
import org.locationtech.jts.geom.Geometry
|
||||
import org.locationtech.jts.geom.LineString
|
||||
import org.locationtech.jts.geom.Polygon
|
||||
import org.locationtech.jts.io.WKTReader
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamDTO
|
||||
|
||||
|
||||
data class GeoJsonItem(
|
||||
val geometry: Geometry,
|
||||
val properties: Map<String, Any>,
|
||||
){
|
||||
companion object {
|
||||
// /**
|
||||
// * Создание слоя интервалов с атрибутивной информацией
|
||||
// * @param
|
||||
// * @return
|
||||
// */
|
||||
// fun createIntervalsLay(ka: KaPass): List<GeoJsonItem> {
|
||||
// val rez = mutableListOf<GeoJsonItem>()
|
||||
// for (view in ka.views) {
|
||||
// var unionGeom: Geometry? = view.matPlan.interval.geometry//.contour[0].geometry
|
||||
//
|
||||
// if (unionGeom == null) continue
|
||||
// val resItem = GeoJsonItem(
|
||||
// geometry = unionGeom,//, //.geometryToDeg(),
|
||||
// properties = mapOf(
|
||||
// "Аппарат" to "${ka.ka.kaId}",
|
||||
// "Время нач." to view.matPlan.tMinLDT.toString(),//.toDateString(),
|
||||
// "Время кон." to view.matPlan.tMaxLDT.toString(),//.toDateString(),
|
||||
// "Виток" to view.matPlan.vit,
|
||||
// "Регион" to "${view.matPlan.interval.obj.reqRegionId}",
|
||||
// )
|
||||
// )
|
||||
// rez.add(resItem)
|
||||
// }
|
||||
//
|
||||
// return rez
|
||||
// }
|
||||
|
||||
/**
|
||||
* Создание слоя интервалов с атрибутивной информацией
|
||||
* @return Возвращает заполненный [GeoJsonItem] с геометрией контура и атрибутивной информацией
|
||||
*/
|
||||
fun createIntervalsLay(interval: List<IntervalItemDTO>): List<GeoJsonItem> {
|
||||
val rez = mutableListOf<GeoJsonItem>()
|
||||
for(interval in interval) {
|
||||
|
||||
if(WKTReader().read(interval.geometry).getGeometryN(0).coordinates.first() != WKTReader().read(interval.geometry).getGeometryN(0).coordinates.last())
|
||||
continue
|
||||
|
||||
val resItem = GeoJsonItem(
|
||||
geometry = WKTReader().read(interval.geometry),
|
||||
properties = mapOf(
|
||||
// "№" to "${interval.num}",
|
||||
// "Region" to "${interval.regionId}",
|
||||
// "Interval" to "${interval.id}",
|
||||
"mpl" to getMplStringForLay(interval.mpl)
|
||||
// "mpl" to interval.mpl.map { "[kaid ${it.noradId}, rev ${it.revolutionBegin}, time ${it.timeBegin}]" },
|
||||
// "mpl" to "${interval.mpl}",
|
||||
)
|
||||
)
|
||||
rez.add(resItem)
|
||||
}
|
||||
return rez
|
||||
}
|
||||
|
||||
/**
|
||||
* Формирование атрибутивной информации по матрице планирования
|
||||
* @return Возвращает строку с перечнем прохождений КА в минималистическом формате
|
||||
* КА[[виток время][..]..]
|
||||
*/
|
||||
fun getMplStringForLay(mpl : List<SquareViewParamDTO>) : String {
|
||||
val kaList = mutableSetOf<Long>()//.addAll(mpl.map { it.noradId })
|
||||
|
||||
mpl.sortedBy { it.timeBegin }
|
||||
|
||||
mpl.forEach {
|
||||
kaList.add(it.noradId)
|
||||
}
|
||||
|
||||
val res = StringBuilder()
|
||||
|
||||
val kaListTmp = kaList.sortedBy { it }
|
||||
|
||||
kaListTmp.forEach { kaId ->
|
||||
val str = mpl.filter { it.noradId == kaId }.sortedBy { it.timeBegin }.map { "[rev ${it.revolutionBegin}, time ${it.timeBegin}, gamma ${(it.gammaMin + it.gammaMax)/2}, gammaMin ${it.gammaMin}, gammaMax ${it.gammaMax}]" }
|
||||
|
||||
res.append("noradId:$kaId")
|
||||
res.append("[")
|
||||
str.forEach { res.append(it) }
|
||||
res.append("]")
|
||||
}
|
||||
|
||||
return res.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание слоя маршрутов с атрибутивной информацией
|
||||
* @return Возвращает заполненный [GeoJsonItem] с геометрией контура и атрибутивной информацией
|
||||
*/
|
||||
fun createSurveyLay(surveys: List<SurveyContour>): List<GeoJsonItem> {
|
||||
val rez = mutableListOf<GeoJsonItem>()
|
||||
for(survey in surveys) {
|
||||
|
||||
val geometry = WKTReader().read(survey.geometry)
|
||||
|
||||
if (geometry !is Polygon) { continue }
|
||||
|
||||
if(geometry.getGeometryN(0).coordinates.first() != geometry.getGeometryN(0).coordinates.last())
|
||||
continue
|
||||
|
||||
val resItem = GeoJsonItem(
|
||||
geometry = geometry,
|
||||
properties = mapOf(
|
||||
"noradId" to survey.mpl.noradId,
|
||||
"rev" to survey.rev,
|
||||
"gamma" to survey.gamma,
|
||||
"time" to survey.time.toString(),
|
||||
"duration" to survey.duration,
|
||||
// "mpl" to survey.mpl.toString(),
|
||||
)
|
||||
)
|
||||
rez.add(resItem)
|
||||
}
|
||||
return rez
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание слоя с атрибутивной информацией
|
||||
* @return Возвращает заполненный [GeoJsonItem] с геометрией контура и атрибутивной информацией
|
||||
*/
|
||||
fun createAnyLay(pols: List<Polygon>): MutableList<GeoJsonItem> {
|
||||
val rez = mutableListOf<GeoJsonItem>()
|
||||
for (pol in pols) {
|
||||
val resItem = GeoJsonItem(
|
||||
geometry = pol,//,
|
||||
properties = mapOf()
|
||||
)
|
||||
rez.add(resItem)
|
||||
}
|
||||
return rez
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание слоя с атрибутивной информацией
|
||||
* @return Возвращает заполненный [GeoJsonItem] с геометрией контура и атрибутивной информацией
|
||||
*/
|
||||
fun createRequestLay(requests: List<RequestItemDTO>): List<GeoJsonItem> {
|
||||
val rez = mutableListOf<GeoJsonItem>()
|
||||
for (item in requests) {
|
||||
val resItem = GeoJsonItem(
|
||||
geometry = WKTReader().read(item.geometry),//,
|
||||
properties = mapOf(
|
||||
"UUID" to "${item.id}",
|
||||
"Name" to "${item.name}",
|
||||
"Interval_begin" to "${item.intervalBegin}",
|
||||
"Interval_end" to "${item.intervalEnd}",
|
||||
"Importance" to "${item.importance}",
|
||||
"Priority" to "${item.priority}",
|
||||
"Resulution" to "${item.resolution}",
|
||||
)
|
||||
)
|
||||
rez.add(resItem)
|
||||
}
|
||||
return rez
|
||||
}
|
||||
/**
|
||||
* Создание слоя с атрибутивной информацией
|
||||
* @return Возвращает заполненный [GeoJsonItem] с геометрией контура и атрибутивной информацией
|
||||
*/
|
||||
fun createAplCatalogLay(aplCatalog: List<AplObjItem>): List<GeoJsonItem> {
|
||||
val rez = mutableListOf<GeoJsonItem>()
|
||||
for (item in aplCatalog) {
|
||||
val resItem = GeoJsonItem(
|
||||
geometry = item.geometry,
|
||||
properties = mapOf(
|
||||
"UUID" to "${item.id}",
|
||||
"Name" to "${item.name}",
|
||||
// "Interval_begin" to "${item.intervalBegin}",
|
||||
// "Interval_end" to "${item.intervalEnd}",
|
||||
// "Importance" to "${item.importance}",
|
||||
// "Priority" to "${item.priority}",
|
||||
// "Resulution" to "${item.resolution}",
|
||||
)
|
||||
)
|
||||
rez.add(resItem)
|
||||
}
|
||||
return rez
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание слой для отображения из LineString
|
||||
* @return Возвращает заполненный [GeoJsonItem] с геометрией контура и атрибутивной информацией
|
||||
*/
|
||||
fun createLineStringLay(lines: List<LineString>): MutableList<GeoJsonItem> {
|
||||
val rez = mutableListOf<GeoJsonItem>()
|
||||
for (item in lines) {
|
||||
val resItem = GeoJsonItem(
|
||||
geometry = item,//,
|
||||
properties = mapOf()
|
||||
)
|
||||
rez.add(resItem)
|
||||
}
|
||||
return rez
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
|
||||
@Schema(description = "Debug-запрос расчета greedy-математики по сохраненным параметрам наблюдения")
|
||||
data class GreedyMathDebugRequestDTO(
|
||||
@get:JsonProperty("requestId")
|
||||
@param:JsonProperty("requestId")
|
||||
val requestId: UUID,
|
||||
|
||||
@get:JsonProperty("satelliteIds")
|
||||
@param:JsonProperty("satelliteIds")
|
||||
val satelliteIds: List<Long>,
|
||||
|
||||
@get:JsonProperty("calculationStart")
|
||||
@param:JsonProperty("calculationStart")
|
||||
val calculationStart: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("calculationEnd")
|
||||
@param:JsonProperty("calculationEnd")
|
||||
val calculationEnd: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("revolutionMode")
|
||||
@param:JsonProperty("revolutionMode")
|
||||
val revolutionMode: DynamicPlanRevolutionMode = DynamicPlanRevolutionMode.BOTH,
|
||||
|
||||
@get:JsonProperty("filterCoveredRoutes")
|
||||
@param:JsonProperty("filterCoveredRoutes")
|
||||
val filterCoveredRoutes: Boolean = false
|
||||
)
|
||||
|
||||
@Schema(description = "Выбранное greedy-включение без геометрии маршрута")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class GreedyMathCandidateDTO(
|
||||
@get:JsonProperty("intervalId")
|
||||
@param:JsonProperty("intervalId")
|
||||
val intervalId: UUID,
|
||||
|
||||
@get:JsonProperty("regionId")
|
||||
@param:JsonProperty("regionId")
|
||||
val regionId: UUID,
|
||||
|
||||
@get:JsonProperty("satelliteId")
|
||||
@param:JsonProperty("satelliteId")
|
||||
val satelliteId: Long,
|
||||
|
||||
@get:JsonProperty("revolution")
|
||||
@param:JsonProperty("revolution")
|
||||
val revolution: Long,
|
||||
|
||||
@get:JsonProperty("day")
|
||||
@param:JsonProperty("day")
|
||||
val day: LocalDate,
|
||||
|
||||
@get:JsonProperty("timeBegin")
|
||||
@param:JsonProperty("timeBegin")
|
||||
val timeBegin: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("timeEnd")
|
||||
@param:JsonProperty("timeEnd")
|
||||
val timeEnd: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("durationSeconds")
|
||||
@param:JsonProperty("durationSeconds")
|
||||
val durationSeconds: Long,
|
||||
|
||||
@get:JsonProperty("gamma")
|
||||
@param:JsonProperty("gamma")
|
||||
val gamma: Double
|
||||
)
|
||||
|
||||
@Schema(description = "Debug-результат greedy-математики по сохраненным параметрам наблюдения")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class GreedyMathDebugResultDTO(
|
||||
@get:JsonProperty("requestId")
|
||||
@param:JsonProperty("requestId")
|
||||
val requestId: UUID,
|
||||
|
||||
@get:JsonProperty("satelliteIds")
|
||||
@param:JsonProperty("satelliteIds")
|
||||
val satelliteIds: List<Long>,
|
||||
|
||||
@get:JsonProperty("missingSatelliteIds")
|
||||
@param:JsonProperty("missingSatelliteIds")
|
||||
val missingSatelliteIds: List<Long>,
|
||||
|
||||
@get:JsonProperty("calculationStart")
|
||||
@param:JsonProperty("calculationStart")
|
||||
val calculationStart: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("calculationEnd")
|
||||
@param:JsonProperty("calculationEnd")
|
||||
val calculationEnd: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("status")
|
||||
@param:JsonProperty("status")
|
||||
val status: ComplexPlanCalculationStatus,
|
||||
|
||||
@get:JsonProperty("revolutionMode")
|
||||
@param:JsonProperty("revolutionMode")
|
||||
val revolutionMode: DynamicPlanRevolutionMode,
|
||||
|
||||
@get:JsonProperty("observationParametersFilePath")
|
||||
@param:JsonProperty("observationParametersFilePath")
|
||||
val observationParametersFilePath: String? = null,
|
||||
|
||||
@get:JsonProperty("intervalsCount")
|
||||
@param:JsonProperty("intervalsCount")
|
||||
val intervalsCount: Int,
|
||||
|
||||
@get:JsonProperty("observationParametersCount")
|
||||
@param:JsonProperty("observationParametersCount")
|
||||
val observationParametersCount: Int,
|
||||
|
||||
@get:JsonProperty("selectedCandidatesCount")
|
||||
@param:JsonProperty("selectedCandidatesCount")
|
||||
val selectedCandidatesCount: Int,
|
||||
|
||||
@get:JsonProperty("failuresCount")
|
||||
@param:JsonProperty("failuresCount")
|
||||
val failuresCount: Int,
|
||||
|
||||
@get:JsonProperty("selectedCandidates")
|
||||
@param:JsonProperty("selectedCandidates")
|
||||
val selectedCandidates: List<GreedyMathCandidateDTO>,
|
||||
|
||||
@get:JsonProperty("filterCoveredRoutes")
|
||||
@param:JsonProperty("filterCoveredRoutes")
|
||||
val filterCoveredRoutes: Boolean,
|
||||
|
||||
@get:JsonProperty("routesBeforeUsefulAreaFilterCount")
|
||||
@param:JsonProperty("routesBeforeUsefulAreaFilterCount")
|
||||
val routesBeforeUsefulAreaFilterCount: Int,
|
||||
|
||||
@get:JsonProperty("routesAfterUsefulAreaFilterCount")
|
||||
@param:JsonProperty("routesAfterUsefulAreaFilterCount")
|
||||
val routesAfterUsefulAreaFilterCount: Int,
|
||||
|
||||
@get:JsonProperty("routeBuildFailuresCount")
|
||||
@param:JsonProperty("routeBuildFailuresCount")
|
||||
val routeBuildFailuresCount: Int,
|
||||
|
||||
@get:JsonProperty("greedySelectionMs")
|
||||
@param:JsonProperty("greedySelectionMs")
|
||||
val greedySelectionMs: Long,
|
||||
|
||||
@get:JsonProperty("routeBuildMs")
|
||||
@param:JsonProperty("routeBuildMs")
|
||||
val routeBuildMs: Long,
|
||||
|
||||
@get:JsonProperty("usefulAreaAnalysisMs")
|
||||
@param:JsonProperty("usefulAreaAnalysisMs")
|
||||
val usefulAreaAnalysisMs: Long,
|
||||
|
||||
@get:JsonProperty("totalMs")
|
||||
@param:JsonProperty("totalMs")
|
||||
val totalMs: Long
|
||||
)
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamDTO
|
||||
import java.util.*
|
||||
|
||||
@Schema(description = "Интервал")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonSerialize
|
||||
data class IntervalItemDTO(
|
||||
@get:JsonProperty("id")
|
||||
@param:JsonProperty("id")
|
||||
val id : UUID = UUID.randomUUID(),
|
||||
|
||||
@get:JsonProperty("regionId")
|
||||
@param:JsonProperty("regionId")
|
||||
val regionId : UUID,
|
||||
|
||||
@get:JsonProperty("revSign")
|
||||
@param:JsonProperty("revSign")
|
||||
val revSign : RevolutionSign,
|
||||
|
||||
@get:JsonProperty("geometry")
|
||||
@param:JsonProperty("geometry")
|
||||
val geometry: String,
|
||||
|
||||
@get:JsonProperty("mpl")
|
||||
@param:JsonProperty("mpl")
|
||||
val mpl: MutableList<SquareViewParamDTO> = mutableListOf()
|
||||
)
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.PositionDTO
|
||||
import java.util.UUID
|
||||
|
||||
@Schema(description = "Модель данных объекта для расчета наблюдения")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonSerialize
|
||||
data class ObjDTO(
|
||||
@get:JsonProperty("id")
|
||||
@param:JsonProperty("id")
|
||||
val id: UUID,
|
||||
|
||||
@get:JsonProperty("position")
|
||||
@param:JsonProperty("position")
|
||||
val position: PositionDTO? = null,
|
||||
|
||||
@get:JsonProperty("contourWKT")
|
||||
@param:JsonProperty("contourWKT")
|
||||
val contourWKT: String? = null
|
||||
)
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Schema(description = "Запрос расчета параметров наблюдения для набора объектов")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonSerialize
|
||||
data class ObjViewRequestDTO(
|
||||
@get:JsonProperty("sunAngleMin")
|
||||
@param:JsonProperty("sunAngleMin")
|
||||
val sunAngleMin: Double? = null,
|
||||
|
||||
@get:JsonProperty("objects")
|
||||
@param:JsonProperty("objects")
|
||||
val objects: List<ObjDTO>,
|
||||
|
||||
@get:JsonProperty("satellites")
|
||||
@param:JsonProperty("satellites")
|
||||
val satellites: Iterable<Long>? = null,
|
||||
|
||||
@get:JsonProperty("timeStart")
|
||||
@param:JsonProperty("timeStart")
|
||||
val timeStart: LocalDateTime? = null,
|
||||
|
||||
@get:JsonProperty("timeStop")
|
||||
@param:JsonProperty("timeStop")
|
||||
val timeStop: LocalDateTime? = null
|
||||
)
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
|
||||
@Schema(description = "Debug-запрос построения интервалов и расчета параметров наблюдения")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class ObservationParametersDebugRequestDTO(
|
||||
@get:JsonProperty("requestId")
|
||||
@param:JsonProperty("requestId")
|
||||
val requestId: UUID,
|
||||
|
||||
@get:JsonProperty("satelliteIds")
|
||||
@param:JsonProperty("satelliteIds")
|
||||
val satelliteIds: List<Long>,
|
||||
|
||||
@get:JsonProperty("calculationStart")
|
||||
@param:JsonProperty("calculationStart")
|
||||
val calculationStart: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("calculationEnd")
|
||||
@param:JsonProperty("calculationEnd")
|
||||
val calculationEnd: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("parallelBallisticsCalculation")
|
||||
@param:JsonProperty("parallelBallisticsCalculation")
|
||||
val parallelBallisticsCalculation: Boolean = false,
|
||||
|
||||
@get:JsonProperty("chunkSize")
|
||||
@param:JsonProperty("chunkSize")
|
||||
val chunkSize: Int? = null,
|
||||
|
||||
@get:JsonProperty("parallelism")
|
||||
@param:JsonProperty("parallelism")
|
||||
val parallelism: Int? = null,
|
||||
|
||||
@get:JsonProperty("saveResultToFile")
|
||||
@param:JsonProperty("saveResultToFile")
|
||||
val saveResultToFile: Boolean = false
|
||||
)
|
||||
|
||||
@Schema(description = "Debug-результат построения интервалов и расчета параметров наблюдения")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class ObservationParametersDebugResultDTO(
|
||||
@get:JsonProperty("requestId")
|
||||
@param:JsonProperty("requestId")
|
||||
val requestId: UUID,
|
||||
|
||||
@get:JsonProperty("satelliteIds")
|
||||
@param:JsonProperty("satelliteIds")
|
||||
val satelliteIds: List<Long>,
|
||||
|
||||
@get:JsonProperty("missingSatelliteIds")
|
||||
@param:JsonProperty("missingSatelliteIds")
|
||||
val missingSatelliteIds: List<Long>,
|
||||
|
||||
@get:JsonProperty("calculationStart")
|
||||
@param:JsonProperty("calculationStart")
|
||||
val calculationStart: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("calculationEnd")
|
||||
@param:JsonProperty("calculationEnd")
|
||||
val calculationEnd: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("status")
|
||||
@param:JsonProperty("status")
|
||||
val status: ComplexPlanCalculationStatus,
|
||||
|
||||
@get:JsonProperty("parallelBallisticsCalculation")
|
||||
@param:JsonProperty("parallelBallisticsCalculation")
|
||||
val parallelBallisticsCalculation: Boolean,
|
||||
|
||||
@get:JsonProperty("intervalsCount")
|
||||
@param:JsonProperty("intervalsCount")
|
||||
val intervalsCount: Int,
|
||||
|
||||
@get:JsonProperty("observationParametersCount")
|
||||
@param:JsonProperty("observationParametersCount")
|
||||
val observationParametersCount: Int,
|
||||
|
||||
@get:JsonProperty("chunksCount")
|
||||
@param:JsonProperty("chunksCount")
|
||||
val chunksCount: Int,
|
||||
|
||||
@get:JsonProperty("intervalsMs")
|
||||
@param:JsonProperty("intervalsMs")
|
||||
val intervalsMs: Long,
|
||||
|
||||
@get:JsonProperty("observationParametersMs")
|
||||
@param:JsonProperty("observationParametersMs")
|
||||
val observationParametersMs: Long,
|
||||
|
||||
@get:JsonProperty("totalMs")
|
||||
@param:JsonProperty("totalMs")
|
||||
val totalMs: Long,
|
||||
|
||||
@get:JsonProperty("savedToFile")
|
||||
@param:JsonProperty("savedToFile")
|
||||
val savedToFile: Boolean = false,
|
||||
|
||||
@get:JsonProperty("resultFilePath")
|
||||
@param:JsonProperty("resultFilePath")
|
||||
val resultFilePath: String? = null
|
||||
)
|
||||
|
||||
@Schema(description = "Файловый артефакт debug-расчета интервалов и параметров наблюдения")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class ObservationParametersDebugFileDTO(
|
||||
@get:JsonProperty("schemaVersion")
|
||||
@param:JsonProperty("schemaVersion")
|
||||
val schemaVersion: Int = 1,
|
||||
|
||||
@get:JsonProperty("createdAt")
|
||||
@param:JsonProperty("createdAt")
|
||||
val createdAt: Instant,
|
||||
|
||||
@get:JsonProperty("requestId")
|
||||
@param:JsonProperty("requestId")
|
||||
val requestId: UUID,
|
||||
|
||||
@get:JsonProperty("satelliteIds")
|
||||
@param:JsonProperty("satelliteIds")
|
||||
val satelliteIds: List<Long>,
|
||||
|
||||
@get:JsonProperty("missingSatelliteIds")
|
||||
@param:JsonProperty("missingSatelliteIds")
|
||||
val missingSatelliteIds: List<Long>,
|
||||
|
||||
@get:JsonProperty("calculationStart")
|
||||
@param:JsonProperty("calculationStart")
|
||||
val calculationStart: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("calculationEnd")
|
||||
@param:JsonProperty("calculationEnd")
|
||||
val calculationEnd: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("parallelBallisticsCalculation")
|
||||
@param:JsonProperty("parallelBallisticsCalculation")
|
||||
val parallelBallisticsCalculation: Boolean,
|
||||
|
||||
@get:JsonProperty("chunkSize")
|
||||
@param:JsonProperty("chunkSize")
|
||||
val chunkSize: Int?,
|
||||
|
||||
@get:JsonProperty("parallelism")
|
||||
@param:JsonProperty("parallelism")
|
||||
val parallelism: Int?,
|
||||
|
||||
@get:JsonProperty("intervalsCount")
|
||||
@param:JsonProperty("intervalsCount")
|
||||
val intervalsCount: Int,
|
||||
|
||||
@get:JsonProperty("observationParametersCount")
|
||||
@param:JsonProperty("observationParametersCount")
|
||||
val observationParametersCount: Int,
|
||||
|
||||
@get:JsonProperty("intervals")
|
||||
@param:JsonProperty("intervals")
|
||||
val intervals: List<IntervalItemDTO>
|
||||
)
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonAlias
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
|
||||
@Schema(description = "Заявка для расчета комплексного плана")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonSerialize
|
||||
data class RequestItemDTO(
|
||||
@get:JsonProperty("id")
|
||||
@param:JsonProperty("id")
|
||||
val id : UUID = UUID.randomUUID(),
|
||||
|
||||
@get:JsonProperty("name")
|
||||
@param:JsonProperty("name")
|
||||
val name : String,
|
||||
|
||||
@get:JsonProperty("originalGeometry")
|
||||
@param:JsonProperty("originalGeometry")
|
||||
@param:JsonAlias("geometry", "remainingGeometry")
|
||||
val geometry : String,
|
||||
|
||||
@get:JsonProperty("intervalBegin")
|
||||
@param:JsonProperty("intervalBegin")
|
||||
val intervalBegin : LocalDateTime,
|
||||
|
||||
@get:JsonProperty("intervalEnd")
|
||||
@param:JsonProperty("intervalEnd")
|
||||
val intervalEnd : LocalDateTime,
|
||||
|
||||
@get:JsonProperty("resolution")
|
||||
@param:JsonProperty("resolution")
|
||||
val resolution : Double,
|
||||
|
||||
@get:JsonProperty("priority")
|
||||
@param:JsonProperty("priority")
|
||||
val priority : Int? = null,
|
||||
|
||||
@get:JsonProperty("importance")
|
||||
@param:JsonProperty("importance")
|
||||
val importance : Double,
|
||||
|
||||
/**Исходная площадь района.
|
||||
* Используется для расчета важности задания */
|
||||
@get:JsonProperty("baseArea")
|
||||
@param:JsonProperty("baseArea")
|
||||
var baseArea : Double? = null,
|
||||
|
||||
/**Текущая площадь района.
|
||||
* Используется для расчета важности задания */
|
||||
@get:JsonProperty("currentArea")
|
||||
@param:JsonProperty("currentArea")
|
||||
var currentArea : Double? = null,
|
||||
|
||||
/**Дата время последнего наблюдения.
|
||||
* Используется для расчета важности задания */
|
||||
@get:JsonProperty("dateTimeLastObservation")
|
||||
@param:JsonProperty("dateTimeLastObservation")
|
||||
var dateTimeLastObservation : LocalDateTime? = null,
|
||||
|
||||
@get:JsonProperty("appType")
|
||||
@param:JsonProperty("appType")
|
||||
val appType : Int = 0,
|
||||
|
||||
@get:JsonProperty("intervals")
|
||||
@param:JsonProperty("intervals")
|
||||
val intervals : MutableList<IntervalItemDTO>? = null,
|
||||
)
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamDTO
|
||||
import java.time.LocalDateTime
|
||||
|
||||
data class SurveyContour(
|
||||
val mpl : SquareViewParamDTO,
|
||||
val time : LocalDateTime,
|
||||
val duration : Double,
|
||||
val rev : Long,
|
||||
val gamma : Double,
|
||||
val geometry : String,
|
||||
)
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package space.nstart.pcp.complan.types
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
|
||||
@Schema(description = "Debug-запрос построения маршрутов по всем параметрам видимости из файла")
|
||||
data class TestCalcMarsDebugRequestDTO(
|
||||
@get:JsonProperty("requestId")
|
||||
@param:JsonProperty("requestId")
|
||||
val requestId: UUID,
|
||||
|
||||
@get:JsonProperty("satelliteIds")
|
||||
@param:JsonProperty("satelliteIds")
|
||||
val satelliteIds: List<Long>,
|
||||
|
||||
@get:JsonProperty("calculationStart")
|
||||
@param:JsonProperty("calculationStart")
|
||||
val calculationStart: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("calculationEnd")
|
||||
@param:JsonProperty("calculationEnd")
|
||||
val calculationEnd: LocalDateTime
|
||||
)
|
||||
|
||||
@Schema(description = "Debug-результат построения маршрутов по всем параметрам видимости из файла")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class TestCalcMarsDebugResultDTO(
|
||||
@get:JsonProperty("requestId")
|
||||
@param:JsonProperty("requestId")
|
||||
val requestId: UUID,
|
||||
|
||||
@get:JsonProperty("satelliteIds")
|
||||
@param:JsonProperty("satelliteIds")
|
||||
val satelliteIds: List<Long>,
|
||||
|
||||
@get:JsonProperty("missingSatelliteIds")
|
||||
@param:JsonProperty("missingSatelliteIds")
|
||||
val missingSatelliteIds: List<Long>,
|
||||
|
||||
@get:JsonProperty("calculationStart")
|
||||
@param:JsonProperty("calculationStart")
|
||||
val calculationStart: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("calculationEnd")
|
||||
@param:JsonProperty("calculationEnd")
|
||||
val calculationEnd: LocalDateTime,
|
||||
|
||||
@get:JsonProperty("status")
|
||||
@param:JsonProperty("status")
|
||||
val status: ComplexPlanCalculationStatus,
|
||||
|
||||
@get:JsonProperty("observationParametersFilePath")
|
||||
@param:JsonProperty("observationParametersFilePath")
|
||||
val observationParametersFilePath: String? = null,
|
||||
|
||||
@get:JsonProperty("intervalsCount")
|
||||
@param:JsonProperty("intervalsCount")
|
||||
val intervalsCount: Int,
|
||||
|
||||
@get:JsonProperty("observationParametersCount")
|
||||
@param:JsonProperty("observationParametersCount")
|
||||
val observationParametersCount: Int,
|
||||
|
||||
@get:JsonProperty("routeBuildAttemptsCount")
|
||||
@param:JsonProperty("routeBuildAttemptsCount")
|
||||
val routeBuildAttemptsCount: Int,
|
||||
|
||||
@get:JsonProperty("routesCount")
|
||||
@param:JsonProperty("routesCount")
|
||||
val routesCount: Int,
|
||||
|
||||
@get:JsonProperty("routeBuildFailuresCount")
|
||||
@param:JsonProperty("routeBuildFailuresCount")
|
||||
val routeBuildFailuresCount: Int,
|
||||
|
||||
@get:JsonProperty("routeBuildMs")
|
||||
@param:JsonProperty("routeBuildMs")
|
||||
val routeBuildMs: Long,
|
||||
|
||||
@get:JsonProperty("totalMs")
|
||||
@param:JsonProperty("totalMs")
|
||||
val totalMs: Long
|
||||
)
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package space.nstart.pcp.complan.utils.extensions
|
||||
|
||||
import org.locationtech.jts.geom.CoordinateFilter
|
||||
import org.locationtech.jts.geom.Geometry
|
||||
import kotlin.math.PI
|
||||
|
||||
|
||||
fun Geometry.radiansToDegrees(): Geometry {
|
||||
// Создаём копию, чтобы не мутировать оригинал
|
||||
val copy = this.copy() as Geometry
|
||||
|
||||
// Применяем фильтр, который конвертирует радианы → градусы
|
||||
copy.apply(CoordinateFilter { coord ->
|
||||
coord.x = coord.x * 180.0 / PI // долгота
|
||||
coord.y = coord.y * 180.0 / PI // широта
|
||||
})
|
||||
|
||||
return copy
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package space.nstart.pcp.complan.utils.extensions
|
||||
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
|
||||
|
||||
fun RevolutionSign.toInt() : Int {
|
||||
return when (this) {
|
||||
RevolutionSign.ASC -> 0
|
||||
RevolutionSign.DESC -> 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
spring:
|
||||
application:
|
||||
name: pcp-dynamic-plan-service
|
||||
profiles:
|
||||
default: local
|
||||
config:
|
||||
import: "configserver:"
|
||||
cloud:
|
||||
config:
|
||||
uri: ${CONFIG_SERVER_URI:http://192.168.100.160:38888}
|
||||
fail-fast: ${CONFIG_SERVER_FAIL_FAST:true}
|
||||
profile: ${SPRING_CLOUD_CONFIG_PROFILE:${SPRING_PROFILES_ACTIVE:${spring.profiles.default}}}
|
||||
label: ${SPRING_CLOUD_CONFIG_LABEL:master}
|
||||
# codec:
|
||||
# max-in-memory-size: 16MB
|
||||
datasource:
|
||||
driver-class-name: org.postgresql.Driver
|
||||
url: jdbc:postgresql://192.168.60.68:5432/complan
|
||||
username: postgres
|
||||
password: password
|
||||
jpa:
|
||||
database-platform: org.hibernate.dialect.PostgreSQLDialect
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
show-sql: false
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: false
|
||||
jdbc:
|
||||
batch_size: 40
|
||||
order_inserts: true
|
||||
order_updates: true
|
||||
|
||||
feign:
|
||||
client:
|
||||
config:
|
||||
default:
|
||||
connectTimeout: 5000
|
||||
readTimeout: 30000
|
||||
loggerLevel: basic
|
||||
|
||||
flyway:
|
||||
enabled: true
|
||||
locations: classpath:db/migration/pcp-complex-plan
|
||||
baseline-on-migrate: true
|
||||
ignore-migration-patterns: "*:missing"
|
||||
baseline-version: 0
|
||||
validate-on-migrate: true
|
||||
repair-on-migrate: true
|
||||
clean-disabled: true
|
||||
out-of-order: true
|
||||
schemas: public
|
||||
|
||||
logging:
|
||||
level:
|
||||
org.flywaydb: DEBUG
|
||||
org.hibernate.SQL: DEBUG
|
||||
org.hibernate.type.descriptor.sql.BasicBinder: TRACE
|
||||
org.springframework.jdbc.core: TRACE
|
||||
|
||||
springdoc:
|
||||
swagger-ui:
|
||||
enabled: true
|
||||
layout: BaseLayout
|
||||
path: /swagger/ui
|
||||
api-docs:
|
||||
enabled: true
|
||||
path: /v3/api-docs
|
||||
|
||||
complex-plan:
|
||||
observation-parameters:
|
||||
parallel-enabled: ${COMPLEX_PLAN_OBSERVATION_PARAMETERS_PARALLEL_ENABLED:true}
|
||||
chunk-size: ${COMPLEX_PLAN_OBSERVATION_PARAMETERS_CHUNK_SIZE:20}
|
||||
parallelism: ${COMPLEX_PLAN_OBSERVATION_PARAMETERS_PARALLELISM:8}
|
||||
debug-output-dir: ${COMPLEX_PLAN_OBSERVATION_PARAMETERS_DEBUG_OUTPUT_DIR:${java.io.tmpdir}/pcp-dynamic-plan-service/obsParams}
|
||||
geo-layers:
|
||||
enabled: false
|
||||
output-dir: ${java.io.tmpdir}/pcp-dynamic-plan-service/geo-layers
|
||||
@@ -0,0 +1 @@
|
||||
SELECT 'Database initialization complete' as status;
|
||||
@@ -0,0 +1,4 @@
|
||||
CREATE TABLE IF NOT EXISTS aplsettings (
|
||||
id SERIAL PRIMARY KEY, --серийный ключ
|
||||
settings TEXT --настройки автомата в виде JSON
|
||||
);
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
CREATE TABLE IF NOT EXISTS aplsettings (
|
||||
id SERIAL PRIMARY KEY, --серийный ключ
|
||||
settings TEXT --настройки автомата в виде JSON
|
||||
);
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
create table if not exists dynamic_plan_run
|
||||
(
|
||||
id uuid primary key,
|
||||
status varchar(32) not null,
|
||||
request_id uuid not null,
|
||||
satellite_ids text not null,
|
||||
calculation_start timestamp not null,
|
||||
calculation_end timestamp not null,
|
||||
created_at timestamp not null,
|
||||
started_at timestamp,
|
||||
finished_at timestamp,
|
||||
duration_ms bigint,
|
||||
result_status varchar(64),
|
||||
routes_count integer,
|
||||
result_json text,
|
||||
error_message text
|
||||
);
|
||||
|
||||
create index if not exists idx_dynamic_plan_run_status on dynamic_plan_run(status);
|
||||
create index if not exists idx_dynamic_plan_run_request_id on dynamic_plan_run(request_id);
|
||||
|
||||
create table if not exists dynamic_plan_route
|
||||
(
|
||||
id bigserial primary key,
|
||||
run_id uuid not null references dynamic_plan_run(id) on delete cascade,
|
||||
request_id uuid not null,
|
||||
satellite_id bigint not null,
|
||||
start_time timestamp not null,
|
||||
end_time timestamp not null,
|
||||
duration double precision not null,
|
||||
revolution bigint not null,
|
||||
roll double precision not null,
|
||||
contour_wkt text not null,
|
||||
route_order integer not null
|
||||
);
|
||||
|
||||
create index if not exists idx_dynamic_plan_route_run_id on dynamic_plan_route(run_id);
|
||||
create index if not exists idx_dynamic_plan_route_request_id on dynamic_plan_route(request_id);
|
||||
create index if not exists idx_dynamic_plan_route_satellite_id on dynamic_plan_route(satellite_id);
|
||||
create index if not exists idx_dynamic_plan_route_start_time on dynamic_plan_route(start_time);
|
||||
create index if not exists idx_dynamic_plan_route_run_start on dynamic_plan_route(run_id, start_time);
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
alter table dynamic_plan_run
|
||||
add column if not exists calculation_mode varchar(32) not null default 'FULL';
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
alter table dynamic_plan_run
|
||||
add column if not exists revolution_mode varchar(32) not null default 'BOTH';
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
alter table dynamic_plan_run
|
||||
add column if not exists filter_covered_routes boolean not null default false;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
alter table dynamic_plan_run
|
||||
add column if not exists min_useful_route_area_km2 double precision not null default 0.0;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
create table if not exists dynamic_plan_interval
|
||||
(
|
||||
id bigserial primary key,
|
||||
run_id uuid not null references dynamic_plan_run(id) on delete cascade,
|
||||
request_id uuid not null,
|
||||
interval_id uuid not null,
|
||||
region_id uuid not null,
|
||||
rev_sign varchar(16) not null,
|
||||
contour_wkt text not null,
|
||||
observation_parameters_count integer not null,
|
||||
interval_order integer not null
|
||||
);
|
||||
|
||||
create index if not exists idx_dynamic_plan_interval_run_id on dynamic_plan_interval(run_id);
|
||||
create index if not exists idx_dynamic_plan_interval_request_id on dynamic_plan_interval(request_id);
|
||||
create index if not exists idx_dynamic_plan_interval_region_id on dynamic_plan_interval(region_id);
|
||||
create index if not exists idx_dynamic_plan_interval_run_order on dynamic_plan_interval(run_id, interval_order);
|
||||
Reference in New Issue
Block a user