From 1a6e51bf2530a0b874f8b6d4e25c2b8d5baa9885 Mon Sep 17 00:00:00 2001 From: emelianov Date: Wed, 27 May 2026 10:24:14 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A3=D1=87=D0=B5=D1=82=20=D0=B7=D0=B0=D0=B1?= =?UTF-8?q?=D1=80=D0=BE=D0=BD=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D1=85=20=D1=81=D0=BB=D0=BE=D1=82=D0=BE=D0=B2=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=20=D1=80=D0=B0=D1=81=D1=87=D0=B5=D1=82=D0=B5=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BC=D0=BF=D0=BB=D0=B5=D0=BA=D1=81=D0=BD=D0=BE?= =?UTF-8?q?=D0=B3=D0=BE=20=D0=BF=D0=BB=D0=B0=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/satellite/TestSatelliteImpl.kt | 77 +++++++++++++++---- .../service/OccupiedIntervalPrefilter.kt | 20 +++++ .../pcp/slots_service/service/SlotService.kt | 16 +++- .../TestSatelliteImplBookedBlockingTest.kt | 62 +++++++++++++++ .../service/OccupiedIntervalPrefilterTest.kt | 53 ++++++++++++- 5 files changed, 205 insertions(+), 23 deletions(-) create mode 100644 services/slots-service/src/test/kotlin/space/nstart/pcp/slots_service/model/satellite/TestSatelliteImplBookedBlockingTest.kt diff --git a/services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/model/satellite/TestSatelliteImpl.kt b/services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/model/satellite/TestSatelliteImpl.kt index c83c350..cd4e733 100644 --- a/services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/model/satellite/TestSatelliteImpl.kt +++ b/services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/model/satellite/TestSatelliteImpl.kt @@ -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() 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): List { + if (intervals.isEmpty()) { + return emptyList() + } + val merged = mutableListOf() + 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) { 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 + ) + } } diff --git a/services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/service/OccupiedIntervalPrefilter.kt b/services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/service/OccupiedIntervalPrefilter.kt index 0ef5518..7efbc14 100644 --- a/services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/service/OccupiedIntervalPrefilter.kt +++ b/services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/service/OccupiedIntervalPrefilter.kt @@ -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, 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 } diff --git a/services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/service/SlotService.kt b/services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/service/SlotService.kt index beaa26f..2709591 100644 --- a/services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/service/SlotService.kt +++ b/services/slots-service/src/main/kotlin/space/nstart/pcp/slots_service/service/SlotService.kt @@ -1686,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 diff --git a/services/slots-service/src/test/kotlin/space/nstart/pcp/slots_service/model/satellite/TestSatelliteImplBookedBlockingTest.kt b/services/slots-service/src/test/kotlin/space/nstart/pcp/slots_service/model/satellite/TestSatelliteImplBookedBlockingTest.kt new file mode 100644 index 0000000..bd886eb --- /dev/null +++ b/services/slots-service/src/test/kotlin/space/nstart/pcp/slots_service/model/satellite/TestSatelliteImplBookedBlockingTest.kt @@ -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" + ) +} diff --git a/services/slots-service/src/test/kotlin/space/nstart/pcp/slots_service/service/OccupiedIntervalPrefilterTest.kt b/services/slots-service/src/test/kotlin/space/nstart/pcp/slots_service/service/OccupiedIntervalPrefilterTest.kt index 4bd5259..02f7c89 100644 --- a/services/slots-service/src/test/kotlin/space/nstart/pcp/slots_service/service/OccupiedIntervalPrefilterTest.kt +++ b/services/slots-service/src/test/kotlin/space/nstart/pcp/slots_service/service/OccupiedIntervalPrefilterTest.kt @@ -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)