2434 lines
117 KiB
HTML
2434 lines
117 KiB
HTML
<!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>
|