Прототип страницы расчета ПУУД
This commit is contained in:
+33
@@ -20,6 +20,14 @@ class DynamicPlanPageController {
|
||||
fun dynamicPlanPage(): String = "dynamic-plan"
|
||||
}
|
||||
|
||||
|
||||
@Controller
|
||||
class AngularMotionPageController {
|
||||
|
||||
@GetMapping("/angular-motion")
|
||||
fun angularMotionPage(): String = "angular-motion"
|
||||
}
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/dynamic-plan")
|
||||
class DynamicPlanRestController(
|
||||
@@ -65,3 +73,28 @@ class DynamicPlanRestController(
|
||||
@RequestParam(defaultValue = "0") offset: Int
|
||||
) = dynamicPlanService.getIntervals(runId, limit, offset)
|
||||
}
|
||||
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/angular-motion")
|
||||
class AngularMotionRestController(
|
||||
private val dynamicPlanService: DynamicPlanService
|
||||
) {
|
||||
@GetMapping("/satellites")
|
||||
@ResponseBody
|
||||
fun satellites(@RequestParam(required = false) time: String?) =
|
||||
dynamicPlanService.getAngularMotionSatellites(time)
|
||||
|
||||
@GetMapping("/satellites/{satelliteId}/flight-line")
|
||||
@ResponseBody
|
||||
fun flightLine(
|
||||
@PathVariable satelliteId: Long,
|
||||
@RequestParam time: String
|
||||
) = dynamicPlanService.getAngularMotionFlightLine(satelliteId, time)
|
||||
|
||||
@PostMapping("/calculate")
|
||||
@ResponseBody
|
||||
fun calculate(@RequestBody request: Map<String, Any?>) =
|
||||
dynamicPlanService.calculateAngularMotion(request)
|
||||
}
|
||||
|
||||
|
||||
+51
@@ -20,6 +20,9 @@ class DynamicPlanService(webClientBuilderProvider: ObjectProvider<WebClient.Buil
|
||||
@Value("\${settings.dynamic-plan-service:pcp-dynamic-plan-service}")
|
||||
private lateinit var dynamicPlanUrl: String
|
||||
|
||||
@Value("\${settings.ballistics-service:pcp-ballistics-service}")
|
||||
private lateinit var ballisticsServiceUrl: String
|
||||
|
||||
fun calculate(request: DynamicPlanCalculationRequestDTO): JsonNode =
|
||||
webClientBuilder.build()
|
||||
.post()
|
||||
@@ -106,6 +109,54 @@ class DynamicPlanService(webClientBuilderProvider: ObjectProvider<WebClient.Buil
|
||||
.bodyToMono(JsonNode::class.java)
|
||||
.block() ?: ObjectMapper().createObjectNode()
|
||||
|
||||
|
||||
fun getAngularMotionSatellites(time: String?): JsonNode {
|
||||
val timeParam = time
|
||||
?.takeIf { value -> value.isNotBlank() }
|
||||
?.let { value -> "?time=${URLEncoder.encode(value, StandardCharsets.UTF_8)}" }
|
||||
?: ""
|
||||
|
||||
return webClientBuilder.build()
|
||||
.get()
|
||||
.uri("$ballisticsServiceUrl/api/angular-motion/satellites$timeParam")
|
||||
.retrieve()
|
||||
.onStatus({ status -> status.isError }) { response ->
|
||||
response.bodyToMono(String::class.java)
|
||||
.defaultIfEmpty("error")
|
||||
.flatMap { body -> Mono.error(CustomErrorException(extractErrorMessage(body))) }
|
||||
}
|
||||
.bodyToMono(JsonNode::class.java)
|
||||
.block() ?: ObjectMapper().createArrayNode()
|
||||
}
|
||||
|
||||
fun getAngularMotionFlightLine(satelliteId: Long, time: String): JsonNode =
|
||||
webClientBuilder.build()
|
||||
.get()
|
||||
.uri("$ballisticsServiceUrl/api/angular-motion/satellites/$satelliteId/flight-line?time=${URLEncoder.encode(time, StandardCharsets.UTF_8)}")
|
||||
.retrieve()
|
||||
.onStatus({ status -> status.isError }) { response ->
|
||||
response.bodyToMono(String::class.java)
|
||||
.defaultIfEmpty("error")
|
||||
.flatMap { body -> Mono.error(CustomErrorException(extractErrorMessage(body))) }
|
||||
}
|
||||
.bodyToMono(JsonNode::class.java)
|
||||
.block() ?: ObjectMapper().createArrayNode()
|
||||
|
||||
fun calculateAngularMotion(request: Any): JsonNode =
|
||||
webClientBuilder.build()
|
||||
.post()
|
||||
.uri("$ballisticsServiceUrl/api/angular-motion/calculate")
|
||||
.header("X-Requested-By", "pcp-ui-service")
|
||||
.bodyValue(request)
|
||||
.retrieve()
|
||||
.onStatus({ status -> status.isError }) { response ->
|
||||
response.bodyToMono(String::class.java)
|
||||
.defaultIfEmpty("error")
|
||||
.flatMap { body -> Mono.error(CustomErrorException(extractErrorMessage(body))) }
|
||||
}
|
||||
.bodyToMono(JsonNode::class.java)
|
||||
.block() ?: ObjectMapper().createObjectNode()
|
||||
|
||||
private fun extractErrorMessage(body: String): String {
|
||||
val text = body.trim()
|
||||
if (text.isEmpty()) return "error"
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
(function () {
|
||||
const state = {
|
||||
satellites: [],
|
||||
selectedSatellite: null,
|
||||
flightLine: [],
|
||||
picking: false,
|
||||
result: null,
|
||||
};
|
||||
|
||||
function el(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
function text(value, fallback = '') {
|
||||
if (value === null || value === undefined || value === '') return fallback;
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return text(value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function showAlert(message, type = 'danger') {
|
||||
el('angular-motion-alert').innerHTML = message
|
||||
? `<div class="alert alert-${type} mb-3" role="alert">${escapeHtml(message)}</div>`
|
||||
: '';
|
||||
}
|
||||
|
||||
async function requestJson(url, options = {}) {
|
||||
const response = await fetch(url, options);
|
||||
const isJson = response.headers.get('content-type')?.includes('application/json');
|
||||
const body = isJson ? await response.json() : await response.text();
|
||||
if (!response.ok) {
|
||||
if (body && typeof body === 'object') {
|
||||
throw new Error(body.error || body.message || Object.values(body).join('; '));
|
||||
}
|
||||
throw new Error(body || `HTTP ${response.status}`);
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
function localDateTimeValue() {
|
||||
const value = el('angular-motion-time').value;
|
||||
return value && value.length === 16 ? `${value}:00` : value;
|
||||
}
|
||||
|
||||
function formatDateTime(value) {
|
||||
return text(value, '-').replace('T', ' ');
|
||||
}
|
||||
|
||||
function formatNumber(value, digits = 6) {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? number.toFixed(digits) : '-';
|
||||
}
|
||||
|
||||
function setLoading(isLoading) {
|
||||
el('angular-motion-submit').disabled = isLoading;
|
||||
el('angular-motion-submit').textContent = isLoading ? 'Расчет...' : 'Рассчитать';
|
||||
}
|
||||
|
||||
async function loadSatellites() {
|
||||
const time = localDateTimeValue();
|
||||
const query = time ? `?time=${encodeURIComponent(time)}` : '';
|
||||
state.satellites = await requestJson(`/api/angular-motion/satellites${query}`);
|
||||
renderSatellites();
|
||||
await loadFlightLine();
|
||||
}
|
||||
|
||||
function renderSatellites() {
|
||||
const select = el('angular-motion-satellite');
|
||||
const previous = select.value;
|
||||
const satellites = Array.isArray(state.satellites) ? state.satellites : [];
|
||||
if (satellites.length === 0) {
|
||||
select.innerHTML = '<option value="">Нет доступных КА</option>';
|
||||
state.selectedSatellite = null;
|
||||
el('angular-motion-satellite-meta').textContent = 'Для заданного времени нет КА с ПДЦМ.';
|
||||
return;
|
||||
}
|
||||
select.innerHTML = satellites.map(satellite => {
|
||||
const label = `${satellite.code || satellite.satelliteId} — ${satellite.name || ''}`.trim();
|
||||
return `<option value="${escapeHtml(satellite.satelliteId)}">${escapeHtml(label)}</option>`;
|
||||
}).join('');
|
||||
if (satellites.some(item => String(item.satelliteId) === previous)) {
|
||||
select.value = previous;
|
||||
}
|
||||
state.selectedSatellite = satellites.find(item => String(item.satelliteId) === select.value) || satellites[0];
|
||||
renderSatelliteMeta();
|
||||
}
|
||||
|
||||
function renderSatelliteMeta() {
|
||||
const sat = state.selectedSatellite;
|
||||
el('angular-motion-satellite-meta').textContent = sat
|
||||
? `Доступность ПДЦМ: ${formatDateTime(sat.availabilityStart)} — ${formatDateTime(sat.availabilityStop)}`
|
||||
: '';
|
||||
}
|
||||
|
||||
async function loadFlightLine() {
|
||||
const satelliteId = el('angular-motion-satellite').value;
|
||||
const time = localDateTimeValue();
|
||||
if (!satelliteId || !time) {
|
||||
state.flightLine = [];
|
||||
drawMap();
|
||||
return;
|
||||
}
|
||||
state.flightLine = await requestJson(`/api/angular-motion/satellites/${satelliteId}/flight-line?time=${encodeURIComponent(time)}`);
|
||||
el('angular-motion-map-meta').textContent = state.flightLine.length > 0
|
||||
? `Трасса и полосы обзора на интервале ${formatDateTime(time)} ± 5 минут.`
|
||||
: 'Нет данных трассы полета для выбранного КА и времени.';
|
||||
drawMap();
|
||||
}
|
||||
|
||||
function lonToX(lon, width) {
|
||||
return (Number(lon) + 180) / 360 * width;
|
||||
}
|
||||
|
||||
function latToY(lat, height) {
|
||||
return (90 - Number(lat)) / 180 * height;
|
||||
}
|
||||
|
||||
function canvasToLonLat(event) {
|
||||
const canvas = el('angular-motion-map');
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const scaleX = canvas.width / rect.width;
|
||||
const scaleY = canvas.height / rect.height;
|
||||
const x = (event.clientX - rect.left) * scaleX;
|
||||
const y = (event.clientY - rect.top) * scaleY;
|
||||
return {
|
||||
lon: x / canvas.width * 360 - 180,
|
||||
lat: 90 - y / canvas.height * 180,
|
||||
};
|
||||
}
|
||||
|
||||
function drawGrid(ctx, width, height) {
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
ctx.fillStyle = '#f8fbff';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
ctx.strokeStyle = '#e2e8f0';
|
||||
ctx.lineWidth = 1;
|
||||
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) {
|
||||
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) {
|
||||
const valid = points.filter(point => Number.isFinite(Number(point[latKey])) && Number.isFinite(Number(point[lonKey])));
|
||||
if (valid.length < 2) return;
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = width;
|
||||
ctx.beginPath();
|
||||
valid.forEach((point, index) => {
|
||||
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);
|
||||
});
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawTarget(ctx) {
|
||||
const lat = Number(el('angular-motion-lat').value);
|
||||
const lon = Number(el('angular-motion-lon').value);
|
||||
if (!Number.isFinite(lat) || !Number.isFinite(lon)) return;
|
||||
const x = lonToX(lon, ctx.canvas.width);
|
||||
const y = latToY(lat, ctx.canvas.height);
|
||||
ctx.strokeStyle = '#dc2626';
|
||||
ctx.fillStyle = '#dc2626';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, 5, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x - 9, y);
|
||||
ctx.lineTo(x + 9, y);
|
||||
ctx.moveTo(x, y - 9);
|
||||
ctx.lineTo(x, y + 9);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawMap() {
|
||||
const canvas = el('angular-motion-map');
|
||||
const ctx = canvas.getContext('2d');
|
||||
drawGrid(ctx, canvas.width, canvas.height);
|
||||
const points = Array.isArray(state.flightLine) ? state.flightLine : [];
|
||||
drawLine(ctx, points, 'leftLatitudeDeg', 'leftLongitudeDeg', '#93c5fd', 1.5);
|
||||
drawLine(ctx, points, 'rightLatitudeDeg', 'rightLongitudeDeg', '#93c5fd', 1.5);
|
||||
drawLine(ctx, points, 'latitudeDeg', 'longitudeDeg', '#0f172a', 2.5);
|
||||
drawTarget(ctx);
|
||||
}
|
||||
|
||||
function setPicking(value) {
|
||||
state.picking = value;
|
||||
el('angular-motion-map').classList.toggle('angular-motion-pick-active', value);
|
||||
el('angular-motion-pick-state').textContent = value
|
||||
? 'Кликните по карте, чтобы перенести координаты.'
|
||||
: 'Режим выбора координат выключен.';
|
||||
}
|
||||
|
||||
function modeChanged() {
|
||||
const mode = el('angular-motion-mode').value;
|
||||
const sdiVisible = mode === 'AZIMUTH' || mode === 'SMOOTH_SDI';
|
||||
document.querySelectorAll('.angular-motion-sdi-fields').forEach(item => item.classList.toggle('d-none', !sdiVisible));
|
||||
document.querySelectorAll('.angular-motion-azimuth-field').forEach(item => item.classList.toggle('d-none', mode === 'CONST_ORIENT'));
|
||||
}
|
||||
|
||||
function payloadFromForm() {
|
||||
const mode = el('angular-motion-mode').value;
|
||||
const payload = {
|
||||
satelliteId: Number(el('angular-motion-satellite').value),
|
||||
approximateTime: localDateTimeValue(),
|
||||
mode,
|
||||
latitudeDeg: Number(el('angular-motion-lat').value),
|
||||
longitudeDeg: Number(el('angular-motion-lon').value),
|
||||
heightM: Number(el('angular-motion-height').value || 0),
|
||||
durationSec: Number(el('angular-motion-duration').value),
|
||||
leadAngleDeg: Number(el('angular-motion-lead-angle').value || 0),
|
||||
azimuthDeg: Number(el('angular-motion-azimuth').value || 0),
|
||||
focusMm: Number(el('angular-motion-focus').value || 5500),
|
||||
stepPuudSec: Number(el('angular-motion-step').value || 0.125),
|
||||
stepSdiSec: Number(el('angular-motion-step-sdi').value || 20),
|
||||
};
|
||||
if (mode === 'AZIMUTH' || mode === 'SMOOTH_SDI') {
|
||||
payload.sdi = Number(el('angular-motion-sdi').value);
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
function renderResult(result) {
|
||||
state.result = result;
|
||||
const rows = Array.isArray(result?.points) ? result.points : [];
|
||||
el('angular-motion-result-meta').textContent = rows.length > 0
|
||||
? `Старт режима: ${formatDateTime(result.startTime)}. Точек: ${rows.length}.`
|
||||
: 'Расчет не вернул точек.';
|
||||
el('angular-motion-export-csv').disabled = rows.length === 0;
|
||||
const body = el('angular-motion-result-body');
|
||||
if (rows.length === 0) {
|
||||
body.innerHTML = '<tr><td colspan="14" class="text-center text-muted py-4">Нет данных</td></tr>';
|
||||
return;
|
||||
}
|
||||
body.innerHTML = rows.map(point => `
|
||||
<tr>
|
||||
<td>${escapeHtml(formatDateTime(point.time))}</td>
|
||||
<td>${escapeHtml(point.revolution)}</td>
|
||||
<td>${escapeHtml(formatNumber(point.tangDeg, 3))}</td>
|
||||
<td>${escapeHtml(formatNumber(point.krenDeg, 3))}</td>
|
||||
<td>${escapeHtml(formatNumber(point.riskDeg, 3))}</td>
|
||||
<td>${escapeHtml(formatNumber(point.groundLatitudeDeg, 5))}</td>
|
||||
<td>${escapeHtml(formatNumber(point.groundLongitudeDeg, 5))}</td>
|
||||
<td>${escapeHtml(formatNumber(point.omegaX, 7))}</td>
|
||||
<td>${escapeHtml(formatNumber(point.omegaY, 7))}</td>
|
||||
<td>${escapeHtml(formatNumber(point.omegaZ, 7))}</td>
|
||||
<td>${escapeHtml(formatNumber(point.wdX, 7))}</td>
|
||||
<td>${escapeHtml(formatNumber(point.wdY, 7))}</td>
|
||||
<td>${escapeHtml(formatNumber(point.wdZ, 7))}</td>
|
||||
<td>${escapeHtml(formatNumber(point.sdi, 4))}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function calculate(event) {
|
||||
event.preventDefault();
|
||||
showAlert('');
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await requestJson('/api/angular-motion/calculate', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payloadFromForm()),
|
||||
});
|
||||
renderResult(result);
|
||||
showAlert('Расчет ПУУД выполнен.', 'success');
|
||||
} catch (error) {
|
||||
showAlert(error.message || String(error));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function exportCsv() {
|
||||
const rows = Array.isArray(state.result?.points) ? state.result.points : [];
|
||||
if (rows.length === 0) return;
|
||||
const header = ['time', 'revolution', 'tangDeg', 'krenDeg', 'riskDeg', 'groundLatitudeDeg', 'groundLongitudeDeg', 'omegaX', 'omegaY', 'omegaZ', 'wdX', 'wdY', 'wdZ', 'sdi'];
|
||||
const lines = [header.join(';')];
|
||||
rows.forEach(row => {
|
||||
lines.push(header.map(key => row[key] ?? '').join(';'));
|
||||
});
|
||||
const blob = new Blob([lines.join('\n')], { type: 'text/csv;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'angular-motion.csv';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function init() {
|
||||
el('angular-motion-time').value = nowLocalInput();
|
||||
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-time').addEventListener('change', () => loadSatellites().catch(error => showAlert(error.message || String(error))));
|
||||
el('angular-motion-satellite').addEventListener('change', () => {
|
||||
state.selectedSatellite = state.satellites.find(item => String(item.satelliteId) === el('angular-motion-satellite').value) || null;
|
||||
renderSatelliteMeta();
|
||||
loadFlightLine().catch(error => showAlert(error.message || String(error)));
|
||||
});
|
||||
el('angular-motion-mode').addEventListener('change', modeChanged);
|
||||
el('angular-motion-pick-from-map').addEventListener('click', () => setPicking(!state.picking));
|
||||
el('angular-motion-map').addEventListener('click', event => {
|
||||
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);
|
||||
modeChanged();
|
||||
drawMap();
|
||||
loadSatellites().catch(error => showAlert(error.message || String(error)));
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
})();
|
||||
@@ -0,0 +1,70 @@
|
||||
.angular-motion-page {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.angular-motion-card {
|
||||
border: 1px solid #dbe4f0;
|
||||
box-shadow: 0 0.25rem 0.75rem rgba(15, 23, 42, 0.05);
|
||||
}
|
||||
|
||||
.angular-motion-section-title {
|
||||
font-weight: 600;
|
||||
color: #1f2a44;
|
||||
}
|
||||
|
||||
.angular-motion-helper {
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.angular-motion-form-body {
|
||||
max-height: calc(100vh - 11rem);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.angular-motion-map {
|
||||
width: 100%;
|
||||
min-height: 320px;
|
||||
border: 1px solid #dbe4f0;
|
||||
border-radius: 0.5rem;
|
||||
background: #f8fbff;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.angular-motion-map.angular-motion-pick-active {
|
||||
outline: 3px solid rgba(13, 110, 253, 0.25);
|
||||
}
|
||||
|
||||
.angular-motion-result-card {
|
||||
min-height: 360px;
|
||||
}
|
||||
|
||||
.angular-motion-table-wrap {
|
||||
max-height: calc(100vh - 35rem);
|
||||
min-height: 320px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.angular-motion-table thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: #f8fafc;
|
||||
z-index: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.angular-motion-table td,
|
||||
.angular-motion-table th {
|
||||
white-space: nowrap;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
@media (max-width: 1199.98px) {
|
||||
.angular-motion-form-body {
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.angular-motion-table-wrap {
|
||||
max-height: 460px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.w3.org/1999/xhtml"
|
||||
layout:decorate="~{layouts/main-layout}">
|
||||
<head>
|
||||
<title>ПУУД</title>
|
||||
<link rel="stylesheet" href="/css/angular-motion.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<th:block layout:fragment="content">
|
||||
<div class="angular-motion-page py-3">
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">Расчет ПУУД</h1>
|
||||
<div class="angular-motion-helper">Программа управления угловым движением для выбранного КА и варианта сканирования.</div>
|
||||
</div>
|
||||
<button id="angular-motion-refresh-satellites" type="button" class="btn btn-outline-secondary btn-sm">Обновить КА</button>
|
||||
</div>
|
||||
|
||||
<div id="angular-motion-alert"></div>
|
||||
|
||||
<div class="row g-3 angular-motion-layout">
|
||||
<div class="col-12 col-xl-4">
|
||||
<form id="angular-motion-form" class="card angular-motion-card h-100">
|
||||
<div class="card-header bg-white">
|
||||
<div class="angular-motion-section-title">Исходные данные</div>
|
||||
</div>
|
||||
<div class="card-body angular-motion-form-body">
|
||||
<div class="mb-3">
|
||||
<label for="angular-motion-time" class="form-label">Примерное время</label>
|
||||
<input id="angular-motion-time" type="datetime-local" class="form-control" required>
|
||||
<div class="form-text">При изменении времени список КА фильтруется по доступным ПДЦМ.</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="angular-motion-satellite" class="form-label">Спутник</label>
|
||||
<select id="angular-motion-satellite" class="form-select" required></select>
|
||||
<div id="angular-motion-satellite-meta" class="form-text"></div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="angular-motion-mode" class="form-label">Вариант сканирования</label>
|
||||
<select id="angular-motion-mode" class="form-select">
|
||||
<option value="CONST_ORIENT">Постоянная ориентация ЦЛВ в ОСК</option>
|
||||
<option value="AZIMUTH">Заданный азимут</option>
|
||||
<option value="SMOOTH_SDI">Плавная СДИ</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="angular-motion-lat" class="form-label">Широта, град</label>
|
||||
<input id="angular-motion-lat" type="number" step="0.000001" min="-90" max="90" class="form-control" required>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="angular-motion-lon" class="form-label">Долгота, град</label>
|
||||
<input id="angular-motion-lon" type="number" step="0.000001" min="-180" max="180" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2 align-items-center mb-3">
|
||||
<button id="angular-motion-pick-from-map" type="button" class="btn btn-outline-primary btn-sm">Выбрать с карты</button>
|
||||
<span id="angular-motion-pick-state" class="angular-motion-helper">Режим выбора координат выключен.</span>
|
||||
</div>
|
||||
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="angular-motion-height" class="form-label">Высота, м</label>
|
||||
<input id="angular-motion-height" type="number" step="1" class="form-control" value="0">
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="angular-motion-duration" class="form-label">Длительность, с</label>
|
||||
<input id="angular-motion-duration" type="number" min="0.125" step="0.125" class="form-control" value="60" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="angular-motion-lead-angle" class="form-label">Упреждающий угол, град</label>
|
||||
<input id="angular-motion-lead-angle" type="number" step="0.001" class="form-control" value="0">
|
||||
</div>
|
||||
<div class="col-12 col-md-6 angular-motion-azimuth-field">
|
||||
<label for="angular-motion-azimuth" class="form-label">Азимут, град</label>
|
||||
<input id="angular-motion-azimuth" type="number" step="0.001" class="form-control" value="0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-2 mb-3 angular-motion-sdi-fields">
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="angular-motion-sdi" class="form-label">СДИ, мм/с</label>
|
||||
<input id="angular-motion-sdi" type="number" step="0.001" class="form-control" value="100">
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="angular-motion-focus" class="form-label">Фокус, мм</label>
|
||||
<input id="angular-motion-focus" type="number" min="1" step="1" class="form-control" value="5500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-2 mb-4">
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="angular-motion-step" class="form-label">Шаг ПУУД, с</label>
|
||||
<input id="angular-motion-step" type="number" min="0.001" step="0.001" class="form-control" value="0.125">
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="angular-motion-step-sdi" class="form-label">Шаг СДИ, с</label>
|
||||
<input id="angular-motion-step-sdi" type="number" min="0.001" step="0.001" class="form-control" value="20">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="angular-motion-submit" type="submit" class="btn btn-primary w-100">Рассчитать</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-xl-8">
|
||||
<div class="card angular-motion-card mb-3">
|
||||
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div class="angular-motion-section-title">Мини-карта</div>
|
||||
<div id="angular-motion-map-meta" class="angular-motion-helper mt-1">Выберите время и спутник.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="angular-motion-map" width="980" height="360" class="angular-motion-map" aria-label="Мини-карта трассы и полос обзора"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card angular-motion-card angular-motion-result-card">
|
||||
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div class="angular-motion-section-title">Результат расчета</div>
|
||||
<div id="angular-motion-result-meta" class="angular-motion-helper mt-1">Расчет еще не запускался.</div>
|
||||
</div>
|
||||
<button id="angular-motion-export-csv" type="button" class="btn btn-outline-secondary btn-sm" disabled>CSV</button>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive angular-motion-table-wrap">
|
||||
<table class="table table-sm table-hover mb-0 angular-motion-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Время</th>
|
||||
<th>Виток</th>
|
||||
<th>Тангаж</th>
|
||||
<th>Крен</th>
|
||||
<th>Рысканье</th>
|
||||
<th>Широта</th>
|
||||
<th>Долгота</th>
|
||||
<th>ωx</th>
|
||||
<th>ωy</th>
|
||||
<th>ωz</th>
|
||||
<th>wd.x</th>
|
||||
<th>wd.y</th>
|
||||
<th>wd.z</th>
|
||||
<th>СДИ</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="angular-motion-result-body">
|
||||
<tr><td colspan="14" class="text-center text-muted py-4">Нет данных</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/angular_motion_scripts.js"></script>
|
||||
</th:block>
|
||||
</body>
|
||||
</html>
|
||||
@@ -70,6 +70,9 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/dynamic-plan">Dynamic Plan</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/angular-motion">ПУУД</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/current-plans">Текущие планы</a>
|
||||
</li>
|
||||
|
||||
Reference in New Issue
Block a user