Задание НУ из ui

This commit is contained in:
emelianov
2026-05-29 10:52:36 +03:00
parent 7dfa1163e9
commit d693bf799d
13 changed files with 393 additions and 8 deletions
@@ -0,0 +1,10 @@
package space.nstart.pcp.pcp_types_lib.dto.ballistics
enum class MovementModel {
FOTO,
KONDOR,
METEORM1,
METEORM2,
BARS,
KONDOR_PROGNOZ
}
@@ -2,6 +2,8 @@ package space.nstart.pcp.pcp_types_lib.dto.ballistics
class SatelliteICDTO( class SatelliteICDTO(
val satelliteId: Long = -1, val satelliteId: Long = -1,
val ic : InitialConditionsDTO = InitialConditionsDTO() val ic : InitialConditionsDTO = InitialConditionsDTO(),
movementModel: MovementModel? = MovementModel.FOTO
) { ) {
val movementModel: MovementModel = movementModel ?: MovementModel.FOTO
} }
@@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter import io.swagger.v3.oas.annotations.Parameter
import jakarta.validation.Valid import jakarta.validation.Valid
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.ResponseEntity
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
@@ -11,9 +12,11 @@ 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.pcp_request_service.service.SatelliteIcEventService
import space.nstart.pcp.pcp_request_service.service.SatelliteService import space.nstart.pcp.pcp_request_service.service.SatelliteService
import space.nstart.pcp.pcp_types_lib.configuration.CustomValidationException import space.nstart.pcp.pcp_types_lib.configuration.CustomValidationException
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ExactTimePositionRequestDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.ExactTimePositionRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -23,6 +26,9 @@ class SatelliteController {
@Autowired @Autowired
private lateinit var satelliteService: SatelliteService private lateinit var satelliteService: SatelliteService
@Autowired
private lateinit var satelliteIcEventService: SatelliteIcEventService
@PostMapping("/{satellite_id}/extract-time") @PostMapping("/{satellite_id}/extract-time")
fun extractTime( fun extractTime(
@PathVariable("satellite_id") satelliteId : Long, @PathVariable("satellite_id") satelliteId : Long,
@@ -30,6 +36,12 @@ class SatelliteController {
) = ) =
satelliteService.exactTime(satelliteId, body) satelliteService.exactTime(satelliteId, body)
@PostMapping("/receive-rv")
fun receiveRv(@RequestBody body: SatelliteICDTO): ResponseEntity<Void> {
satelliteIcEventService.handlePlacedIcRv(body)
return ResponseEntity.accepted().build()
}
@Operation( @Operation(
@@ -6,6 +6,7 @@ import org.springframework.kafka.annotation.KafkaListener
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import space.nstart.pcp.pcp_request_service.service.SatelliteIcEventService import space.nstart.pcp.pcp_request_service.service.SatelliteIcEventService
import space.nstart.pcp.pcp_types_lib.dto.ballistics.InitialConditionsDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.InitialConditionsDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.MovementModel
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO
import tools.jackson.databind.ObjectMapper import tools.jackson.databind.ObjectMapper
import tools.jackson.databind.node.ObjectNode import tools.jackson.databind.node.ObjectNode
@@ -52,6 +53,11 @@ class SatelliteIcRvKafkaListener(
*/ */
private fun normalizePayload(payloadNode: tools.jackson.databind.JsonNode): tools.jackson.databind.JsonNode { private fun normalizePayload(payloadNode: tools.jackson.databind.JsonNode): tools.jackson.databind.JsonNode {
val normalizedPayload = payloadNode.deepCopy() val normalizedPayload = payloadNode.deepCopy()
if (normalizedPayload is ObjectNode &&
(normalizedPayload["movementModel"] == null || normalizedPayload["movementModel"].isNull)
) {
normalizedPayload.put("movementModel", DEFAULT_MOVEMENT_MODEL.name)
}
val initialConditionsNode = normalizedPayload["ic"] val initialConditionsNode = normalizedPayload["ic"]
if (initialConditionsNode is ObjectNode) { if (initialConditionsNode is ObjectNode) {
if (initialConditionsNode["sBall"] == null || initialConditionsNode["sBall"].isNull) { if (initialConditionsNode["sBall"] == null || initialConditionsNode["sBall"].isNull) {
@@ -66,5 +72,6 @@ class SatelliteIcRvKafkaListener(
companion object { companion object {
private val DEFAULT_INITIAL_CONDITIONS = InitialConditionsDTO() private val DEFAULT_INITIAL_CONDITIONS = InitialConditionsDTO()
private val DEFAULT_MOVEMENT_MODEL = MovementModel.FOTO
} }
} }
@@ -7,6 +7,7 @@ import ballistics.types.EarthType
import ballistics.types.FleghtLineSector import ballistics.types.FleghtLineSector
import ballistics.types.InitialConditions import ballistics.types.InitialConditions
import ballistics.types.KeplerParams import ballistics.types.KeplerParams
import ballistics.types.ModDVType
import ballistics.types.OPKatObj import ballistics.types.OPKatObj
import ballistics.types.OrbitalPoint import ballistics.types.OrbitalPoint
import ballistics.types.PPI import ballistics.types.PPI
@@ -46,6 +47,7 @@ import space.nstart.pcp.pcp_request_service.utils.ContourClipService
import space.nstart.pcp.pcp_types_lib.dto.ballistics.AscNodeDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.AscNodeDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ExactTimePositionRequestDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.ExactTimePositionRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.InitialConditionsDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.InitialConditionsDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.MovementModel
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ObjDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.ObjDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO 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.PointViewParamDTO
@@ -184,8 +186,9 @@ class SatelliteService {
suspend fun resieveRV(rv: SatelliteICDTO) { suspend fun resieveRV(rv: SatelliteICDTO) {
val bal = Ballistics() val bal = Ballistics()
bal.modDVType = rv.movementModel.toModDVType()
logger.info("Начало расчета баллистики по RV id = ${rv.satelliteId}") logger.info("Начало расчета баллистики по RV id = ${rv.satelliteId}, model = ${rv.movementModel}")
bal.rollMax = (45.0 * PI / 180.0) bal.rollMax = (45.0 * PI / 180.0)
bal.rollMin = (20.0 * PI / 180.0) bal.rollMin = (20.0 * PI / 180.0)
@@ -231,6 +234,16 @@ class SatelliteService {
logger.info("Расчет для КА ${rv.satelliteId} завершился успешно") logger.info("Расчет для КА ${rv.satelliteId} завершился успешно")
} }
private fun MovementModel.toModDVType(): ModDVType =
when (this) {
MovementModel.FOTO -> ModDVType.FOTO
MovementModel.KONDOR -> ModDVType.KONDOR
MovementModel.METEORM1 -> ModDVType.METEORM1
MovementModel.METEORM2 -> ModDVType.METEORM2
MovementModel.BARS -> ModDVType.BARS
MovementModel.KONDOR_PROGNOZ -> ModDVType.KONDOR_PROGNOZ
}
fun mergeSectors(sectors: List<FleghtLineSector>): List<FleghtLineSector> { fun mergeSectors(sectors: List<FleghtLineSector>): List<FleghtLineSector> {
if (sectors.isEmpty()) return emptyList() if (sectors.isEmpty()) return emptyList()
@@ -4,6 +4,7 @@ import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.springframework.beans.factory.ObjectProvider import org.springframework.beans.factory.ObjectProvider
import space.nstart.pcp.pcp_request_service.service.SatelliteIcEventService import space.nstart.pcp.pcp_request_service.service.SatelliteIcEventService
import space.nstart.pcp.pcp_types_lib.dto.ballistics.MovementModel
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO
import tools.jackson.databind.ObjectMapper import tools.jackson.databind.ObjectMapper
@@ -46,6 +47,7 @@ class SatelliteIcRvKafkaListenerTest {
assertEquals(56756L, satelliteIcEventService.lastMessage?.satelliteId) assertEquals(56756L, satelliteIcEventService.lastMessage?.satelliteId)
assertEquals(42L, satelliteIcEventService.lastMessage?.ic?.orbPoint?.revolution) assertEquals(42L, satelliteIcEventService.lastMessage?.ic?.orbPoint?.revolution)
assertEquals(140.0, satelliteIcEventService.lastMessage?.ic?.f81) assertEquals(140.0, satelliteIcEventService.lastMessage?.ic?.f81)
assertEquals(MovementModel.FOTO, satelliteIcEventService.lastMessage?.movementModel)
} }
@Test @Test
@@ -56,6 +58,7 @@ class SatelliteIcRvKafkaListenerTest {
"type": "ICRVPlacedEvent", "type": "ICRVPlacedEvent",
"data": { "data": {
"satelliteId": 56756, "satelliteId": 56756,
"movementModel": null,
"ic": { "ic": {
"orbPoint": { "orbPoint": {
"time": "2026-04-16T12:00:00", "time": "2026-04-16T12:00:00",
@@ -77,6 +80,7 @@ class SatelliteIcRvKafkaListenerTest {
assertEquals(0.0, satelliteIcEventService.lastMessage?.ic?.sBall) assertEquals(0.0, satelliteIcEventService.lastMessage?.ic?.sBall)
assertEquals(147.8, satelliteIcEventService.lastMessage?.ic?.f81) assertEquals(147.8, satelliteIcEventService.lastMessage?.ic?.f81)
assertEquals(MovementModel.FOTO, satelliteIcEventService.lastMessage?.movementModel)
} }
private class RecordingSatelliteIcEventService : SatelliteIcEventService() { private class RecordingSatelliteIcEventService : SatelliteIcEventService() {
@@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import org.springframework.web.server.ResponseStatusException import org.springframework.web.server.ResponseStatusException
import space.nstart.pcp.pcp_types_lib.dto.ballistics.InitialConditionsDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.InitialConditionsDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCalculationSummaryDTO import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCalculationSummaryDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteCreateDTO import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteCreateDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteGroupCreateDTO import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteGroupCreateDTO
@@ -22,6 +23,7 @@ import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteObservationProfileD
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteSlotProfileDTO import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteSlotProfileDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteUpdateDTO import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteUpdateDTO
import space.nstart.pcp.slots_service.service.GroupStateService import space.nstart.pcp.slots_service.service.GroupStateService
import space.nstart.pcp.slots_service.service.BallisticsService
import space.nstart.pcp.slots_service.service.SatelliteCatalogService import space.nstart.pcp.slots_service.service.SatelliteCatalogService
import space.nstart.pcp.slots_service.service.SatellitePdcmService import space.nstart.pcp.slots_service.service.SatellitePdcmService
import space.nstart.pcp.slots_service.service.SlotService import space.nstart.pcp.slots_service.service.SlotService
@@ -36,7 +38,8 @@ class CatalogRestController(
private val satelliteCatalogService: SatelliteCatalogService, private val satelliteCatalogService: SatelliteCatalogService,
private val slotService: SlotService, private val slotService: SlotService,
private val groupStateService: GroupStateService, private val groupStateService: GroupStateService,
private val satellitePdcmService: SatellitePdcmService private val satellitePdcmService: SatellitePdcmService,
private val ballisticsService: BallisticsService
) { ) {
private val sendInitialConditionsTimeFormatter = private val sendInitialConditionsTimeFormatter =
DateTimeFormatter.ofPattern("dd.MM.uuuu HH:mm:ss.SSS").withResolverStyle(ResolverStyle.STRICT) DateTimeFormatter.ofPattern("dd.MM.uuuu HH:mm:ss.SSS").withResolverStyle(ResolverStyle.STRICT)
@@ -73,6 +76,21 @@ class CatalogRestController(
return ResponseEntity.accepted().build() return ResponseEntity.accepted().build()
} }
@PostMapping("/satellites/pdcm/{satelliteId}/initial-conditions")
fun setSatelliteInitialConditions(
@PathVariable satelliteId: Long,
@RequestBody request: SatelliteICDTO
): ResponseEntity<Void> {
ballisticsService.receiveRv(
SatelliteICDTO(
satelliteId = satelliteId,
ic = request.ic,
movementModel = request.movementModel
)
)
return ResponseEntity.accepted().build()
}
private fun parseSendInitialConditionsTime(time: String?): LocalDateTime { private fun parseSendInitialConditionsTime(time: String?): LocalDateTime {
val normalizedTime = time?.trim()?.takeIf { it.isNotEmpty() } val normalizedTime = time?.trim()?.takeIf { it.isNotEmpty() }
?: return LocalDateTime.now() ?: return LocalDateTime.now()
@@ -9,6 +9,7 @@ import space.nstart.pcp.pcp_types_lib.dto.ballistics.ExactTimePositionRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.AscNodeDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.AscNodeDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.FlightLineDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.FlightLineDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteOrbitAvailabilityDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteOrbitAvailabilityDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TlePointRequestDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.TlePointRequestDTO
@@ -71,6 +72,16 @@ class BallisticsService (webClientBuilderProvider: ObjectProvider<WebClient.Buil
?.firstOrNull() ?.firstOrNull()
?: throw CustomErrorException("Не удалось получить положение спутника $id на время $time") ?: throw CustomErrorException("Не удалось получить положение спутника $id на время $time")
fun receiveRv(request: SatelliteICDTO) {
webClientBuilder.build()
.post()
.uri("$url/api/satellites/receive-rv")
.bodyValue(request)
.retrieve()
.toBodilessEntity()
.block()
}
fun parseTle(tle: TLEDTO): TleParsedDTO = fun parseTle(tle: TLEDTO): TleParsedDTO =
webClientBuilder.build() webClientBuilder.build()
.post() .post()
@@ -15,6 +15,12 @@
width: 15rem; width: 15rem;
} }
.satellite-pdcm-ic-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
gap: 1rem;
}
.satellite-pdcm-status { .satellite-pdcm-status {
display: inline-flex; display: inline-flex;
width: 0.75rem; width: 0.75rem;
@@ -110,6 +110,55 @@
}; };
} }
function parseDateTimeForPayload(value, fieldName) {
const text = value?.trim() || '';
const match = text.match(sendInitialConditionsDateTimePattern);
if (!match) {
throw new Error(`${fieldName}: укажите время в формате dd.mm.yyyy hh:MM:ss.zzz`);
}
const [, day, month, year, hour, minute, second, millisecond] = match;
const parsed = new Date(
Number(year),
Number(month) - 1,
Number(day),
Number(hour),
Number(minute),
Number(second),
Number(millisecond)
);
const validDate = parsed.getFullYear() === Number(year)
&& parsed.getMonth() === Number(month) - 1
&& parsed.getDate() === Number(day)
&& parsed.getHours() === Number(hour)
&& parsed.getMinutes() === Number(minute)
&& parsed.getSeconds() === Number(second)
&& parsed.getMilliseconds() === Number(millisecond);
if (!validDate) {
throw new Error(`${fieldName}: некорректная дата или время`);
}
return `${year}-${month}-${day}T${hour}:${minute}:${second}.${millisecond}`;
}
function parseRequiredNumber(inputId, fieldName) {
const value = document.getElementById(inputId)?.value;
const number = Number(value);
if (value === '' || !Number.isFinite(number)) {
throw new Error(`${fieldName}: укажите число`);
}
return number;
}
function parseRequiredInteger(inputId, fieldName) {
const number = parseRequiredNumber(inputId, fieldName);
if (!Number.isInteger(number)) {
throw new Error(`${fieldName}: укажите целое число`);
}
return number;
}
function formatNumber(value) { function formatNumber(value) {
return Number(value ?? 0).toFixed(3); return Number(value ?? 0).toFixed(3);
} }
@@ -142,6 +191,19 @@
} }
} }
function renderSetInitialConditionsState(details, loading = false) {
const openButton = document.getElementById('satellite-pdcm-open-set-ic');
const submitButton = document.getElementById('satellite-pdcm-set-ic-submit');
if (openButton) {
openButton.disabled = !details || loading;
openButton.title = details ? '' : 'Выберите спутник';
}
if (submitButton) {
submitButton.disabled = loading;
submitButton.textContent = loading ? 'Запуск...' : 'Запустить расчет';
}
}
function renderList() { function renderList() {
const tableBody = document.getElementById('satellite-pdcm-list'); const tableBody = document.getElementById('satellite-pdcm-list');
tableBody.innerHTML = ''; tableBody.innerHTML = '';
@@ -182,6 +244,7 @@
summary.innerHTML = '<div class="catalog-helper">Выберите спутник слева.</div>'; summary.innerHTML = '<div class="catalog-helper">Выберите спутник слева.</div>';
renderExportState(null); renderExportState(null);
renderSendInitialConditionsState(null); renderSendInitialConditionsState(null);
renderSetInitialConditionsState(null);
return; return;
} }
@@ -213,6 +276,7 @@
`; `;
renderExportState(details); renderExportState(details);
renderSendInitialConditionsState(details); renderSendInitialConditionsState(details);
renderSetInitialConditionsState(details);
} }
function renderTable(details) { function renderTable(details) {
@@ -365,10 +429,89 @@
} }
} }
function openSetInitialConditionsModal() {
if (!state.selectedSatelliteId || !state.details) {
showAlert('Выберите спутник для задания начальных условий');
return;
}
document.getElementById('satellite-pdcm-set-ic-target').textContent =
`${state.details.name} · ID ${state.details.satelliteId}`;
document.getElementById('satellite-pdcm-ic-time').value = currentDateTimeInputValue();
document.getElementById('satellite-pdcm-ic-revolution').value = '';
document.getElementById('satellite-pdcm-ic-movement-model').value = 'FOTO';
document.getElementById('satellite-pdcm-ic-s-ball').value = '0';
document.getElementById('satellite-pdcm-ic-f81').value = '147.8';
['x', 'y', 'z', 'vx', 'vy', 'vz'].forEach(field => {
document.getElementById(`satellite-pdcm-ic-${field}`).value = '';
});
if (window.bootstrap) {
window.bootstrap.Modal.getOrCreateInstance(document.getElementById('satellite-pdcm-set-ic-modal')).show();
}
}
function buildInitialConditionsPayload() {
const movementModel = document.getElementById('satellite-pdcm-ic-movement-model').value || 'FOTO';
return {
satelliteId: state.selectedSatelliteId,
movementModel,
ic: {
orbPoint: {
time: parseDateTimeForPayload(document.getElementById('satellite-pdcm-ic-time').value, 'Time'),
revolution: parseRequiredInteger('satellite-pdcm-ic-revolution', 'Revolution'),
vx: parseRequiredNumber('satellite-pdcm-ic-vx', 'Vx'),
vy: parseRequiredNumber('satellite-pdcm-ic-vy', 'Vy'),
vz: parseRequiredNumber('satellite-pdcm-ic-vz', 'Vz'),
x: parseRequiredNumber('satellite-pdcm-ic-x', 'X'),
y: parseRequiredNumber('satellite-pdcm-ic-y', 'Y'),
z: parseRequiredNumber('satellite-pdcm-ic-z', 'Z')
},
sBall: parseRequiredNumber('satellite-pdcm-ic-s-ball', 'S-ball'),
f81: parseRequiredNumber('satellite-pdcm-ic-f81', 'F81')
}
};
}
async function submitInitialConditions(event) {
event.preventDefault();
if (!state.selectedSatelliteId || !state.details) {
showAlert('Выберите спутник для задания начальных условий');
return;
}
let payload;
try {
payload = buildInitialConditionsPayload();
} catch (error) {
showAlert(error.message || 'Проверьте параметры начальных условий');
return;
}
renderSetInitialConditionsState(state.details, true);
showAlert('');
try {
await requestJson(`${apiBase}/${state.selectedSatelliteId}/initial-conditions`, {
method: 'POST',
body: JSON.stringify(payload)
});
const modalElement = document.getElementById('satellite-pdcm-set-ic-modal');
if (window.bootstrap && modalElement) {
window.bootstrap.Modal.getOrCreateInstance(modalElement).hide();
}
showAlert('Расчет ПДЦМ по начальным условиям запущен', 'success');
} catch (error) {
showAlert(error.message || 'Не удалось запустить расчет ПДЦМ по начальным условиям');
} finally {
renderSetInitialConditionsState(state.details);
}
}
async function selectSatellite(satelliteId) { async function selectSatellite(satelliteId) {
state.selectedSatelliteId = satelliteId; state.selectedSatelliteId = satelliteId;
renderList(); renderList();
renderSendInitialConditionsState(null); renderSendInitialConditionsState(null);
renderSetInitialConditionsState(null);
showAlert(''); showAlert('');
try { try {
@@ -414,6 +557,8 @@
}); });
document.getElementById('satellite-pdcm-export').addEventListener('click', exportCurrentCsv); document.getElementById('satellite-pdcm-export').addEventListener('click', exportCurrentCsv);
document.getElementById('satellite-pdcm-send-ic').addEventListener('click', sendInitialConditions); document.getElementById('satellite-pdcm-send-ic').addEventListener('click', sendInitialConditions);
document.getElementById('satellite-pdcm-open-set-ic').addEventListener('click', openSetInitialConditionsModal);
document.getElementById('satellite-pdcm-set-ic-form').addEventListener('submit', submitInitialConditions);
const sendInitialConditionsTimeInput = document.getElementById('satellite-pdcm-send-ic-time'); const sendInitialConditionsTimeInput = document.getElementById('satellite-pdcm-send-ic-time');
sendInitialConditionsTimeInput.value = currentDateTimeInputValue(); sendInitialConditionsTimeInput.value = currentDateTimeInputValue();
sendInitialConditionsTimeInput.addEventListener('input', () => renderSendInitialConditionsState(state.details)); sendInitialConditionsTimeInput.addEventListener('input', () => renderSendInitialConditionsState(state.details));
@@ -28,6 +28,7 @@
/> />
</div> </div>
<button id="satellite-pdcm-send-ic" type="button" class="btn btn-outline-primary" disabled>Передать НУ в slot-service</button> <button id="satellite-pdcm-send-ic" type="button" class="btn btn-outline-primary" disabled>Передать НУ в slot-service</button>
<button id="satellite-pdcm-open-set-ic" type="button" class="btn btn-primary" disabled>Задать НУ</button>
<button id="satellite-pdcm-refresh" type="button" class="btn btn-outline-secondary">Обновить</button> <button id="satellite-pdcm-refresh" type="button" class="btn btn-outline-secondary">Обновить</button>
</div> </div>
</div> </div>
@@ -80,6 +81,84 @@
</div> </div>
</div> </div>
</div> </div>
<div class="modal fade" id="satellite-pdcm-set-ic-modal" tabindex="-1" aria-labelledby="satellite-pdcm-set-ic-title" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<form id="satellite-pdcm-set-ic-form" class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="satellite-pdcm-set-ic-title">Задать начальные условия</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body">
<div id="satellite-pdcm-set-ic-target" class="catalog-helper mb-3">Выберите спутник слева.</div>
<div class="satellite-pdcm-ic-grid">
<div>
<label for="satellite-pdcm-ic-time" class="form-label">Time</label>
<input id="satellite-pdcm-ic-time" type="text" class="form-control" placeholder="dd.mm.yyyy hh:mm:ss.zzz" required/>
</div>
<div>
<label for="satellite-pdcm-ic-revolution" class="form-label">Revolution</label>
<input id="satellite-pdcm-ic-revolution" type="number" class="form-control" required/>
</div>
<div>
<label for="satellite-pdcm-ic-movement-model" class="form-label">Модель движения</label>
<select id="satellite-pdcm-ic-movement-model" class="form-select" required>
<option value="FOTO" selected>FOTO</option>
<option value="KONDOR">KONDOR</option>
<option value="METEORM1">METEORM1</option>
<option value="METEORM2">METEORM2</option>
<option value="BARS">BARS</option>
<option value="KONDOR_PROGNOZ">KONDOR_PROGNOZ</option>
</select>
</div>
<div>
<label for="satellite-pdcm-ic-s-ball" class="form-label">S-ball</label>
<input id="satellite-pdcm-ic-s-ball" type="number" step="any" class="form-control" value="0" required/>
</div>
<div>
<label for="satellite-pdcm-ic-f81" class="form-label">F81</label>
<input id="satellite-pdcm-ic-f81" type="number" step="any" class="form-control" value="147.8" required/>
</div>
</div>
<div class="satellite-pdcm-ic-grid mt-3">
<div>
<label for="satellite-pdcm-ic-x" class="form-label">X</label>
<input id="satellite-pdcm-ic-x" type="number" step="any" class="form-control" required/>
</div>
<div>
<label for="satellite-pdcm-ic-y" class="form-label">Y</label>
<input id="satellite-pdcm-ic-y" type="number" step="any" class="form-control" required/>
</div>
<div>
<label for="satellite-pdcm-ic-z" class="form-label">Z</label>
<input id="satellite-pdcm-ic-z" type="number" step="any" class="form-control" required/>
</div>
</div>
<div class="satellite-pdcm-ic-grid mt-3">
<div>
<label for="satellite-pdcm-ic-vx" class="form-label">Vx</label>
<input id="satellite-pdcm-ic-vx" type="number" step="any" class="form-control" required/>
</div>
<div>
<label for="satellite-pdcm-ic-vy" class="form-label">Vy</label>
<input id="satellite-pdcm-ic-vy" type="number" step="any" class="form-control" required/>
</div>
<div>
<label for="satellite-pdcm-ic-vz" class="form-label">Vz</label>
<input id="satellite-pdcm-ic-vz" type="number" step="any" class="form-control" required/>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Отмена</button>
<button id="satellite-pdcm-set-ic-submit" type="submit" class="btn btn-primary">Запустить расчет</button>
</div>
</form>
</div>
</div>
</div> </div>
<script src="/satellite_pdcm_scripts.js"></script> <script src="/satellite_pdcm_scripts.js"></script>
</th:block> </th:block>
@@ -1,6 +1,7 @@
package space.nstart.pcp.slots_service.controller package space.nstart.pcp.slots_service.controller
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.eq import org.mockito.ArgumentMatchers.eq
import org.mockito.Mockito.doReturn import org.mockito.Mockito.doReturn
@@ -15,6 +16,7 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean
import org.springframework.web.reactive.function.client.WebClient import org.springframework.web.reactive.function.client.WebClient
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import space.nstart.pcp.pcp_types_lib.dto.ballistics.InitialConditionsDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.InitialConditionsDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.MovementModel
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.PositionDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.PositionDTO
@@ -34,6 +36,7 @@ import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteGroupCreateDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteGroupDTO import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteGroupDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteSlotProfileDTO import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteSlotProfileDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteVisualizationDTO import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteVisualizationDTO
import space.nstart.pcp.slots_service.service.BallisticsService
import space.nstart.pcp.slots_service.service.GroupStateService import space.nstart.pcp.slots_service.service.GroupStateService
import space.nstart.pcp.slots_service.service.SatelliteCatalogService import space.nstart.pcp.slots_service.service.SatelliteCatalogService
import space.nstart.pcp.slots_service.service.SatellitePdcmService import space.nstart.pcp.slots_service.service.SatellitePdcmService
@@ -73,6 +76,9 @@ class CatalogControllerTest {
@MockitoBean @MockitoBean
private lateinit var satellitePdcmService: SatellitePdcmService private lateinit var satellitePdcmService: SatellitePdcmService
@MockitoBean
private lateinit var ballisticsService: BallisticsService
@Test @Test
fun `groups page is rendered`() { fun `groups page is rendered`() {
val body = WebClient.create("http://localhost:$port") val body = WebClient.create("http://localhost:$port")
@@ -136,6 +142,9 @@ class CatalogControllerTest {
assertTrue(body.contains("Спутники")) assertTrue(body.contains("Спутники"))
assertTrue(body.contains("Скачать CSV")) assertTrue(body.contains("Скачать CSV"))
assertTrue(body.contains("satellite-pdcm-send-ic-time")) assertTrue(body.contains("satellite-pdcm-send-ic-time"))
assertTrue(body.contains("Задать НУ"))
assertTrue(body.contains("satellite-pdcm-set-ic-modal"))
assertTrue(body.contains("value=\"FOTO\" selected"))
assertTrue(body.contains("dd.mm.yyyy hh:MM:ss.zzz")) assertTrue(body.contains("dd.mm.yyyy hh:MM:ss.zzz"))
assertTrue(body.contains("/webjars/bootstrap/5.3.0/css/bootstrap.min.css")) assertTrue(body.contains("/webjars/bootstrap/5.3.0/css/bootstrap.min.css"))
} }
@@ -343,6 +352,43 @@ class CatalogControllerTest {
verify(slotService, never()).sendSatelliteInitialConditions(eq(501L), anyLocalDateTime()) verify(slotService, never()).sendSatelliteInitialConditions(eq(501L), anyLocalDateTime())
} }
@Test
fun `satellite pdcm set initial conditions endpoint proxies request to ballistics`() {
val request = SatelliteICDTO(
satelliteId = 999L,
movementModel = MovementModel.KONDOR,
ic = InitialConditionsDTO(
orbPoint = OrbPointDTO(
time = LocalDateTime.of(2026, 4, 24, 10, 15, 30, 123_000_000),
revolution = 42L,
vx = 1.0,
vy = 2.0,
vz = 3.0,
x = 4.0,
y = 5.0,
z = 6.0
),
sBall = 0.07,
f81 = 145.2
)
)
val status = WebClient.create("http://localhost:$port")
.post()
.uri("/api/catalog/satellites/pdcm/501/initial-conditions")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(request)
.exchangeToMono { response -> Mono.just(response.statusCode()) }
.block()!!
val requestCaptor = ArgumentCaptor.forClass(SatelliteICDTO::class.java)
assertEquals(HttpStatus.ACCEPTED, status)
verify(ballisticsService).receiveRv(captureSatelliteIc(requestCaptor))
assertEquals(501L, requestCaptor.value.satelliteId)
assertEquals(MovementModel.KONDOR, requestCaptor.value.movementModel)
assertEquals(42L, requestCaptor.value.ic.orbPoint.revolution)
}
@Test @Test
fun `satellite create endpoint proxies request`() { fun `satellite create endpoint proxies request`() {
val request = SatelliteCreateDTO( val request = SatelliteCreateDTO(
@@ -578,4 +624,6 @@ class CatalogControllerTest {
private fun anyInitialConditions(): InitialConditionsDTO = any(InitialConditionsDTO::class.java) ?: InitialConditionsDTO() private fun anyInitialConditions(): InitialConditionsDTO = any(InitialConditionsDTO::class.java) ?: InitialConditionsDTO()
private fun anySlotProfile(): SatelliteSlotProfileDTO = any(SatelliteSlotProfileDTO::class.java) ?: SatelliteSlotProfileDTO() private fun anySlotProfile(): SatelliteSlotProfileDTO = any(SatelliteSlotProfileDTO::class.java) ?: SatelliteSlotProfileDTO()
private fun anyLocalDateTime(): LocalDateTime = any(LocalDateTime::class.java) ?: LocalDateTime.MIN private fun anyLocalDateTime(): LocalDateTime = any(LocalDateTime::class.java) ?: LocalDateTime.MIN
private fun captureSatelliteIc(captor: ArgumentCaptor<SatelliteICDTO>): SatelliteICDTO =
captor.capture() ?: SatelliteICDTO()
} }
@@ -7,7 +7,10 @@ import org.junit.jupiter.api.Test
import org.springframework.beans.factory.ObjectProvider import org.springframework.beans.factory.ObjectProvider
import org.springframework.test.util.ReflectionTestUtils import org.springframework.test.util.ReflectionTestUtils
import org.springframework.web.reactive.function.client.WebClient import org.springframework.web.reactive.function.client.WebClient
import space.nstart.pcp.pcp_types_lib.dto.ballistics.InitialConditionsDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.MovementModel
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteICDTO
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.URI import java.net.URI
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -27,7 +30,8 @@ class BallisticsServiceTest {
@Test @Test
fun `sat data methods forward time interval to ballistics service`() { fun `sat data methods forward time interval to ballistics service`() {
val requests = CopyOnWriteArrayList<URI>() val requests = CopyOnWriteArrayList<URI>()
val serverUrl = startServer(requests) val requestBodies = CopyOnWriteArrayList<String>()
val serverUrl = startServer(requests, requestBodies)
val service = createService(serverUrl) val service = createService(serverUrl)
val timeStart = LocalDateTime.of(2026, 4, 21, 10, 0) val timeStart = LocalDateTime.of(2026, 4, 21, 10, 0)
val timeStop = LocalDateTime.of(2026, 4, 21, 12, 0) val timeStop = LocalDateTime.of(2026, 4, 21, 12, 0)
@@ -37,6 +41,24 @@ class BallisticsServiceTest {
service.points(101L, timeStart, timeStop).collectList().block() service.points(101L, timeStart, timeStop).collectList().block()
val availability = service.orbitAvailability() val availability = service.orbitAvailability()
val exactPoint = service.exactTimePoint(101L, timeStart) val exactPoint = service.exactTimePoint(101L, timeStart)
service.receiveRv(
SatelliteICDTO(
satelliteId = 101L,
movementModel = MovementModel.KONDOR,
ic = InitialConditionsDTO(
orbPoint = OrbPointDTO(
time = timeStart,
revolution = 7L,
vx = 1.0,
vy = 2.0,
vz = 3.0,
x = 4.0,
y = 5.0,
z = 6.0
)
)
)
)
assertEquals( assertEquals(
listOf( listOf(
@@ -44,7 +66,8 @@ class BallisticsServiceTest {
"/api/satellites/101/flight-line", "/api/satellites/101/flight-line",
"/api/satellites/101/orbit", "/api/satellites/101/orbit",
"/api/satellites/orbit/availability", "/api/satellites/orbit/availability",
"/api/satellites/101/extract-time" "/api/satellites/101/extract-time",
"/api/satellites/receive-rv"
), ),
requests.map { it.path } requests.map { it.path }
) )
@@ -54,6 +77,7 @@ class BallisticsServiceTest {
"time_start=2026-04-21T10:00&time_stop=2026-04-21T12:00", "time_start=2026-04-21T10:00&time_stop=2026-04-21T12:00",
"time_start=2026-04-21T10:00&time_stop=2026-04-21T12:00", "time_start=2026-04-21T10:00&time_stop=2026-04-21T12:00",
null, null,
null,
null null
), ),
requests.map { it.query } requests.map { it.query }
@@ -62,6 +86,7 @@ class BallisticsServiceTest {
assertEquals(101L, availability.single().satelliteId) assertEquals(101L, availability.single().satelliteId)
assertNotNull(exactPoint) assertNotNull(exactPoint)
assertEquals(timeStart, exactPoint.time) assertEquals(timeStart, exactPoint.time)
assertEquals(true, requestBodies.single().contains(""""movementModel":"KONDOR""""))
} }
private fun createService(serverUrl: String): BallisticsService { private fun createService(serverUrl: String): BallisticsService {
@@ -77,7 +102,7 @@ class BallisticsServiceTest {
return service return service
} }
private fun startServer(requests: MutableList<URI>): String { private fun startServer(requests: MutableList<URI>, requestBodies: MutableList<String>): String {
val startedServer = HttpServer.create(InetSocketAddress(0), 0) val startedServer = HttpServer.create(InetSocketAddress(0), 0)
listOf( listOf(
"/api/satellites/101/asc-node", "/api/satellites/101/asc-node",
@@ -103,6 +128,11 @@ class BallisticsServiceTest {
"""[{"time":"2026-04-21T10:00:00","revolution":1,"vx":1.0,"vy":2.0,"vz":3.0,"x":4.0,"y":5.0,"z":6.0}]""" """[{"time":"2026-04-21T10:00:00","revolution":1,"vx":1.0,"vy":2.0,"vz":3.0,"x":4.0,"y":5.0,"z":6.0}]"""
) )
} }
startedServer.createContext("/api/satellites/receive-rv") { exchange ->
requests.add(exchange.requestURI)
requestBodies.add(exchange.requestBody.bufferedReader().use { it.readText() })
respond(exchange, "")
}
startedServer.start() startedServer.start()
server = startedServer server = startedServer
return "http://localhost:${startedServer.address.port}" return "http://localhost:${startedServer.address.port}"