Files
dc-observatio/services/pcp-ui-service/src/main/resources/templates/map.html
T
Дмитрий Соловьев d48ddd2657 Init
2026-05-25 14:23:52 +03:00

2434 lines
117 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.w3.org/1999/xhtml"
layout:decorate="~{layouts/main-layout}">
<head>
<meta charset='utf-8'>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
<meta name="cesium-sandcastle-labels" content="Showcases">
<title>Карта</title>
<link href="webjars/cesium/1.107.2/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
<link th:href="@{/css/map.css}" rel="stylesheet">
<!-- Иконки Bootstrap -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<!-- Bootstrap CSS для табов -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="webjars/cesium/1.107.2/Build/Cesium/Cesium.js"></script>
<script type="text/javascript" src="requests_scripts.js"></script>
<script type="text/javascript" src="cesium_scripts.js"></script>
<script type="text/javascript" src="dynamic_plan_map_layers.js"></script>
<style>
@import url(webjars/cesium/1.107.2/Build/Cesium/Widgets/widgets.css);
html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
.nav-tabs {
border-bottom: 1px solid #dee2e6;
padding: 0 15px;
margin-bottom: 0;
}
.nav-tabs .nav-link {
border: none;
color: #6c757d;
font-weight: 500;
padding: 0.75rem 0.5rem;
margin: 0 5px;
}
.nav-tabs .nav-link.active {
color: #0d6efd;
background-color: transparent;
border-bottom: 3px solid #0d6efd;
}
.nav-tabs .nav-link:hover:not(.active) {
color: #495057;
}
.tab-content {
flex: 1;
overflow-y: auto;
padding: 15px;
}
.tab-pane {
height: 100%;
}
.table-container {
height: calc(100% - 150px);
overflow-y: auto;
}
#requests-pane .table-container {
height: auto;
}
.selected-counter {
margin-top: 10px;
padding: 5px 0;
border-top: 1px solid #dee2e6;
}
.sidebar-controls {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #dee2e6;
}
#table-satellites {
table-layout: fixed;
width: 100%;
}
#table-satellites th,
#table-satellites td {
vertical-align: middle;
padding-left: 0.35rem;
padding-right: 0.35rem;
}
#table-satellites th:nth-child(1),
#table-satellites td:nth-child(1) {
width: 5.5rem;
white-space: nowrap;
}
#table-satellites th:nth-child(2),
#table-satellites td:nth-child(2) {
width: auto;
max-width: 8rem;
}
#table-satellites td:nth-child(2) {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#table-satellites th:nth-child(3),
#table-satellites td:nth-child(3),
#table-satellites th:nth-child(4),
#table-satellites td:nth-child(4),
#table-satellites th:nth-child(5),
#table-satellites td:nth-child(5),
#table-satellites th:nth-child(6),
#table-satellites td:nth-child(6) {
width: 3.2rem;
text-align: center;
white-space: nowrap;
}
</style>
</head>
<body>
<th:block layout:fragment="content">
<div class="main-container">
<!-- Боковая панель -->
<div class="sidebar">
<div class="sidebar-content">
<div class="time-range-widget mb-3">
<h6><i class="bi bi-clock me-1"></i> Интервал времени</h6>
<div class="row g-2 align-items-end">
<div class="col-12 col-md-5">
<div class="time-input-group">
<label for="startTime" class="form-label">Начальное время</label>
<input type="datetime-local"
class="form-control form-control-sm"
id="startTime">
</div>
</div>
<div class="col-12 col-md-5">
<div class="time-input-group">
<label for="endTime" class="form-label">Конечное время</label>
<input type="datetime-local"
class="form-control form-control-sm"
id="endTime">
</div>
</div>
<div class="col-12 col-md-2 d-grid">
<button type="button"
class="btn btn-outline-primary btn-sm"
id="redrawSatellitePlansBtn"
onclick="redrawSelectedSatellitePlans()">
<i class="bi bi-arrow-repeat"></i>
</button>
</div>
</div>
</div>
<!-- Навигационные табы -->
<ul class="nav nav-tabs" id="layerTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="satellites-tab" data-bs-toggle="tab"
data-bs-target="#satellites-pane" type="button" role="tab"
aria-controls="satellites-pane" aria-selected="true">
<i class="bi bi-satellite me-1"></i> Спутники
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="stations-tab" data-bs-toggle="tab"
data-bs-target="#stations-pane" type="button" role="tab"
aria-controls="stations-pane" aria-selected="false">
<i class="bi bi-building me-1"></i> Станции
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="requests-tab" data-bs-toggle="tab"
data-bs-target="#requests-pane" type="button" role="tab"
aria-controls="requests-pane" aria-selected="false">
<i class="bi bi-list-check me-1"></i> Заявки
</button>
</li>
</ul>
<!-- Содержимое табов -->
<div class="tab-content" id="layerTabsContent">
<!-- Таб Спутников -->
<div class="tab-pane fade show active" id="satellites-pane" role="tabpanel"
aria-labelledby="satellites-tab" tabindex="0">
<!-- Список спутников -->
<div class="table-container" id="requestsTableContainer">
<div class="table-responsive">
<table class="table table-sm table-hover mb-0" id="table-satellites">
<thead class="table-light">
<tr>
<th scope="col">Идентификатор</th>
<th scope="col">Название</th>
<th scope="col">План</th>
<th scope="col">Брони</th>
<th scope="col">Слоты</th>
<th scope="col">Орбита</th>
</tr>
</thead>
<tbody id="satsTableBody">
<tr th:unless="${satellites == null || #lists.isEmpty(satellites)}"
th:each="req, itrStat: ${satellites}"
style="cursor: pointer;"
th:data-name="${req.name}"
th:data-inoradid="${req.noradId}">
<td>[[${req.noradId}]]</td>
<td th:title="${req.name}">[[${req.name}]]</td>
<td>
<input th:name="sat"
th:value="${req.noradId}"
type="checkbox"
th:checked="false"
th:onchange="checkSat(this.checked, this.value);"
class="sat-checkbox sat-plan-checkbox"/>
</td>
<td>
<input th:name="sat-bookings"
th:value="${req.noradId}"
type="checkbox"
th:checked="false"
th:onchange="checkSatBookings(this.checked, this.value);"
class="sat-checkbox sat-bookings-checkbox"/>
</td>
<td>
<input th:name="sat-slots"
th:value="${req.noradId}"
type="checkbox"
th:checked="false"
th:onchange="checkSatSlots(this.checked, this.value);"
class="sat-checkbox sat-slots-checkbox"/>
</td>
<td>
<input th:name="sat-orbit"
th:value="${req.noradId}"
type="checkbox"
th:checked="false"
th:onchange="checkOrbit(this.checked, this.value);"
class="sat-checkbox"/>
</td>
</tr>
<tr th:if="${satellites == null || #lists.isEmpty(satellites)}">
<td colspan="6" class="text-center text-muted py-5 no-requests">
Нет доступных спутников
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="mt-2">
<label for="satRunId" class="form-label form-label-sm mb-1">Run ID</label>
<input type="number"
min="1"
step="1"
class="form-control form-control-sm"
id="satRunId"
placeholder="null">
</div>
<!-- Элементы управления спутниками -->
<div class="stations-controls mb-3">
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" id="toggleOrbit" checked
onchange="handleOrbits(this.checked)">
<label class="form-check-label" for="toggleOrbit">Показать орбиты</label>
</div>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" id="toggleFL" checked
onchange="handleFl(this.checked)">
<label class="form-check-label" for="toggleFL">Показать трассы полета</label>
</div>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" id="toggleSwath" checked
onchange="handleSw(this.checked)">
<label class="form-check-label" for="toggleSwath">Показать полосы обзора</label>
</div>
</div>
</div>
<!-- Таб Станций -->
<div class="tab-pane fade" id="stations-pane" role="tabpanel"
aria-labelledby="stations-tab" tabindex="0">
<!-- Элементы управления станциями -->
<div class="stations-controls mb-3">
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" id="toggleStations" checked>
<label class="form-check-label" for="toggleStations">Показать станции</label>
</div>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" id="toggleCoverage">
<label class="form-check-label" for="toggleCoverage">Показать зоны покрытия</label>
</div>
<select class="form-select form-select-sm mb-2">
<option selected>Все типы станций</option>
<option value="1">Наземные</option>
<option value="2">Мобильные</option>
<option value="3">Контрольные</option>
</select>
</div>
<!-- Список станций -->
<div class="table-container">
<div class="table-responsive">
<table class="table table-sm table-hover mb-0" id="table-stations">
<thead class="table-light">
<tr>
<th scope="col" style="width: 40px;">
<input type="checkbox" id="selectAllStations">
</th>
<th scope="col">Название</th>
<th scope="col">Номер</th>
</tr>
</thead>
<tbody id="stationsTableBody">
<tr th:unless="${stations == null || #lists.isEmpty(stations)}"
th:each="req, itrStat: ${stations}"
style="cursor: pointer;"
th:data-name="${req.name}"
th:data-inoradid="${req.id}">
<td>
<input th:name="sat"
th:value="${req.id}"
type="checkbox"
th:checked="false"
th:onchange="checkStation(this.checked, this.value);"
class="sat-checkbox"/>
</td>
<td>[[${req.name}]]</td>
<td>[[${req.number}]]</td>
</tr>
<tr th:if="${satellites == null || #lists.isEmpty(satellites)}">
<td colspan="4" class="text-center text-muted py-5 no-requests">
Нет доступных станций
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Таб Заявок -->
<div class="tab-pane fade" id="requests-pane" role="tabpanel"
aria-labelledby="requests-tab" tabindex="0">
<!-- Элементы управления заявками -->
<div class="time-range-widget">
<h6><i class="bi bi-clock me-1"></i> Параметры покрытия</h6>
<div class="d-flex align-items-end gap-2 mb-2">
<div class="form-check form-switch mb-0 flex-grow-1">
<input class="form-check-input" type="checkbox" id="toggleCalcCoverage" checked>
<label class="form-check-label" for="toggleCalcCoverage">Расчет схемы покрытия</label>
</div>
<div style="min-width: 120px;">
<label for="coverageSun" class="form-label form-label-sm mb-1">Солнце</label>
<input type="number"
class="form-control form-control-sm"
id="coverageSun"
min="-90"
max="90"
step="0.1"
value="-90">
</div>
</div>
<select class="form-select form-select-sm mb-2" id="selectNode">
<option selected>Не учитывать признак ветви</option>
<option value="1">Восходящая</option>
<option value="2">Нисходящая</option>
</select>
<div class="coverage-satellites-widget mt-2">
<button class="btn btn-outline-secondary btn-sm w-100 d-flex justify-content-between align-items-center"
type="button"
data-bs-toggle="collapse"
data-bs-target="#coverageSatellitesCollapse"
aria-expanded="false"
aria-controls="coverageSatellitesCollapse">
<span>Спутники для покрытия</span>
<i class="bi bi-chevron-down"></i>
</button>
<div class="collapse mt-2" id="coverageSatellitesCollapse">
<div class="d-flex justify-content-between align-items-center mb-1">
<label class="form-label mb-0">Выбор спутников</label>
<div class="btn-group btn-group-sm" role="group" aria-label="Выбор спутников">
<button type="button"
class="btn btn-outline-secondary"
onclick="toggleCoverageSatellites(true)">
Все
</button>
<button type="button"
class="btn btn-outline-secondary"
onclick="toggleCoverageSatellites(false)">
Сброс
</button>
</div>
</div>
<div class="coverage-satellites-list border rounded p-2">
<div th:unless="${satellites == null || #lists.isEmpty(satellites)}"
th:each="sat : ${satellites}"
class="form-check">
<input class="form-check-input coverage-sat-checkbox"
type="checkbox"
th:id="'coverage-sat-' + ${sat.noradId}"
th:value="${sat.noradId}">
<label class="form-check-label"
th:for="'coverage-sat-' + ${sat.noradId}">
[[${sat.name}]] ([[${sat.noradId}]])
</label>
</div>
<div th:if="${satellites == null || #lists.isEmpty(satellites)}"
class="text-muted small">
Нет доступных спутников
</div>
</div>
</div>
</div>
<div class="row g-2 mt-2">
<div class="col-md-6">
<label for="coverageSchemeRollStep" class="form-label form-label-sm mb-1">Шаг roll, град</label>
<input type="number"
class="form-control form-control-sm"
id="coverageSchemeRollStep"
min="0"
step="0.1"
value="5.0">
</div>
<div class="col-md-6">
<label for="coverageSchemeMinOverlap" class="form-label form-label-sm mb-1">Мин. overlap</label>
<input type="number"
class="form-control form-control-sm"
id="coverageSchemeMinOverlap"
min="0"
step="0.01"
value="0.05">
</div>
<div class="col-md-6">
<label for="coverageSchemeOrientationTolerance" class="form-label form-label-sm mb-1">Допуск ориентации, град</label>
<input type="number"
class="form-control form-control-sm"
id="coverageSchemeOrientationTolerance"
min="0"
step="0.1"
value="15.0">
</div>
<div class="col-md-6 d-flex flex-column justify-content-end">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="coverageSchemeStrictContinuous" checked>
<label class="form-check-label" for="coverageSchemeStrictContinuous">Строгая непрерывность</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="coverageSchemeAllowPartial">
<label class="form-check-label" for="coverageSchemeAllowPartial">Разрешить частичное покрытие</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="coverageSchemeIncludeDebug" checked>
<label class="form-check-label" for="coverageSchemeIncludeDebug">Debug info</label>
</div>
</div>
</div>
</div>
<div class="requests-controls mb-3">
<!-- Кнопка добавления заявки -->
<button class="btn btn-success btn-sm mb-2 w-100"
type="button"
data-bs-toggle="modal"
data-bs-target="#addRequestModal">
<i class="bi bi-plus-circle me-1"></i> Добавить заявку
</button>
<!-- Новое поле фильтрации -->
<div class="input-group input-group-sm mb-2">
<span class="input-group-text">
<i class="bi bi-search"></i>
</span>
<input type="text"
class="form-control"
id="requestFilter"
placeholder="Фильтр по названию заявки...">
<button class="btn btn-outline-secondary"
type="button"
id="clearFilterBtn"
title="Очистить фильтр">
<i class="bi bi-x-lg"></i>
</button>
</div>
</div>
<!-- Список заявок -->
<div class="table-container">
<div class="table-responsive">
<table class="table table-sm table-hover mb-0" id="table-request">
<thead class="table-light">
<tr>
<th scope="col" style="width: 40px;">
<input type="checkbox"
id="selectAllCheckbox"
onchange="toggleSelectAll(this.checked)">
</th>
<th scope="col" class="sortable" data-sort="name" onclick="sortTable('name')">
Название
<span class="sort-icon" id="sort-icon-name">
<i class="bi bi-arrow-down-up"></i>
</span>
</th>
<th scope="col" class="sortable" data-sort="importance"
onclick="sortTable('importance')">
Важность
<span class="sort-icon" id="sort-icon-importance">
<i class="bi bi-arrow-down-up"></i>
</span>
</th>
<th scope="col" class="sortable" data-sort="square"
onclick="sortTable('square')">
Площодь
<span class="sort-icon" id="sort-icon-square">
<i class="bi bi-arrow-down-up"></i>
</span>
</th>
<th scope="col">Найти</th>
<th scope="col">Покрыть</th>
</tr>
</thead>
<tbody id="requestsTableBody">
<tr th:unless="${requests == null || #lists.isEmpty(requests)}"
th:each="req, itrStat: ${requests}"
style="cursor: pointer;"
th:data-name="${req.name}"
th:data-importance="${req.importance}"
th:data-square="${req.square}">
<td>
<input th:name="request"
th:value="${req.requestId}"
type="checkbox"
th:checked="false"
th:onchange="checkReq(this.checked, this.value);"
class="request-checkbox"/>
</td>
<td>[[${req.name}]]</td>
<td>
<span class="badge"
th:classappend="${req.importance} ge 0.8 ? 'bg-danger' :
(${req.importance} ge 0.5 ? 'bg-warning' : 'bg-info')">
[[${#numbers.formatDecimal(req.importance, 1, 4)}]]
</span>
</td>
<td>[[${#numbers.formatDecimal(req.square, 1, 2)}]]</td>
<td>
<button th:name="fly-to-request"
style="width: 30px; height: 20px"
th:value="${req.requestId}"
th:onclick="flyToReq(this.value);"
class="btn btn-sm btn-outline-primary"
title="Показать на карте">
<i class="bi bi-geo-alt"></i>
</button>
</td>
<td>
<div class="d-flex align-items-center gap-2">
<input th:name="cov-request"
th:value="${req.requestId}"
type="checkbox"
th:checked="false"
th:onchange="covToReq(this.checked, this.value);"
class="cov-checkbox"
title="Покрытие слотами"/>
<input th:name="coverage-scheme-request"
th:value="${req.requestId}"
type="checkbox"
th:checked="false"
th:onchange="coverageSchemeToReq(this.checked, this.value);"
class="coverage-scheme-checkbox"
title="Схема покрытия"/>
</div>
</td>
</tr>
<tr th:if="${requests == null || #lists.isEmpty(requests)}">
<td colspan="6" class="text-center text-muted py-5 no-requests">
Нет доступных заявок
</td>
</tr>
</tbody>
</table>
</div>
<!-- Счетчик выбранных элементов -->
<div class="selected-counter" th:if="${requests != null && !#lists.isEmpty(requests)}">
<small class="text-muted">
Выбрано: <span id="selectedCount">0</span> из <span id="totalCount"
th:text="${#lists.size(requests)}">0</span>
</small>
</div>
<!-- Таблица слотов (изначально скрыта) -->
<div class="slots-container" id="slotsContainer" style="display: none; margin-top: 15px; padding-top: 15px; border-top: 1px solid #dee2e6;">
<div class="slots-header d-flex justify-content-between align-items-center mb-2">
<h6 class="mb-0">
<i class="bi bi-calendar-event me-1"></i>
<span id="slotsTitle">Интервалы покрытия заявки</span>
</h6>
<div class="slots-actions">
<button class="btn btn-sm btn-outline-success me-2"
id="bookSlotsBtn"
onclick="bookVisibleSlots(this)"
title="Забронировать отображаемые на карте слоты">
<i class="bi bi-check2-square me-1"></i>
</button>
<button class="btn btn-sm btn-outline-warning me-2"
id="cancelBookingBtn"
onclick="cancelRequestBooking(this)"
title="Отменить бронирование по заявке">
<i class="bi bi-x-octagon me-1"></i>
</button>
<button class="btn btn-sm btn-outline-secondary me-2"
id="exportSlotsBtn"
onclick="exportSlotsToCSV()"
title="Экспортировать в CSV">
<i class="bi bi-download me-1"></i>
</button>
<button class="btn btn-sm btn-outline-danger"
id="clearSlotsBtn"
onclick="clearSlotsTable()"
title="Очистить таблицу">
<i class="bi bi-x-lg"></i>
</button>
</div>
</div>
<div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
<table class="table table-sm table-hover table-bordered mb-0" id="slotsTable">
<thead class="table-light">
<tr>
<th id="slotsPrimaryHeader" scope="col" class="sortable-slot" data-sort="cycle" onclick="sortSlotsTable('cycle')">
№ слота
<span class="sort-icon" id="sort-icon-cycle">
<i class="bi bi-arrow-down-up"></i>
</span>
</th>
<th scope="col" class="sortable-slot" data-sort="satellite" onclick="sortSlotsTable('satellite')">
Спутник
<span class="sort-icon" id="sort-icon-satellite">
<i class="bi bi-arrow-down-up"></i>
</span>
</th>
<th scope="col" class="sortable-slot" data-sort="tn" onclick="sortSlotsTable('tn')">
Начало
<span class="sort-icon" id="sort-icon-tn">
<i class="bi bi-arrow-down-up"></i>
</span>
</th>
<th id="slotsStateHeader" scope="col" class="sortable-slot" data-sort="state" onclick="sortSlotsTable('state')">
Статус
<span class="sort-icon" id="sort-icon-state">
<i class="bi bi-arrow-down-up"></i>
</span>
</th>
<th scope="col" class="sortable-slot" data-sort="duration" onclick="sortSlotsTable('duration')">
Длительность
<span class="sort-icon" id="sort-icon-duration">
<i class="bi bi-arrow-down-up"></i>
</span>
</th>
<th scope="col">Найти</th>
<th scope="col" class="text-center">
<div class="d-flex align-items-center justify-content-center gap-1">
<input type="checkbox" id="toggleAllSlotsVisibility"
class="form-check-input"
checked
onchange="toggleAllSlotsVisibility(this.checked)"
title="Отобразить/скрыть все слоты">
<span>В</span>
</div>
</th>
</tr>
</thead>
<tbody id="slotsTableBody">
<tr>
<td colspan="7" class="text-center text-muted py-4">
Выберите заявку и включите покрытие для отображения интервалов
</td>
</tr>
</tbody>
</table>
</div>
<div class="slots-summary mt-2 p-2 bg-light rounded">
<div class="row">
<div class="col-md-3">
<small><strong>Всего интервалов:</strong> <span id="totalSlotsCount">0</span></small>
</div>
<div class="col-md-3">
<small><strong>Общая длительность:</strong> <span id="totalDuration">0</span> мин</small>
</div>
<div class="col-md-3">
<small><strong>Спутников задействовано:</strong> <span id="satellitesCount">0</span></small>
</div>
<div class="col-md-3">
<small><strong>Период:</strong> <span id="coveragePeriod">-</span></small>
</div>
</div>
</div>
</div>
</div>
<div class="heatmap-toggle">
<div class="d-flex w-100 gap-2">
<button id="heatmapBtn" class="btn btn-outline-primary btn-sm flex-fill"
onclick="toggleHeatmap()">
Тепловая карта
</button>
<button id="complexPlanBtn"
class="btn btn-outline-success btn-sm flex-fill"
onclick="runComplexPlan()">
Ком.План
</button>
</div>
<div class="d-flex justify-content-end mt-2">
<label class="switch">
<input type="checkbox" id="heatmapToggle"
onchange="handleHeatmapToggle(this.checked)">
<span class="slider round"></span>
</label>
</div>
</div>
<!-- Поле ввода минимальной важности -->
<div class="heatmap-settings mt-2" id="heatmapSettings">
<div class="input-group input-group-sm">
<span class="input-group-text">
<i class="bi bi-filter"></i>
</span>
<input type="number"
class="form-control"
id="minImportance"
min="0"
step="0.0001"
value="0"
placeholder="Мин. важность"
title="Минимальная важность для отображения на тепловой карте">
</div>
<div class="form-text mt-1">
<small>Показывать на карте объекты с важностью ≥ указанного значения</small>
</div>
<div class="input-group input-group-sm mt-2">
<span class="input-group-text">Lat</span>
<input type="number"
class="form-control"
id="heatmapCountLat"
min="1"
step="1"
placeholder="countLat"
title="Количество ячеек по широте для объединения">
<span class="input-group-text">Long</span>
<input type="number"
class="form-control"
id="heatmapCountLong"
min="1"
step="1"
placeholder="countLong"
title="Количество ячеек по долготе для объединения">
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Контейнер для карты -->
<div class="cesium-container">
<div id="cesiumContainer"></div>
<!-- Индикатор режима рисования поверх карты -->
<div id="drawingOverlay" style="display: none; position: absolute; top: 10px; left: 50%; transform: translateX(-50%); z-index: 1000; background: rgba(0,0,0,0.7); color: white; padding: 10px 20px; border-radius: 8px; font-size: 14px;">
<i class="bi bi-pencil me-2"></i>
<span id="drawingOverlayText">Кликните на карту для добавления точек. Двойной клик для завершения.</span>
<button type="button" id="cancelDrawingBtn" class="btn btn-sm btn-outline-light ms-3">
<i class="bi bi-x-lg"></i> Отмена
</button>
</div>
</div>
</div>
<!-- Модальное окно для добавления заявки -->
<div class="modal fade" id="addRequestModal" tabindex="-1" aria-labelledby="addRequestModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addRequestModalLabel">
<i class="bi bi-plus-circle me-2"></i>Добавление заявки
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body">
<form id="addRequestForm">
<div class="row">
<div class="col-md-6 mb-3">
<label for="requestName" class="form-label">Название заявки</label>
<input type="text" class="form-control" id="requestName" required
placeholder="Введите название заявки">
</div>
<div class="col-md-6 mb-3">
<label for="requestImportance" class="form-label">Важность</label>
<input type="number" class="form-control" id="requestImportance"
min="0" max="1" step="0.01" value="0.5" required>
<div class="form-text">Значение от 0 до 1</div>
</div>
</div>
<div class="mb-3">
<label for="requestGeoType" class="form-label">Тип геометрии</label>
<select class="form-select" id="requestGeoType" required>
<option value="POLYGON" selected>Полигон</option>
<option value="POINT">Точка</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Контур заявки (WKT)</label>
<div class="d-flex gap-2 mb-2">
<button type="button" class="btn btn-outline-primary btn-sm" id="drawPolygonBtn">
<i class="bi bi-pencil me-1"></i>Нарисовать на карте
</button>
<button type="button" class="btn btn-outline-secondary btn-sm" id="clearPolygonBtn">
<i class="bi bi-trash me-1"></i>Очистить
</button>
</div>
<textarea class="form-control font-monospace" id="requestContour" rows="3"
placeholder="POLYGON ((lon1 lat1, lon2 lat2, ...))" required></textarea>
<div class="form-text">
Контур в формате WKT. Нажмите "Нарисовать на карте", затем кликайте на карту для добавления точек.
Двойной клик завершит рисование и вернёт вас к форме.
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-success" id="saveRequestBtn">
<i class="bi bi-check-lg me-1"></i>Сохранить заявку
</button>
</div>
</div>
</div>
</div>
<script th:inline="javascript" id="cesium_sandcastle_script" charset="utf-8">
// Ждем полной загрузки DOM
document.addEventListener('DOMContentLoaded', function() {
// Инициализируем карту после полной загрузки страницы
initializeCesium();
window.PcpDynamicPlanMapLayers?.initializeMap(window.viewer);
// Инициализируем состояние тепловой карты
window.heatmapActive = false;
// Инициализируем сортировку
initTableSorting();
// Настраиваем высоту таблицы
adjustTableHeight();
initializeTimeRange();
});
// Переменные для сортировки
let currentSort = {
column: null,
direction: 'asc' // 'asc' или 'desc'
};
let checkboxStates = new Map();
// Инициализация сортировки
function initTableSorting() {
// Устанавливаем начальные иконки
updateSortIcons(null, null);
}
// Функция для настройки высоты таблицы
function adjustTableHeight() {
try {
const activeTabPane = document.querySelector('.tab-pane.active');
if (!activeTabPane) {
console.warn('Не найден активный tab-pane');
return;
}
const tableContainer = activeTabPane.querySelector('.table-container');
if (!tableContainer) {
console.warn('Не найден table-container в активной вкладке');
return;
}
const sidebarHeader = document.querySelector('.sidebar-header');
const sidebarControls = activeTabPane.querySelector('.sidebar-controls');
const selectedCounter = activeTabPane.querySelector('.selected-counter');
if (!sidebarHeader) {
console.warn('Не найден sidebar-header');
return;
}
const isRequestsPane = activeTabPane.id === 'requests-pane';
const slotsContainer = document.getElementById('slotsContainer');
const slotsVisible = isRequestsPane
&& slotsContainer
&& slotsContainer.style.display !== 'none';
if (isRequestsPane && !slotsVisible) {
tableContainer.style.height = 'auto';
tableContainer.style.maxHeight = 'none';
tableContainer.style.overflowY = 'visible';
return;
}
const headerHeight = sidebarHeader.offsetHeight || 60;
const controlsHeight = sidebarControls ? sidebarControls.offsetHeight : 0;
const counterHeight = selectedCounter ? selectedCounter.offsetHeight : 0;
const margins = 30; // отступы
const availableHeight = window.innerHeight - headerHeight - controlsHeight - counterHeight - margins;
if (availableHeight > 100) {
tableContainer.style.overflowY = 'auto';
tableContainer.style.maxHeight = availableHeight + 'px';
} else {
console.warn('Недостаточно высоты для таблицы:', availableHeight);
}
} catch (error) {
console.error('Ошибка в adjustTableHeight:', error);
}
}
// Основная функция сортировки
function sortTable(column) {
const tbody = document.getElementById('requestsTableBody');
const rows = Array.from(tbody.querySelectorAll('tr:not(.no-requests)'));
// Проверяем, есть ли данные для сортировки
if (!tbody || rows.length === 0) {
console.log('Нет данных для сортировки');
return;
}
// Определяем направление сортировки
let direction = 'asc';
// Если уже сортировали по этому столбцу, меняем направление на противоположное
if (currentSort.column === column) {
direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
} else {
// Если выбрали новый столбец, сбрасываем направление на asc
direction = 'asc';
}
// Обновляем текущую сортировку
currentSort.column = column;
currentSort.direction = direction;
// Обновляем иконки сортировки
updateSortIcons(column, direction);
// Сохраняем состояния чекбоксов перед сортировкой
saveCheckboxStates();
// Сортируем строки
rows.sort((a, b) => {
let aValue, bValue;
try {
switch (column) {
case 'name':
aValue = a.getAttribute('data-name') || '';
bValue = b.getAttribute('data-name') || '';
return direction === 'asc'
? (aValue || '').localeCompare(bValue || '')
: (bValue || '').localeCompare(aValue || '');
case 'importance':
aValue = parseFloat(a.getAttribute('data-importance')) || 0;
bValue = parseFloat(b.getAttribute('data-importance')) || 0;
return direction === 'asc'
? aValue - bValue
: bValue - aValue;
case 'square':
aValue = parseFloat(a.getAttribute('data-square')) || 0;
bValue = parseFloat(b.getAttribute('data-square')) || 0;
return direction === 'asc'
? aValue - bValue
: bValue - aValue;
default:
return 0;
}
} catch (error) {
console.error('Ошибка при сортировке:', error);
return 0;
}
});
// Перемещаем отсортированные строки в таблицу
try {
// Удаляем все строки
while (tbody.firstChild) {
tbody.removeChild(tbody.firstChild);
}
// Добавляем отсортированные строки
rows.forEach(row => {
tbody.appendChild(row);
});
// Восстанавливаем состояния чекбоксов
restoreCheckboxStates();
// Обновляем счетчик
updateSelectedCount();
} catch (error) {
console.error('Ошибка при обновлении таблицы:', error);
}
setTimeout(() => {
const filterInput = document.getElementById('requestFilter');
if (filterInput) {
filterRequests(filterInput.value);
}
}, 0);
}
// Обновление иконок сортировки
function updateSortIcons(column, direction) {
try {
// Сбрасываем все иконки
const allHeaders = document.querySelectorAll('.sortable');
allHeaders.forEach(th => {
th.classList.remove('asc', 'desc');
const icon = th.querySelector('.sort-icon i');
if (icon) {
icon.className = 'bi bi-arrow-down-up';
}
});
// Устанавливаем активную иконку
if (column) {
const activeHeader = document.querySelector(`.sortable[data-sort="${column}"]`);
if (activeHeader) {
activeHeader.classList.add(direction);
const icon = activeHeader.querySelector('.sort-icon i');
if (icon) {
// Устанавливаем соответствующую иконку в зависимости от направления
if (direction === 'asc') {
icon.className = 'bi bi-sort-down';
} else {
icon.className = 'bi bi-sort-up';
}
}
}
}
} catch (error) {
console.error('Ошибка при обновлении иконок:', error);
}
}
// Сброс сортировки
function resetSorting() {
try {
// Сбрасываем состояние сортировки
currentSort.column = null;
currentSort.direction = 'asc';
// Сбрасываем иконки
updateSortIcons(null, null);
// Восстанавливаем исходный порядок
const tbody = document.getElementById('requestsTableBody');
if (!tbody) return;
const rows = Array.from(tbody.querySelectorAll('tr:not(.no-requests)'));
if (rows.length === 0) return;
// Сохраняем состояния
saveCheckboxStates();
// Сортируем по requestId (исходный порядок)
rows.sort((a, b) => {
const aCheckbox = a.querySelector('.request-checkbox');
const bCheckbox = b.querySelector('.request-checkbox');
const aId = aCheckbox ? parseInt(aCheckbox.value) || 0 : 0;
const bId = bCheckbox ? parseInt(bCheckbox.value) || 0 : 0;
return aId - bId;
});
// Обновляем таблицу
rows.forEach(row => tbody.appendChild(row));
// Восстанавливаем состояния
restoreCheckboxStates();
updateSelectedCount();
} catch (error) {
console.error('Ошибка при сбросе сортировки:', error);
}
}
// Сохранение состояний чекбоксов
function saveCheckboxStates() {
try {
checkboxStates.clear();
const checkboxes = document.querySelectorAll('.request-checkbox');
checkboxes.forEach(checkbox => {
checkboxStates.set(checkbox.value, checkbox.checked);
});
} catch (error) {
console.error('Ошибка при сохранении состояний:', error);
}
}
// Восстановление состояний чекбоксов
function restoreCheckboxStates() {
try {
const checkboxes = document.querySelectorAll('.request-checkbox');
checkboxes.forEach(checkbox => {
if (checkboxStates.has(checkbox.value)) {
checkbox.checked = checkboxStates.get(checkbox.value);
}
});
} catch (error) {
console.error('Ошибка при восстановлении состояний:', error);
}
}
// Обновление основной функции selectAllRequests
function selectAllRequests() {
const checkboxes = document.querySelectorAll('.request-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = true;
// Вызываем обработчик для каждого чекбокса
if (typeof checkReq === 'function') {
checkReq(true, checkbox.value);
}
});
updateSelectedCount();
saveCheckboxStates();
}
// Обновление основной функции deselectAllRequests
function deselectAllRequests() {
const checkboxes = document.querySelectorAll('.request-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = false;
// Вызываем обработчик для каждого чекбокса
if (typeof checkReq === 'function') {
checkReq(false, checkbox.value);
}
});
updateSelectedCount();
saveCheckboxStates();
}
// Обновление основной функции toggleSelectAll
function toggleSelectAll(isChecked) {
const checkboxes = document.querySelectorAll('.request-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = isChecked;
// Вызываем обработчик для каждого чекбокса
if (typeof checkReq === 'function') {
checkReq(isChecked, checkbox.value);
}
});
updateSelectedCount();
saveCheckboxStates();
}
// Функция для обновления счетчика выбранных элементов
function updateSelectedCount() {
const checkboxes = document.querySelectorAll('.request-checkbox:checked');
const selectedCountElement = document.getElementById('selectedCount');
const filterInput = document.getElementById('requestFilter');
if (selectedCountElement) {
// Если активен фильтр, считаем только видимые выбранные
if (filterInput && filterInput.value.trim() !== '') {
const visibleChecked = Array.from(checkboxes).filter(cb => {
const row = cb.closest('tr');
return row && row.style.display !== 'none';
});
selectedCountElement.textContent = visibleChecked.length;
} else {
// Стандартный подсчет
selectedCountElement.textContent = checkboxes.length;
}
}
// Обновляем чекбокс "Выбрать все"
const allCheckboxes = document.querySelectorAll('.request-checkbox');
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
if (allCheckboxes.length > 0) {
const allChecked = Array.from(allCheckboxes).every(cb => cb.checked);
const anyChecked = Array.from(allCheckboxes).some(cb => cb.checked);
selectAllCheckbox.checked = allChecked;
selectAllCheckbox.indeterminate = anyChecked && !allChecked;
}
}
// Функция для переключения тепловой карты (кнопка)
function toggleHeatmap() {
var toggle = document.getElementById('heatmapToggle');
var newState = !toggle.checked;
toggle.checked = newState;
handleHeatmapToggle(newState);
}
// Обработчик переключателя тепловой карты
function handleHeatmapToggle(isChecked) {
window.heatmapActive = isChecked;
// Обновляем стиль кнопки
var heatmapBtn = document.getElementById('heatmapBtn');
if (isChecked) {
heatmapBtn.classList.remove('btn-outline-primary');
heatmapBtn.classList.add('btn-primary');
heatmapBtn.textContent = 'Тепловая карта (вкл)';
} else {
heatmapBtn.classList.remove('btn-primary');
heatmapBtn.classList.add('btn-outline-primary');
heatmapBtn.textContent = 'Тепловая карта';
}
// Вызываем основную функцию для работы с тепловой картой
mapVisibility(isChecked);
}
function flyToReq(id){
let ds = 'req_'+id
const dataSource = viewer.dataSources.getByName(ds)[0];
if (!dataSource) {
console.error('DataSource не найден:', dataSourceName);
return;
}
console.log(ds)
const entityId = 'request_'+id
const entity = dataSource.entities.getById(entityId);
if (entity)
{
console.log('fly')
// Если entity имеет позицию
viewer.flyTo(entity,
{
duration: 2
});
}
}
// Функция для преобразования datetime-local в ISO
function toISOString(datetimeLocal) {
if (!datetimeLocal) {
return null; // или можно вернуть текущее время по умолчанию
}
// datetime-local возвращает строку в формате "YYYY-MM-DDTHH:mm"
// Создаем Date объект из этой строки
let date = new Date(datetimeLocal);
date.setHours(date.getHours() + 3);
// Возвращаем в ISO формате
return date.toISOString().split('Z')[0];
}
function covToReq(show, req_id){
// Сбрасываем чекбокс видимости всех слотов
const toggleAllCheckbox = document.getElementById('toggleAllSlotsVisibility');
if (toggleAllCheckbox) {
toggleAllCheckbox.checked = true;
}
const reqs = [[${requests}]];
const req = reqs.find(function(element) {
return element.requestId == req_id;
});
if (req) {
// if (req.square > 500000){
// alert("Плщадь заявки не должна привышать 500.000 км2")
// return
// }
const startInput = toISOString(document.getElementById('startTime').value);
const endInput = toISOString(document.getElementById('endTime').value);
const node = document.getElementById('selectNode').value;
const cover = document.getElementById('toggleCalcCoverage').checked;
const sunInput = document.getElementById('coverageSun');
const sun = Number.parseFloat(sunInput.value);
if (!Number.isFinite(sun) || sun < -90 || sun > 90) {
alert('Значение поля "Солнце" должно быть в диапазоне от -90 до 90');
sunInput.focus();
return;
}
const satellites = getSelectedCoverageSatellites();
drawReqCov(show, req, startInput, endInput, node, cover, satellites, sun);
}
}
function coverageSchemeToReq(show, req_id) {
const reqs = [[${requests}]];
const req = reqs.find(function(element) {
return element.requestId == req_id;
});
if (!req) {
return;
}
const startInput = toISOString(document.getElementById('startTime').value);
const endInput = toISOString(document.getElementById('endTime').value);
const satellites = getSelectedCoverageSatellites();
if (show && satellites.length === 0) {
alert('Для расчета схемы покрытия выберите хотя бы один спутник');
document.querySelector(`input[name="coverage-scheme-request"][value="${req_id}"]`).checked = false;
return;
}
const options = getCoverageSchemeRequestOptions();
drawReqCoverageScheme(show, req, startInput, endInput, satellites, options);
}
function getSelectedCoverageSatellites() {
return Array.from(document.querySelectorAll('.coverage-sat-checkbox:checked'))
.map(checkbox => checkbox.value);
}
function getSelectedCoverageSatelliteNames() {
return Array.from(document.querySelectorAll('.coverage-sat-checkbox:checked'))
.map(checkbox => {
const label = document.querySelector(`label[for="${checkbox.id}"]`);
return label ? label.textContent.trim() : `SAT-${checkbox.value}`;
});
}
function setComplexPlanButtonLoading(isLoading) {
const button = document.getElementById('complexPlanBtn');
if (!button) {
return;
}
if (!button.dataset.defaultText) {
button.dataset.defaultText = button.innerHTML;
}
button.disabled = isLoading;
button.innerHTML = isLoading
? '<span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>Ком.План'
: button.dataset.defaultText;
}
function getComplexPlanInterval() {
const startValue = document.getElementById('startTime').value;
const endValue = document.getElementById('endTime').value;
if (!startValue || !endValue) {
alert('Укажите интервал расчета перед запуском комплексного плана');
return null;
}
const startDate = new Date(startValue);
const endDate = new Date(endValue);
if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime())) {
alert('Не удалось распознать интервал расчета');
return null;
}
if (startDate >= endDate) {
alert('Начало интервала расчета должно быть раньше окончания');
return null;
}
return {
intervalStart: toISOString(startValue),
intervalEnd: toISOString(endValue)
};
}
function getFirstNumericValue(payload, fieldNames) {
for (const fieldName of fieldNames) {
const rawValue = payload?.[fieldName];
if (typeof rawValue === 'number' && Number.isFinite(rawValue)) {
return rawValue;
}
if (typeof rawValue === 'string' && rawValue.trim() !== '') {
const parsedValue = Number(rawValue);
if (Number.isFinite(parsedValue)) {
return parsedValue;
}
}
}
return null;
}
function formatMillisecondsDuration(durationMs) {
if (!Number.isFinite(durationMs) || durationMs < 0) {
return 'н/д';
}
if (durationMs < 1000) {
return `${Math.round(durationMs)} мс`;
}
const totalSeconds = Math.round(durationMs / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
const parts = [];
if (hours > 0) {
parts.push(`${hours} ч`);
}
if (minutes > 0) {
parts.push(`${minutes} мин`);
}
if (seconds > 0 || parts.length === 0) {
parts.push(`${seconds} сек`);
}
return parts.join(' ');
}
function formatSecondsDuration(durationSeconds) {
if (!Number.isFinite(durationSeconds) || durationSeconds < 0) {
return 'н/д';
}
return formatMillisecondsDuration(durationSeconds * 1000);
}
function extractApiErrorMessage(payload, fallbackStatus) {
if (!payload) {
return `HTTP ${fallbackStatus}`;
}
if (typeof payload === 'string' && payload.trim() !== '') {
return payload;
}
if (payload.error) {
return payload.error;
}
if (payload.message) {
return payload.message;
}
const values = Object.values(payload).filter(value => typeof value === 'string' && value.trim() !== '');
if (values.length > 0) {
return values.join(', ');
}
return `HTTP ${fallbackStatus}`;
}
function getComplexPlanSatelliteNames(payload) {
const satellites = [[${satellites}]];
const responseSatelliteIds = Array.isArray(payload?.satelliteIds)
? payload.satelliteIds
.map(value => Number(value))
.filter(value => Number.isFinite(value))
: [];
if (responseSatelliteIds.length > 0) {
return responseSatelliteIds.map(satelliteId => {
const satellite = satellites.find(item => item.noradId === satelliteId);
return satellite ? `${satellite.name} (${satellite.noradId})` : `SAT-${satelliteId}`;
});
}
return getSelectedCoverageSatelliteNames();
}
function formatComplexPlanResultMessage(payload) {
const routeCount = getFirstNumericValue(payload, [
'routesCount',
'routesTotal',
'routeCount',
'calculatedModesCount'
]);
const satelliteNames = getComplexPlanSatelliteNames(payload);
return [
'Расчет комплексного плана завершен.',
`Длительность расчета: ${formatMillisecondsDuration(getFirstNumericValue(payload, ['durationMs']))}.`,
`Количество маршрутов: ${routeCount !== null ? routeCount : 'н/д'}.`,
`Аппараты: ${satelliteNames.length > 0 ? satelliteNames.join(', ') : 'н/д'}.`
].join('\n');
}
async function runComplexPlan() {
const interval = getComplexPlanInterval();
if (!interval) {
return;
}
const satelliteIds = getSelectedCoverageSatellites()
.map(value => Number(value))
.filter(value => Number.isFinite(value));
if (satelliteIds.length === 0) {
alert('Выберите хотя бы один спутник в секции "Спутники для покрытия" на вкладке "Заявки"');
return;
}
setComplexPlanButtonLoading(true);
try {
const countLat = parseOptionalPositiveInt(document.getElementById('heatmapCountLat').value);
const countLong = parseOptionalPositiveInt(document.getElementById('heatmapCountLong').value);
const response = await fetch('/api/com-plan/process', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
intervalStart: interval.intervalStart,
intervalEnd: interval.intervalEnd,
satelliteIds: satelliteIds,
countLat: countLat,
countLong: countLong
})
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(extractApiErrorMessage(payload, response.status));
}
alert(formatComplexPlanResultMessage(payload));
} catch (error) {
alert('Ошибка при запуске расчета комплексного плана: ' + error.message);
} finally {
setComplexPlanButtonLoading(false);
}
}
function toggleCoverageSatellites(isChecked) {
document.querySelectorAll('.coverage-sat-checkbox').forEach(checkbox => {
checkbox.checked = isChecked;
});
}
// Основная функция для управления видимостью тепловой карты
function mapVisibility(isChecked) {
if (window.viewer) {
if (isChecked) {
// Включение тепловой карты
const input = document.getElementById('minImportance');
let value = parseFloat(input.value);
const countLat = parseOptionalPositiveInt(document.getElementById('heatmapCountLat').value);
const countLong = parseOptionalPositiveInt(document.getElementById('heatmapCountLong').value);
showHeatmap(value, countLat, countLong);
} else {
// Выключение тепловой карты
hideHeatmap();
}
}
}
function parseOptionalPositiveInt(value) {
const parsedValue = parseInt(value, 10);
if (isNaN(parsedValue) || parsedValue <= 0) {
return null;
}
return parsedValue;
}
// Функция для обработки запросов
function checkReq(show, req_id) {
const reqs = [[${requests}]];
const req = reqs.find(function(element) {
return element.requestId == req_id;
});
if (req) {
drawReq(show, req);
}
updateSelectedCount();
}
// Функция для обработки запросов
function checkSat(show, sat_id) {
const sats = [[${satellites}]];
const req = sats.find(function(element) {
return element.noradId == sat_id;
});
if (req) {
const startInput = toISOString(document.getElementById('startTime').value);
const endInput = toISOString(document.getElementById('endTime').value);
drawSat(show, req, startInput, endInput, satellitePlanRunId());
}
}
function satellitePlanRunId() {
const input = document.getElementById('satRunId');
return input?.value?.trim() || null;
}
function checkSatBookings(show, sat_id) {
const sats = [[${satellites}]];
const req = sats.find(function(element) {
return element.noradId == sat_id;
});
if (req) {
const startInput = toISOString(document.getElementById('startTime').value);
const endInput = toISOString(document.getElementById('endTime').value);
drawSatelliteBookedSlots(show, req, startInput, endInput);
}
}
function checkSatSlots(show, sat_id) {
const sats = [[${satellites}]];
const req = sats.find(function(element) {
return element.noradId == sat_id;
});
if (req) {
const startInput = toISOString(document.getElementById('startTime').value);
const endInput = toISOString(document.getElementById('endTime').value);
drawSatelliteSlots(show, req, startInput, endInput);
}
}
function redrawSelectedSatellitePlans() {
const sats = [[${satellites}]];
const startInput = toISOString(document.getElementById('startTime').value);
const endInput = toISOString(document.getElementById('endTime').value);
const selectedPlanCheckboxes = Array.from(
document.querySelectorAll('input[name="sat"].sat-plan-checkbox:checked')
);
selectedPlanCheckboxes.forEach(function(checkbox) {
const req = sats.find(function(element) {
return element.noradId == checkbox.value;
});
if (req) {
const runId = satellitePlanRunId();
drawSat(false, req, startInput, endInput, runId);
drawSat(true, req, startInput, endInput, runId);
}
});
const selectedBookingCheckboxes = Array.from(
document.querySelectorAll('input[name="sat-bookings"].sat-bookings-checkbox:checked')
);
selectedBookingCheckboxes.forEach(function(checkbox) {
const req = sats.find(function(element) {
return element.noradId == checkbox.value;
});
if (req) {
drawSatelliteBookedSlots(false, req, startInput, endInput);
drawSatelliteBookedSlots(true, req, startInput, endInput);
}
});
const selectedSlotCheckboxes = Array.from(
document.querySelectorAll('input[name="sat-slots"].sat-slots-checkbox:checked')
);
selectedSlotCheckboxes.forEach(function(checkbox) {
const req = sats.find(function(element) {
return element.noradId == checkbox.value;
});
if (req) {
drawSatelliteSlots(false, req, startInput, endInput);
drawSatelliteSlots(true, req, startInput, endInput);
}
});
const selectedOrbitCheckboxes = Array.from(
document.querySelectorAll('input[name="sat-orbit"]:checked')
);
selectedOrbitCheckboxes.forEach(function(checkbox) {
const req = sats.find(function(element) {
return element.noradId == checkbox.value;
});
if (req) {
drawOrbit(false, req, startInput, endInput);
drawOrbit(true, req, startInput, endInput);
}
});
}
function checkStation(show, st_id) {
const sts = [[${stations}]];
const req = sts.find(function(element) {
return element.id == st_id;
});
if (req) {
console.log('изменить видимость станции ' + req.id)
drawSt(show, req);
}
}
// Функция для обработки запросов
function checkOrbit(show, sat_id) {
const sats = [[${satellites}]];
const req = sats.find(function(element) {
return element.noradId == sat_id;
});
if (req) {
const startInput = toISOString(document.getElementById('startTime').value);
const endInput = toISOString(document.getElementById('endTime').value);
drawOrbit(show, req, startInput, endInput);
}
}
// Дополнительные вспомогательные функции
function clearAllMarkers() {
if (window.viewer) {
window.viewer.entities.removeAll();
}
}
function zoomToDefault() {
if (window.viewer) {
viewer.camera.setView({
destination: Cesium.Rectangle.fromDegrees(351, 72.6, 76.3, 31.4)
});
}
}
// Инициализация при загрузке
document.addEventListener('DOMContentLoaded', function() {
updateSelectedCount();
saveCheckboxStates(); // Сохраняем начальные состояния
});
// Функция фильтрации заявок по названию
function filterRequests(filterValue) {
const filter = filterValue.toLowerCase().trim();
const tbody = document.getElementById('requestsTableBody');
if (!tbody) return;
const rows = Array.from(tbody.querySelectorAll('tr:not(.no-requests)'));
let visibleCount = 0;
let selectedVisibleCount = 0;
rows.forEach(row => {
const name = (row.getAttribute('data-name') || '').toLowerCase();
const matches = name.includes(filter);
if (matches) {
row.style.display = '';
visibleCount++;
// Проверяем, выбрана ли видимая заявка
const checkbox = row.querySelector('.request-checkbox');
if (checkbox && checkbox.checked) {
selectedVisibleCount++;
}
} else {
row.style.display = 'none';
}
});
// Обновляем счетчик с учетом фильтрации
updateFilteredCount(visibleCount, selectedVisibleCount);
// Обработка сообщения "Нет результатов"
handleNoResultsMessage(tbody, rows.length, visibleCount);
}
// Обновление счетчика с учетом фильтрации
function updateFilteredCount(visibleCount, selectedVisibleCount) {
const totalCountEl = document.getElementById('totalCount');
const selectedCountEl = document.getElementById('selectedCount');
const filterInput = document.getElementById('requestFilter');
if (totalCountEl && selectedCountEl && filterInput) {
const isFiltered = filterInput.value.trim() !== '';
if (isFiltered) {
// Формат: "Выбрано: X из Y (фильтр: Z видно)"
totalCountEl.textContent = `${visibleCount} из ${totalCountEl.dataset.total || '0'}`;
selectedCountEl.textContent = selectedVisibleCount;
// Добавляем визуальный индикатор фильтрации
totalCountEl.closest('.selected-counter').classList.add('filtered');
} else {
// Восстанавливаем обычный счетчик
totalCountEl.textContent = totalCountEl.dataset.total || '0';
updateSelectedCount(); // стандартное обновление
totalCountEl.closest('.selected-counter').classList.remove('filtered');
}
}
}
// Управление сообщением "Нет результатов"
function handleNoResultsMessage(tbody, totalRows, visibleCount) {
let noResultsRow = tbody.querySelector('.no-results-filter');
// Создаем строку "Нет результатов" если её нет
if (!noResultsRow && totalRows > 0) {
noResultsRow = document.createElement('tr');
noResultsRow.className = 'no-results-filter text-center text-muted py-3';
noResultsRow.innerHTML = `<td colspan="6">Нет заявок, соответствующих фильтру</td>`;
tbody.appendChild(noResultsRow);
}
// Показываем/скрываем сообщение
if (noResultsRow) {
noResultsRow.style.display = (visibleCount === 0 && totalRows > 0) ? '' : 'none';
}
// Скрываем оригинальное сообщение "Нет доступных заявок" при фильтрации
const originalNoRequests = tbody.querySelector('.no-requests');
if (originalNoRequests) {
originalNoRequests.style.display = (totalRows === 0) ? '' : 'none';
}
}
// Инициализация фильтрации при загрузке
document.addEventListener('DOMContentLoaded', function() {
const filterInput = document.getElementById('requestFilter');
const clearBtn = document.getElementById('clearFilterBtn');
if (filterInput) {
// Сохраняем общее количество для счетчика
const totalCountEl = document.getElementById('totalCount');
if (totalCountEl) {
totalCountEl.dataset.total = totalCountEl.textContent;
}
// Обработчик ввода
filterInput.addEventListener('input', function(e) {
filterRequests(e.target.value);
});
// Обработчик очистки
if (clearBtn) {
clearBtn.addEventListener('click', function() {
filterInput.value = '';
filterRequests('');
});
}
// Применяем фильтр при загрузке (на случай сохраненного значения)
filterRequests(filterInput.value);
}
});
function initializeTimeRange() {
const now = new Date();
// Текущее время
const startTime = now;
// Текущее время + 14 дней
const endTime = new Date(now);
endTime.setDate(endTime.getDate() + 14);
// Форматируем даты для поля ввода datetime-local (формат: YYYY-MM-DDTHH:mm)
const formatDateTimeLocal = (date) => {
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}`;
};
// Устанавливаем значения в поля ввода
const startTimeInput = document.getElementById('startTime');
const endTimeInput = document.getElementById('endTime');
if (startTimeInput && endTimeInput) {
startTimeInput.value = formatDateTimeLocal(startTime);
endTimeInput.value = formatDateTimeLocal(endTime);
}
}
// Обработчик изменения размера окна
window.addEventListener('resize', adjustTableHeight);
// ==================== Функции для рисования полигона и создания заявки ====================
let drawingHandler = null;
let drawnPoints = [];
let drawnEntity = null;
let isDrawing = false;
// Функция для перевода градусов в радианы
function toRadians(degrees) {
return degrees * Math.PI / 180;
}
// Функция для перевода радианов в градусы
function toDegrees(radians) {
return radians * 180 / Math.PI;
}
// Преобразование координат Cesium в WKT формат
function cartesianToWKT(positions) {
if (!positions || positions.length < 3) {
return '';
}
const coordinates = positions.map(pos => {
const cartographic = Cesium.Cartographic.fromCartesian(pos);
const lon = toDegrees(cartographic.longitude);
const lat = toDegrees(cartographic.latitude);
return `${lon.toFixed(6)} ${lat.toFixed(6)}`;
});
// Замыкаем полигон (первая точка = последняя точка)
const firstCoord = coordinates[0];
const wkt = `POLYGON ((${coordinates.join(', ')}, ${firstCoord}))`;
return wkt;
}
// Начало рисования полигона
function startDrawing() {
if (!window.viewer) {
console.error('Viewer не инициализирован');
return;
}
// Очищаем предыдущее рисование
stopDrawing();
clearDrawing();
isDrawing = true;
drawnPoints = [];
// Закрываем модальное окно
const modal = bootstrap.Modal.getInstance(document.getElementById('addRequestModal'));
if (modal) {
modal.hide();
}
// Показываем оверлей поверх карты
document.getElementById('drawingOverlay').style.display = 'block';
updateDrawingOverlayText('Кликните на карту для добавления точек. Двойной клик для завершения.');
// Изменяем курсор
viewer.canvas.style.cursor = 'crosshair';
// Создаем обработчик событий
drawingHandler = new Cesium.ScreenSpaceEventHandler(viewer.canvas);
// Обработчик клика (добавление точки)
drawingHandler.setInputAction(function(click) {
const cartesian = viewer.camera.pickEllipsoid(click.position, viewer.scene.globe.ellipsoid);
if (cartesian) {
drawnPoints.push(cartesian);
updateDrawnPolygon();
updateDrawingOverlayText('Добавлено точек: ' + drawnPoints.length + '. Двойной клик для завершения.');
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
// Обработчик двойного клика (завершение)
drawingHandler.setInputAction(function(click) {
if (drawnPoints.length >= 3) {
finishDrawing();
} else {
updateDrawingOverlayText('Для полигона нужно минимум 3 точки. Продолжайте кликать.');
}
}, Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK);
// Обработчик движения мыши (предпросмотр)
drawingHandler.setInputAction(function(movement) {
if (drawnPoints.length > 0) {
const cartesian = viewer.camera.pickEllipsoid(movement.endPosition, viewer.scene.globe.ellipsoid);
if (cartesian) {
updateDrawnPolygonPreview(cartesian);
}
}
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
}
// Обновление текста в оверлее
function updateDrawingOverlayText(text) {
document.getElementById('drawingOverlayText').textContent = text;
}
// Обновление нарисованного полигона
function updateDrawnPolygon() {
if (drawnPoints.length < 2) return;
// Удаляем предыдущую сущность
if (drawnEntity) {
viewer.entities.remove(drawnEntity);
}
// Создаем новую сущность
drawnEntity = viewer.entities.add({
name: 'drawing_polygon',
polygon: {
hierarchy: new Cesium.PolygonHierarchy(drawnPoints),
material: Cesium.Color.GREEN.withAlpha(0.3),
outline: true,
outlineColor: Cesium.Color.GREEN,
outlineWidth: 2
},
polyline: {
positions: [...drawnPoints, drawnPoints[0]],
material: Cesium.Color.GREEN,
width: 2
}
});
}
// Предпросмотр полигона при движении мыши
function updateDrawnPolygonPreview(currentPosition) {
if (drawnPoints.length < 1) return;
// Удаляем предыдущую сущность
if (drawnEntity) {
viewer.entities.remove(drawnEntity);
}
// Создаем с предпросмотром
const previewPoints = [...drawnPoints, currentPosition];
drawnEntity = viewer.entities.add({
name: 'drawing_polygon_preview',
polygon: {
hierarchy: new Cesium.PolygonHierarchy(previewPoints),
material: Cesium.Color.GREEN.withAlpha(0.2),
outline: true,
outlineColor: Cesium.Color.YELLOW,
outlineWidth: 2
},
polyline: {
positions: [...previewPoints, drawnPoints[0]],
material: Cesium.Color.YELLOW,
width: 2,
clampToGround: true
}
});
}
// Завершение рисования
function finishDrawing() {
if (drawnPoints.length < 3) {
alert('Для создания полигона необходимо минимум 3 точки');
return;
}
stopDrawing();
// Преобразуем в WKT
const wkt = cartesianToWKT(drawnPoints);
document.getElementById('requestContour').value = wkt;
// Скрываем оверлей
document.getElementById('drawingOverlay').style.display = 'none';
// Финальное отображение полигона
if (drawnEntity) {
viewer.entities.remove(drawnEntity);
}
drawnEntity = viewer.entities.add({
name: 'final_polygon',
polygon: {
hierarchy: new Cesium.PolygonHierarchy(drawnPoints),
material: Cesium.Color.GREEN.withAlpha(0.4),
outline: true,
outlineColor: Cesium.Color.DARKGREEN,
outlineWidth: 3
}
});
// Открываем модальное окно с заполненными данными
const modal = new bootstrap.Modal(document.getElementById('addRequestModal'));
modal.show();
}
// Остановка рисования
function stopDrawing() {
isDrawing = false;
if (drawingHandler) {
drawingHandler.destroy();
drawingHandler = null;
}
if (viewer && viewer.canvas) {
viewer.canvas.style.cursor = 'default';
}
}
// Отмена рисования
function cancelDrawing() {
stopDrawing();
clearDrawing();
document.getElementById('drawingOverlay').style.display = 'none';
// Возвращаемся к модальному окну
const modal = new bootstrap.Modal(document.getElementById('addRequestModal'));
modal.show();
}
// Очистка рисования
function clearDrawing() {
stopDrawing();
if (drawnEntity && viewer) {
viewer.entities.remove(drawnEntity);
drawnEntity = null;
}
drawnPoints = [];
document.getElementById('requestContour').value = '';
}
// Сохранение заявки
function saveRequest() {
const name = document.getElementById('requestName').value.trim();
const importance = parseFloat(document.getElementById('requestImportance').value);
const geoType = document.getElementById('requestGeoType').value;
const contour = document.getElementById('requestContour').value.trim();
// Валидация
if (!name) {
alert('Введите название заявки');
return;
}
if (isNaN(importance) || importance < 0 || importance > 1) {
alert('Важность должна быть от 0 до 1');
return;
}
if (!contour) {
alert('Нарисуйте или введите контур заявки');
return;
}
// Формируем данные заявки (requestId генерируется на сервере)
const requestData = {
name: name,
importance: importance,
geoType: geoType,
contour: contour
};
// Отправляем на сервер
fetch('/api/requests', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
})
.then(response => {
if (!response.ok) {
throw new Error('HTTP error! status: ' + response.status);
}
return response.json();
})
.then(data => {
// Закрываем модальное окно
const modal = bootstrap.Modal.getInstance(document.getElementById('addRequestModal'));
modal.hide();
// Очищаем форму
document.getElementById('addRequestForm').reset();
clearDrawing();
// Перезагружаем страницу для обновления списка
window.location.reload();
})
.catch(error => {
console.error('Ошибка при создании заявки:', error);
alert('Ошибка при создании заявки: ' + error.message);
});
}
// Инициализация обработчиков для модального окна
document.addEventListener('DOMContentLoaded', function() {
const drawBtn = document.getElementById('drawPolygonBtn');
const clearBtn = document.getElementById('clearPolygonBtn');
const saveBtn = document.getElementById('saveRequestBtn');
const cancelDrawBtn = document.getElementById('cancelDrawingBtn');
if (drawBtn) {
drawBtn.addEventListener('click', function() {
startDrawing();
});
}
if (clearBtn) {
clearBtn.addEventListener('click', clearDrawing);
}
if (saveBtn) {
saveBtn.addEventListener('click', saveRequest);
}
if (cancelDrawBtn) {
cancelDrawBtn.addEventListener('click', cancelDrawing);
}
// Очистка при закрытии модального окна (только если не в режиме рисования)
document.getElementById('addRequestModal').addEventListener('hidden.bs.modal', function() {
if (!isDrawing) {
stopDrawing();
if (drawnEntity && viewer) {
viewer.entities.remove(drawnEntity);
drawnEntity = null;
}
drawnPoints = [];
}
});
});
function getSlotIndexFromVisibilityButton(button) {
const dataSlotIndex = button.getAttribute('data-slot-index');
if (dataSlotIndex !== null && dataSlotIndex !== '') {
const parsedIndex = Number(dataSlotIndex);
if (Number.isFinite(parsedIndex)) {
return parsedIndex;
}
}
const onClickValue = button.getAttribute('onclick') || '';
const matches = onClickValue.match(/viewSlot\((\d+)/);
if (!matches) {
return null;
}
const fallbackIndex = Number(matches[1]);
return Number.isFinite(fallbackIndex) ? fallbackIndex : null;
}
function collectVisibleSlotsForBooking() {
const visibleButtons = document.querySelectorAll('#slotsTableBody button[title="Скрыть на карте"]');
const uniqueSlots = new Map();
visibleButtons.forEach(button => {
const slotIndex = getSlotIndexFromVisibilityButton(button);
if (!Number.isFinite(slotIndex) || !currentSlotsData[slotIndex]) {
return;
}
const slot = currentSlotsData[slotIndex];
const satelliteId = Number(slot.satelliteId);
const cycle = Number(slot.cycle);
const rawSlotNum = slot.slotNumber ?? slot.slotNum;
const slotNum = Number(rawSlotNum);
if (!Number.isFinite(satelliteId) || !Number.isFinite(slotNum) || !Number.isFinite(cycle)) {
return;
}
const slotKey = `${satelliteId}:${slotNum}:${cycle}`;
if (!uniqueSlots.has(slotKey)) {
uniqueSlots.set(slotKey, {
satelliteId: satelliteId,
slotNum: slotNum,
cycle: cycle
});
}
});
return Array.from(uniqueSlots.values());
}
function bookVisibleSlots(button) {
if (!currentSlotsContext || currentSlotsContext.type !== 'request') {
alert('Бронирование доступно только для интервалов покрытия заявки');
return;
}
if (!currentRequestId) {
alert('Нет выбранной заявки для бронирования');
return;
}
const slots = collectVisibleSlotsForBooking();
if (slots.length === 0) {
alert('Нет слотов с активной кнопкой "Скрыть на карте"');
return;
}
const requestBody = {
requestId: String(currentRequestId),
slots: slots
};
if (button) {
button.disabled = true;
}
fetch('/api/slots/booking/request', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(json => {
const bookedCount = Array.isArray(json) ? json.length : 0;
alert(`Забронировано слотов: ${bookedCount}`);
})
.catch(error => {
console.error('Ошибка при бронировании слотов:', error);
alert('Ошибка при бронировании слотов: ' + error.message);
})
.finally(() => {
if (button) {
button.disabled = false;
}
});
}
function cancelRequestBooking(button) {
if (!currentSlotsContext || currentSlotsContext.type !== 'request') {
alert('Отмена бронирования доступна только для отображаемой заявки');
return;
}
if (!currentRequestId) {
alert('Нет выбранной заявки для отмены бронирования');
return;
}
if (button) {
button.disabled = true;
}
fetch(`/api/slots/booking/request?requestId=${encodeURIComponent(String(currentRequestId))}`, {
method: 'DELETE',
headers: {
'Accept': 'application/json'
}
})
.then(async response => {
if (!response.ok) {
const rawError = await response.text();
let errorMessage = rawError;
try {
const parsedError = JSON.parse(rawError);
errorMessage = parsedError.error || parsedError.message || rawError;
} catch (_) {}
throw new Error(`HTTP error! status: ${response.status} message: ${errorMessage}`);
}
return response.json();
})
.then(result => {
const cancelledCount = Number(result) || 0;
alert(`Отменено бронирований: ${cancelledCount}`);
})
.catch(error => {
console.error('Ошибка при отмене бронирования:', error);
alert('Ошибка при отмене бронирования: ' + error.message);
})
.finally(() => {
if (button) {
button.disabled = false;
}
});
}
// Управление видимостью всех слотов
function toggleAllSlotsVisibility(isVisible) {
if (!currentSlotsContext || currentSlotsData.length === 0) return;
const dataSource = viewer.dataSources.getByName(currentSlotsContext.dataSourceName)[0];
if (!dataSource) {
console.warn('DataSource не найден для слотов');
return;
}
// Проходим по всем слотам и меняем их видимость
currentSlotsData.forEach((slot, index) => {
const entityName = slot.entityId || `slot_${slot.satelliteId}_${slot.cycle}_${slot.slotNumber}`;
const entity = dataSource.entities.getById(entityName);
if (entity) {
entity.polygon.show = isVisible;
}
});
// Обновляем все кнопки видимости в таблице
const visibilityButtons = document.querySelectorAll('#slotsTableBody .btn-outline-primary, #slotsTableBody .btn-outline-secondary');
visibilityButtons.forEach(button => {
// Проверяем, что это кнопка видимости (имеет иконку глаза)
const icon = button.querySelector('i');
if (icon && (icon.classList.contains('bi-eye') || icon.classList.contains('bi-eye-slash'))) {
updateVisibilityButton(button, isVisible);
}
});
}
</script>
</th:block>
</body>
</html>