Merge branch 'dev' into route-processing
Conflict resolution: - build.gradle.kts: dev version (keep nstart.cloud repo + jacoco/asm mavenCentral) - settings.gradle.kts: project name from route-processing (observatio-terrae), rest from dev - config addresses (application-local, pcp-mission-planing-service, pcp-dynamic-plan application.yaml): dev versions - menu.html: merged both branches (TGU planning + bookings items) - CatalogControllerTest.kt: merged both mocks (TguPlanningService + BallisticsService) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
FROM bellsoft/liberica-openjre-alpine:21.0.5
|
||||
ARG RUNTIME_IMAGE=repo.nstart.cloud/bellsoft/liberica-openjre-alpine:21.0.5
|
||||
FROM ${RUNTIME_IMAGE}
|
||||
|
||||
ENV JAVA_OPTS=""
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ plugins {
|
||||
kotlin("plugin.lombok")
|
||||
id("org.springframework.boot")
|
||||
id("io.spring.dependency-management")
|
||||
id("org.sonarqube")
|
||||
//id("org.sonarqube")
|
||||
jacoco
|
||||
}
|
||||
|
||||
@@ -103,11 +103,3 @@ tasks.jacocoTestReport {
|
||||
}
|
||||
}
|
||||
|
||||
sonar {
|
||||
properties {
|
||||
property("sonar.projectKey", "pcp")
|
||||
property("sonar.login", "sqp_tokenExample")
|
||||
property("sonar.qualitygate.wait", "${property("sonar.qualitygate.wait")}")
|
||||
property("sonar.host.url", "${property("sonar.host.url")}")
|
||||
}
|
||||
}
|
||||
|
||||
+15
@@ -1,6 +1,7 @@
|
||||
package space.nstart.pcp.slots_service.controller
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.format.annotation.DateTimeFormat
|
||||
import org.springframework.web.bind.annotation.DeleteMapping
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
@@ -28,6 +29,20 @@ class BookingController {
|
||||
|
||||
@GetMapping
|
||||
fun all() = slotService.allBooked()
|
||||
|
||||
@GetMapping("/search")
|
||||
fun search(
|
||||
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) timeStart: LocalDateTime?,
|
||||
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) timeStop: LocalDateTime?,
|
||||
@RequestParam(required = false) requestId: String?,
|
||||
@RequestParam(required = false) satelliteIds: List<Long>?
|
||||
) = slotService.searchBookedSlots(
|
||||
timeStart = timeStart,
|
||||
timeStop = timeStop,
|
||||
requestId = requestId,
|
||||
satelliteIds = satelliteIds
|
||||
)
|
||||
|
||||
@GetMapping("/{booked_slot_id}")
|
||||
fun byId(@PathVariable("booked_slot_id") id : Long) = slotService.bookedById(id)
|
||||
@GetMapping("/by-request/{request_id}")
|
||||
|
||||
+61
-16
@@ -2,6 +2,7 @@ package space.nstart.pcp.slots_service.model.satellite
|
||||
|
||||
import ballistics.types.InitialConditions
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.BookedSlotStatus
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotStatus
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO
|
||||
import space.nstart.pcp.slots_service.entity.BookedSlotEntity
|
||||
@@ -40,10 +41,19 @@ class TestSatelliteImpl(
|
||||
val selectedSlots = mutableListOf<PreparedCoverageSlot>()
|
||||
val cycleBegin = Duration.between(tnCalc, timeStart).toDays() / durationCalc
|
||||
val cycleEnd = Duration.between(tnCalc, timeStop).toDays() / durationCalc
|
||||
val booked = bookedSlots.map { BookedInfo(it.slot.slotId?:0, it.cycle) }
|
||||
val bookedData = (
|
||||
bookedSlots.map { BookedSlot(it.slot.tn, it.slot.tk,it.slot.roll, 0, it.cycle) }.toMutableList()
|
||||
)
|
||||
val activeBookedSlots = bookedSlots.filter { it.status in ACTIVE_BOOKED_STATUSES }
|
||||
val booked = activeBookedSlots.map { BookedInfo(it.slot.slotId?:0, it.cycle) }
|
||||
val bookedData = activeBookedSlots.map { bookedSlot ->
|
||||
val offsetDays = bookedSlot.cycle * durationCalc
|
||||
BookedSlot(
|
||||
bookedSlot.slot.tn.plusDays(offsetDays),
|
||||
bookedSlot.slot.tk.plusDays(offsetDays),
|
||||
bookedSlot.slot.roll,
|
||||
0,
|
||||
bookedSlot.cycle
|
||||
)
|
||||
}.toMutableList()
|
||||
val bookedIntervals = mergeBookedIntervals(bookedData.map { BookedInterval(it.t, it.tEnd) })
|
||||
calculateChainLengths(bookedData)
|
||||
for (cycleN in cycleBegin..cycleEnd) {
|
||||
val currentSlots = slots.map { ent ->
|
||||
@@ -68,24 +78,27 @@ class TestSatelliteImpl(
|
||||
slot.tn >= timeStart && slot.tk <= timeStop &&
|
||||
sign?.let { slot.revolutionSign == it.toString() } ?: true
|
||||
}.map {
|
||||
val band = bookedData.filter { booked ->
|
||||
val dif = intersection(it.tn.minusSeconds(mmi), it.tk.plusSeconds(mmi), booked.t, booked.tEnd)
|
||||
val isBooked = booked.any { booked -> booked.cycle == it.cycle && booked.id == it.slotId }
|
||||
val insideBookedInterval = !isBooked && bookedIntervals.any { booked -> booked.contains(it.tn, it.tk) }
|
||||
val band = if (insideBookedInterval) {
|
||||
true
|
||||
} else {
|
||||
bookedData.any { booked ->
|
||||
val dif = intersection(it.tn.minusSeconds(mmi), it.tk.plusSeconds(mmi), booked.t, booked.tEnd)
|
||||
val gam = abs(it.roll - booked.roll)
|
||||
|
||||
val gam = abs(it.roll - booked.roll)
|
||||
|
||||
if (dif) {
|
||||
if (gam < 0.01 && booked.neighbours < maxChainLength) {
|
||||
// if (gam < 0.01) {
|
||||
// booked.neighbours++
|
||||
if (dif) {
|
||||
!(gam < 0.01 && booked.neighbours < maxChainLength)
|
||||
} else {
|
||||
false
|
||||
} else true
|
||||
} else false
|
||||
}
|
||||
}
|
||||
}
|
||||
PreparedCoverageSlot(
|
||||
sourceSlotId = it.slotId ?: 0L,
|
||||
slot = it.toDTO(
|
||||
booked.count { booked -> booked.cycle == it.cycle && booked.id == it.slotId } > 0,
|
||||
band.isNotEmpty()
|
||||
isBooked,
|
||||
band
|
||||
)
|
||||
)
|
||||
})
|
||||
@@ -95,6 +108,30 @@ class TestSatelliteImpl(
|
||||
return selectedSlots
|
||||
}
|
||||
|
||||
private data class BookedInterval(val start: LocalDateTime, val end: LocalDateTime) {
|
||||
fun contains(slotStart: LocalDateTime, slotEnd: LocalDateTime): Boolean =
|
||||
!slotStart.isBefore(start) && !slotEnd.isAfter(end)
|
||||
}
|
||||
|
||||
private fun mergeBookedIntervals(intervals: List<BookedInterval>): List<BookedInterval> {
|
||||
if (intervals.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
val merged = mutableListOf<BookedInterval>()
|
||||
intervals.sortedBy { it.start }.forEach { interval ->
|
||||
val last = merged.lastOrNull()
|
||||
if (last != null && !interval.start.isAfter(last.end)) {
|
||||
merged[merged.lastIndex] = BookedInterval(
|
||||
start = minOf(last.start, interval.start),
|
||||
end = maxOf(last.end, interval.end)
|
||||
)
|
||||
} else {
|
||||
merged += interval
|
||||
}
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
private fun calculateChainLengths(slots: MutableList<BookedSlot>) {
|
||||
if (slots.isEmpty())
|
||||
return
|
||||
@@ -127,4 +164,12 @@ class TestSatelliteImpl(
|
||||
slots[j].neighbours = lastChainLength
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
private val ACTIVE_BOOKED_STATUSES = setOf(
|
||||
BookedSlotStatus.BOOKED.name,
|
||||
BookedSlotStatus.PROCESSED.name
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+23
@@ -29,6 +29,29 @@ interface BookedSlotsRepository : JpaRepository<BookedSlotEntity, Long>{
|
||||
@EntityGraph(attributePaths = ["slot"])
|
||||
fun findAllByStatus(status: String): List<BookedSlotEntity>
|
||||
|
||||
@EntityGraph(attributePaths = ["slot", "requests"])
|
||||
@Query("""
|
||||
select distinct bookedSlot
|
||||
from BookedSlotEntity bookedSlot
|
||||
where bookedSlot.slot.satelliteId in :satelliteIds
|
||||
and bookedSlot.cycle between :cycleBegin and :cycleEnd
|
||||
""")
|
||||
fun findBookingDetailsBySatelliteIdsAndCycleBetween(
|
||||
@Param("satelliteIds") satelliteIds: Collection<Long>,
|
||||
@Param("cycleBegin") cycleBegin: Long,
|
||||
@Param("cycleEnd") cycleEnd: Long
|
||||
): List<BookedSlotEntity>
|
||||
|
||||
@EntityGraph(attributePaths = ["slot", "requests"])
|
||||
@Query("""
|
||||
select distinct bookedSlot
|
||||
from BookedSlotEntity bookedSlot
|
||||
where bookedSlot.slot.satelliteId in :satelliteIds
|
||||
""")
|
||||
fun findBookingDetailsBySatelliteIds(
|
||||
@Param("satelliteIds") satelliteIds: Collection<Long>
|
||||
): List<BookedSlotEntity>
|
||||
|
||||
@Modifying
|
||||
@Transactional
|
||||
@Query("DELETE FROM booked_slot WHERE booked_slot_id =:id", nativeQuery = true)
|
||||
|
||||
+20
@@ -2,6 +2,7 @@ package space.nstart.pcp.slots_service.service
|
||||
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.OccupiedIntervalDTO
|
||||
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.slots_service.model.satellite.AbstractSatellite
|
||||
import java.time.Duration
|
||||
import kotlin.math.abs
|
||||
@@ -71,6 +72,15 @@ internal object OccupiedIntervalPrefilter {
|
||||
occupiedIntervals: List<OccupiedIntervalDTO>,
|
||||
satellite: AbstractSatellite
|
||||
): Boolean {
|
||||
val insideStrictReservedInterval = candidate.state != SlotStatus.BOOKED &&
|
||||
occupiedIntervals.any { occupied ->
|
||||
isStrictReservedSource(occupied.source) &&
|
||||
isInside(candidate.tn, candidate.tk, occupied.startTime, occupied.endTime)
|
||||
}
|
||||
if (insideStrictReservedInterval) {
|
||||
return false
|
||||
}
|
||||
|
||||
val conflictingDifferentRoll = occupiedIntervals.any { occupied ->
|
||||
!sameRoll(candidate.roll, occupied.roll) &&
|
||||
intersectsWithMmi(candidate.tn, candidate.tk, occupied.startTime, occupied.endTime, satellite.mmi)
|
||||
@@ -101,5 +111,15 @@ internal object OccupiedIntervalPrefilter {
|
||||
mmi: Long
|
||||
): Boolean = start1 <= end2.plusSeconds(mmi) && end1 >= start2.minusSeconds(mmi)
|
||||
|
||||
private fun isInside(
|
||||
start: java.time.LocalDateTime,
|
||||
end: java.time.LocalDateTime,
|
||||
containerStart: java.time.LocalDateTime,
|
||||
containerEnd: java.time.LocalDateTime
|
||||
): Boolean = !start.isBefore(containerStart) && !end.isAfter(containerEnd)
|
||||
|
||||
private fun isStrictReservedSource(source: String?): Boolean =
|
||||
source.equals("SLOTS", ignoreCase = true)
|
||||
|
||||
private fun sameRoll(left: Double, right: Double): Boolean = abs(left - right) <= ROLL_EPSILON
|
||||
}
|
||||
|
||||
+114
-11
@@ -47,6 +47,7 @@ import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.StationDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TargetPositionDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.BookedSlotDetailsDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.*
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteSlotAngleDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteSlotProfileDTO
|
||||
@@ -1451,8 +1452,8 @@ class SlotService {
|
||||
val cycleWindows = selectedSatellites.associate { satellite ->
|
||||
satellite.id to SatelliteCycleWindow(
|
||||
satellite = satellite,
|
||||
cycleBegin = Duration.between(satellite.tnCalc, timeStart).toDays() / satellite.durationCalc,
|
||||
cycleEnd = Duration.between(satellite.tnCalc, timeStop).toDays() / satellite.durationCalc
|
||||
cycleBegin = Duration.between(satellite.tnCalc, timeStart!!).toDays() / satellite.durationCalc,
|
||||
cycleEnd = Duration.between(satellite.tnCalc, timeStop!!).toDays() / satellite.durationCalc
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1685,16 +1686,26 @@ class SlotService {
|
||||
val satellite = batchContext.selectedSatellitesById[satelliteId] ?: return@forEach
|
||||
val preparedSlotsBySourceSlotId =
|
||||
batchContext.preparedSlotsBySatelliteIdAndSourceSlotId[satelliteId].orEmpty()
|
||||
val preparedSlots = sourceSlotIdsForSatellite.flatMap { slotId ->
|
||||
val preparedSlotsIncludingBlocked = sourceSlotIdsForSatellite.flatMap { slotId ->
|
||||
preparedSlotsBySourceSlotId[slotId].orEmpty()
|
||||
}.map { it.slot }
|
||||
preparedSlotsBeforeOccupiedFilter += preparedSlots.size
|
||||
preparedSlotsBeforeOccupiedFilter += preparedSlotsIncludingBlocked.size
|
||||
|
||||
// BUSY слоты возникают, в частности, когда слот по времени попадает внутрь
|
||||
// активного забронированного интервала. Такие слоты не должны попадать
|
||||
// в дальнейший расчет покрытия: booked-слоты учитываются как BOOKED,
|
||||
// а все остальные слоты внутри их времени считаются заблокированными.
|
||||
val preparedSlots = preparedSlotsIncludingBlocked.filter { slot ->
|
||||
slot.state == SlotStatus.AVAILABLE || slot.state == SlotStatus.BOOKED
|
||||
}
|
||||
val blockedByBookedSlots = preparedSlotsIncludingBlocked.size - preparedSlots.size
|
||||
|
||||
val filteredSlots = OccupiedIntervalPrefilter.filterCandidates(
|
||||
candidateSlots = preparedSlots,
|
||||
occupiedIntervals = batchContext.occupiedIntervalsBySatelliteId[satelliteId].orEmpty(),
|
||||
satellite = satellite
|
||||
)
|
||||
occupiedFilteredSlots += preparedSlots.size - filteredSlots.size
|
||||
occupiedFilteredSlots += blockedByBookedSlots + (preparedSlots.size - filteredSlots.size)
|
||||
candidateSlotsBeforeSunFilter += filteredSlots.size
|
||||
val sunFilterResult = filterBySunAngle(filteredSlots, sun)
|
||||
sunFilteredSlots += sunFilterResult.filteredCount
|
||||
@@ -1883,6 +1894,98 @@ class SlotService {
|
||||
@Transactional(readOnly = true)
|
||||
fun allBooked() = bookedSlotsRepository.findAll().map { it.toDTO() }
|
||||
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun searchBookedSlots(
|
||||
timeStart: LocalDateTime?,
|
||||
timeStop: LocalDateTime?,
|
||||
requestId: String?,
|
||||
satelliteIds: List<Long>?
|
||||
): List<BookedSlotDetailsDTO> {
|
||||
if ((timeStart == null) != (timeStop == null)) {
|
||||
throw CustomValidationException("Для фильтра по времени должны быть заданы timeStart и timeStop")
|
||||
}
|
||||
if (timeStart != null && timeStop != null && timeStop < timeStart) {
|
||||
throw CustomValidationException("Параметр timeStop должен быть больше или равен timeStart")
|
||||
}
|
||||
|
||||
val requestedSatelliteIds = satelliteIds.orEmpty().filter { it > 0 }.distinct()
|
||||
val satellites = if (requestedSatelliteIds.isEmpty()) {
|
||||
satelliteCatalogClient.allSatellites()
|
||||
} else {
|
||||
loadSatellites(requestedSatelliteIds)
|
||||
}
|
||||
if (satellites.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val satellitesById = satellites.associateBy { it.id }
|
||||
val selectedSatelliteIds = satellites.map { it.id }
|
||||
val normalizedRequestId = requestId?.trim()?.takeIf { it.isNotEmpty() }
|
||||
val hasInterval = timeStart != null && timeStop != null
|
||||
|
||||
val bookedSlots = if (hasInterval) {
|
||||
val cycleWindows = satellites.associate { satellite ->
|
||||
val cycleBegin = Duration.between(satellite.tnCalc, timeStart!!).toDays() / satellite.durationCalc
|
||||
val cycleEnd = Duration.between(satellite.tnCalc, timeStop!!).toDays() / satellite.durationCalc
|
||||
satellite.id to (cycleBegin - 1 to cycleEnd + 1)
|
||||
}
|
||||
val minCycle = cycleWindows.values.minOf { it.first }
|
||||
val maxCycle = cycleWindows.values.maxOf { it.second }
|
||||
bookedSlotsRepository.findBookingDetailsBySatelliteIdsAndCycleBetween(
|
||||
satelliteIds = selectedSatelliteIds,
|
||||
cycleBegin = minCycle,
|
||||
cycleEnd = maxCycle
|
||||
)
|
||||
} else {
|
||||
bookedSlotsRepository.findBookingDetailsBySatelliteIds(selectedSatelliteIds)
|
||||
}
|
||||
|
||||
return bookedSlots
|
||||
.asSequence()
|
||||
.filter { booked -> normalizedRequestId == null || booked.requests.orEmpty().any { it.requestId == normalizedRequestId } }
|
||||
.mapNotNull { booked ->
|
||||
val satellite = satellitesById[booked.slot.satelliteId] ?: return@mapNotNull null
|
||||
booked.toDetails(satellite)
|
||||
}
|
||||
.filter { details ->
|
||||
if (!hasInterval) {
|
||||
true
|
||||
} else {
|
||||
val start = details.timeStart
|
||||
val stop = details.timeStop
|
||||
start != null && stop != null && stop > timeStart!! && start < timeStop!!
|
||||
}
|
||||
}
|
||||
.sortedWith(
|
||||
compareBy<BookedSlotDetailsDTO> { it.timeStart ?: LocalDateTime.MAX }
|
||||
.thenBy { it.satelliteId }
|
||||
.thenBy { it.slotNum }
|
||||
.thenBy { it.cycle }
|
||||
.thenBy { it.bookedSlotId }
|
||||
)
|
||||
.toList()
|
||||
}
|
||||
|
||||
private fun BookedSlotEntity.toDetails(satellite: AbstractSatellite): BookedSlotDetailsDTO {
|
||||
val timeStart = slot.tn.plusDays(cycle * satellite.durationCalc)
|
||||
val timeStop = slot.tk.plusDays(cycle * satellite.durationCalc)
|
||||
return BookedSlotDetailsDTO(
|
||||
bookedSlotId = bookedSlotId ?: 0,
|
||||
satelliteId = slot.satelliteId,
|
||||
slotNum = slot.slotNum,
|
||||
cycle = cycle,
|
||||
timeStart = timeStart,
|
||||
timeStop = timeStop,
|
||||
durationSeconds = Duration.between(timeStart, timeStop).seconds,
|
||||
roll = slot.roll,
|
||||
revolution = slot.revolution + cycle * satellite.cycleRevs,
|
||||
revolutionSign = slot.revolutionSign,
|
||||
status = status,
|
||||
requestIds = requests.orEmpty().map { it.requestId }.distinct().sorted()
|
||||
)
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun allByInterval(
|
||||
satelliteId: Long,
|
||||
@@ -1894,8 +1997,8 @@ class SlotService {
|
||||
}
|
||||
|
||||
val satellite = satelliteById(satelliteId)
|
||||
val startCycle = Duration.between(satellite.tnCalc, timeStart).toDays() / satellite.durationCalc
|
||||
val stopCycle = Duration.between(satellite.tnCalc, timeStop).toDays() / satellite.durationCalc
|
||||
val startCycle = Duration.between(satellite.tnCalc, timeStart!!).toDays() / satellite.durationCalc
|
||||
val stopCycle = Duration.between(satellite.tnCalc, timeStop!!).toDays() / satellite.durationCalc
|
||||
val cycleBegin = startCycle - 1
|
||||
val cycleEnd = stopCycle + 1
|
||||
|
||||
@@ -1957,8 +2060,8 @@ class SlotService {
|
||||
|
||||
val satellite = satelliteById(satelliteId)
|
||||
|
||||
val startCycle = Duration.between(satellite.tnCalc, timeStart).toDays() / satellite.durationCalc
|
||||
val stopCycle = Duration.between(satellite.tnCalc, timeStop).toDays() / satellite.durationCalc
|
||||
val startCycle = Duration.between(satellite.tnCalc, timeStart!!).toDays() / satellite.durationCalc
|
||||
val stopCycle = Duration.between(satellite.tnCalc, timeStop!!).toDays() / satellite.durationCalc
|
||||
val cycleBegin = startCycle - 1
|
||||
val cycleEnd = stopCycle + 1
|
||||
|
||||
@@ -1982,8 +2085,8 @@ class SlotService {
|
||||
val selectedSatellites = loadSatellites(satelliteIds)
|
||||
|
||||
val cycleWindows = selectedSatellites.associate { satellite ->
|
||||
val startCycle = Duration.between(satellite.tnCalc, timeStart).toDays() / satellite.durationCalc
|
||||
val stopCycle = Duration.between(satellite.tnCalc, timeStop).toDays() / satellite.durationCalc
|
||||
val startCycle = Duration.between(satellite.tnCalc, timeStart!!).toDays() / satellite.durationCalc
|
||||
val stopCycle = Duration.between(satellite.tnCalc, timeStop!!).toDays() / satellite.durationCalc
|
||||
satellite.id to (startCycle - 1 to stopCycle + 1)
|
||||
}
|
||||
val minCycle = cycleWindows.values.minOf { it.first }
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package space.nstart.pcp.slots_service.model.satellite
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.BookedSlotStatus
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotStatus
|
||||
import space.nstart.pcp.slots_service.entity.BookedSlotEntity
|
||||
import space.nstart.pcp.slots_service.entity.SlotEntity
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class TestSatelliteImplBookedBlockingTest {
|
||||
|
||||
@Test
|
||||
fun `non-booked slot inside active booked interval is marked busy and exact booked slot stays booked`() {
|
||||
val baseTime = LocalDateTime.of(2026, 5, 21, 0, 0)
|
||||
val satellite = TestSatelliteImpl(
|
||||
id = 1L,
|
||||
tnCalc = baseTime,
|
||||
durationCalc = 1,
|
||||
maxChainLength = 3,
|
||||
maxSurveyDuration = 300,
|
||||
mmi = 10
|
||||
)
|
||||
val bookedRawSlot = slot(slotId = 1L, slotNum = 1L, baseTime = baseTime, startSecond = 0, endSecond = 10)
|
||||
val candidateInsideBooked = slot(slotId = 2L, slotNum = 2L, baseTime = baseTime, startSecond = 2, endSecond = 8)
|
||||
val booked = BookedSlotEntity(slot = bookedRawSlot, cycle = 0).apply {
|
||||
status = BookedSlotStatus.PROCESSED.name
|
||||
}
|
||||
|
||||
val result = satellite.prepareCoverageSlots(
|
||||
slots = listOf(bookedRawSlot, candidateInsideBooked),
|
||||
bookedSlots = listOf(booked),
|
||||
timeStart = baseTime,
|
||||
timeStop = baseTime.plusSeconds(20),
|
||||
sign = null,
|
||||
states = null
|
||||
).map { it.slot }.associateBy { it.slotNumber }
|
||||
|
||||
assertEquals(SlotStatus.BOOKED, result.getValue(1L).state)
|
||||
assertEquals(SlotStatus.BUSY, result.getValue(2L).state)
|
||||
}
|
||||
|
||||
private fun slot(
|
||||
slotId: Long,
|
||||
slotNum: Long,
|
||||
baseTime: LocalDateTime,
|
||||
startSecond: Long,
|
||||
endSecond: Long
|
||||
) = SlotEntity(
|
||||
slotId = slotId,
|
||||
slotNum = slotNum,
|
||||
cycle = 0,
|
||||
satelliteId = 1L,
|
||||
coveringType = 0,
|
||||
tn = baseTime.plusSeconds(startSecond),
|
||||
tk = baseTime.plusSeconds(endSecond),
|
||||
roll = 10.0,
|
||||
contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
|
||||
revolution = 1L,
|
||||
revolutionSign = "ASC"
|
||||
)
|
||||
}
|
||||
+49
-4
@@ -81,6 +81,48 @@ class OccupiedIntervalPrefilterTest {
|
||||
assertEquals(listOf(candidate), filtered)
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `available candidate inside reserved slots interval is rejected even with same roll`() {
|
||||
val satellite = testSatellite(maxSurveyDuration = 120, mmi = 5)
|
||||
val candidate = candidateSlot(roll = 10.0, startOffsetSeconds = 10, endOffsetSeconds = 20)
|
||||
val occupied = listOf(
|
||||
occupiedInterval(
|
||||
roll = 10.0,
|
||||
startOffsetSeconds = 0,
|
||||
endOffsetSeconds = 30,
|
||||
source = "SLOTS"
|
||||
)
|
||||
)
|
||||
|
||||
val filtered = OccupiedIntervalPrefilter.filterCandidates(listOf(candidate), occupied, satellite)
|
||||
|
||||
assertTrue(filtered.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `booked candidate inside reserved slots interval is kept for coverage accounting`() {
|
||||
val satellite = testSatellite(maxSurveyDuration = 120, mmi = 5)
|
||||
val candidate = candidateSlot(
|
||||
roll = 10.0,
|
||||
startOffsetSeconds = 10,
|
||||
endOffsetSeconds = 20,
|
||||
state = SlotStatus.BOOKED
|
||||
)
|
||||
val occupied = listOf(
|
||||
occupiedInterval(
|
||||
roll = 10.0,
|
||||
startOffsetSeconds = 0,
|
||||
endOffsetSeconds = 30,
|
||||
source = "SLOTS"
|
||||
)
|
||||
)
|
||||
|
||||
val filtered = OccupiedIntervalPrefilter.filterCandidates(listOf(candidate), occupied, satellite)
|
||||
|
||||
assertEquals(listOf(candidate), filtered)
|
||||
}
|
||||
|
||||
private fun testSatellite(maxSurveyDuration: Int, mmi: Long): AbstractSatellite =
|
||||
object : AbstractSatellite(id = 22, maxSurveyDuration = maxSurveyDuration, mmi = mmi) {
|
||||
override fun prepareCoverageSlots(
|
||||
@@ -96,26 +138,29 @@ class OccupiedIntervalPrefilterTest {
|
||||
private fun occupiedInterval(
|
||||
roll: Double,
|
||||
startOffsetSeconds: Long,
|
||||
endOffsetSeconds: Long
|
||||
endOffsetSeconds: Long,
|
||||
source: String = "TEST"
|
||||
) = OccupiedIntervalDTO(
|
||||
satelliteId = 22,
|
||||
startTime = baseTime().plusSeconds(startOffsetSeconds),
|
||||
endTime = baseTime().plusSeconds(endOffsetSeconds),
|
||||
roll = roll,
|
||||
source = "TEST"
|
||||
source = source
|
||||
)
|
||||
|
||||
private fun candidateSlot(
|
||||
roll: Double,
|
||||
startOffsetSeconds: Long,
|
||||
endOffsetSeconds: Long
|
||||
endOffsetSeconds: Long,
|
||||
state: SlotStatus = SlotStatus.AVAILABLE
|
||||
) = SlotDTO(
|
||||
cycle = 0,
|
||||
satelliteId = 22,
|
||||
tn = baseTime().plusSeconds(startOffsetSeconds),
|
||||
tk = baseTime().plusSeconds(endOffsetSeconds),
|
||||
roll = roll,
|
||||
contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"
|
||||
contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
|
||||
state = state
|
||||
)
|
||||
|
||||
private fun baseTime(): LocalDateTime = LocalDateTime.of(2026, 3, 26, 10, 0, 0)
|
||||
|
||||
Reference in New Issue
Block a user