Init
This commit is contained in:
+97
@@ -0,0 +1,97 @@
|
||||
package space.nstart.pcp.pcp_coverage_scheme_service.controller
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito.doReturn
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.boot.test.web.server.LocalServerPort
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean
|
||||
import org.springframework.web.reactive.function.client.WebClient
|
||||
import space.nstart.pcp.pcp_coverage_scheme_service.service.CoverageSchemeCalculationService
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.coverage.CoverageObservationDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.coverage.CoverageSchemeCalculateRequestDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.coverage.CoverageSchemeResponseDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SurveySlotDTO
|
||||
import tools.jackson.databind.ObjectMapper
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@SpringBootTest(
|
||||
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
|
||||
properties = [
|
||||
"spring.boot.admin.client.enabled=false",
|
||||
"spring.cloud.config.enabled=false",
|
||||
"spring.cloud.config.import-check.enabled=false",
|
||||
"spring.config.import="
|
||||
]
|
||||
)
|
||||
class CoverageSchemeControllerTest {
|
||||
|
||||
@LocalServerPort
|
||||
private var port: Int = 0
|
||||
|
||||
@MockitoBean
|
||||
private lateinit var coverageSchemeCalculationService: CoverageSchemeCalculationService
|
||||
|
||||
private val objectMapper = ObjectMapper()
|
||||
|
||||
@Test
|
||||
fun `calculate endpoint returns observations with contours`() {
|
||||
doReturn(
|
||||
CoverageSchemeResponseDTO(
|
||||
targetPolygonWkt = "POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))",
|
||||
observations = listOf(
|
||||
CoverageObservationDTO(
|
||||
observation = SquareViewParamDTO(
|
||||
noradId = 11L,
|
||||
timeBegin = LocalDateTime.of(2026, 4, 14, 10, 0),
|
||||
timeEnd = LocalDateTime.of(2026, 4, 14, 10, 5),
|
||||
revSignBegin = RevolutionSign.ASC,
|
||||
revSignEnd = RevolutionSign.ASC,
|
||||
gammaMin = 0.0,
|
||||
gammaMax = 10.0
|
||||
),
|
||||
intersectionContourWkts = listOf("POLYGON ((0.5 0, 1.5 0, 1.5 2, 0.5 2, 0.5 0))")
|
||||
)
|
||||
),
|
||||
coverageScheme = listOf(
|
||||
SurveySlotDTO(
|
||||
satelliteId = 11L,
|
||||
tn = LocalDateTime.of(2026, 4, 14, 10, 0),
|
||||
tk = LocalDateTime.of(2026, 4, 14, 10, 5),
|
||||
roll = 5.0,
|
||||
contour = "POLYGON ((0.5 0, 1.5 0, 1.5 2, 0.5 2, 0.5 0))",
|
||||
revolutionSign = RevolutionSign.ASC
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
.`when`(coverageSchemeCalculationService)
|
||||
.calculate(org.mockito.ArgumentMatchers.any(CoverageSchemeCalculateRequestDTO::class.java) ?: CoverageSchemeCalculateRequestDTO())
|
||||
|
||||
val actual = WebClient.create("http://localhost:$port")
|
||||
.post()
|
||||
.uri("/api/coverage-schemes/calculate")
|
||||
.bodyValue(
|
||||
CoverageSchemeCalculateRequestDTO(
|
||||
polygonWkt = "POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))",
|
||||
satelliteIds = listOf(11L),
|
||||
timeStart = LocalDateTime.of(2026, 4, 14, 10, 0),
|
||||
timeEnd = LocalDateTime.of(2026, 4, 14, 11, 0)
|
||||
)
|
||||
)
|
||||
.retrieve()
|
||||
.bodyToMono(String::class.java)
|
||||
.map(objectMapper::readTree)
|
||||
.block()!!
|
||||
|
||||
assertEquals(1, actual["observations"].size())
|
||||
assertEquals(1, actual["coverageScheme"].size())
|
||||
assertEquals(11L, actual["observations"][0]["observation"]["noradId"].asLong())
|
||||
assertEquals(
|
||||
"POLYGON ((0.5 0, 1.5 0, 1.5 2, 0.5 2, 0.5 0))",
|
||||
actual["observations"][0]["intersectionContourWkts"][0].textValue()
|
||||
)
|
||||
}
|
||||
}
|
||||
+328
@@ -0,0 +1,328 @@
|
||||
package space.nstart.pcp.pcp_coverage_scheme_service.service
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import space.nstart.pcp.pcp_coverage_scheme_service.client.CoverageBallisticsClient
|
||||
import space.nstart.pcp.pcp_coverage_scheme_service.client.CoverageSatelliteClient
|
||||
import space.nstart.pcp.pcp_coverage_scheme_service.configuration.CoverageSchemeProperties
|
||||
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.ObjViewRequestDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.PointViewParamDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.coverage.CoverageSchemeCalculateRequestDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteInfoDTO
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class CoverageSchemeCalculationServiceTest {
|
||||
|
||||
private val geometrySupport = GeometrySupport()
|
||||
|
||||
@Test
|
||||
fun `calculate returns all mplSquare observations with strip intersection contours`() {
|
||||
val observation = SquareViewParamDTO(
|
||||
noradId = 11L,
|
||||
timeBegin = LocalDateTime.of(2026, 4, 14, 10, 0),
|
||||
timeEnd = LocalDateTime.of(2026, 4, 14, 10, 5),
|
||||
revSignBegin = RevolutionSign.ASC,
|
||||
revSignEnd = RevolutionSign.ASC,
|
||||
gammaMin = 0.0,
|
||||
gammaMax = 0.0
|
||||
)
|
||||
val fakeClient = FakeBallisticsClient(
|
||||
observations = listOf(observation),
|
||||
flightLinesBySatellite = mapOf(
|
||||
11L to listOf(
|
||||
flightPoint(LocalDateTime.of(2026, 4, 14, 10, 0), lat = 0.0, lon = 196.0, leftLon = 195.5, rightLon = 196.5),
|
||||
flightPoint(LocalDateTime.of(2026, 4, 14, 10, 5), lat = 2.0, lon = 196.0, leftLon = 195.5, rightLon = 196.5)
|
||||
)
|
||||
),
|
||||
orbitalPointsBySatellite = mapOf(11L to sampleOrbitalPoints())
|
||||
)
|
||||
val service = buildService(fakeClient)
|
||||
|
||||
val response = service.calculate(
|
||||
CoverageSchemeCalculateRequestDTO(
|
||||
polygonWkt = "POLYGON ((195 0, 197 0, 197 2, 195 2, 195 0))",
|
||||
satelliteIds = listOf(11L),
|
||||
timeStart = LocalDateTime.of(2026, 4, 14, 9, 55),
|
||||
timeEnd = LocalDateTime.of(2026, 4, 14, 10, 10)
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(1, response.observations.size)
|
||||
assertEquals(observation.noradId, response.observations.first().observation.noradId)
|
||||
assertEquals(observation.timeBegin, response.observations.first().observation.timeBegin)
|
||||
assertEquals(observation.timeEnd, response.observations.first().observation.timeEnd)
|
||||
assertEquals(1, response.observations.first().intersectionContourWkts.size)
|
||||
assertTrue(response.observations.first().intersectionContourWkts.first().contains("195.5 0"))
|
||||
assertTrue(response.observations.first().intersectionContourWkts.first().contains("196.5 2"))
|
||||
assertEquals(1, response.coverageScheme.size)
|
||||
assertEquals(0.0, response.coverageScheme.first().roll)
|
||||
assertEquals(observation.timeBegin, response.coverageScheme.first().tn)
|
||||
assertEquals(observation.timeEnd, response.coverageScheme.first().tk)
|
||||
assertTrue(response.coverageScheme.first().contour.startsWith("POLYGON"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculate keeps observation when flight line does not produce polygon`() {
|
||||
val observation = SquareViewParamDTO(
|
||||
noradId = 11L,
|
||||
timeBegin = LocalDateTime.of(2026, 4, 14, 10, 0),
|
||||
timeEnd = LocalDateTime.of(2026, 4, 14, 10, 5)
|
||||
)
|
||||
val service = buildService(
|
||||
FakeBallisticsClient(
|
||||
observations = listOf(observation),
|
||||
flightLinesBySatellite = mapOf(
|
||||
11L to listOf(
|
||||
flightPoint(LocalDateTime.of(2026, 4, 14, 10, 0), lat = 0.0, lon = 196.0, leftLon = 195.5, rightLon = 196.5)
|
||||
)
|
||||
),
|
||||
orbitalPointsBySatellite = emptyMap()
|
||||
)
|
||||
)
|
||||
|
||||
val response = service.calculate(
|
||||
CoverageSchemeCalculateRequestDTO(
|
||||
polygonWkt = "POLYGON ((195 0, 197 0, 197 2, 195 2, 195 0))",
|
||||
satelliteIds = listOf(11L),
|
||||
timeStart = LocalDateTime.of(2026, 4, 14, 9, 55),
|
||||
timeEnd = LocalDateTime.of(2026, 4, 14, 10, 10)
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(1, response.observations.size)
|
||||
assertTrue(response.observations.first().intersectionContourWkts.isEmpty())
|
||||
assertTrue(response.coverageScheme.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculate preserves inner exclusion zone inside strip contour`() {
|
||||
val observation = SquareViewParamDTO(
|
||||
noradId = 11L,
|
||||
timeBegin = LocalDateTime.of(2026, 4, 14, 10, 0),
|
||||
timeEnd = LocalDateTime.of(2026, 4, 14, 10, 5)
|
||||
)
|
||||
val service = buildService(
|
||||
FakeBallisticsClient(
|
||||
observations = listOf(observation),
|
||||
flightLinesBySatellite = mapOf(
|
||||
11L to listOf(
|
||||
flightPoint(
|
||||
LocalDateTime.of(2026, 4, 14, 10, 0),
|
||||
lat = 0.0,
|
||||
lon = 196.0,
|
||||
leftLon = 195.5,
|
||||
rightLon = 196.5,
|
||||
innerLeftLon = 195.9,
|
||||
innerRightLon = 196.1
|
||||
),
|
||||
flightPoint(
|
||||
LocalDateTime.of(2026, 4, 14, 10, 5),
|
||||
lat = 2.0,
|
||||
lon = 196.0,
|
||||
leftLon = 195.5,
|
||||
rightLon = 196.5,
|
||||
innerLeftLon = 195.9,
|
||||
innerRightLon = 196.1
|
||||
)
|
||||
)
|
||||
),
|
||||
orbitalPointsBySatellite = mapOf(11L to sampleOrbitalPoints())
|
||||
)
|
||||
)
|
||||
|
||||
val response = service.calculate(
|
||||
CoverageSchemeCalculateRequestDTO(
|
||||
polygonWkt = "POLYGON ((195 0, 197 0, 197 2, 195 2, 195 0))",
|
||||
satelliteIds = listOf(11L),
|
||||
timeStart = LocalDateTime.of(2026, 4, 14, 9, 55),
|
||||
timeEnd = LocalDateTime.of(2026, 4, 14, 10, 10)
|
||||
)
|
||||
)
|
||||
|
||||
val contours = response.observations.first().intersectionContourWkts
|
||||
assertEquals(2, contours.size)
|
||||
assertTrue(contours.any { it.contains("195.5 0") && it.contains("195.9 2") })
|
||||
assertTrue(contours.any { it.contains("196.1 0") && it.contains("196.5 2") })
|
||||
assertEquals(1, response.coverageScheme.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculate normalizes ballistics flight line to standard band for zero crossing target`() {
|
||||
val observation = SquareViewParamDTO(
|
||||
noradId = 11L,
|
||||
timeBegin = LocalDateTime.of(2026, 4, 14, 10, 0),
|
||||
timeEnd = LocalDateTime.of(2026, 4, 14, 10, 5),
|
||||
gammaMin = 0.0,
|
||||
gammaMax = 0.0
|
||||
)
|
||||
val service = buildService(
|
||||
FakeBallisticsClient(
|
||||
observations = listOf(observation),
|
||||
flightLinesBySatellite = mapOf(
|
||||
11L to listOf(
|
||||
flightPoint(
|
||||
time = LocalDateTime.of(2026, 4, 14, 10, 0),
|
||||
lat = 0.0,
|
||||
lon = 0.0,
|
||||
leftLon = 359.5,
|
||||
rightLon = 0.5
|
||||
),
|
||||
flightPoint(
|
||||
time = LocalDateTime.of(2026, 4, 14, 10, 5),
|
||||
lat = 2.0,
|
||||
lon = 0.0,
|
||||
leftLon = 359.5,
|
||||
rightLon = 0.5
|
||||
)
|
||||
)
|
||||
),
|
||||
orbitalPointsBySatellite = mapOf(11L to sampleOrbitalPoints())
|
||||
)
|
||||
)
|
||||
|
||||
val response = service.calculate(
|
||||
CoverageSchemeCalculateRequestDTO(
|
||||
polygonWkt = "POLYGON ((-1 0, 1 0, 1 2, -1 2, -1 0))",
|
||||
satelliteIds = listOf(11L),
|
||||
timeStart = LocalDateTime.of(2026, 4, 14, 9, 55),
|
||||
timeEnd = LocalDateTime.of(2026, 4, 14, 10, 10)
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(1, response.observations.size)
|
||||
assertEquals(1, response.observations.first().intersectionContourWkts.size)
|
||||
assertTrue(response.observations.first().intersectionContourWkts.first().contains("-0.5 0"))
|
||||
assertTrue(response.observations.first().intersectionContourWkts.first().contains("0.5 2"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculate returns observations in stable order even if mplSquare order changes`() {
|
||||
val first = SquareViewParamDTO(
|
||||
noradId = 11L,
|
||||
revolutionBegin = 3L,
|
||||
timeBegin = LocalDateTime.of(2026, 4, 14, 10, 0),
|
||||
timeEnd = LocalDateTime.of(2026, 4, 14, 10, 5),
|
||||
gammaMin = 1.0,
|
||||
gammaMax = 3.0
|
||||
)
|
||||
val second = SquareViewParamDTO(
|
||||
noradId = 22L,
|
||||
revolutionBegin = 2L,
|
||||
timeBegin = LocalDateTime.of(2026, 4, 14, 9, 55),
|
||||
timeEnd = LocalDateTime.of(2026, 4, 14, 10, 1),
|
||||
gammaMin = -1.0,
|
||||
gammaMax = 2.0
|
||||
)
|
||||
val service = buildService(
|
||||
FakeBallisticsClient(
|
||||
observations = listOf(first, second),
|
||||
flightLinesBySatellite = emptyMap(),
|
||||
orbitalPointsBySatellite = emptyMap()
|
||||
)
|
||||
)
|
||||
|
||||
val response = service.calculate(
|
||||
CoverageSchemeCalculateRequestDTO(
|
||||
polygonWkt = "POLYGON ((195 0, 197 0, 197 2, 195 2, 195 0))",
|
||||
satelliteIds = listOf(22L, 11L),
|
||||
timeStart = LocalDateTime.of(2026, 4, 14, 9, 55),
|
||||
timeEnd = LocalDateTime.of(2026, 4, 14, 10, 10)
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(listOf(11L, 22L), response.observations.map { it.observation.noradId })
|
||||
assertEquals(
|
||||
listOf(
|
||||
LocalDateTime.of(2026, 4, 14, 10, 0),
|
||||
LocalDateTime.of(2026, 4, 14, 9, 55)
|
||||
),
|
||||
response.observations.map { it.observation.timeBegin }
|
||||
)
|
||||
}
|
||||
|
||||
private class FakeBallisticsClient(
|
||||
private val observations: List<SquareViewParamDTO>,
|
||||
private val flightLinesBySatellite: Map<Long, List<FlightLineDTO>>,
|
||||
private val orbitalPointsBySatellite: Map<Long, List<OrbPointDTO>>
|
||||
) : CoverageBallisticsClient {
|
||||
override fun mplSquare(req: ObjViewRequestDTO): List<SquareViewParamDTO> = observations
|
||||
|
||||
override fun mplPoint(req: ObjViewRequestDTO): List<PointViewParamDTO> = emptyList()
|
||||
|
||||
override fun flightLine(
|
||||
satelliteId: Long,
|
||||
timeStart: LocalDateTime,
|
||||
timeStop: LocalDateTime
|
||||
): List<FlightLineDTO> = flightLinesBySatellite[satelliteId].orEmpty()
|
||||
.filter { point -> !point.time.isBefore(timeStart) && !point.time.isAfter(timeStop) }
|
||||
|
||||
override fun exactTime(
|
||||
satelliteId: Long,
|
||||
request: ExactTimePositionRequestDTO
|
||||
): List<OrbPointDTO> = orbitalPointsBySatellite[satelliteId].orEmpty()
|
||||
.filter { point -> !point.time.isBefore(request.time) && !point.time.isAfter(request.timeStop ?: request.time) }
|
||||
}
|
||||
|
||||
private fun flightPoint(
|
||||
time: LocalDateTime,
|
||||
lat: Double,
|
||||
lon: Double,
|
||||
leftLon: Double,
|
||||
rightLon: Double,
|
||||
innerLeftLon: Double = lon,
|
||||
innerRightLon: Double = lon
|
||||
) = FlightLineDTO(
|
||||
time = time,
|
||||
lat = lat,
|
||||
long = lon,
|
||||
latLeft = lat,
|
||||
longLeft = leftLon,
|
||||
latInnerLeft = lat,
|
||||
longInnerLeft = innerLeftLon,
|
||||
latInnerRight = lat,
|
||||
longInnerRight = innerRightLon,
|
||||
latRight = lat,
|
||||
longRight = rightLon
|
||||
)
|
||||
|
||||
private fun buildService(fakeClient: FakeBallisticsClient) = CoverageSchemeCalculationService(
|
||||
ballisticsClient = fakeClient,
|
||||
coverageSchemeBuilderService = CoverageSchemeBuilderService(fakeClient, FakeSatelliteClient(), geometrySupport),
|
||||
geometrySupport = geometrySupport,
|
||||
properties = CoverageSchemeProperties(flightLinePaddingSeconds = 0)
|
||||
)
|
||||
|
||||
private class FakeSatelliteClient : CoverageSatelliteClient {
|
||||
override fun satellite(satelliteId: Long): SatelliteInfoDTO =
|
||||
SatelliteInfoDTO(noradId = satelliteId, captureAngle = 1.5)
|
||||
}
|
||||
|
||||
private fun sampleOrbitalPoints() = listOf(
|
||||
OrbPointDTO(
|
||||
time = LocalDateTime.of(2026, 4, 14, 10, 0),
|
||||
revolution = 1L,
|
||||
vx = -401.529,
|
||||
vy = 1413.431,
|
||||
vz = 7547.696,
|
||||
x = -6603039.949,
|
||||
y = -1870023.148,
|
||||
z = 0.0
|
||||
),
|
||||
OrbPointDTO(
|
||||
time = LocalDateTime.of(2026, 4, 14, 10, 1),
|
||||
revolution = 1L,
|
||||
vx = -420.0,
|
||||
vy = 1390.0,
|
||||
vz = 7540.0,
|
||||
x = -6595000.0,
|
||||
y = -1860000.0,
|
||||
z = 452000.0
|
||||
)
|
||||
)
|
||||
}
|
||||
+652
@@ -0,0 +1,652 @@
|
||||
package space.nstart.pcp.pcp_coverage_scheme_service.service
|
||||
|
||||
import ballistics.flightLine.PointOnEarthCalculator
|
||||
import ballistics.types.EarthType
|
||||
import ballistics.types.Orientation
|
||||
import ballistics.types.OrbitalPoint
|
||||
import ballistics.types.WorkCSType
|
||||
import ballistics.utils.fromDateTime
|
||||
import ballistics.utils.math.Vector3D
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.locationtech.jts.geom.Geometry
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.PositionDTO
|
||||
import space.nstart.pcp.pcp_coverage_scheme_service.client.CoverageBallisticsClient
|
||||
import space.nstart.pcp.pcp_coverage_scheme_service.client.CoverageSatelliteClient
|
||||
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.ObjViewRequestDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.PointViewParamDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.coverage.CoverageObservationDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteInfoDTO
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.math.PI
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class CoverageSchemeBuilderServiceTest {
|
||||
|
||||
private val geometrySupport = GeometrySupport()
|
||||
|
||||
@Test
|
||||
fun `buildCoverageScheme uses dominant branch and sorts by left boundary`() {
|
||||
val service = CoverageSchemeBuilderService(
|
||||
ballisticsClient = FakeBallisticsClient(
|
||||
orbitalPointsByRequest = mapOf(
|
||||
requestKey(11L, LocalDateTime.of(2026, 4, 14, 10, 0)) to reverseOrbitalPoints(),
|
||||
requestKey(22L, LocalDateTime.of(2026, 4, 14, 9, 55)) to reverseOrbitalPoints(),
|
||||
requestKey(33L, LocalDateTime.of(2026, 4, 14, 10, 2)) to sampleOrbitalPoints()
|
||||
)
|
||||
),
|
||||
satelliteClient = FakeSatelliteClient(
|
||||
mapOf(
|
||||
11L to 1.5,
|
||||
22L to 1.5,
|
||||
33L to 1.5
|
||||
)
|
||||
),
|
||||
geometrySupport = geometrySupport
|
||||
)
|
||||
|
||||
val result = service.buildCoverageScheme(
|
||||
observations = listOf(
|
||||
observation(
|
||||
satelliteId = 11L,
|
||||
timeBegin = LocalDateTime.of(2026, 4, 14, 10, 0),
|
||||
timeEnd = LocalDateTime.of(2026, 4, 14, 10, 5),
|
||||
branch = RevolutionSign.ASC,
|
||||
gammaMin = -2.0,
|
||||
gammaMax = 6.0,
|
||||
contour = "POLYGON ((195.5 2.0, 196.5 2.0, 196.5 4.2, 195.5 4.2, 195.5 2.0))"
|
||||
),
|
||||
observation(
|
||||
satelliteId = 22L,
|
||||
timeBegin = LocalDateTime.of(2026, 4, 14, 9, 55),
|
||||
timeEnd = LocalDateTime.of(2026, 4, 14, 10, 1),
|
||||
branch = RevolutionSign.ASC,
|
||||
gammaMin = 1.0,
|
||||
gammaMax = 5.0,
|
||||
contour = "POLYGON ((195.5 -0.2, 196.5 -0.2, 196.5 2.1, 195.5 2.1, 195.5 -0.2))"
|
||||
),
|
||||
observation(
|
||||
satelliteId = 33L,
|
||||
timeBegin = LocalDateTime.of(2026, 4, 14, 10, 2),
|
||||
timeEnd = LocalDateTime.of(2026, 4, 14, 10, 7),
|
||||
branch = RevolutionSign.DESC,
|
||||
gammaMin = 3.0,
|
||||
gammaMax = 7.0,
|
||||
contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(2, result.size)
|
||||
assertEquals(listOf(22L, 11L), result.map { it.satelliteId })
|
||||
assertEquals(5.0, result.first().roll)
|
||||
assertEquals(6.0, result.last().roll)
|
||||
assertTrue(result.all { it.revolutionSign == RevolutionSign.ASC })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `buildCoverageScheme moves overlapping next contour to the right while preserving continuity`() {
|
||||
val requestTime = LocalDateTime.of(2026, 4, 14, 10, 0)
|
||||
val mplPointRequests = mutableListOf<ObjViewRequestDTO>()
|
||||
val service = CoverageSchemeBuilderService(
|
||||
ballisticsClient = FakeBallisticsClient(
|
||||
orbitalPointsByRequest = mapOf(
|
||||
requestKey(11L, requestTime) to reverseOrbitalPoints(),
|
||||
requestKey(22L, requestTime.plusMinutes(10)) to reverseOrbitalPoints()
|
||||
),
|
||||
mplPointsBySatellite = mapOf(
|
||||
11L to listOf(
|
||||
PointViewParamDTO(
|
||||
noradId = 11L,
|
||||
time = requestTime,
|
||||
gamma = 6.0
|
||||
)
|
||||
),
|
||||
22L to listOf(
|
||||
PointViewParamDTO(
|
||||
noradId = 22L,
|
||||
time = requestTime.plusMinutes(10),
|
||||
gamma = 6.0
|
||||
)
|
||||
)
|
||||
),
|
||||
capturedMplPointRequests = mplPointRequests
|
||||
),
|
||||
satelliteClient = FakeSatelliteClient(
|
||||
mapOf(
|
||||
11L to 1.5,
|
||||
22L to 1.5
|
||||
)
|
||||
),
|
||||
geometrySupport = geometrySupport
|
||||
)
|
||||
|
||||
val result = service.buildCoverageScheme(
|
||||
observations = listOf(
|
||||
observation(
|
||||
satelliteId = 11L,
|
||||
timeBegin = requestTime,
|
||||
timeEnd = requestTime.plusMinutes(5),
|
||||
branch = RevolutionSign.ASC,
|
||||
gammaMin = 4.0,
|
||||
gammaMax = 4.0,
|
||||
contour = "POLYGON ((195.5 -0.1, 196.3 -0.1, 196.3 4.0, 195.5 4.0, 195.5 -0.1))"
|
||||
),
|
||||
observation(
|
||||
satelliteId = 22L,
|
||||
timeBegin = requestTime.plusMinutes(10),
|
||||
timeEnd = requestTime.plusMinutes(15),
|
||||
branch = RevolutionSign.ASC,
|
||||
gammaMin = 0.0,
|
||||
gammaMax = 8.0,
|
||||
contour = "POLYGON ((195.5 -0.1, 196.3 -0.1, 196.3 4.0, 195.5 4.0, 195.5 -0.1))"
|
||||
)
|
||||
),
|
||||
rollStepDegrees = 1.0
|
||||
)
|
||||
|
||||
assertEquals(2, result.size)
|
||||
assertEquals(4.0, result.first().roll)
|
||||
assertEquals(7.5, result.last().roll)
|
||||
assertEquals(1, mplPointRequests.size)
|
||||
|
||||
val firstContour = geometrySupport.parseAndNormalizeTarget(result.first().contour)
|
||||
val secondContour = geometrySupport.parseAndNormalizeTarget(result.last().contour)
|
||||
assertTrue(secondContour.area > 0.0)
|
||||
assertTrue(firstContour.area > 0.0)
|
||||
assertPositionEquals(
|
||||
expectedRightmostStartPosition(firstContour),
|
||||
mplPointRequests.single().obj.position
|
||||
)
|
||||
assertTrue(requireNotNull(mplPointRequests.single().obj.position).long in 0.0..360.0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `buildCoverageScheme uses minimum roll and right-side anchor for desc branch`() {
|
||||
val requestTime = LocalDateTime.of(2026, 4, 14, 10, 0)
|
||||
val mplPointRequests = mutableListOf<ObjViewRequestDTO>()
|
||||
val service = CoverageSchemeBuilderService(
|
||||
ballisticsClient = FakeBallisticsClient(
|
||||
orbitalPointsByRequest = mapOf(
|
||||
requestKey(11L, requestTime) to reverseOrbitalPoints(),
|
||||
requestKey(22L, requestTime.plusMinutes(10)) to reverseOrbitalPoints()
|
||||
),
|
||||
mplPointsBySatellite = mapOf(
|
||||
22L to listOf(
|
||||
PointViewParamDTO(
|
||||
noradId = 22L,
|
||||
time = requestTime.plusMinutes(10),
|
||||
gamma = -6.0
|
||||
)
|
||||
)
|
||||
),
|
||||
capturedMplPointRequests = mplPointRequests
|
||||
),
|
||||
satelliteClient = FakeSatelliteClient(
|
||||
mapOf(
|
||||
11L to 1.5,
|
||||
22L to 1.5
|
||||
)
|
||||
),
|
||||
geometrySupport = geometrySupport
|
||||
)
|
||||
|
||||
val result = service.buildCoverageScheme(
|
||||
observations = listOf(
|
||||
observation(
|
||||
satelliteId = 11L,
|
||||
timeBegin = requestTime,
|
||||
timeEnd = requestTime.plusMinutes(5),
|
||||
branch = RevolutionSign.DESC,
|
||||
gammaMin = -7.0,
|
||||
gammaMax = -2.0,
|
||||
contour = "POLYGON ((190 -5, 200 -5, 200 10, 190 10, 190 -5))"
|
||||
),
|
||||
observation(
|
||||
satelliteId = 22L,
|
||||
timeBegin = requestTime.plusMinutes(10),
|
||||
timeEnd = requestTime.plusMinutes(15),
|
||||
branch = RevolutionSign.DESC,
|
||||
gammaMin = -8.0,
|
||||
gammaMax = 0.0,
|
||||
contour = "POLYGON ((190 -5, 200 -5, 200 10, 190 10, 190 -5))"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(2, result.size)
|
||||
assertEquals(-7.0, result.first().roll)
|
||||
assertEquals(-4.5, result.last().roll)
|
||||
assertEquals(1, mplPointRequests.size)
|
||||
assertPositionEquals(
|
||||
expectedRightmostStartPosition(geometrySupport.parseAndNormalizeTarget(result.first().contour)),
|
||||
mplPointRequests.single().obj.position
|
||||
)
|
||||
assertTrue(requireNotNull(mplPointRequests.single().obj.position).long in 0.0..360.0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `buildCoverageScheme skips subsequent contour when required sign is unavailable`() {
|
||||
val requestTime = LocalDateTime.of(2026, 4, 14, 10, 0)
|
||||
val service = CoverageSchemeBuilderService(
|
||||
ballisticsClient = FakeBallisticsClient(
|
||||
orbitalPointsByRequest = mapOf(
|
||||
requestKey(11L, requestTime) to reverseOrbitalPoints(),
|
||||
requestKey(22L, requestTime.plusMinutes(10)) to reverseOrbitalPoints()
|
||||
)
|
||||
),
|
||||
satelliteClient = FakeSatelliteClient(
|
||||
mapOf(
|
||||
11L to 1.5,
|
||||
22L to 1.5
|
||||
)
|
||||
),
|
||||
geometrySupport = geometrySupport
|
||||
)
|
||||
|
||||
val result = service.buildCoverageScheme(
|
||||
observations = listOf(
|
||||
observation(
|
||||
satelliteId = 11L,
|
||||
timeBegin = requestTime,
|
||||
timeEnd = requestTime.plusMinutes(5),
|
||||
branch = RevolutionSign.ASC,
|
||||
gammaMin = 4.0,
|
||||
gammaMax = 8.0,
|
||||
contour = "POLYGON ((195.5 -0.1, 196.3 -0.1, 196.3 4.0, 195.5 4.0, 195.5 -0.1))"
|
||||
),
|
||||
observation(
|
||||
satelliteId = 22L,
|
||||
timeBegin = requestTime.plusMinutes(10),
|
||||
timeEnd = requestTime.plusMinutes(15),
|
||||
branch = RevolutionSign.ASC,
|
||||
gammaMin = -8.0,
|
||||
gammaMax = -2.0,
|
||||
contour = "POLYGON ((196.5 -0.1, 197.3 -0.1, 197.3 4.0, 196.5 4.0, 196.5 -0.1))"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(1, result.size)
|
||||
assertEquals(8.0, result.single().roll)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `buildCoverageScheme splits long observation by satellite max duration and mmi`() {
|
||||
val start = LocalDateTime.of(2026, 4, 14, 10, 0, 0)
|
||||
val service = CoverageSchemeBuilderService(
|
||||
ballisticsClient = FakeBallisticsClient(
|
||||
orbitalPointsByRequest = mapOf(
|
||||
requestKey(11L, start) to reverseOrbitalPoints(),
|
||||
requestKey(11L, start.plusSeconds(70)) to sampleOrbitalPoints(),
|
||||
requestKey(11L, start.plusSeconds(140)) to shiftedReverseOrbitalPoints()
|
||||
)
|
||||
),
|
||||
satelliteClient = FakeSatelliteClient(
|
||||
captureAnglesBySatellite = mapOf(11L to 1.5),
|
||||
maxSurveyDurationSecondsBySatellite = mapOf(11L to 60L),
|
||||
mmiSecondsBySatellite = mapOf(11L to 10L)
|
||||
),
|
||||
geometrySupport = geometrySupport
|
||||
)
|
||||
|
||||
val result = service.buildCoverageScheme(
|
||||
observations = listOf(
|
||||
observation(
|
||||
satelliteId = 11L,
|
||||
timeBegin = start,
|
||||
timeEnd = start.plusSeconds(180),
|
||||
branch = RevolutionSign.ASC,
|
||||
gammaMin = 1.0,
|
||||
gammaMax = 5.0,
|
||||
contour = "POLYGON ((195.5 -0.1, 196.3 -0.1, 196.3 4.0, 195.5 4.0, 195.5 -0.1))"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(3, result.size)
|
||||
assertEquals(start, result[0].tn)
|
||||
assertEquals(start.plusSeconds(60), result[0].tk)
|
||||
assertEquals(start.plusSeconds(70), result[1].tn)
|
||||
assertEquals(start.plusSeconds(130), result[1].tk)
|
||||
assertEquals(start.plusSeconds(140), result[2].tn)
|
||||
assertEquals(start.plusSeconds(180), result[2].tk)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `buildCoverageScheme skips candidate when contour does not intersect required area`() {
|
||||
val start = LocalDateTime.of(2026, 4, 14, 10, 0, 0)
|
||||
val service = CoverageSchemeBuilderService(
|
||||
ballisticsClient = FakeBallisticsClient(
|
||||
orbitalPointsByRequest = mapOf(
|
||||
requestKey(11L, start) to sampleOrbitalPoints()
|
||||
)
|
||||
),
|
||||
satelliteClient = FakeSatelliteClient(
|
||||
captureAnglesBySatellite = mapOf(11L to 1.5)
|
||||
),
|
||||
geometrySupport = geometrySupport
|
||||
)
|
||||
|
||||
val result = service.buildCoverageScheme(
|
||||
observations = listOf(
|
||||
observation(
|
||||
satelliteId = 11L,
|
||||
timeBegin = start,
|
||||
timeEnd = start.plusMinutes(5),
|
||||
branch = RevolutionSign.ASC,
|
||||
gammaMin = 1.0,
|
||||
gammaMax = 5.0,
|
||||
contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
assertTrue(result.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `buildCoverageScheme skips candidate when overlap with existing coverage is above ninety percent`() {
|
||||
val start = LocalDateTime.of(2026, 4, 14, 10, 0, 0)
|
||||
val service = CoverageSchemeBuilderService(
|
||||
ballisticsClient = FakeBallisticsClient(
|
||||
orbitalPointsByRequest = mapOf(
|
||||
requestKey(11L, start) to reverseOrbitalPoints(),
|
||||
requestKey(22L, start.plusMinutes(10)) to reverseOrbitalPoints()
|
||||
)
|
||||
),
|
||||
satelliteClient = FakeSatelliteClient(
|
||||
captureAnglesBySatellite = mapOf(
|
||||
11L to 1.5,
|
||||
22L to 1.5
|
||||
)
|
||||
),
|
||||
geometrySupport = geometrySupport
|
||||
)
|
||||
|
||||
val result = service.buildCoverageScheme(
|
||||
observations = listOf(
|
||||
observation(
|
||||
satelliteId = 11L,
|
||||
timeBegin = start,
|
||||
timeEnd = start.plusMinutes(5),
|
||||
branch = RevolutionSign.ASC,
|
||||
gammaMin = 4.0,
|
||||
gammaMax = 4.0,
|
||||
contour = "POLYGON ((190 -5, 200 -5, 200 10, 190 10, 190 -5))"
|
||||
),
|
||||
observation(
|
||||
satelliteId = 22L,
|
||||
timeBegin = start.plusMinutes(10),
|
||||
timeEnd = start.plusMinutes(15),
|
||||
branch = RevolutionSign.ASC,
|
||||
gammaMin = 4.0,
|
||||
gammaMax = 4.0,
|
||||
contour = "POLYGON ((190 -5, 200 -5, 200 10, 190 10, 190 -5))"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(1, result.size)
|
||||
assertEquals(11L, result.single().satelliteId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `buildCoverageScheme falls back to minimum roll when mplPoint returns multiple records`() {
|
||||
val requestTime = LocalDateTime.of(2026, 4, 14, 10, 0)
|
||||
val service = CoverageSchemeBuilderService(
|
||||
ballisticsClient = FakeBallisticsClient(
|
||||
orbitalPointsByRequest = mapOf(
|
||||
requestKey(11L, requestTime) to reverseOrbitalPoints(),
|
||||
requestKey(22L, requestTime.plusMinutes(10)) to reverseOrbitalPoints()
|
||||
),
|
||||
mplPointsBySatellite = mapOf(
|
||||
22L to listOf(
|
||||
PointViewParamDTO(noradId = 22L, time = requestTime.plusMinutes(10), gamma = 4.0),
|
||||
PointViewParamDTO(noradId = 22L, time = requestTime.plusMinutes(11), gamma = 5.0)
|
||||
)
|
||||
)
|
||||
),
|
||||
satelliteClient = FakeSatelliteClient(
|
||||
mapOf(
|
||||
11L to 1.5,
|
||||
22L to 1.5
|
||||
)
|
||||
),
|
||||
geometrySupport = geometrySupport
|
||||
)
|
||||
|
||||
val result = service.buildCoverageScheme(
|
||||
observations = listOf(
|
||||
observation(
|
||||
satelliteId = 11L,
|
||||
timeBegin = requestTime,
|
||||
timeEnd = requestTime.plusMinutes(5),
|
||||
branch = RevolutionSign.ASC,
|
||||
gammaMin = 4.0,
|
||||
gammaMax = 4.0,
|
||||
contour = "POLYGON ((195.5 -0.1, 196.3 -0.1, 196.3 4.0, 195.5 4.0, 195.5 -0.1))"
|
||||
),
|
||||
observation(
|
||||
satelliteId = 22L,
|
||||
timeBegin = requestTime.plusMinutes(10),
|
||||
timeEnd = requestTime.plusMinutes(15),
|
||||
branch = RevolutionSign.ASC,
|
||||
gammaMin = 0.0,
|
||||
gammaMax = 8.0,
|
||||
contour = "POLYGON ((195.5 -0.1, 196.3 -0.1, 196.3 4.0, 195.5 4.0, 195.5 -0.1))"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(2, result.size)
|
||||
assertEquals(8.0, result.last().roll)
|
||||
}
|
||||
|
||||
private class FakeBallisticsClient(
|
||||
private val orbitalPointsByRequest: Map<Pair<Long, LocalDateTime>, List<OrbPointDTO>>,
|
||||
private val mplPointsBySatellite: Map<Long, List<PointViewParamDTO>> = emptyMap(),
|
||||
private val capturedMplPointRequests: MutableList<ObjViewRequestDTO> = mutableListOf()
|
||||
) : CoverageBallisticsClient {
|
||||
override fun mplSquare(req: ObjViewRequestDTO): List<SquareViewParamDTO> = emptyList()
|
||||
|
||||
override fun mplPoint(req: ObjViewRequestDTO): List<PointViewParamDTO> {
|
||||
capturedMplPointRequests += req
|
||||
return req.satellites.singleOrNull()?.let { mplPointsBySatellite[it].orEmpty() } ?: emptyList()
|
||||
}
|
||||
|
||||
override fun flightLine(
|
||||
satelliteId: Long,
|
||||
timeStart: LocalDateTime,
|
||||
timeStop: LocalDateTime
|
||||
): List<FlightLineDTO> = emptyList()
|
||||
|
||||
override fun exactTime(satelliteId: Long, request: ExactTimePositionRequestDTO): List<OrbPointDTO> =
|
||||
orbitalPointsByRequest[satelliteId to request.time].orEmpty()
|
||||
}
|
||||
|
||||
private class FakeSatelliteClient(
|
||||
private val captureAnglesBySatellite: Map<Long, Double>,
|
||||
private val maxSurveyDurationSecondsBySatellite: Map<Long, Long> = emptyMap(),
|
||||
private val mmiSecondsBySatellite: Map<Long, Long> = emptyMap()
|
||||
) : CoverageSatelliteClient {
|
||||
override fun satellite(satelliteId: Long): SatelliteInfoDTO =
|
||||
SatelliteInfoDTO(
|
||||
noradId = satelliteId,
|
||||
captureAngle = captureAnglesBySatellite.getValue(satelliteId),
|
||||
maxSurveyDurationSeconds = maxSurveyDurationSecondsBySatellite[satelliteId] ?: 300L,
|
||||
mmiSeconds = mmiSecondsBySatellite[satelliteId] ?: 10L
|
||||
)
|
||||
}
|
||||
|
||||
private fun observation(
|
||||
satelliteId: Long,
|
||||
timeBegin: LocalDateTime,
|
||||
timeEnd: LocalDateTime,
|
||||
branch: RevolutionSign,
|
||||
gammaMin: Double,
|
||||
gammaMax: Double,
|
||||
contour: String
|
||||
) = CoverageObservationDTO(
|
||||
observation = SquareViewParamDTO(
|
||||
noradId = satelliteId,
|
||||
revolutionBegin = satelliteId,
|
||||
timeBegin = timeBegin,
|
||||
timeEnd = timeEnd,
|
||||
gammaMin = gammaMin,
|
||||
gammaMax = gammaMax,
|
||||
revSignBegin = branch,
|
||||
revSignEnd = branch
|
||||
),
|
||||
intersectionContourWkts = listOf(contour)
|
||||
)
|
||||
|
||||
private fun requestKey(satelliteId: Long, time: LocalDateTime) = satelliteId to time
|
||||
|
||||
private fun expectedBoundaryPosition(
|
||||
branch: RevolutionSign,
|
||||
orbitalPoints: List<OrbPointDTO>,
|
||||
roll: Double,
|
||||
capture: Double
|
||||
): PositionDTO {
|
||||
val firstPoint = orbitalPoints.first().toOrbitalPoint()
|
||||
val calculator = PointOnEarthCalculator(EarthType.PZ90d02, WorkCSType.WCSOrbit)
|
||||
val gamma = when (branch) {
|
||||
RevolutionSign.ASC -> roll - capture
|
||||
RevolutionSign.DESC -> roll + capture
|
||||
}
|
||||
val pointOnEarth = calculator.pointOnEarth(firstPoint, Orientation(0.0, gamma * PI / 180.0, 0.0))!!
|
||||
return PositionDTO(
|
||||
lat = pointOnEarth.lat * 180.0 / PI,
|
||||
long = pointOnEarth.long * 180.0 / PI,
|
||||
height = 0.0
|
||||
)
|
||||
}
|
||||
|
||||
private fun expectedRightmostStartPosition(contour: Geometry): PositionDTO {
|
||||
val coordinates = contour.coordinates
|
||||
val rightmostStartPoint = listOf(
|
||||
coordinates.first(),
|
||||
coordinates[coordinates.size - 2]
|
||||
).maxByOrNull { it.x }!!
|
||||
return PositionDTO(
|
||||
lat = rightmostStartPoint.y,
|
||||
long = rightmostStartPoint.x,
|
||||
height = 0.0
|
||||
)
|
||||
}
|
||||
|
||||
private fun assertPositionEquals(expected: PositionDTO, actual: PositionDTO?) {
|
||||
fun normalizeToStandardLongitude(longitude: Double): Double {
|
||||
var normalized = longitude
|
||||
while (normalized > 180.0) normalized -= 360.0
|
||||
while (normalized <= -180.0) normalized += 360.0
|
||||
return normalized
|
||||
}
|
||||
|
||||
requireNotNull(actual)
|
||||
assertEquals(expected.lat, actual.lat, 1e-9)
|
||||
assertEquals(normalizeToStandardLongitude(expected.long), normalizeToStandardLongitude(actual.long), 1e-9)
|
||||
}
|
||||
|
||||
private fun sampleOrbitalPoints() = listOf(
|
||||
orbPoint(
|
||||
time = LocalDateTime.of(2026, 4, 14, 10, 0),
|
||||
revolution = 42L,
|
||||
x = -6603039.949,
|
||||
y = -1870023.148,
|
||||
z = 0.0,
|
||||
vx = -401.529,
|
||||
vy = 1413.431,
|
||||
vz = 7547.696
|
||||
),
|
||||
orbPoint(
|
||||
time = LocalDateTime.of(2026, 4, 14, 10, 1),
|
||||
revolution = 42L,
|
||||
x = -6595000.0,
|
||||
y = -1860000.0,
|
||||
z = 452000.0,
|
||||
vx = -420.0,
|
||||
vy = 1390.0,
|
||||
vz = 7540.0
|
||||
)
|
||||
)
|
||||
|
||||
private fun reverseOrbitalPoints() = listOf(
|
||||
orbPoint(
|
||||
time = LocalDateTime.of(2026, 4, 14, 10, 0),
|
||||
revolution = 42L,
|
||||
x = -6595000.0,
|
||||
y = -1860000.0,
|
||||
z = 452000.0,
|
||||
vx = 420.0,
|
||||
vy = -1390.0,
|
||||
vz = -7540.0
|
||||
),
|
||||
orbPoint(
|
||||
time = LocalDateTime.of(2026, 4, 14, 10, 1),
|
||||
revolution = 42L,
|
||||
x = -6603039.949,
|
||||
y = -1870023.148,
|
||||
z = 0.0,
|
||||
vx = 401.529,
|
||||
vy = -1413.431,
|
||||
vz = -7547.696
|
||||
)
|
||||
)
|
||||
|
||||
private fun shiftedReverseOrbitalPoints() = listOf(
|
||||
orbPoint(
|
||||
time = LocalDateTime.of(2026, 4, 14, 10, 0),
|
||||
revolution = 42L,
|
||||
x = -6495000.0,
|
||||
y = -1760000.0,
|
||||
z = 452000.0,
|
||||
vx = 420.0,
|
||||
vy = -1390.0,
|
||||
vz = -7540.0
|
||||
),
|
||||
orbPoint(
|
||||
time = LocalDateTime.of(2026, 4, 14, 10, 1),
|
||||
revolution = 42L,
|
||||
x = -6503039.949,
|
||||
y = -1770023.148,
|
||||
z = 0.0,
|
||||
vx = 401.529,
|
||||
vy = -1413.431,
|
||||
vz = -7547.696
|
||||
)
|
||||
)
|
||||
|
||||
private fun orbPoint(
|
||||
time: LocalDateTime,
|
||||
revolution: Long,
|
||||
x: Double,
|
||||
y: Double,
|
||||
z: Double,
|
||||
vx: Double,
|
||||
vy: Double,
|
||||
vz: Double
|
||||
) = OrbPointDTO(
|
||||
time = time,
|
||||
revolution = revolution,
|
||||
vx = vx,
|
||||
vy = vy,
|
||||
vz = vz,
|
||||
x = x,
|
||||
y = y,
|
||||
z = z
|
||||
)
|
||||
|
||||
private fun OrbPointDTO.toOrbitalPoint() = OrbitalPoint(
|
||||
t = fromDateTime(time),
|
||||
vit = revolution.toInt(),
|
||||
r = Vector3D(x, y, z),
|
||||
v = Vector3D(vx, vy, vz)
|
||||
)
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package space.nstart.pcp.pcp_coverage_scheme_service.service
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import space.nstart.pcp.pcp_coverage_scheme_service.client.CoverageBallisticsClient
|
||||
import space.nstart.pcp.pcp_coverage_scheme_service.client.CoverageSatelliteClient
|
||||
import space.nstart.pcp.pcp_coverage_scheme_service.configuration.CoverageSchemeProperties
|
||||
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.ObjViewRequestDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.PointViewParamDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.coverage.CoverageSchemeCalculateRequestDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteInfoDTO
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class CoverageSchemeMultiContourTest {
|
||||
|
||||
private val service = CoverageSchemeCalculationService(
|
||||
ballisticsClient = FakeBallisticsClient(),
|
||||
coverageSchemeBuilderService = CoverageSchemeBuilderService(FakeBallisticsClient(), FakeSatelliteClient(), GeometrySupport()),
|
||||
geometrySupport = GeometrySupport(),
|
||||
properties = CoverageSchemeProperties(flightLinePaddingSeconds = 0)
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `calculate splits multipolygon intersection into separate contours`() {
|
||||
val response = service.calculate(
|
||||
CoverageSchemeCalculateRequestDTO(
|
||||
polygonWkt = "MULTIPOLYGON (((195 0, 196 0, 196 2, 195 2, 195 0)), ((197 0, 198 0, 198 2, 197 2, 197 0)))",
|
||||
satelliteIds = listOf(11L),
|
||||
timeStart = LocalDateTime.of(2026, 4, 14, 9, 55),
|
||||
timeEnd = LocalDateTime.of(2026, 4, 14, 10, 10)
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(1, response.observations.size)
|
||||
assertEquals(2, response.observations.first().intersectionContourWkts.size)
|
||||
assertEquals(1, response.coverageScheme.size)
|
||||
}
|
||||
|
||||
private class FakeBallisticsClient : CoverageBallisticsClient {
|
||||
override fun mplSquare(req: ObjViewRequestDTO): List<SquareViewParamDTO> =
|
||||
listOf(
|
||||
SquareViewParamDTO(
|
||||
noradId = 11L,
|
||||
timeBegin = LocalDateTime.of(2026, 4, 14, 10, 0),
|
||||
timeEnd = LocalDateTime.of(2026, 4, 14, 10, 5)
|
||||
)
|
||||
)
|
||||
|
||||
override fun mplPoint(req: ObjViewRequestDTO): List<PointViewParamDTO> = emptyList()
|
||||
|
||||
override fun flightLine(
|
||||
satelliteId: Long,
|
||||
timeStart: LocalDateTime,
|
||||
timeStop: LocalDateTime
|
||||
): List<FlightLineDTO> = listOf(
|
||||
FlightLineDTO(
|
||||
time = LocalDateTime.of(2026, 4, 14, 10, 0),
|
||||
lat = 0.0,
|
||||
long = 196.5,
|
||||
latLeft = 0.0,
|
||||
longLeft = 195.5,
|
||||
latRight = 0.0,
|
||||
longRight = 197.5
|
||||
),
|
||||
FlightLineDTO(
|
||||
time = LocalDateTime.of(2026, 4, 14, 10, 5),
|
||||
lat = 2.0,
|
||||
long = 196.5,
|
||||
latLeft = 2.0,
|
||||
longLeft = 195.5,
|
||||
latRight = 2.0,
|
||||
longRight = 197.5
|
||||
)
|
||||
)
|
||||
|
||||
override fun exactTime(
|
||||
satelliteId: Long,
|
||||
request: ExactTimePositionRequestDTO
|
||||
): List<OrbPointDTO> = listOf(
|
||||
OrbPointDTO(
|
||||
time = LocalDateTime.of(2026, 4, 14, 10, 0),
|
||||
revolution = 1L,
|
||||
vx = -401.529,
|
||||
vy = 1413.431,
|
||||
vz = 7547.696,
|
||||
x = -6603039.949,
|
||||
y = -1870023.148,
|
||||
z = 0.0
|
||||
),
|
||||
OrbPointDTO(
|
||||
time = LocalDateTime.of(2026, 4, 14, 10, 1),
|
||||
revolution = 1L,
|
||||
vx = -420.0,
|
||||
vy = 1390.0,
|
||||
vz = 7540.0,
|
||||
x = -6595000.0,
|
||||
y = -1860000.0,
|
||||
z = 452000.0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private class FakeSatelliteClient : CoverageSatelliteClient {
|
||||
override fun satellite(satelliteId: Long): SatelliteInfoDTO =
|
||||
SatelliteInfoDTO(noradId = satelliteId, captureAngle = 1.5)
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package space.nstart.pcp.pcp_coverage_scheme_service.service
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class GeometrySupportTest {
|
||||
|
||||
private val geometrySupport = GeometrySupport()
|
||||
|
||||
@Test
|
||||
fun `resolveLongitudeModel uses standard band when polygon crosses zero meridian`() {
|
||||
val model = geometrySupport.resolveLongitudeModel(
|
||||
"POLYGON ((-1 0, 1 0, 1 1, -1 1, -1 0))"
|
||||
)
|
||||
|
||||
assertEquals(GeometrySupport.LongitudeModel.STANDARD_180, model)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolveLongitudeModel uses positive band when polygon crosses one hundred eighty meridian`() {
|
||||
val model = geometrySupport.resolveLongitudeModel(
|
||||
"POLYGON ((179 0, -179 0, -179 1, 179 1, 179 0))"
|
||||
)
|
||||
|
||||
assertEquals(GeometrySupport.LongitudeModel.POSITIVE_360, model)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parseAndNormalizeTarget keeps zero crossing polygon in standard band`() {
|
||||
val geometry = geometrySupport.parseAndNormalizeTarget(
|
||||
"POLYGON ((-1 0, 1 0, 1 1, -1 1, -1 0))",
|
||||
GeometrySupport.LongitudeModel.STANDARD_180
|
||||
)
|
||||
|
||||
assertTrue(geometry.coordinates.all { it.x in -180.0..180.0 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
spring:
|
||||
config:
|
||||
import: ""
|
||||
cloud:
|
||||
config:
|
||||
enabled: false
|
||||
import-check:
|
||||
enabled: false
|
||||
boot:
|
||||
admin:
|
||||
client:
|
||||
enabled: false
|
||||
Reference in New Issue
Block a user