Прототип страницы расчета ПУУД
This commit is contained in:
+77
@@ -163,6 +163,83 @@ class AngularMotionCalculatorSmokeTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun chekSDI(){
|
||||||
|
val ballistics = Ballistics()
|
||||||
|
val t = LocalDateTime.of(2023, 4, 12, 17, 41, 18, 0)
|
||||||
|
val ic = InitialConditions(
|
||||||
|
OrbitalPoint(
|
||||||
|
fromDateTime(t),
|
||||||
|
1,
|
||||||
|
Vector3D(
|
||||||
|
-3101926.630,
|
||||||
|
6018678.8,
|
||||||
|
0.0,
|
||||||
|
),
|
||||||
|
Vector3D(
|
||||||
|
1287.651,
|
||||||
|
663.634,
|
||||||
|
7612.951,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
0.005,
|
||||||
|
160.0
|
||||||
|
)
|
||||||
|
val result = ballistics.calculateOrbPoints(
|
||||||
|
ic,
|
||||||
|
ic.point.t,
|
||||||
|
ic.point.t + 86400 * 10
|
||||||
|
)
|
||||||
|
assertEquals(result, BallisticsError.OK)
|
||||||
|
if (result != BallisticsError.OK)
|
||||||
|
return
|
||||||
|
val stepper = ballistics.getStepper()
|
||||||
|
assert(stepper != null)
|
||||||
|
if (stepper == null)
|
||||||
|
return
|
||||||
|
val calculator = SmoothSDIPUUD(stepper, EarthType.PZ90d02)
|
||||||
|
val id = SurveyId(
|
||||||
|
oep = listOf(false, false, false, true),
|
||||||
|
t = fromDateTime(LocalDateTime.of(2023, 4, 16, 3, 48, 22, 0)),
|
||||||
|
b = 49.25824 * PI / 180,
|
||||||
|
l = 153.65914 * PI / 180,
|
||||||
|
h = 0.0,
|
||||||
|
duration = 69.0,
|
||||||
|
sdi = listOf(30.0),
|
||||||
|
azimuth = 25.567 * PI / 180,
|
||||||
|
uprAngle = 6.0 * PI / 180,
|
||||||
|
pointInCenter = false
|
||||||
|
)
|
||||||
|
val rc = calculator.calculate(id)
|
||||||
|
|
||||||
|
println(rc.mode)
|
||||||
|
println(rc.points.size)
|
||||||
|
println(rc.startTime)
|
||||||
|
|
||||||
|
for (v in rc.points) {
|
||||||
|
println(
|
||||||
|
String.format(
|
||||||
|
Locale.US,
|
||||||
|
"%s %.3f %.3f %.3f , %s %s , %.7f %.7f %.7f , %.7f %.7f %.7f , %.3f",
|
||||||
|
toDateTime(v.t).format(outputTimeFormatter),
|
||||||
|
v.orientation.tang * 180 / PI,
|
||||||
|
v.orientation.kren * 180 / PI,
|
||||||
|
v.orientation.risk * 180 / PI,
|
||||||
|
v.groundPoint?.let { String.format(Locale.US, "%.3f", it.lat * 180 / PI) },
|
||||||
|
v.groundPoint?.let { String.format(Locale.US, "%.3f", it.long * 180 / PI) },
|
||||||
|
v.omega.x,
|
||||||
|
v.omega.y,
|
||||||
|
v.omega.z,
|
||||||
|
v.wd.x,
|
||||||
|
v.wd.y,
|
||||||
|
v.wd.z,
|
||||||
|
v.sdi
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun chekAzimuth(){
|
fun chekAzimuth(){
|
||||||
val ballistics = Ballistics()
|
val ballistics = Ballistics()
|
||||||
|
|||||||
+106
-11
@@ -1,6 +1,5 @@
|
|||||||
package space.nstart.pcp.pcp_request_service.angular
|
package space.nstart.pcp.pcp_request_service.angular
|
||||||
|
|
||||||
import org.springframework.format.annotation.DateTimeFormat
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
import org.springframework.web.bind.annotation.PathVariable
|
import org.springframework.web.bind.annotation.PathVariable
|
||||||
import org.springframework.web.bind.annotation.PostMapping
|
import org.springframework.web.bind.annotation.PostMapping
|
||||||
@@ -8,6 +7,10 @@ import org.springframework.web.bind.annotation.RequestBody
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
import org.springframework.web.bind.annotation.RequestParam
|
import org.springframework.web.bind.annotation.RequestParam
|
||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
import space.nstart.pcp.angularmotion.AngularMotionMode
|
||||||
|
import space.nstart.pcp.pcp_request_service.configuration.CustomValidationException
|
||||||
|
import java.net.URLDecoder
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -17,20 +20,112 @@ class AngularMotionController(
|
|||||||
) {
|
) {
|
||||||
@GetMapping("/satellites")
|
@GetMapping("/satellites")
|
||||||
fun availableSatellites(
|
fun availableSatellites(
|
||||||
@RequestParam(required = false)
|
@RequestParam(required = false) time: String?,
|
||||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
|
): List<AngularMotionSatelliteDTO> = angularMotionService.availableSatellites(parseTime(time, required = false))
|
||||||
time: LocalDateTime?,
|
|
||||||
): List<AngularMotionSatelliteDTO> = angularMotionService.availableSatellites(time)
|
|
||||||
|
|
||||||
@GetMapping("/satellites/{satelliteId}/flight-line")
|
@GetMapping("/satellites/{satelliteId}/flight-line")
|
||||||
fun flightLine(
|
fun flightLine(
|
||||||
@PathVariable satelliteId: Long,
|
@PathVariable satelliteId: Long,
|
||||||
@RequestParam
|
@RequestParam time: String,
|
||||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
|
): List<AngularMotionFlightLinePointDTO> = angularMotionService.flightLine(
|
||||||
time: LocalDateTime,
|
satelliteId,
|
||||||
): List<AngularMotionFlightLinePointDTO> = angularMotionService.flightLine(satelliteId, time)
|
parseTime(time, required = true) ?: throw CustomValidationException("Не задано время для построения трассы"),
|
||||||
|
)
|
||||||
|
|
||||||
@PostMapping("/calculate")
|
@PostMapping("/calculate")
|
||||||
fun calculate(@RequestBody request: AngularMotionCalculationRequestDTO): AngularMotionCalculationResultDTO =
|
fun calculate(@RequestBody request: Map<String, Any?>): AngularMotionCalculationResultDTO =
|
||||||
angularMotionService.calculate(request)
|
angularMotionService.calculate(AngularMotionRequestMapper.calculationRequest(request))
|
||||||
|
|
||||||
|
private fun parseTime(value: String?, required: Boolean): LocalDateTime? {
|
||||||
|
val raw = value?.trim().orEmpty()
|
||||||
|
if (raw.isEmpty()) {
|
||||||
|
if (required) throw CustomValidationException("Не задан параметр time")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val decoded = URLDecoder.decode(raw, StandardCharsets.UTF_8)
|
||||||
|
val normalized = if (decoded.length == 16) "$decoded:00" else decoded
|
||||||
|
return runCatching { LocalDateTime.parse(normalized) }
|
||||||
|
.getOrElse {
|
||||||
|
throw CustomValidationException(
|
||||||
|
"Некорректный формат времени '$decoded'. Ожидается yyyy-MM-dd'T'HH:mm или yyyy-MM-dd'T'HH:mm:ss",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal object AngularMotionRequestMapper {
|
||||||
|
fun calculationRequest(payload: Map<String, Any?>): AngularMotionCalculationRequestDTO =
|
||||||
|
AngularMotionCalculationRequestDTO(
|
||||||
|
satelliteId = payload.requiredLong("satelliteId"),
|
||||||
|
approximateTime = parseLocalDateTime(payload.requiredString("approximateTime"), "approximateTime"),
|
||||||
|
mode = payload.optionalEnum("mode") ?: AngularMotionMode.CONST_ORIENT,
|
||||||
|
latitudeDeg = payload.requiredDouble("latitudeDeg"),
|
||||||
|
longitudeDeg = payload.requiredDouble("longitudeDeg"),
|
||||||
|
heightM = payload.optionalDouble("heightM") ?: 0.0,
|
||||||
|
durationSec = payload.requiredDouble("durationSec"),
|
||||||
|
leadAngleDeg = payload.optionalDouble("leadAngleDeg") ?: 0.0,
|
||||||
|
azimuthDeg = payload.optionalDouble("azimuthDeg") ?: 0.0,
|
||||||
|
sdi = payload.optionalDouble("sdi"),
|
||||||
|
pointInCenter = payload.optionalBoolean("pointInCenter") ?: false,
|
||||||
|
focusMm = payload.optionalDouble("focusMm"),
|
||||||
|
stepPuudSec = payload.optionalDouble("stepPuudSec"),
|
||||||
|
stepSdiSec = payload.optionalDouble("stepSdiSec"),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun Map<String, Any?>.requiredString(field: String): String {
|
||||||
|
val value = this[field]?.toString()?.trim().orEmpty()
|
||||||
|
if (value.isEmpty()) {
|
||||||
|
throw CustomValidationException("Не задан параметр $field")
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Map<String, Any?>.requiredLong(field: String): Long =
|
||||||
|
optionalLong(field) ?: throw CustomValidationException("Не задан параметр $field")
|
||||||
|
|
||||||
|
private fun Map<String, Any?>.requiredDouble(field: String): Double =
|
||||||
|
optionalDouble(field) ?: throw CustomValidationException("Не задан параметр $field")
|
||||||
|
|
||||||
|
private fun Map<String, Any?>.optionalLong(field: String): Long? {
|
||||||
|
val value = this[field] ?: return null
|
||||||
|
return when (value) {
|
||||||
|
is Number -> value.toLong()
|
||||||
|
is String -> value.trim().takeIf { it.isNotEmpty() }?.toLongOrNull()
|
||||||
|
else -> null
|
||||||
|
} ?: throw CustomValidationException("Параметр $field должен быть целым числом")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Map<String, Any?>.optionalDouble(field: String): Double? {
|
||||||
|
val value = this[field] ?: return null
|
||||||
|
return when (value) {
|
||||||
|
is Number -> value.toDouble()
|
||||||
|
is String -> value.trim().takeIf { it.isNotEmpty() }?.toDoubleOrNull()
|
||||||
|
else -> null
|
||||||
|
}?.takeIf { it.isFinite() } ?: throw CustomValidationException("Параметр $field должен быть числом")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Map<String, Any?>.optionalBoolean(field: String): Boolean? {
|
||||||
|
val value = this[field] ?: return null
|
||||||
|
return when (value) {
|
||||||
|
is Boolean -> value
|
||||||
|
is String -> value.trim().takeIf { it.isNotEmpty() }?.toBooleanStrictOrNull()
|
||||||
|
else -> null
|
||||||
|
} ?: throw CustomValidationException("Параметр $field должен быть true или false")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Map<String, Any?>.optionalEnum(field: String): AngularMotionMode? {
|
||||||
|
val value = this[field]?.toString()?.trim()?.takeIf { it.isNotEmpty() } ?: return null
|
||||||
|
return runCatching { AngularMotionMode.valueOf(value) }
|
||||||
|
.getOrElse { throw CustomValidationException("Некорректный режим ПУУД '$value'") }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseLocalDateTime(value: String, field: String): LocalDateTime {
|
||||||
|
val normalized = if (value.length == 16) "$value:00" else value
|
||||||
|
return runCatching { LocalDateTime.parse(normalized) }
|
||||||
|
.getOrElse {
|
||||||
|
throw CustomValidationException(
|
||||||
|
"Некорректный формат времени $field='$value'. Ожидается yyyy-MM-dd'T'HH:mm или yyyy-MM-dd'T'HH:mm:ss",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -35,7 +35,11 @@ class AngularMotionService(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
.toMap()
|
.toMap()
|
||||||
return satelliteService.getOrbitAvailability(time)
|
val availability = time
|
||||||
|
?.let { satelliteService.getOrbitAvailabilityForDate(it) }
|
||||||
|
?: satelliteService.getOrbitAvailability(null)
|
||||||
|
|
||||||
|
return availability
|
||||||
.map { availability ->
|
.map { availability ->
|
||||||
val satellite = satellitesByBallisticsId[availability.satelliteId]
|
val satellite = satellitesByBallisticsId[availability.satelliteId]
|
||||||
AngularMotionSatelliteDTO(
|
AngularMotionSatelliteDTO(
|
||||||
|
|||||||
+18
@@ -46,4 +46,22 @@ interface PDCMRepository : JpaRepository<PDCMEntity, Long>{
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
fun findSatelliteOrbitAvailabilityAtTime(@Param("time") time: LocalDateTime): List<SatelliteOrbitAvailabilityDTO>
|
fun findSatelliteOrbitAvailabilityAtTime(@Param("time") time: LocalDateTime): List<SatelliteOrbitAvailabilityDTO>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
select new space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteOrbitAvailabilityDTO(
|
||||||
|
p.satelliteId,
|
||||||
|
min(p.time),
|
||||||
|
max(p.time)
|
||||||
|
)
|
||||||
|
from PDCMEntity p
|
||||||
|
where p.time >= :dayStart and p.time < :dayEnd
|
||||||
|
group by p.satelliteId
|
||||||
|
order by p.satelliteId
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun findSatelliteOrbitAvailabilityForDate(
|
||||||
|
@Param("dayStart") dayStart: LocalDateTime,
|
||||||
|
@Param("dayEnd") dayEnd: LocalDateTime
|
||||||
|
): List<SatelliteOrbitAvailabilityDTO>
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -773,6 +773,11 @@ class SatelliteService {
|
|||||||
pdcmRepository.findSatelliteOrbitAvailabilityAtTime(time)
|
pdcmRepository.findSatelliteOrbitAvailabilityAtTime(time)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getOrbitAvailabilityForDate(time: LocalDateTime): List<SatelliteOrbitAvailabilityDTO> {
|
||||||
|
val dayStart = time.toLocalDate().atStartOfDay()
|
||||||
|
return pdcmRepository.findSatelliteOrbitAvailabilityForDate(dayStart, dayStart.plusDays(1))
|
||||||
|
}
|
||||||
|
|
||||||
fun getFL(id : Long, tn : LocalDateTime?, tk : LocalDateTime?, step : Double?) =
|
fun getFL(id : Long, tn : LocalDateTime?, tk : LocalDateTime?, step : Double?) =
|
||||||
flightLineRepository.findBySatelliteIdAndTimeBetween(id,
|
flightLineRepository.findBySatelliteIdAndTimeBetween(id,
|
||||||
tn?: LocalDateTime.now(),
|
tn?: LocalDateTime.now(),
|
||||||
@@ -924,4 +929,3 @@ data class SatelliteBallisticsDeletionSummary(
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+70
@@ -0,0 +1,70 @@
|
|||||||
|
package space.nstart.pcp.pcp_request_service.angular
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.assertThrows
|
||||||
|
import space.nstart.pcp.angularmotion.AngularMotionMode
|
||||||
|
import space.nstart.pcp.pcp_request_service.configuration.CustomValidationException
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFalse
|
||||||
|
|
||||||
|
class AngularMotionRequestMapperTest {
|
||||||
|
@Test
|
||||||
|
fun `maps calculation payload sent by UI`() {
|
||||||
|
val request = AngularMotionRequestMapper.calculationRequest(
|
||||||
|
mapOf(
|
||||||
|
"satelliteId" to 123L,
|
||||||
|
"approximateTime" to "2026-06-15T10:30:00",
|
||||||
|
"mode" to "CONST_ORIENT",
|
||||||
|
"latitudeDeg" to 55.75,
|
||||||
|
"longitudeDeg" to 37.61,
|
||||||
|
"heightM" to 0,
|
||||||
|
"durationSec" to 60,
|
||||||
|
"leadAngleDeg" to 0,
|
||||||
|
"azimuthDeg" to 0,
|
||||||
|
"focusMm" to 5500,
|
||||||
|
"stepPuudSec" to 0.125,
|
||||||
|
"stepSdiSec" to 20,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(123L, request.satelliteId)
|
||||||
|
assertEquals(LocalDateTime.of(2026, 6, 15, 10, 30), request.approximateTime)
|
||||||
|
assertEquals(AngularMotionMode.CONST_ORIENT, request.mode)
|
||||||
|
assertEquals(55.75, request.latitudeDeg)
|
||||||
|
assertEquals(37.61, request.longitudeDeg)
|
||||||
|
assertFalse(request.pointInCenter)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `maps datetime without seconds`() {
|
||||||
|
val request = AngularMotionRequestMapper.calculationRequest(
|
||||||
|
mapOf(
|
||||||
|
"satelliteId" to 123L,
|
||||||
|
"approximateTime" to "2026-06-15T10:30",
|
||||||
|
"latitudeDeg" to 55.75,
|
||||||
|
"longitudeDeg" to 37.61,
|
||||||
|
"durationSec" to 60,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(LocalDateTime.of(2026, 6, 15, 10, 30), request.approximateTime)
|
||||||
|
assertEquals(AngularMotionMode.CONST_ORIENT, request.mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `rejects unknown calculation mode with validation error`() {
|
||||||
|
assertThrows<CustomValidationException> {
|
||||||
|
AngularMotionRequestMapper.calculationRequest(
|
||||||
|
mapOf(
|
||||||
|
"satelliteId" to 123L,
|
||||||
|
"approximateTime" to "2026-06-15T10:30:00",
|
||||||
|
"mode" to "UNKNOWN",
|
||||||
|
"latitudeDeg" to 55.75,
|
||||||
|
"longitudeDeg" to 37.61,
|
||||||
|
"durationSec" to 60,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
@@ -82,6 +82,25 @@ class PDCMRepositoryTest {
|
|||||||
assertEquals(LocalDateTime.of(2026, 4, 14, 12, 0), actual.single().timeStop)
|
assertEquals(LocalDateTime.of(2026, 4, 14, 12, 0), actual.single().timeStop)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `find satellite orbit availability for date returns satellites with points on requested date`() {
|
||||||
|
persistPoint(11L, LocalDateTime.of(2026, 4, 14, 10, 0))
|
||||||
|
persistPoint(11L, LocalDateTime.of(2026, 4, 14, 12, 0))
|
||||||
|
persistPoint(22L, LocalDateTime.of(2026, 4, 14, 23, 30))
|
||||||
|
persistPoint(33L, LocalDateTime.of(2026, 4, 15, 9, 30))
|
||||||
|
|
||||||
|
val actual = repository.findSatelliteOrbitAvailabilityForDate(
|
||||||
|
LocalDateTime.of(2026, 4, 14, 0, 0),
|
||||||
|
LocalDateTime.of(2026, 4, 15, 0, 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(listOf(11L, 22L), actual.map { it.satelliteId })
|
||||||
|
assertEquals(LocalDateTime.of(2026, 4, 14, 10, 0), actual[0].timeStart)
|
||||||
|
assertEquals(LocalDateTime.of(2026, 4, 14, 12, 0), actual[0].timeStop)
|
||||||
|
assertEquals(LocalDateTime.of(2026, 4, 14, 23, 30), actual[1].timeStart)
|
||||||
|
assertEquals(LocalDateTime.of(2026, 4, 14, 23, 30), actual[1].timeStop)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `satellite initial conditions repository returns last calculated condition`() {
|
fun `satellite initial conditions repository returns last calculated condition`() {
|
||||||
val oldCondition = satelliteInitialConditionRepository.save(sampleInitialCondition(11L, 1L))
|
val oldCondition = satelliteInitialConditionRepository.save(sampleInitialCondition(11L, 1L))
|
||||||
|
|||||||
+3
-2
@@ -9,6 +9,7 @@ import space.nstart.pcp.slots_service.configuration.CustomErrorException
|
|||||||
import space.nstart.pcp.slots_service.dto.dynamicplan.DynamicPlanCalculationRequestDTO
|
import space.nstart.pcp.slots_service.dto.dynamicplan.DynamicPlanCalculationRequestDTO
|
||||||
import tools.jackson.databind.JsonNode
|
import tools.jackson.databind.JsonNode
|
||||||
import tools.jackson.databind.ObjectMapper
|
import tools.jackson.databind.ObjectMapper
|
||||||
|
import java.net.URI
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
@@ -118,7 +119,7 @@ class DynamicPlanService(webClientBuilderProvider: ObjectProvider<WebClient.Buil
|
|||||||
|
|
||||||
return webClientBuilder.build()
|
return webClientBuilder.build()
|
||||||
.get()
|
.get()
|
||||||
.uri("$ballisticsServiceUrl/api/angular-motion/satellites$timeParam")
|
.uri(URI.create("$ballisticsServiceUrl/api/angular-motion/satellites$timeParam"))
|
||||||
.retrieve()
|
.retrieve()
|
||||||
.onStatus({ status -> status.isError }) { response ->
|
.onStatus({ status -> status.isError }) { response ->
|
||||||
response.bodyToMono(String::class.java)
|
response.bodyToMono(String::class.java)
|
||||||
@@ -132,7 +133,7 @@ class DynamicPlanService(webClientBuilderProvider: ObjectProvider<WebClient.Buil
|
|||||||
fun getAngularMotionFlightLine(satelliteId: Long, time: String): JsonNode =
|
fun getAngularMotionFlightLine(satelliteId: Long, time: String): JsonNode =
|
||||||
webClientBuilder.build()
|
webClientBuilder.build()
|
||||||
.get()
|
.get()
|
||||||
.uri("$ballisticsServiceUrl/api/angular-motion/satellites/$satelliteId/flight-line?time=${URLEncoder.encode(time, StandardCharsets.UTF_8)}")
|
.uri(URI.create("$ballisticsServiceUrl/api/angular-motion/satellites/$satelliteId/flight-line?time=${URLEncoder.encode(time, StandardCharsets.UTF_8)}"))
|
||||||
.retrieve()
|
.retrieve()
|
||||||
.onStatus({ status -> status.isError }) { response ->
|
.onStatus({ status -> status.isError }) { response ->
|
||||||
response.bodyToMono(String::class.java)
|
response.bodyToMono(String::class.java)
|
||||||
|
|||||||
+213
-80
@@ -5,6 +5,10 @@
|
|||||||
flightLine: [],
|
flightLine: [],
|
||||||
picking: false,
|
picking: false,
|
||||||
result: null,
|
result: null,
|
||||||
|
viewer: null,
|
||||||
|
mapDataSource: null,
|
||||||
|
pickHandler: null,
|
||||||
|
cursorCoordsPanel: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
function el(id) {
|
function el(id) {
|
||||||
@@ -56,6 +60,15 @@
|
|||||||
return value && value.length === 16 ? `${value}:00` : value;
|
return value && value.length === 16 ? `${value}:00` : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toDateTimeInputValue(value) {
|
||||||
|
return text(value).slice(0, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDateTimeSeconds(value) {
|
||||||
|
const textValue = text(value);
|
||||||
|
return textValue && textValue.length === 16 ? `${textValue}:00` : textValue;
|
||||||
|
}
|
||||||
|
|
||||||
function formatDateTime(value) {
|
function formatDateTime(value) {
|
||||||
return text(value, '-').replace('T', ' ');
|
return text(value, '-').replace('T', ' ');
|
||||||
}
|
}
|
||||||
@@ -96,9 +109,22 @@
|
|||||||
select.value = previous;
|
select.value = previous;
|
||||||
}
|
}
|
||||||
state.selectedSatellite = satellites.find(item => String(item.satelliteId) === select.value) || satellites[0];
|
state.selectedSatellite = satellites.find(item => String(item.satelliteId) === select.value) || satellites[0];
|
||||||
|
ensureTimeInsideSelectedSatelliteAvailability();
|
||||||
renderSatelliteMeta();
|
renderSatelliteMeta();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureTimeInsideSelectedSatelliteAvailability() {
|
||||||
|
const sat = state.selectedSatellite;
|
||||||
|
if (!sat?.availabilityStart || !sat?.availabilityStop) return;
|
||||||
|
|
||||||
|
const current = normalizeDateTimeSeconds(localDateTimeValue());
|
||||||
|
const start = normalizeDateTimeSeconds(sat.availabilityStart);
|
||||||
|
const stop = normalizeDateTimeSeconds(sat.availabilityStop);
|
||||||
|
if (!current || current < start || current > stop) {
|
||||||
|
el('angular-motion-time').value = toDateTimeInputValue(start);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderSatelliteMeta() {
|
function renderSatelliteMeta() {
|
||||||
const sat = state.selectedSatellite;
|
const sat = state.selectedSatellite;
|
||||||
el('angular-motion-satellite-meta').textContent = sat
|
el('angular-motion-satellite-meta').textContent = sat
|
||||||
@@ -118,98 +144,211 @@
|
|||||||
el('angular-motion-map-meta').textContent = state.flightLine.length > 0
|
el('angular-motion-map-meta').textContent = state.flightLine.length > 0
|
||||||
? `Трасса и полосы обзора на интервале ${formatDateTime(time)} ± 5 минут.`
|
? `Трасса и полосы обзора на интервале ${formatDateTime(time)} ± 5 минут.`
|
||||||
: 'Нет данных трассы полета для выбранного КА и времени.';
|
: 'Нет данных трассы полета для выбранного КА и времени.';
|
||||||
drawMap();
|
drawMap(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function lonToX(lon, width) {
|
function initMap() {
|
||||||
return (Number(lon) + 180) / 360 * width;
|
if (typeof Cesium === 'undefined') {
|
||||||
|
el('angular-motion-map-meta').textContent = 'Библиотека Cesium не загружена.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Cesium.Camera.DEFAULT_VIEW_RECTANGLE = Cesium.Rectangle.fromDegrees(20, 35, 190, 80);
|
||||||
|
state.viewer = new Cesium.Viewer('angular-motion-map', {
|
||||||
|
animation: false,
|
||||||
|
timeline: false,
|
||||||
|
fullscreenButton: false,
|
||||||
|
geocoder: false,
|
||||||
|
homeButton: false,
|
||||||
|
infoBox: false,
|
||||||
|
navigationHelpButton: false,
|
||||||
|
sceneModePicker: false,
|
||||||
|
selectionIndicator: false,
|
||||||
|
baseLayerPicker: false,
|
||||||
|
sceneMode: Cesium.SceneMode.SCENE2D,
|
||||||
|
...naturalEarthLayerOptions(),
|
||||||
|
});
|
||||||
|
state.viewer.scene.globe.enableLighting = false;
|
||||||
|
state.viewer.scene.requestRenderMode = true;
|
||||||
|
state.mapDataSource = new Cesium.CustomDataSource('angular-motion-map-data');
|
||||||
|
state.viewer.dataSources.add(state.mapDataSource);
|
||||||
|
initCursorCoordinates();
|
||||||
|
state.pickHandler = new Cesium.ScreenSpaceEventHandler(state.viewer.scene.canvas);
|
||||||
|
state.pickHandler.setInputAction(function (movement) {
|
||||||
|
if (!state.picking) return;
|
||||||
|
const cartesian = pickGlobePoint(movement.position);
|
||||||
|
if (!cartesian) return;
|
||||||
|
const cartographic = Cesium.Cartographic.fromCartesian(cartesian);
|
||||||
|
el('angular-motion-lat').value = Cesium.Math.toDegrees(cartographic.latitude).toFixed(6);
|
||||||
|
el('angular-motion-lon').value = Cesium.Math.toDegrees(cartographic.longitude).toFixed(6);
|
||||||
|
setPicking(false);
|
||||||
|
drawMap(false);
|
||||||
|
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
||||||
|
drawMap(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function latToY(lat, height) {
|
function initCursorCoordinates() {
|
||||||
return (90 - Number(lat)) / 180 * height;
|
state.cursorCoordsPanel = document.createElement('div');
|
||||||
|
state.cursorCoordsPanel.className = 'angular-motion-cursor-coords-panel';
|
||||||
|
state.cursorCoordsPanel.textContent = 'Координаты: наведите курсор на карту';
|
||||||
|
state.viewer.cesiumWidget.container.appendChild(state.cursorCoordsPanel);
|
||||||
|
|
||||||
|
state.viewer.scene.canvas.addEventListener('mousemove', event => {
|
||||||
|
const rect = state.viewer.scene.canvas.getBoundingClientRect();
|
||||||
|
const screenPosition = new Cesium.Cartesian2(
|
||||||
|
event.clientX - rect.left,
|
||||||
|
event.clientY - rect.top
|
||||||
|
);
|
||||||
|
state.cursorCoordsPanel.textContent = formatCursorCoordinates(screenPosition);
|
||||||
|
});
|
||||||
|
|
||||||
|
state.viewer.scene.canvas.addEventListener('mouseleave', () => {
|
||||||
|
state.cursorCoordsPanel.textContent = 'Координаты: курсор вне карты';
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function canvasToLonLat(event) {
|
function naturalEarthLayerOptions() {
|
||||||
const canvas = el('angular-motion-map');
|
const url = '/webjars/cesium/1.107.2/Build/Cesium/Assets/Textures/NaturalEarthII';
|
||||||
const rect = canvas.getBoundingClientRect();
|
if (Cesium.ImageryLayer?.fromProviderAsync && Cesium.TileMapServiceImageryProvider?.fromUrl) {
|
||||||
const scaleX = canvas.width / rect.width;
|
return {
|
||||||
const scaleY = canvas.height / rect.height;
|
baseLayer: Cesium.ImageryLayer.fromProviderAsync(
|
||||||
const x = (event.clientX - rect.left) * scaleX;
|
Cesium.TileMapServiceImageryProvider.fromUrl(url)
|
||||||
const y = (event.clientY - rect.top) * scaleY;
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
lon: x / canvas.width * 360 - 180,
|
imageryProvider: new Cesium.TileMapServiceImageryProvider({ url }),
|
||||||
lat: 90 - y / canvas.height * 180,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawGrid(ctx, width, height) {
|
function pickGlobePoint(position) {
|
||||||
ctx.clearRect(0, 0, width, height);
|
if (!state.viewer || !position) return null;
|
||||||
ctx.fillStyle = '#f8fbff';
|
const scene = state.viewer.scene;
|
||||||
ctx.fillRect(0, 0, width, height);
|
if (scene.pickPositionSupported) {
|
||||||
ctx.strokeStyle = '#e2e8f0';
|
const picked = scene.pickPosition(position);
|
||||||
ctx.lineWidth = 1;
|
if (Cesium.defined(picked)) return picked;
|
||||||
for (let lon = -180; lon <= 180; lon += 30) {
|
|
||||||
const x = lonToX(lon, width);
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(x, 0);
|
|
||||||
ctx.lineTo(x, height);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
}
|
||||||
for (let lat = -60; lat <= 60; lat += 30) {
|
return state.viewer.camera.pickEllipsoid(position, scene.globe.ellipsoid);
|
||||||
const y = latToY(lat, height);
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(0, y);
|
|
||||||
ctx.lineTo(width, y);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
ctx.fillStyle = '#94a3b8';
|
|
||||||
ctx.font = '12px sans-serif';
|
|
||||||
ctx.fillText('Широта / долгота, схематичная проекция', 12, 20);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawLine(ctx, points, latKey, lonKey, color, width) {
|
function formatDegree(value, positiveSuffix, negativeSuffix) {
|
||||||
const valid = points.filter(point => Number.isFinite(Number(point[latKey])) && Number.isFinite(Number(point[lonKey])));
|
if (!Number.isFinite(value)) {
|
||||||
if (valid.length < 2) return;
|
return '-';
|
||||||
ctx.strokeStyle = color;
|
}
|
||||||
ctx.lineWidth = width;
|
|
||||||
ctx.beginPath();
|
const suffix = value >= 0 ? positiveSuffix : negativeSuffix;
|
||||||
valid.forEach((point, index) => {
|
return `${Math.abs(value).toFixed(6)}° ${suffix}`;
|
||||||
const x = lonToX(point[lonKey], ctx.canvas.width);
|
}
|
||||||
const y = latToY(point[latKey], ctx.canvas.height);
|
|
||||||
if (index === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
function formatCursorCoordinates(screenPosition) {
|
||||||
|
const point = pickGlobePoint(screenPosition);
|
||||||
|
if (!Cesium.defined(point)) {
|
||||||
|
return 'Координаты: -';
|
||||||
|
}
|
||||||
|
|
||||||
|
const cartographic = state.viewer.scene.globe.ellipsoid.cartesianToCartographic(point);
|
||||||
|
if (!Cesium.defined(cartographic)) {
|
||||||
|
return 'Координаты: -';
|
||||||
|
}
|
||||||
|
|
||||||
|
const lon = Cesium.Math.toDegrees(cartographic.longitude);
|
||||||
|
const lat = Cesium.Math.toDegrees(cartographic.latitude);
|
||||||
|
return `Lon: ${formatDegree(lon, 'E', 'W')} | Lat: ${formatDegree(lat, 'N', 'S')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validDegreesArray(points, latKey, lonKey) {
|
||||||
|
const result = [];
|
||||||
|
(Array.isArray(points) ? points : []).forEach(point => {
|
||||||
|
const lat = Number(point[latKey]);
|
||||||
|
const lon = Number(point[lonKey]);
|
||||||
|
if (Number.isFinite(lat) && Number.isFinite(lon)) {
|
||||||
|
result.push(lon, lat);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
ctx.stroke();
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawTarget(ctx) {
|
function addPolyline(points, latKey, lonKey, color, width, name) {
|
||||||
|
if (!state.mapDataSource) return;
|
||||||
|
const degrees = validDegreesArray(points, latKey, lonKey);
|
||||||
|
if (degrees.length < 4) return;
|
||||||
|
state.mapDataSource.entities.add({
|
||||||
|
name,
|
||||||
|
polyline: {
|
||||||
|
positions: Cesium.Cartesian3.fromDegreesArray(degrees),
|
||||||
|
width,
|
||||||
|
material: color,
|
||||||
|
clampToGround: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addTarget() {
|
||||||
|
if (!state.mapDataSource) return;
|
||||||
const lat = Number(el('angular-motion-lat').value);
|
const lat = Number(el('angular-motion-lat').value);
|
||||||
const lon = Number(el('angular-motion-lon').value);
|
const lon = Number(el('angular-motion-lon').value);
|
||||||
if (!Number.isFinite(lat) || !Number.isFinite(lon)) return;
|
if (!Number.isFinite(lat) || !Number.isFinite(lon)) return;
|
||||||
const x = lonToX(lon, ctx.canvas.width);
|
state.mapDataSource.entities.add({
|
||||||
const y = latToY(lat, ctx.canvas.height);
|
name: 'Точка съемки',
|
||||||
ctx.strokeStyle = '#dc2626';
|
position: Cesium.Cartesian3.fromDegrees(lon, lat, 0),
|
||||||
ctx.fillStyle = '#dc2626';
|
point: {
|
||||||
ctx.lineWidth = 2;
|
pixelSize: 10,
|
||||||
ctx.beginPath();
|
color: Cesium.Color.RED,
|
||||||
ctx.arc(x, y, 5, 0, Math.PI * 2);
|
outlineColor: Cesium.Color.WHITE,
|
||||||
ctx.stroke();
|
outlineWidth: 2,
|
||||||
ctx.beginPath();
|
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
|
||||||
ctx.moveTo(x - 9, y);
|
},
|
||||||
ctx.lineTo(x + 9, y);
|
label: {
|
||||||
ctx.moveTo(x, y - 9);
|
text: 'ЦЛМ',
|
||||||
ctx.lineTo(x, y + 9);
|
font: '12px sans-serif',
|
||||||
ctx.stroke();
|
fillColor: Cesium.Color.RED,
|
||||||
|
outlineColor: Cesium.Color.WHITE,
|
||||||
|
outlineWidth: 2,
|
||||||
|
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
|
||||||
|
pixelOffset: new Cesium.Cartesian2(0, -18),
|
||||||
|
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawMap() {
|
function fitMapToData() {
|
||||||
const canvas = el('angular-motion-map');
|
if (!state.viewer) return;
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
drawGrid(ctx, canvas.width, canvas.height);
|
|
||||||
const points = Array.isArray(state.flightLine) ? state.flightLine : [];
|
const points = Array.isArray(state.flightLine) ? state.flightLine : [];
|
||||||
drawLine(ctx, points, 'leftLatitudeDeg', 'leftLongitudeDeg', '#93c5fd', 1.5);
|
const lats = [];
|
||||||
drawLine(ctx, points, 'rightLatitudeDeg', 'rightLongitudeDeg', '#93c5fd', 1.5);
|
const lons = [];
|
||||||
drawLine(ctx, points, 'latitudeDeg', 'longitudeDeg', '#0f172a', 2.5);
|
points.forEach(point => {
|
||||||
drawTarget(ctx);
|
['latitudeDeg', 'leftLatitudeDeg', 'rightLatitudeDeg'].forEach(key => {
|
||||||
|
const value = Number(point[key]);
|
||||||
|
if (Number.isFinite(value)) lats.push(value);
|
||||||
|
});
|
||||||
|
['longitudeDeg', 'leftLongitudeDeg', 'rightLongitudeDeg'].forEach(key => {
|
||||||
|
const value = Number(point[key]);
|
||||||
|
if (Number.isFinite(value)) lons.push(value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (lats.length === 0 || lons.length === 0) return;
|
||||||
|
const minLat = Math.max(-89.0, Math.min(...lats) - 1.0);
|
||||||
|
const maxLat = Math.min(89.0, Math.max(...lats) + 1.0);
|
||||||
|
const minLon = Math.max(-180.0, Math.min(...lons) - 1.0);
|
||||||
|
const maxLon = Math.min(180.0, Math.max(...lons) + 1.0);
|
||||||
|
if (maxLon - minLon > 180.0) return;
|
||||||
|
state.viewer.camera.flyTo({
|
||||||
|
destination: Cesium.Rectangle.fromDegrees(minLon, minLat, maxLon, maxLat),
|
||||||
|
duration: 0.4,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawMap(fit = false) {
|
||||||
|
if (!state.mapDataSource || typeof Cesium === 'undefined') return;
|
||||||
|
state.mapDataSource.entities.removeAll();
|
||||||
|
const points = Array.isArray(state.flightLine) ? state.flightLine : [];
|
||||||
|
addPolyline(points, 'leftLatitudeDeg', 'leftLongitudeDeg', Cesium.Color.CORNFLOWERBLUE.withAlpha(0.85), 2, 'Левая граница полосы');
|
||||||
|
addPolyline(points, 'rightLatitudeDeg', 'rightLongitudeDeg', Cesium.Color.CORNFLOWERBLUE.withAlpha(0.85), 2, 'Правая граница полосы');
|
||||||
|
addPolyline(points, 'latitudeDeg', 'longitudeDeg', Cesium.Color.YELLOW, 3, 'Трасса полета');
|
||||||
|
addTarget();
|
||||||
|
if (fit) fitMapToData();
|
||||||
|
state.viewer.scene.requestRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setPicking(value) {
|
function setPicking(value) {
|
||||||
@@ -228,6 +367,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function payloadFromForm() {
|
function payloadFromForm() {
|
||||||
|
ensureTimeInsideSelectedSatelliteAvailability();
|
||||||
const mode = el('angular-motion-mode').value;
|
const mode = el('angular-motion-mode').value;
|
||||||
const payload = {
|
const payload = {
|
||||||
satelliteId: Number(el('angular-motion-satellite').value),
|
satelliteId: Number(el('angular-motion-satellite').value),
|
||||||
@@ -322,28 +462,21 @@
|
|||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
el('angular-motion-time').value = nowLocalInput();
|
el('angular-motion-time').value = nowLocalInput();
|
||||||
|
initMap();
|
||||||
el('angular-motion-form').addEventListener('submit', calculate);
|
el('angular-motion-form').addEventListener('submit', calculate);
|
||||||
el('angular-motion-refresh-satellites').addEventListener('click', () => loadSatellites().catch(error => showAlert(error.message || String(error))));
|
el('angular-motion-refresh-satellites').addEventListener('click', () => loadSatellites().catch(error => showAlert(error.message || String(error))));
|
||||||
el('angular-motion-time').addEventListener('change', () => loadSatellites().catch(error => showAlert(error.message || String(error))));
|
el('angular-motion-time').addEventListener('change', () => loadSatellites().catch(error => showAlert(error.message || String(error))));
|
||||||
el('angular-motion-satellite').addEventListener('change', () => {
|
el('angular-motion-satellite').addEventListener('change', () => {
|
||||||
state.selectedSatellite = state.satellites.find(item => String(item.satelliteId) === el('angular-motion-satellite').value) || null;
|
state.selectedSatellite = state.satellites.find(item => String(item.satelliteId) === el('angular-motion-satellite').value) || null;
|
||||||
|
ensureTimeInsideSelectedSatelliteAvailability();
|
||||||
renderSatelliteMeta();
|
renderSatelliteMeta();
|
||||||
loadFlightLine().catch(error => showAlert(error.message || String(error)));
|
loadFlightLine().catch(error => showAlert(error.message || String(error)));
|
||||||
});
|
});
|
||||||
el('angular-motion-mode').addEventListener('change', modeChanged);
|
el('angular-motion-mode').addEventListener('change', modeChanged);
|
||||||
el('angular-motion-pick-from-map').addEventListener('click', () => setPicking(!state.picking));
|
el('angular-motion-pick-from-map').addEventListener('click', () => setPicking(!state.picking));
|
||||||
el('angular-motion-map').addEventListener('click', event => {
|
['angular-motion-lat', 'angular-motion-lon'].forEach(id => el(id).addEventListener('input', () => drawMap(false)));
|
||||||
if (!state.picking) return;
|
|
||||||
const coord = canvasToLonLat(event);
|
|
||||||
el('angular-motion-lat').value = coord.lat.toFixed(6);
|
|
||||||
el('angular-motion-lon').value = coord.lon.toFixed(6);
|
|
||||||
setPicking(false);
|
|
||||||
drawMap();
|
|
||||||
});
|
|
||||||
['angular-motion-lat', 'angular-motion-lon'].forEach(id => el(id).addEventListener('input', drawMap));
|
|
||||||
el('angular-motion-export-csv').addEventListener('click', exportCsv);
|
el('angular-motion-export-csv').addEventListener('click', exportCsv);
|
||||||
modeChanged();
|
modeChanged();
|
||||||
drawMap();
|
|
||||||
loadSatellites().catch(error => showAlert(error.message || String(error)));
|
loadSatellites().catch(error => showAlert(error.message || String(error)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,18 +23,48 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.angular-motion-map {
|
.angular-motion-map {
|
||||||
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 420px;
|
||||||
min-height: 320px;
|
min-height: 320px;
|
||||||
border: 1px solid #dbe4f0;
|
border: 1px solid #dbe4f0;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
background: #f8fbff;
|
background: #f8fbff;
|
||||||
cursor: crosshair;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.angular-motion-map.angular-motion-pick-active {
|
.angular-motion-map.angular-motion-pick-active {
|
||||||
outline: 3px solid rgba(13, 110, 253, 0.25);
|
outline: 3px solid rgba(13, 110, 253, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.angular-motion-map .cesium-viewer,
|
||||||
|
.angular-motion-map .cesium-widget,
|
||||||
|
.angular-motion-map .cesium-widget canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.angular-motion-map.angular-motion-pick-active,
|
||||||
|
.angular-motion-map.angular-motion-pick-active canvas {
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
|
||||||
|
.angular-motion-cursor-coords-panel {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
z-index: 1000;
|
||||||
|
max-width: calc(100% - 20px);
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(0, 0, 0, 0.75);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.3;
|
||||||
|
pointer-events: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.angular-motion-result-card {
|
.angular-motion-result-card {
|
||||||
min-height: 360px;
|
min-height: 360px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
layout:decorate="~{layouts/main-layout}">
|
layout:decorate="~{layouts/main-layout}">
|
||||||
<head>
|
<head>
|
||||||
<title>ПУУД</title>
|
<title>ПУУД</title>
|
||||||
|
<link href="/webjars/cesium/1.107.2/Build/Cesium/Widgets/widgets.css" rel="stylesheet"/>
|
||||||
<link rel="stylesheet" href="/css/angular-motion.css"/>
|
<link rel="stylesheet" href="/css/angular-motion.css"/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -123,7 +124,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<canvas id="angular-motion-map" width="980" height="360" class="angular-motion-map" aria-label="Мини-карта трассы и полос обзора"></canvas>
|
<div id="angular-motion-map" class="angular-motion-map" aria-label="Мини-карта трассы и полос обзора"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -166,6 +167,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<script src="/webjars/cesium/1.107.2/Build/Cesium/Cesium.js"></script>
|
||||||
<script src="/angular_motion_scripts.js"></script>
|
<script src="/angular_motion_scripts.js"></script>
|
||||||
</th:block>
|
</th:block>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user