Формат времени в UTC

This commit is contained in:
emelianov
2026-06-25 16:21:17 +03:00
parent d0ec0ef754
commit fe54c095ab
164 changed files with 1847 additions and 634 deletions
@@ -0,0 +1,55 @@
package space.nstart.pcp.slots_service.configuration
import org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.convert.converter.Converter
import org.springframework.format.FormatterRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import tools.jackson.core.JsonGenerator
import tools.jackson.core.JsonParser
import tools.jackson.databind.DeserializationContext
import tools.jackson.databind.SerializationContext
import tools.jackson.databind.deser.std.StdDeserializer
import tools.jackson.databind.module.SimpleModule
import tools.jackson.databind.ser.std.StdSerializer
import java.time.Clock
import java.time.LocalDateTime
@Configuration
class UtcTimeConfiguration : WebMvcConfigurer {
@Bean
fun clock(): Clock = Clock.systemUTC()
@Bean
fun pcpUiUtcLocalDateTimeCustomizer(): JsonMapperBuilderCustomizer = JsonMapperBuilderCustomizer { builder ->
builder.addModule(
SimpleModule("pcp-ui-service-utc-local-date-time")
.addSerializer(LocalDateTime::class.java, UtcLocalDateTimeSerializer())
.addDeserializer(LocalDateTime::class.java, UtcLocalDateTimeDeserializer())
)
}
override fun addFormatters(registry: FormatterRegistry) {
registry.addConverter(UtcLocalDateTimeQueryParamConverter())
}
}
private class UtcLocalDateTimeQueryParamConverter : Converter<String, LocalDateTime> {
override fun convert(source: String): LocalDateTime = UtcDateTimes.parse(source)
}
private class UtcLocalDateTimeSerializer : StdSerializer<LocalDateTime>(LocalDateTime::class.java) {
override fun serialize(value: LocalDateTime, gen: JsonGenerator, ctxt: SerializationContext) {
gen.writeString(UtcDateTimes.format(value))
}
}
private class UtcLocalDateTimeDeserializer : StdDeserializer<LocalDateTime>(LocalDateTime::class.java) {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): LocalDateTime {
val value = p.valueAsString ?: throw IllegalArgumentException("Expected ISO-8601 UTC date-time string")
return UtcDateTimes.parse(value)
}
}
@@ -1,6 +1,5 @@
package space.nstart.pcp.slots_service.controller
import org.springframework.format.annotation.DateTimeFormat
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
@@ -46,8 +45,8 @@ class BookingsController(
@GetMapping
fun bookedSlots(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) timeStart: LocalDateTime?,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) timeStop: LocalDateTime?,
@RequestParam(required = false) timeStart: LocalDateTime?,
@RequestParam(required = false) timeStop: LocalDateTime?,
@RequestParam(required = false) requestId: String?,
@RequestParam(required = false) satelliteIds: List<Long>?,
): List<BookedSlotViewDTO> {
@@ -27,6 +27,7 @@ import space.nstart.pcp.slots_service.service.BallisticsService
import space.nstart.pcp.slots_service.service.SatelliteCatalogService
import space.nstart.pcp.slots_service.service.SatellitePdcmService
import space.nstart.pcp.slots_service.service.SlotService
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException
@@ -103,14 +104,16 @@ class CatalogRestController(
private fun parseSendInitialConditionsTime(time: String?): LocalDateTime {
val normalizedTime = time?.trim()?.takeIf { it.isNotEmpty() }
?: return LocalDateTime.now()
?: return UtcDateTimes.now()
UtcDateTimes.parseOrNull(normalizedTime)?.let { return it }
return try {
LocalDateTime.parse(normalizedTime, sendInitialConditionsTimeFormatter)
} catch (exception: DateTimeParseException) {
throw ResponseStatusException(
HttpStatus.BAD_REQUEST,
"Параметр time должен быть в формате dd.mm.yyyy hh:MM:ss.zzz",
"Параметр time должен быть ISO-8601 UTC/offset или legacy dd.mm.yyyy hh:MM:ss.zzz",
exception
)
}
@@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.RestController
import space.nstart.pcp.pcp_types_lib.dto.ballistics.AngularMotionCalculationRequestDTO
import space.nstart.pcp.slots_service.dto.dynamicplan.DynamicPlanCalculationRequestDTO
import space.nstart.pcp.slots_service.service.DynamicPlanService
import java.time.LocalDateTime
import java.util.UUID
@Controller
@@ -83,14 +84,14 @@ class AngularMotionRestController(
) {
@GetMapping("/satellites")
@ResponseBody
fun satellites(@RequestParam(required = false) time: String?) =
fun satellites(@RequestParam(required = false) time: LocalDateTime?) =
dynamicPlanService.getAngularMotionSatellites(time)
@GetMapping("/satellites/{satelliteId}/flight-line")
@ResponseBody
fun flightLine(
@PathVariable satelliteId: Long,
@RequestParam time: String
@RequestParam time: LocalDateTime
) = dynamicPlanService.getAngularMotionFlightLine(satelliteId, time)
@PostMapping("/calculate")
@@ -41,7 +41,7 @@ class MapController {
fun showMap(model: Model): Mono<String> {
model.addAttribute("requests", earthService.reqs().collectList().block())
model.addAttribute("satellites", complexMissionService.satellites().collectList().block())
model.addAttribute("stations", stationService.stations().collectList().block())
model.addAttribute("stations", stationService.allStationsOrEmpty())
return Mono.just("map")
}
@@ -15,9 +15,9 @@ import space.nstart.pcp.slots_service.dto.tlecomparison.TlePositionDTO
import space.nstart.pcp.slots_service.dto.tlecomparison.TlePositionDeltaDTO
import space.nstart.pcp.slots_service.dto.tlecomparison.TleTextRequestDTO
import space.nstart.pcp.slots_service.service.BallisticsService
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.Duration
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.Locale
import kotlin.math.abs
import kotlin.math.sqrt
@@ -144,14 +144,13 @@ class TleComparisonController(
)
private fun timeRow(code: String, name: String, first: LocalDateTime?, second: LocalDateTime?): TleCompareRowDTO {
val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME
val delta = if (first != null && second != null) {
val seconds = Duration.between(first, second).seconds
formatSecondsDelta(seconds)
} else {
null
}
return TleCompareRowDTO(code, name, first?.format(formatter), second?.format(formatter), delta)
return TleCompareRowDTO(code, name, first?.let(UtcDateTimes::format), second?.let(UtcDateTimes::format), delta)
}
private fun numberRow(code: String, name: String, first: Double?, second: Double?): TleCompareRowDTO =
@@ -6,6 +6,7 @@ import io.swagger.v3.oas.annotations.media.Schema
import lombok.AllArgsConstructor
import lombok.Data
import lombok.NoArgsConstructor
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime
@@ -23,13 +24,8 @@ class CZMLClock(
var step : String = "SYSTEM_CLOCK_MULTIPLIER"
) {
constructor(tn : LocalDateTime, tk : LocalDateTime, cur : LocalDateTime) : this(
interval ="${
tn.toString()
}/${
tk
.toString()
}",
currentTime = cur.toString()
interval = "${UtcDateTimes.format(tn)}/${UtcDateTimes.format(tk)}",
currentTime = UtcDateTimes.format(cur)
)
}
@@ -7,6 +7,7 @@ import lombok.AllArgsConstructor
import lombok.Data
import lombok.NoArgsConstructor
import space.nstart.pcp.pcp_types_lib.dto.ballistics.AscNodeDTO
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime
import java.time.ZoneOffset
@@ -28,6 +29,10 @@ class CZMLPath(
private fun fromDateTime(t: LocalDateTime) = t.toEpochSecond(ZoneOffset.UTC) + t.nano / 1e9
private fun utcIso(t: Double): String = UtcDateTimes.format(
LocalDateTime.ofEpochSecond(t.toLong(), ((t % 1) * 1e9).toInt(), ZoneOffset.UTC)
)
constructor(
vuz: Iterable<AscNodeDTO>, epoch: LocalDateTime, epochStop: LocalDateTime, width: Int, resolution: Int,
material: CZMLMaterial
@@ -44,26 +49,16 @@ class CZMLPath(
if (dt > 1) {
lt.add(
CZMLInterval(
"${
LocalDateTime.ofEpochSecond((t).toLong(), (t % 1 * 1e9).toInt(), ZoneOffset.UTC)
}/${
LocalDateTime.ofEpochSecond(fromDateTime(v.time).toLong(), (fromDateTime(v.time) % 1 * 1e9).toInt(), ZoneOffset.UTC)
.toString()
}",
LocalDateTime.ofEpochSecond((t).toLong(), (t % 1 * 1e9).toInt(), ZoneOffset.UTC).toString(),
"${utcIso(t)}/${utcIso(fromDateTime(v.time))}",
utcIso(t),
listOf(0.0, dt, dt, 0.0)
)
)
tt.add(
CZMLInterval(
"${
LocalDateTime.ofEpochSecond(t.toLong(), ((t) % 1 * 1e9).toInt(), ZoneOffset.UTC)
}/${
LocalDateTime.ofEpochSecond((fromDateTime(v.time)).toLong(), (fromDateTime(v.time) % 1 * 1e9).toInt(), ZoneOffset.UTC)
.toString()
}",
LocalDateTime.ofEpochSecond((t).toLong(), (t % 1 * 1e9).toInt(), ZoneOffset.UTC).toString(),
"${utcIso(t)}/${utcIso(fromDateTime(v.time))}",
utcIso(t),
listOf(0.0, 0.0, dt, dt)
)
)
@@ -76,26 +71,16 @@ class CZMLPath(
if (dt > 1) {
lt.add(
CZMLInterval(
"${
LocalDateTime.ofEpochSecond((t).toLong(), (t % 1 * 1e9).toInt(), ZoneOffset.UTC)
}/${
LocalDateTime.ofEpochSecond((tk).toLong(), (tk % 1 * 1e9).toInt(), ZoneOffset.UTC)
.toString()
}",
LocalDateTime.ofEpochSecond((t).toLong(), (t % 1 * 1e9).toInt(), ZoneOffset.UTC).toString(),
"${utcIso(t)}/${utcIso(tk)}",
utcIso(t),
listOf(0.0, dt, dt, 0.0)
)
)
tt.add(
CZMLInterval(
"${
LocalDateTime.ofEpochSecond((t).toLong(), (t % 1 * 1e9).toInt(), ZoneOffset.UTC)
}/${
LocalDateTime.ofEpochSecond((tk).toLong(), (tk % 1 * 1e9).toInt(), ZoneOffset.UTC)
.toString()
}",
LocalDateTime.ofEpochSecond((t).toLong(), (t % 1 * 1e9).toInt(), ZoneOffset.UTC).toString(),
"${utcIso(t)}/${utcIso(tk)}",
utcIso(t),
listOf(0.0, 0.0, dt, dt)
)
)
@@ -16,6 +16,7 @@ import space.nstart.pcp.pcp_types_lib.dto.ballistics.TlePointRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TleRevolutionsRequestDTO
import space.nstart.pcp.slots_service.dto.tlecomparison.TleParsedDTO
import space.nstart.pcp.slots_service.configuration.CustomErrorException
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.net.URI
import java.time.LocalDateTime
@@ -133,8 +134,8 @@ class BallisticsService (webClientBuilderProvider: ObjectProvider<WebClient.Buil
timeStop: LocalDateTime?
): URI {
val builder = UriComponentsBuilder.fromUriString("$url$path")
timeStart?.let { builder.queryParam("time_start", it) }
timeStop?.let { builder.queryParam("time_stop", it) }
timeStart?.let { builder.queryParam("time_start", UtcDateTimes.format(it)) }
timeStop?.let { builder.queryParam("time_stop", UtcDateTimes.format(it)) }
return builder.build().toUri()
}
}
@@ -15,6 +15,7 @@ import space.nstart.pcp.pcp_types_lib.utils.wkt.WKTParser
import space.nstart.pcp.slots_service.configuration.CustomErrorException
import space.nstart.pcp.slots_service.czml.*
import java.time.Duration
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime
import java.time.ZoneOffset
import kotlin.math.PI
@@ -426,7 +427,7 @@ private val docReqCovName ="req_cov_"
timeStop: LocalDateTime? = null
): Iterable<CZMLEntity> {
val tstart = timeStart ?: LocalDateTime.now()
val tstart = timeStart ?: UtcDateTimes.now()
val tstop = timeStop ?: tstart.plusDays(1)
var vuz = ballisticsService.ascNodes(id, tstart, tstop).collectList().block()
vuz = vuz?.filter { it.time >= tstart && it.time <= tstop }?.sortedBy { it.time }
@@ -489,7 +490,7 @@ private val docReqCovName ="req_cov_"
cartographicDegrees = flr,
interpolationAlgorithm = "LAGRANGE",
interpolationDegree = 1.0,
epoch = tstart.toString()
epoch = UtcDateTimes.format(tstart)
),
path = path.apply { show = showSw }
@@ -500,7 +501,7 @@ private val docReqCovName ="req_cov_"
cartographicDegrees = fll,
interpolationAlgorithm = "LAGRANGE",
interpolationDegree = 1.0,
epoch = tstart.toString()
epoch = UtcDateTimes.format(tstart)
),
path = path.apply { show = showSw }
),
@@ -521,7 +522,7 @@ private val docReqCovName ="req_cov_"
cartographicDegrees = sw,
interpolationAlgorithm = "LAGRANGE",
interpolationDegree = 1.0,
epoch = tstart.toString()
epoch = UtcDateTimes.format(tstart)
),
path = path.apply { show = showFl }
),
@@ -550,7 +551,7 @@ private val docReqCovName ="req_cov_"
cartesian = orb,
interpolationAlgorithm = "LAGRANGE",
interpolationDegree = 1.0,
epoch = tstart.toString()
epoch = UtcDateTimes.format(tstart)
),
path = path.apply { show = showOrbit }
)
@@ -19,6 +19,7 @@ import space.nstart.pcp.slots_service.czml.CZMLMaterial
import space.nstart.pcp.slots_service.czml.CZMLPolygon
import space.nstart.pcp.slots_service.czml.CZMLPosition
import tools.jackson.databind.ObjectMapper
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime
@Service
@@ -185,8 +186,8 @@ class CoverageSchemeModeRowDTO(
val entityId: String = "",
val sequence: Int = 0,
val satelliteId: Long = 0,
val tn: LocalDateTime = LocalDateTime.now(),
val tk: LocalDateTime = LocalDateTime.now(),
val tn: LocalDateTime = UtcDateTimes.now(),
val tk: LocalDateTime = UtcDateTimes.now(),
val roll: Double? = null,
val revolutionSign: RevolutionSign? = null,
val contour: String = "",
@@ -8,11 +8,13 @@ import reactor.core.publisher.Mono
import space.nstart.pcp.pcp_types_lib.dto.ballistics.AngularMotionCalculationRequestDTO
import space.nstart.pcp.slots_service.configuration.CustomErrorException
import space.nstart.pcp.slots_service.dto.dynamicplan.DynamicPlanCalculationRequestDTO
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import tools.jackson.databind.JsonNode
import tools.jackson.databind.ObjectMapper
import java.net.URI
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import java.time.LocalDateTime
import java.util.UUID
@Service
@@ -112,10 +114,9 @@ class DynamicPlanService(webClientBuilderProvider: ObjectProvider<WebClient.Buil
.block() ?: ObjectMapper().createObjectNode()
fun getAngularMotionSatellites(time: String?): JsonNode {
fun getAngularMotionSatellites(time: LocalDateTime?): JsonNode {
val timeParam = time
?.takeIf { value -> value.isNotBlank() }
?.let { value -> "?time=${URLEncoder.encode(value, StandardCharsets.UTF_8)}" }
?.let { value -> "?time=${URLEncoder.encode(UtcDateTimes.format(value), StandardCharsets.UTF_8)}" }
?: ""
return webClientBuilder.build()
@@ -131,10 +132,10 @@ class DynamicPlanService(webClientBuilderProvider: ObjectProvider<WebClient.Buil
.block() ?: ObjectMapper().createArrayNode()
}
fun getAngularMotionFlightLine(satelliteId: Long, time: String): JsonNode =
fun getAngularMotionFlightLine(satelliteId: Long, time: LocalDateTime): JsonNode =
webClientBuilder.build()
.get()
.uri(URI.create("$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(UtcDateTimes.format(time), StandardCharsets.UTF_8)}"))
.retrieve()
.onStatus({ status -> status.isError }) { response ->
response.bodyToMono(String::class.java)
@@ -11,6 +11,7 @@ import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.MissionResponseDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.MissionSurveyCalculationResponseDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.mission.ModeResponseDTO
import space.nstart.pcp.slots_service.dto.currentplans.CurrentPlansMissionPageDTO
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.util.UUID
import java.time.LocalDateTime
@@ -25,7 +26,7 @@ class MissionService(webClientBuilderProvider: ObjectProvider<WebClient.Builder>
fun plan(sat : Long, tn : LocalDateTime, tk : LocalDateTime) =
webClientBuilder.build()
.get()
.uri("$url/api/missions/modes/satellite?satellite_id=$sat&timeStart=$tn&timeStop=$tk")
.uri("$url/api/missions/modes/satellite?satellite_id=$sat&timeStart=${UtcDateTimes.format(tn)}&timeStop=${UtcDateTimes.format(tk)}")
.retrieve()
.bodyToFlux<ModeResponseDTO>()
@@ -6,6 +6,7 @@ import space.nstart.pcp.slots_service.configuration.CustomErrorException
import space.nstart.pcp.slots_service.dto.pdcm.SatellitePdcmAvailabilityDTO
import space.nstart.pcp.slots_service.dto.pdcm.SatellitePdcmDetailsDTO
import space.nstart.pcp.slots_service.dto.pdcm.SatellitePdcmSummaryDTO
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime
@Service
@@ -14,7 +15,7 @@ class SatellitePdcmService(
private val ballisticsService: BallisticsService
) {
fun satelliteSummaries(): List<SatellitePdcmSummaryDTO> = satelliteSummaries(LocalDateTime.now())
fun satelliteSummaries(): List<SatellitePdcmSummaryDTO> = satelliteSummaries(UtcDateTimes.now())
fun satelliteSummaries(referenceTime: LocalDateTime): List<SatellitePdcmSummaryDTO> {
val satellites = satelliteCatalogService.allSatellites()
@@ -39,7 +40,7 @@ class SatellitePdcmService(
}
fun satelliteDetails(satelliteId: Long): SatellitePdcmDetailsDTO =
satelliteDetails(satelliteId, LocalDateTime.now())
satelliteDetails(satelliteId, UtcDateTimes.now())
fun satelliteDetails(satelliteId: Long, referenceTime: LocalDateTime): SatellitePdcmDetailsDTO {
val satellites = satelliteCatalogService.allSatellites()
@@ -23,6 +23,7 @@ import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteSlotProfileDTO
import tools.jackson.databind.ObjectMapper
import space.nstart.pcp.slots_service.configuration.CustomErrorException
import space.nstart.pcp.pcp_types_lib.dto.BookedSlotDetailsDTO
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.LocalDateTime
@Service
@@ -47,8 +48,8 @@ class SlotService(webClientBuilderProvider: ObjectProvider<WebClient.Builder>) {
UriComponentsBuilder
.fromUriString("$url/api/slots/request-cover")
.queryParam("requestId", id)
.queryParam("timeStart", tn)
.queryParam("timeStop", tk)
.queryParam("timeStart", UtcDateTimes.format(tn))
.queryParam("timeStop", UtcDateTimes.format(tk))
.queryParamIfPresent("revSign", java.util.Optional.ofNullable(sign))
.queryParam("cov", cov)
.queryParamIfPresent("sun", java.util.Optional.ofNullable(sun))
@@ -70,7 +71,7 @@ class SlotService(webClientBuilderProvider: ObjectProvider<WebClient.Builder>) {
fun plan(id : Long, tn : LocalDateTime, tk : LocalDateTime) =
webClientBuilder.build()
.get()
.uri("$url/api/slots/mission/satellite?satellite_id=$id&timeStart=$tn&timeStop=$tk")
.uri("$url/api/slots/mission/satellite?satellite_id=$id&timeStart=${UtcDateTimes.format(tn)}&timeStop=${UtcDateTimes.format(tk)}")
.retrieve()
.onStatus({ status -> status.isError }) { response ->
response.bodyToMono(String::class.java)
@@ -82,7 +83,7 @@ class SlotService(webClientBuilderProvider: ObjectProvider<WebClient.Builder>) {
fun allSlotsByInterval(id: Long, tn: LocalDateTime, tk: LocalDateTime): List<SlotDTO> =
webClientBuilder.build()
.get()
.uri("$url/api/slots/interval?satelliteId=$id&timeStart=$tn&timeStop=$tk")
.uri("$url/api/slots/interval?satelliteId=$id&timeStart=${UtcDateTimes.format(tn)}&timeStop=${UtcDateTimes.format(tk)}")
.retrieve()
.onStatus({ status -> status.isError }) { response ->
response.bodyToMono(String::class.java)
@@ -193,8 +194,8 @@ class SlotService(webClientBuilderProvider: ObjectProvider<WebClient.Builder>) {
.uri(
UriComponentsBuilder
.fromUriString("$url/api/slots/booking/search")
.queryParamIfPresent("timeStart", java.util.Optional.ofNullable(timeStart))
.queryParamIfPresent("timeStop", java.util.Optional.ofNullable(timeStop))
.queryParamIfPresent("timeStart", java.util.Optional.ofNullable(timeStart?.let { UtcDateTimes.format(it) }))
.queryParamIfPresent("timeStop", java.util.Optional.ofNullable(timeStop?.let { UtcDateTimes.format(it) }))
.queryParamIfPresent("requestId", java.util.Optional.ofNullable(requestId?.trim()?.takeIf { it.isNotEmpty() }))
.apply {
satelliteIds.orEmpty().distinct().forEach { queryParam("satelliteIds", it) }
@@ -331,7 +332,7 @@ class SlotService(webClientBuilderProvider: ObjectProvider<WebClient.Builder>) {
.uri(
UriComponentsBuilder
.fromUriString("$url/api/satellite/{satelliteId}/send-ic")
.queryParam("time", time)
.queryParam("time", UtcDateTimes.format(time))
.buildAndExpand(satelliteId)
.toUriString()
)
@@ -2,6 +2,7 @@ package space.nstart.pcp.slots_service.service
import org.springframework.beans.factory.ObjectProvider
import org.springframework.beans.factory.annotation.Value
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import org.springframework.web.reactive.function.client.WebClient
import reactor.core.publisher.Mono
@@ -11,6 +12,7 @@ import tools.jackson.databind.ObjectMapper
@Service
class StationService(webClientBuilderProvider: ObjectProvider<WebClient.Builder>) {
private val log = LoggerFactory.getLogger(StationService::class.java)
private val webClientBuilder: WebClient.Builder = webClientBuilderProvider.ifAvailable ?: WebClient.builder()
@Value("\${settings.stations-service:stations-service}")
@@ -36,6 +38,13 @@ class StationService(webClientBuilderProvider: ObjectProvider<WebClient.Builder>
fun allStations(): List<StationDTO> =
stations().collectList().block() ?: emptyList()
fun allStationsOrEmpty(): List<StationDTO> =
runCatching { allStations() }
.onFailure { error ->
log.warn("Stations service is unavailable. Map page will be opened without stations.", error)
}
.getOrDefault(emptyList())
fun saveStation(request: StationDTO): StationDTO =
webClientBuilder.build()
.post()
@@ -53,26 +53,24 @@
function nowLocalInput() {
const d = new Date();
d.setSeconds(0, 0);
const offsetMs = d.getTimezoneOffset() * 60000;
return new Date(d.getTime() - offsetMs).toISOString().slice(0, 16);
return PcpUtcTime.toLocalInputValue(d);
}
function localDateTimeValue() {
const value = el('angular-motion-time').value;
return value && value.length === 16 ? `${value}:00` : value;
return PcpUtcTime.toUtcIsoFromInput(el('angular-motion-time').value);
}
function toDateTimeInputValue(value) {
return text(value).slice(0, 16);
return PcpUtcTime.toLocalInputValue(value);
}
function normalizeDateTimeSeconds(value) {
const textValue = text(value);
return textValue && textValue.length === 16 ? `${textValue}:00` : textValue;
function dateTimeMillis(value) {
const date = PcpUtcTime.parseApiDate(value);
return date ? date.getTime() : NaN;
}
function formatDateTime(value) {
return text(value, '-').replace('T', ' ');
return PcpUtcTime.formatDateTime(value);
}
function formatNumber(value, digits = 6) {
@@ -119,11 +117,11 @@
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);
const current = dateTimeMillis(localDateTimeValue());
const start = dateTimeMillis(sat.availabilityStart);
const stop = dateTimeMillis(sat.availabilityStop);
if (!Number.isFinite(current) || current < start || current > stop) {
el('angular-motion-time').value = toDateTimeInputValue(sat.availabilityStart);
}
}
@@ -63,8 +63,8 @@
const timeStop = el('bookings-time-stop')?.value;
const requestId = el('bookings-request')?.value;
if (timeStart) params.set('timeStart', toIsoLocal(timeStart));
if (timeStop) params.set('timeStop', toIsoLocal(timeStop));
if (timeStart) params.set('timeStart', PcpUtcTime.toUtcIsoFromInput(timeStart));
if (timeStop) params.set('timeStop', PcpUtcTime.toUtcIsoFromInput(timeStop));
if (requestId) params.set('requestId', requestId);
state.selectedSatelliteIds.forEach((id) => params.append('satelliteIds', id));
@@ -240,13 +240,7 @@
}
function formatDateTime(value) {
if (!value) return '—';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return escapeHtml(value);
return date.toLocaleString('ru-RU', {
year: '2-digit', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit'
});
return value ? PcpUtcTime.formatDateTime(value) : '—';
}
function formatNumber(value) {
@@ -257,12 +251,7 @@
}
function toDateTimeLocal(date) {
const pad = (value) => String(value).padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
}
function toIsoLocal(value) {
return value && value.length === 16 ? `${value}:00` : value;
return PcpUtcTime.toLocalInputValue(date);
}
function showAlert(message, type = 'info') {
@@ -50,14 +50,10 @@
}
function formatDateTimeInput(value) {
if (!value) return '';
const match = String(value).match(
/^(\d{4})-(\d{2})-(\d{2})[T\s](\d{2}):(\d{2})(?::(\d{2})(?:[.,](\d{1,9}))?)?/
);
if (!match) return value;
const [, year, month, day, hour, minute, second = '00', millis = '000'] = match;
return `${day}.${month}.${year} ${hour}:${minute}:${second}.${millis.slice(0, 3).padEnd(3, '0')}`;
const date = PcpUtcTime.parseApiDate(value);
if (!date) return value || '';
const pad = (number, length = 2) => String(number).padStart(length, '0');
return `${pad(date.getDate())}.${pad(date.getMonth() + 1)}.${date.getFullYear()} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}`;
}
function parseDateTimeInput(value, fieldName, required = false) {
@@ -73,14 +69,14 @@
if (displayMatch) {
const [, day, month, year, hour, minute, second, millis] = displayMatch;
validateDateTimeParts(day, month, year, hour, minute, second, fieldName);
return `${year}-${month}-${day}T${hour}:${minute}:${second}.${millis.padEnd(3, '0')}`;
return PcpUtcTime.toUtcIsoFromRussianDateTime(`${day}.${month}.${year} ${hour}:${minute}:${second}.${millis.padEnd(3, '0')}`);
}
const isoMatch = text.match(/^(\d{4})-(\d{2})-(\d{2})[T\s](\d{2}):(\d{2})(?::(\d{2})(?:[.,](\d{1,3}))?)?$/);
if (isoMatch) {
const [, year, month, day, hour, minute, second = '00', millis = '000'] = isoMatch;
validateDateTimeParts(day, month, year, hour, minute, second, fieldName);
return `${year}-${month}-${day}T${hour}:${minute}:${second}.${millis.padEnd(3, '0')}`;
return PcpUtcTime.toUtcIsoFromRussianDateTime(`${day}.${month}.${year} ${hour}:${minute}:${second}.${millis.padEnd(3, '0')}`);
}
throw new Error(`${fieldName}: укажите время в формате dd.mm.yyyy hh:mm:ss.zzz`);
@@ -19,45 +19,54 @@
}
/** получение строки декретного московского времени
*/
var TDMT = function GetDMTime(e,t)
function localDateFromJulianDate(julianDate)
{
var d = new Cesium.JulianDate();
Cesium.JulianDate.addHours(e,3,d)
var gregorian = new Cesium.JulianDate.toGregorianDate(d);
//var s = new String();
if (Math.abs(t._clockViewModel.multiplier)<1)
s = STRTOFORMAT(gregorian.hour,2) + ':' + STRTOFORMAT(gregorian.minute,2) + ':' + STRTOFORMAT(gregorian.second,2)+'.'+STRTOFORMAT(Math.round(gregorian.millisecond))
else
s = STRTOFORMAT(gregorian.hour,2) + ':' + STRTOFORMAT(gregorian.minute,2) + ':' + STRTOFORMAT(gregorian.second,2)+' ДМВ';
return s;
return Cesium.JulianDate.toDate(julianDate);
}
/** получение строки декретной московской даты
*/
var TDMD = function GetDMDate(e)
function localTimeZoneLabel()
{
var d = new Cesium.JulianDate();
Cesium.JulianDate.addHours(e,3,d)
var gregorian = new Cesium.JulianDate.toGregorianDate(d);
s = STRTOFORMAT(gregorian.day,2)+ ' ' + MNTH[gregorian.month-1] + ' ' + STRTOFORMAT(gregorian.year,4);
return s;
try {
var parts = new Intl.DateTimeFormat('ru-RU', {timeZoneName: 'short'}).formatToParts(new Date());
var part = parts.find(function (item) { return item.type === 'timeZoneName'; });
return part && part.value ? part.value : 'лок.';
} catch (error) {
return 'лок.';
}
}
/** получение строки декретного московского даты и времени
/** получение строки времени в часовом поясе браузера
*/
var TDMDT = function GetDMDdateTime(e)
var TDMT = function GetLocalTime(e,t)
{
var date = localDateFromJulianDate(e);
if (Math.abs(t._clockViewModel.multiplier)<1)
s = STRTOFORMAT(date.getHours(),2) + ':' + STRTOFORMAT(date.getMinutes(),2) + ':' + STRTOFORMAT(date.getSeconds(),2)+'.'+STRTOFORMAT(date.getMilliseconds(),3)
else
s = STRTOFORMAT(date.getHours(),2) + ':' + STRTOFORMAT(date.getMinutes(),2) + ':' + STRTOFORMAT(date.getSeconds(),2)+' '+localTimeZoneLabel();
return s;
}
/** получение строки даты в часовом поясе браузера
*/
var TDMD = function GetLocalDate(e)
{
var date = localDateFromJulianDate(e);
s = STRTOFORMAT(date.getDate(),2)+ ' ' + MNTH[date.getMonth()] + ' ' + STRTOFORMAT(date.getFullYear(),4);
return s;
}
/** получение строки даты и времени в часовом поясе браузера
*/
var TDMDT = function GetLocalDateTime(e)
{
s = "";
var d = new Cesium.JulianDate();
Cesium.JulianDate.addHours(e,3,d);
var gregorian = new Cesium.JulianDate.toGregorianDate(d);
s = gregorian.day+" "+ MNTH[gregorian.month-1]+" "+ gregorian.year+" "+STRTOFORMAT(gregorian.hour,2)+":"+STRTOFORMAT(gregorian.minute,2)+":"+STRTOFORMAT(gregorian.second,2)+" ДМВ";
return s;
var date = localDateFromJulianDate(e);
s = date.getDate()+" "+ MNTH[date.getMonth()]+" "+date.getFullYear()+" "+STRTOFORMAT(date.getHours(),2)+":"+STRTOFORMAT(date.getMinutes(),2)+":"+STRTOFORMAT(date.getSeconds(),2)+" "+localTimeZoneLabel();
return s;
}
@@ -34,12 +34,7 @@
}
function formatDateTime(value) {
if (!value) return '-';
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return value;
}
return date.toLocaleString('ru-RU');
return value ? PcpUtcTime.formatDateTime(value) : '-';
}
function formatDuration(value) {
@@ -70,23 +65,12 @@
function isoDateTime(value) {
if (!value) return '';
const date = new Date(value);
return Number.isNaN(date.getTime()) ? String(value) : date.toISOString();
const date = PcpUtcTime.parseApiDate(value);
return date ? date.toISOString() : String(value);
}
function formatDateTimeLocal(value) {
if (!value) return '';
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return String(value).slice(0, 16);
}
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day}T${hours}:${minutes}`;
return PcpUtcTime.toLocalInputValue(value);
}
async function requestJson(url, options) {
@@ -464,8 +448,8 @@
'Content-Type': 'application/json'
},
body: JSON.stringify({
intervalStart: intervalStart,
intervalEnd: intervalEnd,
intervalStart: PcpUtcTime.toUtcIsoFromInput(intervalStart),
intervalEnd: PcpUtcTime.toUtcIsoFromInput(intervalEnd),
satelliteId: null,
satelliteIds: satelliteIds,
sun: sunValue ? Number(sunValue) : null,
@@ -565,9 +549,7 @@
}
function parseDate(value) {
if (!value) return null;
const date = new Date(value);
return Number.isNaN(date.getTime()) ? null : date;
return PcpUtcTime.parseApiDate(value);
}
function intervalDurationDays(run) {
@@ -1,12 +1,27 @@
.angular-motion-page {
height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
}
.angular-motion-card {
min-height: 0;
border: 1px solid #dbe4f0;
box-shadow: 0 0.25rem 0.75rem rgba(15, 23, 42, 0.05);
}
.angular-motion-layout {
flex: 1 1 auto;
min-height: 0;
}
.angular-motion-layout > [class*="col-"] {
min-height: 0;
display: flex;
flex-direction: column;
}
.angular-motion-section-title {
font-weight: 600;
color: #1f2a44;
@@ -18,8 +33,7 @@
}
.angular-motion-form-body {
max-height: calc(100vh - 11rem);
overflow: auto;
overflow: visible;
}
.angular-motion-map {
@@ -123,8 +137,16 @@
}
@media (max-width: 1199.98px) {
.angular-motion-page {
height: auto;
}
.angular-motion-layout > [class*="col-"] {
display: block;
}
.angular-motion-form-body {
max-height: none;
overflow: visible;
}
.angular-motion-table-wrap {
@@ -475,8 +475,8 @@
const body = {
satelliteId: Number(state.selectedSatelliteId),
station: trimToNull(el('current-plans-create-station')?.value),
missionStart,
missionStop,
missionStart: PcpUtcTime.toUtcIsoFromInput(missionStart),
missionStop: PcpUtcTime.toUtcIsoFromInput(missionStop),
status: trimToNull(el('current-plans-create-status')?.value) || 'NEW'
};
@@ -741,23 +741,7 @@
}
function formatDateTime(value) {
if (!value) return '—';
const match = String(value).match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?/);
if (match) {
const [, year, month, day, hour, minute, second] = match;
return `${day}.${month}.${year.slice(2)}, ${hour}:${minute}:${second || '00'}`;
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) return escapeHtml(value);
return date.toLocaleString('ru-RU', {
year: '2-digit',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
return value ? PcpUtcTime.formatDateTime(value) : '—';
}
function formatNumber(value) {
@@ -782,8 +766,7 @@
}
function toDateTimeLocalValue(date) {
const pad = (number) => String(number).padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
return PcpUtcTime.toLocalInputValue(date);
}
function trimToNull(value) {
@@ -124,7 +124,11 @@
}
function formatValue(value) {
return value === null || value === undefined || value === '' ? '-' : String(value).replace('T', ' ');
return value === null || value === undefined || value === '' ? '-' : String(value);
}
function formatDateValue(value) {
return value === null || value === undefined || value === '' ? '-' : PcpUtcTime.formatDateTime(value);
}
function createRoutesDataSource(run, routes, intervals) {
@@ -178,8 +182,8 @@
`Run ID: ${formatValue(run.runId)}`,
`Request ID: ${formatValue(route.requestId || run.requestId)}`,
`Satellite: ${formatValue(route.satelliteId)}`,
`Start: ${formatValue(route.startTime)}`,
`End: ${formatValue(route.endTime)}`,
`Start: ${formatDateValue(route.startTime)}`,
`End: ${formatDateValue(route.endTime)}`,
`Duration: ${formatValue(route.duration)}`,
`Revolution: ${formatValue(route.revolution)}`,
`Roll: ${formatValue(route.roll)}`
@@ -51,7 +51,7 @@
}
function formatDateTime(value) {
return text(value, '-').replace('T', ' ');
return value ? PcpUtcTime.formatDateTime(value) : '-';
}
function startOfDay(value) {
@@ -51,14 +51,11 @@
}
function formatDateTimeLabel(value) {
return value ? value.replace('T', ' ') : '-';
return value ? PcpUtcTime.formatDateTime(value) : '-';
}
function formatInputDateTime(value) {
if (!value) {
return '';
}
return value.length >= 19 ? value.slice(0, 19) : value;
return PcpUtcTime.toLocalInputValue(value, true);
}
function formatAngle(value) {
@@ -273,7 +270,7 @@
state.selectedGroupId = groupId;
renderGroupList();
const query = requestedTime ? `?time=${encodeURIComponent(requestedTime)}` : '';
const query = requestedTime ? `?time=${encodeURIComponent(PcpUtcTime.toUtcIsoFromInput(requestedTime))}` : '';
const details = await requestJson(`${apiBase}/${groupId}${query}`);
renderDetails(details);
}
@@ -971,13 +971,13 @@ function renderSlotsTable() {
return (aValue - bValue) * direction;
case 'tn':
aValue = new Date(a.tn).getTime();
bValue = new Date(b.tn).getTime();
aValue = PcpUtcTime.parseApiDate(a.tn)?.getTime() ?? 0;
bValue = PcpUtcTime.parseApiDate(b.tn)?.getTime() ?? 0;
return (aValue - bValue) * direction;
case 'tk':
aValue = new Date(a.tk).getTime();
bValue = new Date(b.tk).getTime();
aValue = PcpUtcTime.parseApiDate(a.tk)?.getTime() ?? 0;
bValue = PcpUtcTime.parseApiDate(b.tk)?.getTime() ?? 0;
return (aValue - bValue) * direction;
case 'duration':
@@ -1078,29 +1078,23 @@ function createSlotRowHTML(slot, index) {
// Форматирование даты и времени
function formatDateTime(dateTimeString) {
if (!dateTimeString) return '-';
try {
const date = new Date(dateTimeString);
return date.toLocaleString('ru-RU', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
} catch (e) {
console.error('Ошибка форматирования даты:', e);
return dateTimeString;
}
return PcpUtcTime.formatDateTime(dateTimeString, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
// Расчет длительности в минутах
function calculateDuration(tn, tk) {
if (!tn || !tk) return 0;
const startDate = new Date(tn);
const endDate = new Date(tk);
const startDate = PcpUtcTime.parseApiDate(tn);
const endDate = PcpUtcTime.parseApiDate(tk);
if (!startDate || !endDate) return 0;
return (endDate - startDate) / (1000 * 60); // в минутах
}
@@ -1206,12 +1200,14 @@ function updateSlotsSummary() {
).size;
// Период покрытия
const startTimes = currentSlotsData.map(slot => new Date(slot.tn));
const endTimes = currentSlotsData.map(slot => new Date(slot.tk));
const earliest = new Date(Math.min(...startTimes));
const latest = new Date(Math.max(...endTimes));
const startTimes = currentSlotsData.map(slot => PcpUtcTime.parseApiDate(slot.tn)).filter(Boolean);
const endTimes = currentSlotsData.map(slot => PcpUtcTime.parseApiDate(slot.tk)).filter(Boolean);
const earliest = startTimes.length ? new Date(Math.min(...startTimes)) : null;
const latest = endTimes.length ? new Date(Math.max(...endTimes)) : null;
const period = `${earliest.toLocaleDateString('ru-RU')} - ${latest.toLocaleDateString('ru-RU')}`;
const period = earliest && latest
? `${earliest.toLocaleDateString('ru-RU')} - ${latest.toLocaleDateString('ru-RU')}`
: '-';
// Обновляем элементы
document.getElementById('totalSlotsCount').textContent = totalSlots;
@@ -22,12 +22,7 @@
}
function formatDateTime(value) {
if (!value) return '';
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return value;
}
return date.toLocaleString('ru-RU');
return value ? PcpUtcTime.formatDateTime(value) : '';
}
function formatNumber(value, digits = 3) {
@@ -56,7 +56,7 @@
}
function formatDateTimeLabel(value) {
return value ? value.replace('T', ' ') : '-';
return value ? PcpUtcTime.formatDateTime(value) : '-';
}
function pad(value, length = 2) {
@@ -69,18 +69,11 @@
}
function formatDateTimeInput(value) {
if (!value) {
return '';
const date = PcpUtcTime.parseApiDate(value);
if (!date) {
return value || '';
}
const match = String(value).match(
/^(\d{4})-(\d{2})-(\d{2})[T\s](\d{2}):(\d{2})(?::(\d{2})(?:[.,](\d{1,9}))?)?/
);
if (!match) {
return value;
}
const [, year, month, day, hour, minute, second = '00', millis = '000'] = match;
return `${day}.${month}.${year} ${hour}:${minute}:${second}.${millis.slice(0, 3).padEnd(3, '0')}`;
return `${pad(date.getDate())}.${pad(date.getMonth() + 1)}.${date.getFullYear()} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}`;
}
function parseSendInitialConditionsTime() {
@@ -121,7 +114,8 @@
return {
valid: true,
text: value
text: value,
iso: PcpUtcTime.toUtcIsoFromRussianDateTime(value)
};
}
@@ -154,7 +148,7 @@
throw new Error(`${fieldName}: некорректная дата или время`);
}
return `${year}-${month}-${day}T${hour}:${minute}:${second}.${millisecond}`;
return PcpUtcTime.toUtcIsoFromRussianDateTime(text);
}
function parseRequiredNumber(inputId, fieldName) {
@@ -436,7 +430,7 @@
renderSendInitialConditionsState(state.details, true);
showAlert('');
try {
await requestJson(`${apiBase}/${state.selectedSatelliteId}/send-ic?time=${encodeURIComponent(time.text)}`, {method: 'POST'});
await requestJson(`${apiBase}/${state.selectedSatelliteId}/send-ic?time=${encodeURIComponent(time.iso)}`, {method: 'POST'});
showAlert('Начальные условия спутника переданы в slot-service', 'success');
} catch (error) {
showAlert(error.message || 'Не удалось передать начальные условия в slot-service');
@@ -480,9 +480,7 @@
}
function parsePlanDate(value) {
if (!value) return null;
const date = new Date(value);
return Number.isNaN(date.getTime()) ? null : date;
return PcpUtcTime.parseApiDate(value);
}
function startOfToday() {
@@ -491,8 +489,7 @@
}
function toDatetimeLocalValue(date) {
const pad = (value) => String(value).padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
return PcpUtcTime.toLocalInputValue(date);
}
function timelineTicks(from, to) {
@@ -632,9 +629,7 @@
function formatDateTime(value) {
if (!value) return '—';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return escapeHtml(value);
return date.toLocaleString('ru-RU', {
const formatted = PcpUtcTime.formatDateTime(value, {
year: '2-digit',
month: '2-digit',
day: '2-digit',
@@ -642,6 +637,7 @@
minute: '2-digit',
second: '2-digit'
});
return formatted === '—' ? escapeHtml(value) : formatted;
}
function formatAxisTick(value) {
@@ -235,8 +235,7 @@
}
function formatDateTime(value) {
if (!value) return '—';
return String(value).replace('T', ' ');
return value ? PcpUtcTime.formatDateTime(value) : '—';
}
function formatNumber(value, fractionDigits) {
@@ -64,7 +64,9 @@
return;
}
const minTime = maxEpoch();
if (minTime && time < minTime) {
const calculationDate = parseCalculationInput();
const minDate = PcpUtcTime.parseApiDate(minTime);
if (calculationDate && minDate && calculationDate.getTime() < minDate.getTime()) {
showAlert(`Время расчета должно быть не раньше максимальной эпохи TLE: ${formatDateTime(minTime)}.`, 'warning');
return;
}
@@ -145,8 +147,9 @@
const input = el('tle-comparison-time');
const minTime = maxEpoch();
if (!input || !minTime || !input.value) return;
const time = getCalculationTime();
if (time && time < minTime) {
const time = parseCalculationInput();
const minDate = PcpUtcTime.parseApiDate(minTime);
if (time && minDate && time.getTime() < minDate.getTime()) {
showAlert(`Время расчета должно быть не раньше максимальной эпохи TLE: ${formatDateTime(minTime)}.`, 'warning');
}
}
@@ -186,13 +189,20 @@
}
function maxEpoch() {
if (!state.first?.time || !state.second?.time) return null;
return state.first.time >= state.second.time ? state.first.time : state.second.time;
const first = PcpUtcTime.parseApiDate(state.first?.time);
const second = PcpUtcTime.parseApiDate(state.second?.time);
if (!first || !second) return null;
return first.getTime() >= second.getTime() ? state.first.time : state.second.time;
}
function parseCalculationInput() {
const value = el('tle-comparison-time')?.value;
return value ? new Date(value) : null;
}
function getCalculationTime() {
const value = el('tle-comparison-time')?.value;
return value ? value : null;
return value ? PcpUtcTime.toUtcIsoFromInput(value) : null;
}
function getTleText(side) {
@@ -225,13 +235,11 @@
}
function toDateTimeLocalValue(value) {
if (!value) return '';
return String(value).slice(0, 19);
return PcpUtcTime.toLocalInputValue(value, true);
}
function formatDateTime(value) {
if (!value) return '';
return String(value).replace('T', ' ');
return value ? PcpUtcTime.formatDateTime(value) : '';
}
function formatNumber(value) {
@@ -0,0 +1,72 @@
(function () {
function parseApiDate(value) {
if (!value) return null;
const text = String(value).trim();
if (!text) return null;
const isoLike = /^\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}/.test(text);
const hasZone = /(?:Z|[+-]\d{2}:?\d{2})$/.test(text);
const normalized = isoLike && !hasZone ? `${text.replace(' ', 'T')}Z` : text;
const date = new Date(normalized);
return Number.isNaN(date.getTime()) ? null : date;
}
function pad(value, length = 2) {
return String(value).padStart(length, '0');
}
function toLocalInputValue(value, includeSeconds = false) {
const date = value instanceof Date ? value : parseApiDate(value);
if (!date) return '';
const base = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
return includeSeconds ? `${base}:${pad(date.getSeconds())}` : base;
}
function toUtcIsoFromInput(value) {
if (!value) return '';
const date = new Date(value);
return Number.isNaN(date.getTime()) ? '' : date.toISOString();
}
function toUtcIsoFromRussianDateTime(value) {
const match = String(value || '').trim().match(/^(\d{2})\.(\d{2})\.(\d{4}) (\d{2}):(\d{2}):(\d{2})\.(\d{1,3})$/);
if (!match) return '';
const [, day, month, year, hour, minute, second, millisecond] = match;
const date = new Date(
Number(year),
Number(month) - 1,
Number(day),
Number(hour),
Number(minute),
Number(second),
Number(millisecond.padEnd(3, '0'))
);
const valid = date.getFullYear() === Number(year)
&& date.getMonth() === Number(month) - 1
&& date.getDate() === Number(day)
&& date.getHours() === Number(hour)
&& date.getMinutes() === Number(minute)
&& date.getSeconds() === Number(second);
return valid ? date.toISOString() : '';
}
function formatDateTime(value, options = {}) {
const date = value instanceof Date ? value : parseApiDate(value);
if (!date) return options.fallback || '-';
return date.toLocaleString(options.locale || 'ru-RU', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: options.seconds === false ? undefined : '2-digit'
});
}
window.PcpUtcTime = {
parseApiDate,
toLocalInputValue,
toUtcIsoFromInput,
toUtcIsoFromRussianDateTime,
formatDateTime
};
})();
@@ -1,10 +1,11 @@
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org">
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org">
<head th:fragment="head">
<meta charset="UTF-8">
<link rel="stylesheet"
href="/webjars/bootstrap/5.3.0/css/bootstrap.min.css"/>
<script src="/utc_time.js"></script>
</head>
<body>
</body>
@@ -1268,23 +1268,22 @@
}
// Функция для преобразования datetime-local в ISO
// Преобразует локальное datetime-local браузера в UTC ISO для API
function toISOString(datetimeLocal) {
if (!datetimeLocal) {
return null; // или можно вернуть текущее время по умолчанию
return null;
}
// datetime-local возвращает строку в формате "YYYY-MM-DDTHH:mm"
// Создаем Date объект из этой строки
let date = new Date(datetimeLocal);
date.setHours(date.getHours() + 3);
const date = new Date(datetimeLocal);
if (Number.isNaN(date.getTime())) {
throw new Error('Некорректный формат даты');
}
// Возвращаем в ISO формате
return date.toISOString().split('Z')[0];
return date.toISOString();
}
function covToReq(show, req_id){
// Сбрасываем чекбокс видимости всех слотов