Учет забронированных слотов при расчете комплексного плана
This commit is contained in:
+113
-7
@@ -18,8 +18,11 @@ import space.nstart.pcp.pcp_types_lib.dto.requests.EarthCellWithRequestsDTO
|
|||||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.OccupiedIntervalDTO
|
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.OccupiedIntervalDTO
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageStrategy
|
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageStrategy
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageTargetDTO
|
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageTargetDTO
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SurveySlotDTO
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
import kotlin.math.abs
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -36,6 +39,11 @@ class ComplexPlanCalculationService(
|
|||||||
|
|
||||||
private val logger: Logger = LoggerFactory.getLogger(this::class.java)
|
private val logger: Logger = LoggerFactory.getLogger(this::class.java)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val LOG_INTERVAL_SAMPLE_LIMIT = 10
|
||||||
|
private const val ROLL_EPSILON = 0.01
|
||||||
|
}
|
||||||
|
|
||||||
fun process(
|
fun process(
|
||||||
intervalStart: LocalDateTime,
|
intervalStart: LocalDateTime,
|
||||||
intervalEnd: LocalDateTime,
|
intervalEnd: LocalDateTime,
|
||||||
@@ -134,7 +142,7 @@ class ComplexPlanCalculationService(
|
|||||||
|
|
||||||
logger.info("Шаг batch: runId={}, загрузка booked-маршрутов из slots-service", runId)
|
logger.info("Шаг batch: runId={}, загрузка booked-маршрутов из slots-service", runId)
|
||||||
val plannedModesMs = measureTimeMillis {
|
val plannedModesMs = measureTimeMillis {
|
||||||
addPlannedModes(workingSatellites, intervalStart, intervalEnd)
|
addPlannedModes(runId, workingSatellites, intervalStart, intervalEnd)
|
||||||
}
|
}
|
||||||
logger.info(
|
logger.info(
|
||||||
"Stage planned-modes-load: runId={}, satellites={}, durationMs={}",
|
"Stage planned-modes-load: runId={}, satellites={}, durationMs={}",
|
||||||
@@ -161,8 +169,9 @@ class ComplexPlanCalculationService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val targetsChunk = buildTargetsChunk(chunk)
|
val targetsChunk = buildTargetsChunk(chunk)
|
||||||
var coverageByTargetId = emptyMap<Long, List<space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO>>()
|
var coverageByTargetId = emptyMap<Long, List<SlotDTO>>()
|
||||||
val occupiedIntervals = buildOccupiedIntervalsSnapshot(workingSatellites)
|
val occupiedIntervals = buildOccupiedIntervalsSnapshot(workingSatellites)
|
||||||
|
logOccupiedIntervalsSnapshot(runId, chunkIndex + 1, chunkCount, occupiedIntervals)
|
||||||
val coverageFetchMs = measureTimeMillis {
|
val coverageFetchMs = measureTimeMillis {
|
||||||
coverageByTargetId = coverageCalculationClient.coverageModes(
|
coverageByTargetId = coverageCalculationClient.coverageModes(
|
||||||
targets = targetsChunk,
|
targets = targetsChunk,
|
||||||
@@ -189,6 +198,7 @@ class ComplexPlanCalculationService(
|
|||||||
var acceptedForChunk = 0
|
var acceptedForChunk = 0
|
||||||
val chunkProcessMs = measureTimeMillis {
|
val chunkProcessMs = measureTimeMillis {
|
||||||
acceptedForChunk = processChunk(
|
acceptedForChunk = processChunk(
|
||||||
|
runId = runId,
|
||||||
workingSatellites = workingSatellites,
|
workingSatellites = workingSatellites,
|
||||||
cells = chunk,
|
cells = chunk,
|
||||||
coverageByTargetId = coverageByTargetId,
|
coverageByTargetId = coverageByTargetId,
|
||||||
@@ -294,6 +304,7 @@ class ComplexPlanCalculationService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun addPlannedModes(
|
private fun addPlannedModes(
|
||||||
|
runId: Long?,
|
||||||
workingSatellites: List<SatelliteModel>,
|
workingSatellites: List<SatelliteModel>,
|
||||||
intervalStart: LocalDateTime,
|
intervalStart: LocalDateTime,
|
||||||
intervalEnd: LocalDateTime
|
intervalEnd: LocalDateTime
|
||||||
@@ -309,9 +320,8 @@ class ComplexPlanCalculationService(
|
|||||||
|
|
||||||
workingSatellites.forEach { satellite ->
|
workingSatellites.forEach { satellite ->
|
||||||
val plannedForSatellite = plannedModesBySatelliteId[satellite.satelliteId].orEmpty()
|
val plannedForSatellite = plannedModesBySatelliteId[satellite.satelliteId].orEmpty()
|
||||||
plannedModesBySatelliteId[satellite.satelliteId]
|
logBookedSlotsLoaded(runId, satellite.satelliteId, plannedForSatellite)
|
||||||
.orEmpty()
|
plannedForSatellite.forEach { slot ->
|
||||||
.forEach { slot ->
|
|
||||||
surveyPlacementService.mergeSurvey(
|
surveyPlacementService.mergeSurvey(
|
||||||
existingSurveys = satellite.longMission.surveys,
|
existingSurveys = satellite.longMission.surveys,
|
||||||
candidateSurvey = SurveyMode(
|
candidateSurvey = SurveyMode(
|
||||||
@@ -332,7 +342,8 @@ class ComplexPlanCalculationService(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
logger.info(
|
logger.info(
|
||||||
"Добавлены booked-маршруты для satelliteId={}: count={}",
|
"Добавлены booked-маршруты: runId={}, satelliteId={}, count={}",
|
||||||
|
runId,
|
||||||
satellite.satelliteId,
|
satellite.satelliteId,
|
||||||
plannedForSatellite.size
|
plannedForSatellite.size
|
||||||
)
|
)
|
||||||
@@ -367,10 +378,59 @@ class ComplexPlanCalculationService(
|
|||||||
.sortedWith(compareBy<OccupiedIntervalDTO> { it.satelliteId }.thenBy { it.startTime }.thenBy { it.endTime })
|
.sortedWith(compareBy<OccupiedIntervalDTO> { it.satelliteId }.thenBy { it.startTime }.thenBy { it.endTime })
|
||||||
.toList()
|
.toList()
|
||||||
|
|
||||||
|
private fun logBookedSlotsLoaded(runId: Long?, satelliteId: Long, slots: List<SurveySlotDTO>) {
|
||||||
|
if (slots.isEmpty()) {
|
||||||
|
logger.info("Booked slots loaded: runId={}, satelliteId={}, count=0", runId, satelliteId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Booked slots loaded: runId={}, satelliteId={}, count={}, interval=[{} - {}], sample={}",
|
||||||
|
runId,
|
||||||
|
satelliteId,
|
||||||
|
slots.size,
|
||||||
|
slots.minOf { it.tn },
|
||||||
|
slots.maxOf { it.tk },
|
||||||
|
slots.take(LOG_INTERVAL_SAMPLE_LIMIT).map { slot ->
|
||||||
|
"start=${slot.tn},end=${slot.tk},roll=${slot.roll},revolution=${slot.revolution},slotIds=${slot.slotIds}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun logOccupiedIntervalsSnapshot(
|
||||||
|
runId: Long?,
|
||||||
|
chunkNumber: Int,
|
||||||
|
chunkCount: Int,
|
||||||
|
occupiedIntervals: List<OccupiedIntervalDTO>
|
||||||
|
) {
|
||||||
|
val bySource = occupiedIntervals.groupingBy { it.source ?: "UNKNOWN" }.eachCount()
|
||||||
|
val slotsBySatellite = occupiedIntervals
|
||||||
|
.filter { it.source == SatelliteModeSource.SLOTS.name }
|
||||||
|
.groupingBy { it.satelliteId }
|
||||||
|
.eachCount()
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Occupied intervals snapshot before coverage request: runId={}, chunk={}/{}, total={}, bySource={}, slotsBySatellite={}, slotsSample={}",
|
||||||
|
runId,
|
||||||
|
chunkNumber,
|
||||||
|
chunkCount,
|
||||||
|
occupiedIntervals.size,
|
||||||
|
bySource,
|
||||||
|
slotsBySatellite,
|
||||||
|
occupiedIntervals
|
||||||
|
.filter { it.source == SatelliteModeSource.SLOTS.name }
|
||||||
|
.take(LOG_INTERVAL_SAMPLE_LIMIT)
|
||||||
|
.map { interval ->
|
||||||
|
"satelliteId=${interval.satelliteId},start=${interval.startTime},end=${interval.endTime},roll=${interval.roll},source=${interval.source}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun processChunk(
|
private fun processChunk(
|
||||||
|
runId: Long?,
|
||||||
workingSatellites: List<SatelliteModel>,
|
workingSatellites: List<SatelliteModel>,
|
||||||
cells: List<EarthCellWithRequestsDTO>,
|
cells: List<EarthCellWithRequestsDTO>,
|
||||||
coverageByTargetId: Map<Long, List<space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO>>,
|
coverageByTargetId: Map<Long, List<SlotDTO>>,
|
||||||
intervalStart: LocalDateTime,
|
intervalStart: LocalDateTime,
|
||||||
intervalEnd: LocalDateTime
|
intervalEnd: LocalDateTime
|
||||||
): Int {
|
): Int {
|
||||||
@@ -391,6 +451,13 @@ class ComplexPlanCalculationService(
|
|||||||
val satellite = workingSatellites.find { it.satelliteId == slot.satelliteId }
|
val satellite = workingSatellites.find { it.satelliteId == slot.satelliteId }
|
||||||
?: throw CustomValidationException("KA ${slot.satelliteId} не зарегистророван")
|
?: throw CustomValidationException("KA ${slot.satelliteId} не зарегистророван")
|
||||||
|
|
||||||
|
logComplanCandidateBookedSlotOverlaps(
|
||||||
|
runId = runId,
|
||||||
|
cell = cell,
|
||||||
|
slot = slot,
|
||||||
|
existingSurveys = satellite.longMission.surveys
|
||||||
|
)
|
||||||
|
|
||||||
val decision = surveyPlacementService.tryPlaceSurvey(
|
val decision = surveyPlacementService.tryPlaceSurvey(
|
||||||
existingSurveys = satellite.longMission.surveys,
|
existingSurveys = satellite.longMission.surveys,
|
||||||
candidateSurvey = SurveyMode(
|
candidateSurvey = SurveyMode(
|
||||||
@@ -425,6 +492,45 @@ class ComplexPlanCalculationService(
|
|||||||
return acceptedModes
|
return acceptedModes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun logComplanCandidateBookedSlotOverlaps(
|
||||||
|
runId: Long?,
|
||||||
|
cell: EarthCellWithRequestsDTO,
|
||||||
|
slot: SlotDTO,
|
||||||
|
existingSurveys: List<SurveyMode>
|
||||||
|
) {
|
||||||
|
val overlappingBookedSlots = existingSurveys
|
||||||
|
.filter { survey -> survey.source == SatelliteModeSource.SLOTS }
|
||||||
|
.filter { survey -> intersectsInTime(survey.time, survey.timeStop, slot.tn, slot.tk) }
|
||||||
|
|
||||||
|
if (overlappingBookedSlots.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warn(
|
||||||
|
"COMPLAN candidate overlaps booked SLOTS before placement: runId={}, satelliteId={}, targetId={}, cellNum={}, candidateInterval=[{} - {}], candidateRoll={}, candidateRevolution={}, overlaps={}, sameRollOverlaps={}, bookedSlotsSample={}",
|
||||||
|
runId,
|
||||||
|
slot.satelliteId,
|
||||||
|
cell.targetId(),
|
||||||
|
cell.num,
|
||||||
|
slot.tn,
|
||||||
|
slot.tk,
|
||||||
|
slot.roll,
|
||||||
|
slot.revolution,
|
||||||
|
overlappingBookedSlots.size,
|
||||||
|
overlappingBookedSlots.count { survey -> abs(survey.roll - slot.roll) <= ROLL_EPSILON },
|
||||||
|
overlappingBookedSlots.take(LOG_INTERVAL_SAMPLE_LIMIT).map { survey ->
|
||||||
|
"start=${survey.time},end=${survey.timeStop},roll=${survey.roll},source=${survey.source},bookedSlotIds=${survey.bookedSlotIds}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun intersectsInTime(
|
||||||
|
start1: LocalDateTime,
|
||||||
|
stop1: LocalDateTime,
|
||||||
|
start2: LocalDateTime,
|
||||||
|
stop2: LocalDateTime
|
||||||
|
): Boolean = start1 < stop2 && start2 < stop1
|
||||||
|
|
||||||
private fun hasRemainingCapacity(
|
private fun hasRemainingCapacity(
|
||||||
workingSatellites: List<SatelliteModel>,
|
workingSatellites: List<SatelliteModel>,
|
||||||
intervalStart: LocalDateTime,
|
intervalStart: LocalDateTime,
|
||||||
|
|||||||
+29
-4
@@ -18,6 +18,7 @@ import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageBatchRespon
|
|||||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageStrategy
|
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageStrategy
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageTargetDTO
|
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageTargetDTO
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO
|
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO
|
||||||
|
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotStatus
|
||||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SurveySlotDTO
|
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SurveySlotDTO
|
||||||
import tools.jackson.databind.ObjectMapper
|
import tools.jackson.databind.ObjectMapper
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
@@ -161,16 +162,40 @@ class CoverageBatchClient(webClientBuilderProvider: ObjectProvider<WebClient.Bui
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val rawCandidateSlots = result.response.sumOf { it.slots.size }
|
||||||
|
val filteredResponse = removeBookedSlotsFromCoverageResponse(result.response)
|
||||||
|
val filteredCandidateSlots = filteredResponse.sumOf { it.slots.size }
|
||||||
|
val removedBookedSlots = rawCandidateSlots - filteredCandidateSlots
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"slots-service coverage batch finish: targets={}, returnedTargets={}, candidateSlots={}, durationMs={}, effectiveCoverageStrategy={}",
|
"slots-service coverage batch finish: targets={}, returnedTargets={}, candidateSlotsRaw={}, candidateSlotsAfterBookedFilter={}, removedBookedSlots={}, durationMs={}, effectiveCoverageStrategy={}",
|
||||||
targets.size,
|
targets.size,
|
||||||
result.response.size,
|
filteredResponse.size,
|
||||||
result.response.sumOf { it.slots.size },
|
rawCandidateSlots,
|
||||||
|
filteredCandidateSlots,
|
||||||
|
removedBookedSlots,
|
||||||
result.durationMs,
|
result.durationMs,
|
||||||
result.coverageStrategy
|
result.coverageStrategy
|
||||||
)
|
)
|
||||||
|
|
||||||
return result.response.associate { it.targetId to it.slots }
|
return filteredResponse.associate { it.targetId to it.slots }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complex plan loads booked slots before asking slots-service for coverage candidates.
|
||||||
|
* slots-service still has to see booked slots internally: it marks them as BOOKED and the
|
||||||
|
* coverage solver subtracts their geometry from the uncovered target area. However these
|
||||||
|
* BOOKED slots are already present in the working complex plan and must not be returned
|
||||||
|
* to chunk processing again as COMPLAN candidates.
|
||||||
|
*/
|
||||||
|
private fun removeBookedSlotsFromCoverageResponse(
|
||||||
|
response: List<SlotCoverageBatchResponseDTO>
|
||||||
|
): List<SlotCoverageBatchResponseDTO> =
|
||||||
|
response.map { targetResponse ->
|
||||||
|
targetResponse.copy(
|
||||||
|
slots = targetResponse.slots.filterNot { slot -> slot.state != SlotStatus.AVAILABLE }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun requestCoverageModes(
|
private fun requestCoverageModes(
|
||||||
|
|||||||
+14
@@ -27,6 +27,10 @@ class CoverageSchemeCalculationClient(webClientBuilderProvider: ObjectProvider<W
|
|||||||
private val webClientBuilder: WebClient.Builder = webClientBuilderProvider.ifAvailable ?: WebClient.builder()
|
private val webClientBuilder: WebClient.Builder = webClientBuilderProvider.ifAvailable ?: WebClient.builder()
|
||||||
private val logger = LoggerFactory.getLogger(this::class.java)
|
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val LOG_INTERVAL_SAMPLE_LIMIT = 10
|
||||||
|
}
|
||||||
|
|
||||||
@Value("\${settings.coverage-scheme-service:pcp-coverage-scheme-service}")
|
@Value("\${settings.coverage-scheme-service:pcp-coverage-scheme-service}")
|
||||||
val url = ""
|
val url = ""
|
||||||
|
|
||||||
@@ -55,6 +59,16 @@ class CoverageSchemeCalculationClient(webClientBuilderProvider: ObjectProvider<W
|
|||||||
intervalStart,
|
intervalStart,
|
||||||
intervalEnd
|
intervalEnd
|
||||||
)
|
)
|
||||||
|
if (occupiedIntervals.isNotEmpty()) {
|
||||||
|
logger.warn(
|
||||||
|
"coverage-scheme-service client received occupiedIntervals but does not pass them to coverage-scheme-service request: occupiedIntervals={}, bySource={}, sample={}",
|
||||||
|
occupiedIntervals.size,
|
||||||
|
occupiedIntervals.groupingBy { it.source ?: "UNKNOWN" }.eachCount(),
|
||||||
|
occupiedIntervals.take(LOG_INTERVAL_SAMPLE_LIMIT).map { interval ->
|
||||||
|
"satelliteId=${interval.satelliteId},start=${interval.startTime},end=${interval.endTime},roll=${interval.roll},source=${interval.source}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val result = linkedMapOf<Long, List<SlotDTO>>()
|
val result = linkedMapOf<Long, List<SlotDTO>>()
|
||||||
val durationMs = measureTimeMillis {
|
val durationMs = measureTimeMillis {
|
||||||
|
|||||||
+18
@@ -226,6 +226,9 @@ class SurveyPlacementService {
|
|||||||
if (!canMergeInTime(existing.time, existing.timeStop, mergedSurvey.time, mergedSurvey.timeStop)) {
|
if (!canMergeInTime(existing.time, existing.timeStop, mergedSurvey.time, mergedSurvey.timeStop)) {
|
||||||
return@forEach
|
return@forEach
|
||||||
}
|
}
|
||||||
|
if (shouldSkipComplanInsideSlotsMerge(existing, mergedSurvey)) {
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
val overlapsInTime = intersectsInTime(
|
val overlapsInTime = intersectsInTime(
|
||||||
existing.time,
|
existing.time,
|
||||||
existing.timeStop,
|
existing.timeStop,
|
||||||
@@ -377,6 +380,21 @@ class SurveyPlacementService {
|
|||||||
return gapSeconds in 0..MERGE_MAX_TIME_GAP_SECONDS
|
return gapSeconds in 0..MERGE_MAX_TIME_GAP_SECONDS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun shouldSkipComplanInsideSlotsMerge(
|
||||||
|
left: SurveyMode,
|
||||||
|
right: SurveyMode
|
||||||
|
): Boolean {
|
||||||
|
val complanSurvey = when {
|
||||||
|
left.source == SatelliteModeSource.COMPLAN && right.source == SatelliteModeSource.SLOTS -> left
|
||||||
|
right.source == SatelliteModeSource.COMPLAN && left.source == SatelliteModeSource.SLOTS -> right
|
||||||
|
else -> return false
|
||||||
|
}
|
||||||
|
val slotsSurvey = if (complanSurvey === left) right else left
|
||||||
|
|
||||||
|
return !complanSurvey.time.isBefore(slotsSurvey.time) &&
|
||||||
|
!complanSurvey.timeStop.isAfter(slotsSurvey.timeStop)
|
||||||
|
}
|
||||||
|
|
||||||
private fun intersectsInTime(
|
private fun intersectsInTime(
|
||||||
start1: LocalDateTime,
|
start1: LocalDateTime,
|
||||||
stop1: LocalDateTime,
|
stop1: LocalDateTime,
|
||||||
|
|||||||
+33
@@ -121,6 +121,39 @@ class SurveyPlacementServiceTest {
|
|||||||
assertFalse(surveys.single().contourWKT.startsWith("MULTIPOLYGON"))
|
assertFalse(surveys.single().contourWKT.startsWith("MULTIPOLYGON"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `complan inside booked slot is not merged to mixed`() {
|
||||||
|
val surveys = mutableListOf(
|
||||||
|
survey(
|
||||||
|
start = 0,
|
||||||
|
end = 100,
|
||||||
|
roll = 10.0,
|
||||||
|
source = SatelliteModeSource.SLOTS,
|
||||||
|
contour = "POLYGON ((0 0, 3 0, 3 3, 0 3, 0 0))"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val satellite = satellite()
|
||||||
|
|
||||||
|
val decision = service.tryPlaceSurvey(
|
||||||
|
surveys,
|
||||||
|
survey(
|
||||||
|
start = 0,
|
||||||
|
end = 40,
|
||||||
|
roll = 10.0,
|
||||||
|
source = SatelliteModeSource.COMPLAN,
|
||||||
|
contour = "POLYGON ((1 1, 2 1, 2 2, 1 2, 1 1))"
|
||||||
|
),
|
||||||
|
satellite
|
||||||
|
)
|
||||||
|
|
||||||
|
assertTrue(decision.accepted)
|
||||||
|
assertEquals(2, surveys.size)
|
||||||
|
assertTrue(decision.absorbedSurveys.isEmpty())
|
||||||
|
assertEquals(1, surveys.count { it.source == SatelliteModeSource.SLOTS })
|
||||||
|
assertEquals(1, surveys.count { it.source == SatelliteModeSource.COMPLAN })
|
||||||
|
assertFalse(surveys.any { it.source == SatelliteModeSource.MIXED })
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `different roll conflict resolved by trimming`() {
|
fun `different roll conflict resolved by trimming`() {
|
||||||
val surveys = mutableListOf(survey(start = 51, end = 80, roll = 20.0))
|
val surveys = mutableListOf(survey(start = 51, end = 80, roll = 20.0))
|
||||||
|
|||||||
Reference in New Issue
Block a user