2247 lines
113 KiB
HTML
2247 lines
113 KiB
HTML
{% extends 'mainapp/base.html' %}
|
||
{% load static %}
|
||
{% load static leaflet_tags %}
|
||
|
||
{% block title %}Список объектов{% endblock %}
|
||
|
||
{% block extra_css %}
|
||
<link href="{% static 'leaflet/leaflet.css' %}" rel="stylesheet">
|
||
<link href="{% static 'leaflet-draw/leaflet.draw.css' %}" rel="stylesheet">
|
||
<style>
|
||
.table-responsive tr.selected {
|
||
background-color: #d4edff;
|
||
}
|
||
.sticky-top {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 10;
|
||
}
|
||
.btn-group .badge {
|
||
position: absolute;
|
||
top: -5px;
|
||
right: -5px;
|
||
font-size: 0.65rem;
|
||
padding: 0.2em 0.4em;
|
||
}
|
||
.btn-group .btn {
|
||
position: relative;
|
||
}
|
||
#polygonFilterMap {
|
||
z-index: 1;
|
||
}
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="container-fluid px-3">
|
||
<div class="row mb-3">
|
||
<div class="col-12">
|
||
<h2>Список объектов</h2>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Toolbar -->
|
||
<div class="row mb-3">
|
||
<div class="col-12">
|
||
<div class="card">
|
||
<div class="card-body">
|
||
<div class="d-flex flex-wrap align-items-center gap-3">
|
||
<!-- Search bar -->
|
||
<div style="min-width: 200px; flex-grow: 1; max-width: 400px;">
|
||
<div class="input-group">
|
||
<input type="text" id="toolbar-search" class="form-control" placeholder="Поиск по ID или имени..."
|
||
value="{{ search_query|default:'' }}">
|
||
<button type="button" class="btn btn-outline-primary"
|
||
onclick="performSearch()">Найти</button>
|
||
<button type="button" class="btn btn-outline-secondary"
|
||
onclick="clearSearch()">Очистить</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Items per page select -->
|
||
<div>
|
||
<label for="items-per-page" class="form-label mb-0">Показать:</label>
|
||
<select name="items_per_page" id="items-per-page"
|
||
class="form-select form-select-sm d-inline-block" style="width: auto;"
|
||
onchange="updateItemsPerPage()">
|
||
{% for option in available_items_per_page %}
|
||
<option value="{{ option }}" {% if option == items_per_page %}selected{% endif %}>
|
||
{{ option }}
|
||
</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Action buttons -->
|
||
<div class="d-flex gap-2">
|
||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
||
<a href="{% url 'mainapp:source_create' %}" class="btn btn-success btn-sm" title="Создать новый источник">
|
||
<i class="bi bi-plus-circle"></i> Создать
|
||
</a>
|
||
{% endif %}
|
||
<!-- <a href="{% url 'mainapp:data_entry' %}" class="btn btn-info btn-sm" title="Ввод данных точек спутников">
|
||
Передача точек
|
||
</a> -->
|
||
<a href="{% url 'mainapp:load_excel_data' %}" class="btn btn-primary btn-sm" title="Загрузка данных из Excel">
|
||
<i class="bi bi-file-earmark-excel"></i> Excel
|
||
</a>
|
||
<a href="{% url 'mainapp:load_csv_data' %}" class="btn btn-success btn-sm" title="Загрузка данных из CSV">
|
||
<i class="bi bi-file-earmark-text"></i> CSV
|
||
</a>
|
||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
||
<button type="button" class="btn btn-danger btn-sm" title="Удалить"
|
||
onclick="deleteSelectedSources()">
|
||
<i class="bi bi-trash"></i> Удалить
|
||
</button>
|
||
{% endif %}
|
||
<button type="button" class="btn btn-primary btn-sm" title="Показать на карте"
|
||
onclick="showSelectedOnMap()">
|
||
<i class="bi bi-map"></i> Карта
|
||
</button>
|
||
<a href="{% url 'mainapp:points_averaging' %}" class="btn btn-warning btn-sm" title="Усреднение точек">
|
||
<i class="bi bi-calculator"></i> Усреднение
|
||
</a>
|
||
<a href="{% url 'mainapp:tech_analyze_list' %}" class="btn btn-info btn-sm" title="Технический анализ">
|
||
<i class="bi bi-gear-wide-connected"></i> Тех. анализ
|
||
</a>
|
||
</div>
|
||
|
||
<!-- Add to List Button -->
|
||
<div>
|
||
<button class="btn btn-outline-success btn-sm" type="button" onclick="addSelectedToList()">
|
||
<i class="bi bi-plus-circle"></i> Добавить к
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Selected Sources Counter Button -->
|
||
<div>
|
||
<button class="btn btn-outline-info btn-sm" type="button" data-bs-toggle="offcanvas"
|
||
data-bs-target="#selectedSourcesOffcanvas" aria-controls="selectedSourcesOffcanvas">
|
||
<i class="bi bi-list-check"></i> Список
|
||
<span id="selectedSourcesCounter" class="badge bg-info" style="display: none;">0</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Filter Toggle Button -->
|
||
<div>
|
||
<button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="offcanvas"
|
||
data-bs-target="#offcanvasFilters" aria-controls="offcanvasFilters">
|
||
<i class="bi bi-funnel"></i> Фильтры
|
||
<span id="filterCounter" class="badge bg-danger" style="display: none;">0</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Column visibility toggle button -->
|
||
<div>
|
||
<div class="dropdown">
|
||
<button type="button" class="btn btn-outline-secondary btn-sm dropdown-toggle"
|
||
id="columnVisibilityDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
||
<i class="bi bi-gear"></i> Колонки
|
||
</button>
|
||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="columnVisibilityDropdown" style="max-height: 400px; overflow-y: auto;">
|
||
<li>
|
||
<label class="dropdown-item">
|
||
<input type="checkbox" id="select-all-columns" onchange="toggleAllColumns(this)"> Выбрать всё
|
||
</label>
|
||
</li>
|
||
<li><hr class="dropdown-divider"></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="0" checked onchange="toggleColumn(this)"> Выбрать</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="1" checked onchange="toggleColumn(this)"> ID</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="2" checked onchange="toggleColumn(this)"> Имя</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="3" checked onchange="toggleColumn(this)"> Спутник</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="4" checked onchange="toggleColumn(this)"> Тип объекта</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="5" checked onchange="toggleColumn(this)"> Принадлежность объекта</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="6" checked onchange="toggleColumn(this)"> Координаты ГЛ</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="7" checked onchange="toggleColumn(this)"> Кол-во ГЛ(точек)</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="8" checked onchange="toggleColumn(this)"> Координаты Кубсата</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="9" checked onchange="toggleColumn(this)"> Координаты визуального наблюдения</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="10" checked onchange="toggleColumn(this)"> Координаты справочные</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="11" checked onchange="toggleColumn(this)"> Примечание</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="12" onchange="toggleColumn(this)"> Создано</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="13" onchange="toggleColumn(this)"> Обновлено</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="14" checked onchange="toggleColumn(this)"> Дата подтверждения</label></li>
|
||
<!-- <li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="15" checked onchange="toggleColumn(this)"> Последний сигнал</label></li> -->
|
||
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="16" checked onchange="toggleColumn(this)"> Действия</label></li>
|
||
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Pagination -->
|
||
<div class="ms-auto">
|
||
{% include 'mainapp/components/_pagination.html' with page_obj=page_obj show_info=True %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Offcanvas Filter Panel -->
|
||
<div class="offcanvas offcanvas-start" tabindex="-1" id="offcanvasFilters" aria-labelledby="offcanvasFiltersLabel">
|
||
<div class="offcanvas-header">
|
||
<h5 class="offcanvas-title" id="offcanvasFiltersLabel">Фильтры</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Закрыть"></button>
|
||
</div>
|
||
<div class="offcanvas-body">
|
||
<form method="get" id="filter-form">
|
||
<!-- Hidden field to preserve polygon filter -->
|
||
{% if polygon_coords %}
|
||
<input type="hidden" name="polygon" value="{{ polygon_coords }}">
|
||
{% endif %}
|
||
|
||
<!-- Polygon Filter Section -->
|
||
<div class="mb-3">
|
||
<label class="form-label fw-bold">
|
||
<i class="bi bi-pentagon"></i> Фильтр по полигону
|
||
</label>
|
||
<div class="d-grid gap-2">
|
||
<button type="button" class="btn btn-outline-success btn-sm"
|
||
onclick="openPolygonFilterMap()">
|
||
<i class="bi bi-pentagon"></i> Нарисовать полигон
|
||
{% if polygon_coords %}
|
||
<span class="badge bg-success ms-1">✓ Активен</span>
|
||
{% endif %}
|
||
</button>
|
||
{% if polygon_coords %}
|
||
<button type="button" class="btn btn-outline-danger btn-sm"
|
||
onclick="clearPolygonFilter()" title="Очистить фильтр по полигону">
|
||
<i class="bi bi-x-circle"></i> Очистить полигон
|
||
</button>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<hr class="my-3">
|
||
|
||
<!-- Satellite Selection - Multi-select -->
|
||
<div class="mb-2">
|
||
<label class="form-label">Спутник:</label>
|
||
<div class="d-flex justify-content-between mb-1">
|
||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||
onclick="selectAllOptions('satellite_id', true)">Выбрать</button>
|
||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||
onclick="selectAllOptions('satellite_id', false)">Снять</button>
|
||
</div>
|
||
<select name="satellite_id" class="form-select form-select-sm mb-2" multiple size="6">
|
||
{% for satellite in satellites %}
|
||
<option value="{{ satellite.id }}" {% if satellite.id in selected_satellites %}selected{% endif %}>
|
||
{{ satellite.name }}
|
||
</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Coordinates Average Filter -->
|
||
<div class="mb-2">
|
||
<label class="form-label">Усредненные координаты:</label>
|
||
<div>
|
||
<div class="form-check form-check-inline">
|
||
<input class="form-check-input" type="checkbox" name="has_coords_average" id="has_coords_average_1"
|
||
value="1" {% if has_coords_average == '1' %}checked{% endif %}>
|
||
<label class="form-check-label" for="has_coords_average_1">Есть</label>
|
||
</div>
|
||
<div class="form-check form-check-inline">
|
||
<input class="form-check-input" type="checkbox" name="has_coords_average" id="has_coords_average_0"
|
||
value="0" {% if has_coords_average == '0' %}checked{% endif %}>
|
||
<label class="form-check-label" for="has_coords_average_0">Нет</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Kubsat Coordinates Filter -->
|
||
<div class="mb-2">
|
||
<label class="form-label">Координаты Кубсата:</label>
|
||
<div>
|
||
<div class="form-check form-check-inline">
|
||
<input class="form-check-input" type="checkbox" name="has_coords_kupsat" id="has_coords_kupsat_1"
|
||
value="1" {% if has_coords_kupsat == '1' %}checked{% endif %}>
|
||
<label class="form-check-label" for="has_coords_kupsat_1">Есть</label>
|
||
</div>
|
||
<div class="form-check form-check-inline">
|
||
<input class="form-check-input" type="checkbox" name="has_coords_kupsat" id="has_coords_kupsat_0"
|
||
value="0" {% if has_coords_kupsat == '0' %}checked{% endif %}>
|
||
<label class="form-check-label" for="has_coords_kupsat_0">Нет</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Valid Coordinates Filter -->
|
||
<div class="mb-2">
|
||
<label class="form-label">Координаты визуального наблюдения:</label>
|
||
<div>
|
||
<div class="form-check form-check-inline">
|
||
<input class="form-check-input" type="checkbox" name="has_coords_valid" id="has_coords_valid_1"
|
||
value="1" {% if has_coords_valid == '1' %}checked{% endif %}>
|
||
<label class="form-check-label" for="has_coords_valid_1">Есть</label>
|
||
</div>
|
||
<div class="form-check form-check-inline">
|
||
<input class="form-check-input" type="checkbox" name="has_coords_valid" id="has_coords_valid_0"
|
||
value="0" {% if has_coords_valid == '0' %}checked{% endif %}>
|
||
<label class="form-check-label" for="has_coords_valid_0">Нет</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Reference Coordinates Filter -->
|
||
<div class="mb-2">
|
||
<label class="form-label">Координаты справочные:</label>
|
||
<div>
|
||
<div class="form-check form-check-inline">
|
||
<input class="form-check-input" type="checkbox" name="has_coords_reference" id="has_coords_reference_1"
|
||
value="1" {% if has_coords_reference == '1' %}checked{% endif %}>
|
||
<label class="form-check-label" for="has_coords_reference_1">Есть</label>
|
||
</div>
|
||
<div class="form-check form-check-inline">
|
||
<input class="form-check-input" type="checkbox" name="has_coords_reference" id="has_coords_reference_0"
|
||
value="0" {% if has_coords_reference == '0' %}checked{% endif %}>
|
||
<label class="form-check-label" for="has_coords_reference_0">Нет</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ObjectInfo Filter -->
|
||
<div class="mb-2">
|
||
<label class="form-label">Тип объекта:</label>
|
||
<div class="d-flex justify-content-between mb-1">
|
||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||
onclick="selectAllOptions('info_id', true)">Выбрать</button>
|
||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||
onclick="selectAllOptions('info_id', false)">Снять</button>
|
||
</div>
|
||
<select name="info_id" class="form-select form-select-sm mb-2" multiple size="4">
|
||
{% for info in object_infos %}
|
||
<option value="{{ info.id }}" {% if info.id in selected_info %}selected{% endif %}>
|
||
{{ info.name }}
|
||
</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
|
||
<!-- ObjectOwnership Filter -->
|
||
<div class="mb-2">
|
||
<label class="form-label">Принадлежность объекта:</label>
|
||
<div class="d-flex justify-content-between mb-1">
|
||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||
onclick="selectAllOptions('ownership_id', true)">Выбрать</button>
|
||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||
onclick="selectAllOptions('ownership_id', false)">Снять</button>
|
||
</div>
|
||
<select name="ownership_id" class="form-select form-select-sm mb-2" multiple size="4">
|
||
{% for ownership in object_ownerships %}
|
||
<option value="{{ ownership.id }}" {% if ownership.id in selected_ownership %}selected{% endif %}>
|
||
{{ ownership.name }}
|
||
</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Point Count Filter -->
|
||
<div class="mb-2">
|
||
<label class="form-label">Количество точек:</label>
|
||
<input type="number" name="objitem_count_min" class="form-control form-control-sm mb-1"
|
||
placeholder="От" value="{{ objitem_count_min|default:'' }}">
|
||
<input type="number" name="objitem_count_max" class="form-control form-control-sm"
|
||
placeholder="До" value="{{ objitem_count_max|default:'' }}">
|
||
</div>
|
||
|
||
<!-- Date Filter -->
|
||
<div class="mb-2">
|
||
<label class="form-label">Дата создания:</label>
|
||
<input type="date" name="date_from" id="date_from" class="form-control form-control-sm mb-1"
|
||
placeholder="От" value="{{ date_from|default:'' }}">
|
||
<input type="date" name="date_to" id="date_to" class="form-control form-control-sm"
|
||
placeholder="До" value="{{ date_to|default:'' }}">
|
||
</div>
|
||
|
||
<hr class="my-3">
|
||
<h6 class="text-muted mb-2"><i class="bi bi-sliders"></i> Фильтры по параметрам точек</h6>
|
||
|
||
<!-- Geo Timestamp Filter -->
|
||
<div class="mb-2">
|
||
<label class="form-label">Дата ГЛ:</label>
|
||
<input type="date" name="geo_date_from" id="geo_date_from" class="form-control form-control-sm mb-1"
|
||
placeholder="От" value="{{ geo_date_from|default:'' }}">
|
||
<input type="date" name="geo_date_to" id="geo_date_to" class="form-control form-control-sm"
|
||
placeholder="До" value="{{ geo_date_to|default:'' }}">
|
||
</div>
|
||
|
||
<!-- Polarization Selection - Multi-select -->
|
||
<div class="mb-2">
|
||
<label class="form-label">Поляризация:</label>
|
||
<div class="d-flex justify-content-between mb-1">
|
||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||
onclick="selectAllOptions('polarization_id', true)">Выбрать</button>
|
||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||
onclick="selectAllOptions('polarization_id', false)">Снять</button>
|
||
</div>
|
||
<select name="polarization_id" class="form-select form-select-sm mb-2" multiple size="4">
|
||
{% for polarization in polarizations %}
|
||
<option value="{{ polarization.id }}" {% if polarization.id in selected_polarizations %}selected{% endif %}>
|
||
{{ polarization.name }}
|
||
</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Modulation Selection - Multi-select -->
|
||
<div class="mb-2">
|
||
<label class="form-label">Модуляция:</label>
|
||
<div class="d-flex justify-content-between mb-1">
|
||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||
onclick="selectAllOptions('modulation_id', true)">Выбрать</button>
|
||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||
onclick="selectAllOptions('modulation_id', false)">Снять</button>
|
||
</div>
|
||
<select name="modulation_id" class="form-select form-select-sm mb-2" multiple size="4">
|
||
{% for modulation in modulations %}
|
||
<option value="{{ modulation.id }}" {% if modulation.id in selected_modulations %}selected{% endif %}>
|
||
{{ modulation.name }}
|
||
</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Frequency Filter -->
|
||
<div class="mb-2">
|
||
<label class="form-label">Частота, МГц:</label>
|
||
<input type="number" step="0.001" name="freq_min" class="form-control form-control-sm mb-1"
|
||
placeholder="От" value="{{ freq_min|default:'' }}">
|
||
<input type="number" step="0.001" name="freq_max" class="form-control form-control-sm"
|
||
placeholder="До" value="{{ freq_max|default:'' }}">
|
||
</div>
|
||
|
||
<!-- Frequency Range (Bandwidth) Filter -->
|
||
<div class="mb-2">
|
||
<label class="form-label">Полоса, МГц:</label>
|
||
<input type="number" step="0.001" name="freq_range_min" class="form-control form-control-sm mb-1"
|
||
placeholder="От" value="{{ freq_range_min|default:'' }}">
|
||
<input type="number" step="0.001" name="freq_range_max" class="form-control form-control-sm"
|
||
placeholder="До" value="{{ freq_range_max|default:'' }}">
|
||
</div>
|
||
|
||
<!-- Symbol Rate Filter -->
|
||
<div class="mb-2">
|
||
<label class="form-label">Символьная скорость, БОД:</label>
|
||
<input type="number" step="0.001" name="bod_velocity_min" class="form-control form-control-sm mb-1"
|
||
placeholder="От" value="{{ bod_velocity_min|default:'' }}">
|
||
<input type="number" step="0.001" name="bod_velocity_max" class="form-control form-control-sm"
|
||
placeholder="До" value="{{ bod_velocity_max|default:'' }}">
|
||
</div>
|
||
|
||
<!-- SNR Filter -->
|
||
<div class="mb-2">
|
||
<label class="form-label">ОСШ, дБ:</label>
|
||
<input type="number" step="0.1" name="snr_min" class="form-control form-control-sm mb-1"
|
||
placeholder="От" value="{{ snr_min|default:'' }}">
|
||
<input type="number" step="0.1" name="snr_max" class="form-control form-control-sm"
|
||
placeholder="До" value="{{ snr_max|default:'' }}">
|
||
</div>
|
||
|
||
<!-- Mirrors Selection - Multi-select -->
|
||
<div class="mb-2">
|
||
<label class="form-label">Зеркала:</label>
|
||
<div class="d-flex justify-content-between mb-1">
|
||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||
onclick="selectAllOptions('mirror_id', true)">Выбрать</button>
|
||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||
onclick="selectAllOptions('mirror_id', false)">Снять</button>
|
||
</div>
|
||
<select name="mirror_id" class="form-select form-select-sm mb-2" multiple size="4">
|
||
{% for mirror in mirrors %}
|
||
<option value="{{ mirror.id }}" {% if mirror.id in selected_mirrors %}selected{% endif %}>
|
||
{{ mirror.name }}
|
||
</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Apply Filters and Reset Buttons -->
|
||
<div class="d-grid gap-2 mt-2">
|
||
<button type="submit" class="btn btn-primary btn-sm">Применить</button>
|
||
<a href="?" class="btn btn-secondary btn-sm">Сбросить</a>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Main Table -->
|
||
<div class="row">
|
||
<div class="col-12">
|
||
<div class="card">
|
||
<div class="card-body p-0">
|
||
<div class="table-responsive" style="max-height: 75vh; overflow-y: auto;">
|
||
<table class="table table-striped table-hover table-sm table-bordered" style="font-size: 0.85rem;">
|
||
<thead class="table-dark sticky-top">
|
||
<tr>
|
||
<th scope="col" class="text-center" style="width: 3%;">
|
||
<input type="checkbox" id="select-all" class="form-check-input">
|
||
</th>
|
||
<th scope="col" class="text-center" style="min-width: 60px;">
|
||
{% include 'mainapp/components/_sort_header.html' with field='id' label='ID' current_sort=sort %}
|
||
</th>
|
||
<th scope="col" style="min-width: 150px;">Имя</th>
|
||
<th scope="col" style="min-width: 120px;">Спутник</th>
|
||
<th scope="col" style="min-width: 120px;">Тип объекта</th>
|
||
<th scope="col" style="min-width: 150px;">Принадлежность объекта</th>
|
||
<th scope="col" style="min-width: 150px;">Координаты ГЛ</th>
|
||
<th scope="col" class="text-center" style="min-width: 100px;">
|
||
{% include 'mainapp/components/_sort_header.html' with field='objitem_count' label='Кол-во ГЛ(точек)' current_sort=sort %}
|
||
</th>
|
||
<th scope="col" style="min-width: 150px;">Координаты Кубсата</th>
|
||
<th scope="col" style="min-width: 150px;">Координаты визуального наблюдения</th>
|
||
<th scope="col" style="min-width: 150px;">Координаты справочные</th>
|
||
<th scope="col" style="min-width: 120px;">Примечание</th>
|
||
<th scope="col" style="min-width: 120px;">
|
||
{% include 'mainapp/components/_sort_header.html' with field='created_at' label='Создано' current_sort=sort %}
|
||
</th>
|
||
<th scope="col" style="min-width: 120px;">
|
||
{% include 'mainapp/components/_sort_header.html' with field='updated_at' label='Обновлено' current_sort=sort %}
|
||
</th>
|
||
<th scope="col" style="min-width: 150px;">Дата подтверждения</th>
|
||
<!-- <th scope="col" style="min-width: 150px;">Последний сигнал</th> -->
|
||
<th scope="col" class="text-center" style="min-width: 150px;">Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for source in processed_sources %}
|
||
<tr>
|
||
<td class="text-center">
|
||
<input type="checkbox" class="form-check-input item-checkbox"
|
||
value="{{ source.id }}">
|
||
</td>
|
||
<td class="text-center">{{ source.id }}</td>
|
||
<td>{{ source.name }}</td>
|
||
<td>
|
||
{% if source.satellite_id %}
|
||
<a href="#" class="text-decoration-underline"
|
||
onclick="showSatelliteModal({{ source.satellite_id }}); return false;">
|
||
{{ source.satellite }}
|
||
</a>
|
||
{% else %}
|
||
{{ source.satellite }}
|
||
{% endif %}
|
||
</td>
|
||
<td>{{ source.info }}</td>
|
||
<td>
|
||
{% if source.ownership == "ТВ" and source.has_lyngsat %}
|
||
<a href="#" class="text-primary text-decoration-none"
|
||
onclick="showLyngsatModal({{ source.lyngsat_id }}); return false;">
|
||
<i class="bi bi-tv"></i> {{ source.ownership }}
|
||
</a>
|
||
{% else %}
|
||
{{ source.ownership }}
|
||
{% endif %}
|
||
</td>
|
||
<td>{{ source.coords_average }}</td>
|
||
<td class="text-center">{{ source.objitem_count }}</td>
|
||
<td>{{ source.coords_kupsat }}</td>
|
||
<td>{{ source.coords_valid }}</td>
|
||
<td>{{ source.coords_reference }}</td>
|
||
<td>{{ source.note }}</td>
|
||
<td>{{ source.created_at|date:"d.m.Y H:i" }}</td>
|
||
<td>{{ source.updated_at|date:"d.m.Y H:i" }}</td>
|
||
<td>{{ source.confirm_at|date:"d.m.Y H:i"|default:"-" }}</td>
|
||
<!-- <td>{{ source.last_signal_at|date:"d.m.Y H:i"|default:"-" }}</td> -->
|
||
<td class="text-center">
|
||
<div class="btn-group" role="group">
|
||
{% if source.objitem_count > 0 %}
|
||
<a href="{% url 'mainapp:show_source_with_points_map' source.id %}"
|
||
target="_blank"
|
||
class="btn btn-sm btn-outline-success"
|
||
title="Показать объект с точками на карте">
|
||
<i class="bi bi-geo-alt"></i>
|
||
<span class="badge bg-success">{{ source.objitem_count }}</span>
|
||
</a>
|
||
{% else %}
|
||
<button type="button" class="btn btn-sm btn-outline-secondary" disabled title="Нет точек для отображения">
|
||
<i class="bi bi-geo-alt"></i>
|
||
</button>
|
||
{% endif %}
|
||
|
||
{% if source.objitem_count > 1 %}
|
||
<a href="{% url 'mainapp:show_source_averaging_map' source.id %}"
|
||
target="_blank"
|
||
class="btn btn-sm btn-outline-info"
|
||
title="Визуализация усреднения">
|
||
<i class="bi bi-diagram-3"></i>
|
||
</a>
|
||
{% else %}
|
||
<button type="button" class="btn btn-sm btn-outline-secondary" disabled title="Недостаточно точек для усреднения">
|
||
<i class="bi bi-diagram-3"></i>
|
||
</button>
|
||
{% endif %}
|
||
|
||
<button type="button" class="btn btn-sm btn-outline-primary"
|
||
onclick="showSourceDetails({{ source.id }})"
|
||
title="Показать детали">
|
||
<i class="bi bi-eye"></i>
|
||
</button>
|
||
|
||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
||
<a href="{% url 'mainapp:source_update' source.id %}"
|
||
class="btn btn-sm btn-outline-warning"
|
||
title="Редактировать объект">
|
||
<i class="bi bi-pencil"></i>
|
||
</a>
|
||
{% else %}
|
||
<button type="button" class="btn btn-sm btn-outline-secondary" disabled title="Недостаточно прав">
|
||
<i class="bi bi-pencil"></i>
|
||
</button>
|
||
{% endif %}
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
{% empty %}
|
||
<tr>
|
||
<td colspan="15" class="text-center text-muted">Нет данных для отображения</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal for Source Details -->
|
||
<div class="modal fade" id="sourceDetailsModal" tabindex="-1" aria-labelledby="sourceDetailsModalLabel" aria-hidden="true">
|
||
<div class="modal-dialog modal-fullscreen">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="sourceDetailsModalLabel">Детали объекта #<span id="modalSourceId"></span></h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div id="modalLoadingSpinner" class="text-center py-4">
|
||
<div class="spinner-border text-primary" role="status">
|
||
<span class="visually-hidden">Загрузка...</span>
|
||
</div>
|
||
</div>
|
||
<div id="modalErrorMessage" class="alert alert-danger" style="display: none;"></div>
|
||
<div id="modalContent" style="display: none;">
|
||
<!-- Marks Section -->
|
||
<div id="marksSection" class="mb-3" style="display: none;">
|
||
<h6 class="mb-2">Наличие сигнала объекта (<span id="marksCount">0</span>):</h6>
|
||
<div class="table-responsive" style="max-height: 200px; overflow-y: auto;">
|
||
<table class="table table-sm table-bordered">
|
||
<thead class="table-light">
|
||
<tr>
|
||
<th style="width: 20%;">Наличие сигнала</th>
|
||
<th style="width: 40%;">Дата и время</th>
|
||
<th style="width: 40%;">Пользователь</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="marksTableBody">
|
||
<!-- Marks will be loaded here -->
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||
<h6 class="mb-0">Связанные точки (<span id="objitemCount">0</span>):</h6>
|
||
<div class="dropdown">
|
||
<button type="button" class="btn btn-outline-secondary btn-sm dropdown-toggle"
|
||
id="modalColumnVisibilityDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
||
<i class="bi bi-gear"></i> Колонки
|
||
</button>
|
||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="modalColumnVisibilityDropdown" style="max-height: 400px; overflow-y: auto;">
|
||
<li>
|
||
<label class="dropdown-item">
|
||
<input type="checkbox" id="modal-select-all-columns" onchange="toggleAllModalColumns(this)"> Выбрать всё
|
||
</label>
|
||
</li>
|
||
<li><hr class="dropdown-divider"></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="0" checked onchange="toggleModalColumn(this)"> Выбрать</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="1" checked onchange="toggleModalColumn(this)"> ID</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="2" checked onchange="toggleModalColumn(this)"> Имя</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="3" checked onchange="toggleModalColumn(this)"> Спутник</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="4" checked onchange="toggleModalColumn(this)"> Транспондер</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="5" checked onchange="toggleModalColumn(this)"> Частота, МГц</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="6" checked onchange="toggleModalColumn(this)"> Полоса, МГц</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="7" checked onchange="toggleModalColumn(this)"> Поляризация</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="8" checked onchange="toggleModalColumn(this)"> Сим. V</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="9" checked onchange="toggleModalColumn(this)"> Модул</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="10" checked onchange="toggleModalColumn(this)"> ОСШ</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="11" checked onchange="toggleModalColumn(this)"> Время ГЛ</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="12" checked onchange="toggleModalColumn(this)"> Местоположение</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="13" checked onchange="toggleModalColumn(this)"> Геолокация</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="14" checked onchange="toggleModalColumn(this)"> Обновлено</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="15" checked onchange="toggleModalColumn(this)"> Кем(обн)</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="16" onchange="toggleModalColumn(this)"> Создано</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="17" onchange="toggleModalColumn(this)"> Кем(созд)</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="18" onchange="toggleModalColumn(this)"> Комментарий</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="19" onchange="toggleModalColumn(this)"> Усреднённое</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="20" onchange="toggleModalColumn(this)"> Стандарт</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="21" checked onchange="toggleModalColumn(this)"> Тип объекта</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="22" onchange="toggleModalColumn(this)"> Sigma</label></li>
|
||
<li><label class="dropdown-item"><input type="checkbox" class="modal-column-toggle" data-column="23" checked onchange="toggleModalColumn(this)"> Зеркала</label></li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
<div class="table-responsive" style="max-height: 80vh; overflow-y: auto;">
|
||
<table class="table table-striped table-hover table-sm table-bordered" style="font-size: 0.85rem;">
|
||
<thead class="table-light sticky-top">
|
||
<tr>
|
||
<th class="text-center" style="width: 3%;">
|
||
<input type="checkbox" id="modal-select-all" class="form-check-input">
|
||
</th>
|
||
<th class="text-center" style="min-width: 60px;">ID</th>
|
||
<th style="min-width: 120px;">Имя</th>
|
||
<th style="min-width: 120px;">Спутник</th>
|
||
<th style="min-width: 100px;">Транспондер</th>
|
||
<th style="min-width: 100px;">Частота, МГц</th>
|
||
<th style="min-width: 100px;">Полоса, МГц</th>
|
||
<th style="min-width: 100px;">Поляризация</th>
|
||
<th style="min-width: 100px;">Сим. V</th>
|
||
<th style="min-width: 100px;">Модул</th>
|
||
<th style="min-width: 80px;">ОСШ</th>
|
||
<th style="min-width: 120px;">Время ГЛ</th>
|
||
<th style="min-width: 120px;">Местоположение</th>
|
||
<th style="min-width: 120px;">Геолокация</th>
|
||
<th style="min-width: 120px;">Обновлено</th>
|
||
<th style="min-width: 100px;">Кем(обн)</th>
|
||
<th style="min-width: 120px;">Создано</th>
|
||
<th style="min-width: 100px;">Кем(созд)</th>
|
||
<th style="min-width: 150px;">Комментарий</th>
|
||
<th style="min-width: 100px;">Усреднённое</th>
|
||
<th style="min-width: 100px;">Стандарт</th>
|
||
<th style="min-width: 100px;">Тип объекта</th>
|
||
<th style="min-width: 80px;">Sigma</th>
|
||
<th style="min-width: 80px;">Зеркала</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="objitemTableBody">
|
||
<!-- Data will be loaded here via AJAX -->
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
<div id="modalNoData" class="text-center text-muted py-4" style="display: none;">
|
||
Нет связанных точек
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
<script src="{% static 'leaflet/leaflet.js' %}"></script>
|
||
<script src="{% static 'leaflet-draw/leaflet.draw.js' %}"></script>
|
||
<script>
|
||
// Polygon filter map variables
|
||
let polygonFilterMapInstance = null;
|
||
let drawnItems = null;
|
||
let drawControl = null;
|
||
let currentPolygon = null;
|
||
|
||
// Initialize polygon filter map
|
||
function initPolygonFilterMap() {
|
||
if (polygonFilterMapInstance) {
|
||
return; // Already initialized
|
||
}
|
||
|
||
// Create map centered on Russia
|
||
polygonFilterMapInstance = L.map('polygonFilterMap').setView([55.7558, 37.6173], 4);
|
||
|
||
// Add OpenStreetMap tile layer
|
||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||
attribution: '© OpenStreetMap contributors',
|
||
maxZoom: 19
|
||
}).addTo(polygonFilterMapInstance);
|
||
|
||
// Initialize FeatureGroup to store drawn items
|
||
drawnItems = new L.FeatureGroup();
|
||
polygonFilterMapInstance.addLayer(drawnItems);
|
||
|
||
// Initialize draw control
|
||
drawControl = new L.Control.Draw({
|
||
position: 'topright',
|
||
draw: {
|
||
polygon: {
|
||
allowIntersection: false,
|
||
showArea: true,
|
||
drawError: {
|
||
color: '#e1e100',
|
||
message: '<strong>Ошибка:</strong> полигон не должен пересекать сам себя!'
|
||
},
|
||
shapeOptions: {
|
||
color: '#3388ff',
|
||
fillOpacity: 0.2
|
||
}
|
||
},
|
||
polyline: false,
|
||
rectangle: {
|
||
shapeOptions: {
|
||
color: '#3388ff',
|
||
fillOpacity: 0.2
|
||
}
|
||
},
|
||
circle: false,
|
||
circlemarker: false,
|
||
marker: false
|
||
},
|
||
edit: {
|
||
featureGroup: drawnItems,
|
||
remove: true
|
||
}
|
||
});
|
||
polygonFilterMapInstance.addControl(drawControl);
|
||
|
||
// Handle polygon creation
|
||
polygonFilterMapInstance.on(L.Draw.Event.CREATED, function (event) {
|
||
const layer = event.layer;
|
||
|
||
// Remove existing polygon
|
||
drawnItems.clearLayers();
|
||
|
||
// Add new polygon
|
||
drawnItems.addLayer(layer);
|
||
currentPolygon = layer;
|
||
});
|
||
|
||
// Handle polygon edit
|
||
polygonFilterMapInstance.on(L.Draw.Event.EDITED, function (event) {
|
||
const layers = event.layers;
|
||
layers.eachLayer(function (layer) {
|
||
currentPolygon = layer;
|
||
});
|
||
});
|
||
|
||
// Handle polygon deletion
|
||
polygonFilterMapInstance.on(L.Draw.Event.DELETED, function () {
|
||
currentPolygon = null;
|
||
});
|
||
|
||
// Load existing polygon if present
|
||
{% if polygon_coords %}
|
||
try {
|
||
const coords = {{ polygon_coords|safe }};
|
||
if (coords && coords.length > 0) {
|
||
const latLngs = coords.map(coord => [coord[1], coord[0]]); // [lng, lat] -> [lat, lng]
|
||
const polygon = L.polygon(latLngs, {
|
||
color: '#3388ff',
|
||
fillOpacity: 0.2
|
||
});
|
||
drawnItems.addLayer(polygon);
|
||
currentPolygon = polygon;
|
||
|
||
// Fit map to polygon bounds
|
||
polygonFilterMapInstance.fitBounds(polygon.getBounds());
|
||
}
|
||
} catch (e) {
|
||
console.error('Error loading existing polygon:', e);
|
||
}
|
||
{% endif %}
|
||
}
|
||
|
||
// Open polygon filter map modal
|
||
function openPolygonFilterMap() {
|
||
const modal = new bootstrap.Modal(document.getElementById('polygonFilterModal'));
|
||
modal.show();
|
||
|
||
// Initialize map after modal is shown (to ensure proper rendering)
|
||
setTimeout(() => {
|
||
initPolygonFilterMap();
|
||
if (polygonFilterMapInstance) {
|
||
polygonFilterMapInstance.invalidateSize();
|
||
}
|
||
}, 300);
|
||
}
|
||
|
||
// Clear polygon on map
|
||
function clearPolygonOnMap() {
|
||
if (drawnItems) {
|
||
drawnItems.clearLayers();
|
||
currentPolygon = null;
|
||
}
|
||
}
|
||
|
||
// Apply polygon filter
|
||
function applyPolygonFilter() {
|
||
if (!currentPolygon) {
|
||
alert('Пожалуйста, нарисуйте полигон на карте');
|
||
return;
|
||
}
|
||
|
||
// Get polygon coordinates
|
||
const latLngs = currentPolygon.getLatLngs()[0]; // Get first ring for polygon
|
||
const coords = latLngs.map(latLng => [latLng.lng, latLng.lat]); // [lat, lng] -> [lng, lat]
|
||
|
||
// Close the polygon by adding first point at the end
|
||
coords.push(coords[0]);
|
||
|
||
// Add polygon coordinates to URL and reload
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
urlParams.set('polygon', JSON.stringify(coords));
|
||
urlParams.delete('page'); // Reset to first page
|
||
|
||
window.location.search = urlParams.toString();
|
||
}
|
||
|
||
// Clear polygon filter
|
||
function clearPolygonFilter() {
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
urlParams.delete('polygon');
|
||
urlParams.delete('page');
|
||
window.location.search = urlParams.toString();
|
||
}
|
||
</script>
|
||
<script>
|
||
let lastCheckedIndex = null;
|
||
|
||
function updateRowHighlight(checkbox) {
|
||
const row = checkbox.closest('tr');
|
||
if (checkbox.checked) {
|
||
row.classList.add('selected');
|
||
} else {
|
||
row.classList.remove('selected');
|
||
}
|
||
}
|
||
|
||
function handleCheckboxClick(e) {
|
||
if (e.shiftKey && lastCheckedIndex !== null) {
|
||
const checkboxes = document.querySelectorAll('.item-checkbox');
|
||
const currentIndex = Array.from(checkboxes).indexOf(e.target);
|
||
const startIndex = Math.min(lastCheckedIndex, currentIndex);
|
||
const endIndex = Math.max(lastCheckedIndex, currentIndex);
|
||
|
||
for (let i = startIndex; i <= endIndex; i++) {
|
||
checkboxes[i].checked = e.target.checked;
|
||
updateRowHighlight(checkboxes[i]);
|
||
}
|
||
} else {
|
||
updateRowHighlight(e.target);
|
||
}
|
||
lastCheckedIndex = Array.from(document.querySelectorAll('.item-checkbox')).indexOf(e.target);
|
||
}
|
||
|
||
// Function to show selected sources on map
|
||
function showSelectedOnMap() {
|
||
// Get all checked checkboxes
|
||
const checkedCheckboxes = document.querySelectorAll('.item-checkbox:checked');
|
||
|
||
if (checkedCheckboxes.length === 0) {
|
||
alert('Пожалуйста, выберите хотя бы один объект для отображения на карте');
|
||
return;
|
||
}
|
||
|
||
// Extract IDs from checked checkboxes
|
||
const selectedIds = [];
|
||
checkedCheckboxes.forEach(checkbox => {
|
||
selectedIds.push(checkbox.value);
|
||
});
|
||
|
||
// Build URL with IDs and preserve polygon filter if present
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const polygonParam = urlParams.get('polygon');
|
||
|
||
let url = '{% url "mainapp:show_sources_map" %}' + '?ids=' + selectedIds.join(',');
|
||
if (polygonParam) {
|
||
url += '&polygon=' + encodeURIComponent(polygonParam);
|
||
}
|
||
|
||
window.open(url, '_blank'); // Open in a new tab
|
||
}
|
||
|
||
// Function to delete selected sources
|
||
function deleteSelectedSources() {
|
||
// Get all checked checkboxes
|
||
const checkedCheckboxes = document.querySelectorAll('.item-checkbox:checked');
|
||
|
||
if (checkedCheckboxes.length === 0) {
|
||
alert('Пожалуйста, выберите хотя бы один объект для удаления');
|
||
return;
|
||
}
|
||
|
||
// Extract IDs from checked checkboxes
|
||
const selectedIds = [];
|
||
checkedCheckboxes.forEach(checkbox => {
|
||
selectedIds.push(checkbox.value);
|
||
});
|
||
|
||
// Redirect to confirmation page
|
||
const url = '{% url "mainapp:delete_selected_sources" %}' + '?ids=' + selectedIds.join(',');
|
||
window.location.href = url;
|
||
}
|
||
|
||
// Search functionality
|
||
function performSearch() {
|
||
const searchValue = document.getElementById('toolbar-search').value.trim();
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
|
||
if (searchValue) {
|
||
urlParams.set('search', searchValue);
|
||
} else {
|
||
urlParams.delete('search');
|
||
}
|
||
|
||
urlParams.delete('page');
|
||
// Preserve polygon filter
|
||
// (already in urlParams from window.location.search)
|
||
window.location.search = urlParams.toString();
|
||
}
|
||
|
||
function clearSearch() {
|
||
document.getElementById('toolbar-search').value = '';
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
urlParams.delete('search');
|
||
urlParams.delete('page');
|
||
// Preserve polygon filter
|
||
// (already in urlParams from window.location.search)
|
||
window.location.search = urlParams.toString();
|
||
}
|
||
|
||
// Handle Enter key in search input
|
||
document.getElementById('toolbar-search').addEventListener('keypress', function(e) {
|
||
if (e.key === 'Enter') {
|
||
performSearch();
|
||
}
|
||
});
|
||
|
||
// Items per page functionality
|
||
function updateItemsPerPage() {
|
||
const itemsPerPage = document.getElementById('items-per-page').value;
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
urlParams.set('items_per_page', itemsPerPage);
|
||
urlParams.delete('page');
|
||
// Preserve polygon filter
|
||
// (already in urlParams from window.location.search)
|
||
window.location.search = urlParams.toString();
|
||
}
|
||
|
||
// Sorting functionality is now handled by sorting.js (loaded via base.html)
|
||
|
||
// Setup radio-like behavior for filter checkboxes
|
||
function setupRadioLikeCheckboxes(name) {
|
||
const checkboxes = document.querySelectorAll(`input[name="${name}"]`);
|
||
checkboxes.forEach(checkbox => {
|
||
checkbox.addEventListener('change', function () {
|
||
if (this.checked) {
|
||
checkboxes.forEach(other => {
|
||
if (other !== this) {
|
||
other.checked = false;
|
||
}
|
||
});
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// Function to select/deselect all options in a select element
|
||
function selectAllOptions(selectName, selectAll) {
|
||
const selectElement = document.querySelector(`select[name="${selectName}"]`);
|
||
if (selectElement) {
|
||
for (let i = 0; i < selectElement.options.length; i++) {
|
||
selectElement.options[i].selected = selectAll;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Filter counter functionality
|
||
function updateFilterCounter() {
|
||
const form = document.getElementById('filter-form');
|
||
const formData = new FormData(form);
|
||
let filterCount = 0;
|
||
|
||
// Count non-empty form fields
|
||
for (const [key, value] of formData.entries()) {
|
||
if (value && value.trim() !== '') {
|
||
// For multi-select fields, skip counting individual selections
|
||
if (key === 'satellite_id') {
|
||
continue;
|
||
}
|
||
filterCount++;
|
||
}
|
||
}
|
||
|
||
// Count selected options in satellite multi-select field
|
||
const satelliteSelect = document.querySelector('select[name="satellite_id"]');
|
||
if (satelliteSelect) {
|
||
const selectedOptions = Array.from(satelliteSelect.selectedOptions).filter(opt => opt.selected);
|
||
if (selectedOptions.length > 0) {
|
||
filterCount++;
|
||
}
|
||
}
|
||
|
||
// Check if polygon filter is active
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
if (urlParams.has('polygon')) {
|
||
filterCount++;
|
||
}
|
||
|
||
// Display the filter counter
|
||
const counterElement = document.getElementById('filterCounter');
|
||
if (counterElement) {
|
||
if (filterCount > 0) {
|
||
counterElement.textContent = filterCount;
|
||
counterElement.style.display = 'inline';
|
||
} else {
|
||
counterElement.style.display = 'none';
|
||
}
|
||
}
|
||
}
|
||
|
||
// Column visibility functions with localStorage support
|
||
function getColumnVisibilityKey() {
|
||
return 'sourceListColumnVisibility';
|
||
}
|
||
|
||
function saveColumnVisibility() {
|
||
const columnCheckboxes = document.querySelectorAll('.column-toggle');
|
||
const visibility = {};
|
||
columnCheckboxes.forEach(checkbox => {
|
||
const columnIndex = checkbox.getAttribute('data-column');
|
||
visibility[columnIndex] = checkbox.checked;
|
||
});
|
||
localStorage.setItem(getColumnVisibilityKey(), JSON.stringify(visibility));
|
||
}
|
||
|
||
function loadColumnVisibility() {
|
||
const saved = localStorage.getItem(getColumnVisibilityKey());
|
||
if (saved) {
|
||
return JSON.parse(saved);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function toggleColumn(checkbox) {
|
||
const columnIndex = parseInt(checkbox.getAttribute('data-column'));
|
||
const table = document.querySelector('.table');
|
||
const cells = table.querySelectorAll(`td:nth-child(${columnIndex + 1}), th:nth-child(${columnIndex + 1})`);
|
||
|
||
if (checkbox.checked) {
|
||
cells.forEach(cell => {
|
||
cell.style.display = '';
|
||
});
|
||
} else {
|
||
cells.forEach(cell => {
|
||
cell.style.display = 'none';
|
||
});
|
||
}
|
||
|
||
// Save state after toggle
|
||
saveColumnVisibility();
|
||
}
|
||
|
||
function toggleAllColumns(selectAllCheckbox) {
|
||
const columnCheckboxes = document.querySelectorAll('.column-toggle');
|
||
columnCheckboxes.forEach(checkbox => {
|
||
checkbox.checked = selectAllCheckbox.checked;
|
||
toggleColumn(checkbox);
|
||
});
|
||
}
|
||
|
||
// Initialize column visibility - hide Создано and Обновлено columns by default
|
||
function initColumnVisibility() {
|
||
const savedVisibility = loadColumnVisibility();
|
||
|
||
if (savedVisibility) {
|
||
// Restore saved state
|
||
const columnCheckboxes = document.querySelectorAll('.column-toggle');
|
||
columnCheckboxes.forEach(checkbox => {
|
||
const columnIndex = checkbox.getAttribute('data-column');
|
||
if (savedVisibility.hasOwnProperty(columnIndex)) {
|
||
checkbox.checked = savedVisibility[columnIndex];
|
||
toggleColumnWithoutSave(checkbox);
|
||
}
|
||
});
|
||
} else {
|
||
// Default state: hide Создано and Обновлено columns
|
||
const createdAtCheckbox = document.querySelector('input[data-column="11"]');
|
||
const updatedAtCheckbox = document.querySelector('input[data-column="12"]');
|
||
const noteCheckbox = document.querySelector('input[data-column="13"]');
|
||
|
||
if (createdAtCheckbox) {
|
||
createdAtCheckbox.checked = false;
|
||
toggleColumnWithoutSave(createdAtCheckbox);
|
||
}
|
||
|
||
if (updatedAtCheckbox) {
|
||
updatedAtCheckbox.checked = false;
|
||
toggleColumnWithoutSave(updatedAtCheckbox);
|
||
}
|
||
|
||
if (noteCheckbox) {
|
||
noteCheckbox.checked = false;
|
||
toggleColumnWithoutSave(noteCheckbox);
|
||
}
|
||
|
||
// Save initial state
|
||
saveColumnVisibility();
|
||
}
|
||
}
|
||
|
||
// Helper function to toggle without saving (used during initialization)
|
||
function toggleColumnWithoutSave(checkbox) {
|
||
const columnIndex = parseInt(checkbox.getAttribute('data-column'));
|
||
const table = document.querySelector('.table');
|
||
const cells = table.querySelectorAll(`td:nth-child(${columnIndex + 1}), th:nth-child(${columnIndex + 1})`);
|
||
|
||
if (checkbox.checked) {
|
||
cells.forEach(cell => {
|
||
cell.style.display = '';
|
||
});
|
||
} else {
|
||
cells.forEach(cell => {
|
||
cell.style.display = 'none';
|
||
});
|
||
}
|
||
}
|
||
|
||
// Initialize selected sources array from localStorage
|
||
function loadSelectedSourcesFromStorage() {
|
||
try {
|
||
const storedSources = localStorage.getItem('selectedSources');
|
||
if (storedSources) {
|
||
window.selectedSources = JSON.parse(storedSources);
|
||
} else {
|
||
window.selectedSources = [];
|
||
}
|
||
} catch (e) {
|
||
console.error('Error loading selected sources from storage:', e);
|
||
window.selectedSources = [];
|
||
}
|
||
}
|
||
|
||
// Function to save selected sources to localStorage
|
||
window.saveSelectedSourcesToStorage = function () {
|
||
try {
|
||
localStorage.setItem('selectedSources', JSON.stringify(window.selectedSources));
|
||
} catch (e) {
|
||
console.error('Error saving selected sources to storage:', e);
|
||
}
|
||
}
|
||
|
||
// Function to update the selected sources counter
|
||
window.updateSelectedSourcesCounter = function () {
|
||
const counterElement = document.getElementById('selectedSourcesCounter');
|
||
if (window.selectedSources && window.selectedSources.length > 0) {
|
||
counterElement.textContent = window.selectedSources.length;
|
||
counterElement.style.display = 'inline';
|
||
} else {
|
||
counterElement.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// Function to add selected sources to the list
|
||
window.addSelectedToList = function () {
|
||
const checkedCheckboxes = document.querySelectorAll('.item-checkbox:checked');
|
||
|
||
if (checkedCheckboxes.length === 0) {
|
||
alert('Пожалуйста, выберите хотя бы один источник для добавления в список');
|
||
return;
|
||
}
|
||
|
||
// Get the data for each selected row and add to the selectedSources array
|
||
checkedCheckboxes.forEach(checkbox => {
|
||
const row = checkbox.closest('tr');
|
||
const sourceId = checkbox.value;
|
||
|
||
const sourceExists = window.selectedSources.some(source => source.id === sourceId);
|
||
if (!sourceExists) {
|
||
const rowData = {
|
||
id: sourceId,
|
||
name: row.cells[2].textContent.trim(),
|
||
satellite: row.cells[3].textContent.trim(),
|
||
info: row.cells[4].textContent.trim(),
|
||
ownership: row.cells[5].textContent.trim(),
|
||
coords_average: row.cells[6].textContent.trim(),
|
||
objitem_count: row.cells[7].textContent.trim()
|
||
};
|
||
|
||
window.selectedSources.push(rowData);
|
||
}
|
||
});
|
||
|
||
// Update the counter
|
||
updateSelectedSourcesCounter();
|
||
|
||
// Save selected sources to localStorage
|
||
saveSelectedSourcesToStorage();
|
||
|
||
// Clear selections in the main table
|
||
const itemCheckboxes = document.querySelectorAll('.item-checkbox');
|
||
itemCheckboxes.forEach(checkbox => {
|
||
checkbox.checked = false;
|
||
updateRowHighlight(checkbox);
|
||
});
|
||
const selectAllCheckbox = document.getElementById('select-all');
|
||
if (selectAllCheckbox) {
|
||
selectAllCheckbox.checked = false;
|
||
}
|
||
}
|
||
|
||
// Initialize on page load
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// Load selected sources from localStorage
|
||
loadSelectedSourcesFromStorage();
|
||
updateSelectedSourcesCounter();
|
||
|
||
// Setup select-all checkbox
|
||
const selectAllCheckbox = document.getElementById('select-all');
|
||
const itemCheckboxes = document.querySelectorAll('.item-checkbox');
|
||
|
||
if (selectAllCheckbox && itemCheckboxes.length > 0) {
|
||
selectAllCheckbox.addEventListener('change', function () {
|
||
itemCheckboxes.forEach(checkbox => {
|
||
checkbox.checked = selectAllCheckbox.checked;
|
||
updateRowHighlight(checkbox);
|
||
});
|
||
});
|
||
|
||
itemCheckboxes.forEach(checkbox => {
|
||
checkbox.addEventListener('change', function () {
|
||
const allChecked = Array.from(itemCheckboxes).every(cb => cb.checked);
|
||
selectAllCheckbox.checked = allChecked;
|
||
});
|
||
|
||
// Add shift-click handler
|
||
checkbox.addEventListener('click', handleCheckboxClick);
|
||
});
|
||
}
|
||
|
||
// Setup radio-like checkboxes for filters
|
||
setupRadioLikeCheckboxes('has_coords_average');
|
||
setupRadioLikeCheckboxes('has_coords_kupsat');
|
||
setupRadioLikeCheckboxes('has_coords_valid');
|
||
setupRadioLikeCheckboxes('has_coords_reference');
|
||
|
||
// Update filter counter on page load
|
||
updateFilterCounter();
|
||
|
||
// Add event listeners to form elements to update counter when filters change
|
||
const form = document.getElementById('filter-form');
|
||
if (form) {
|
||
const inputFields = form.querySelectorAll('input[type="text"], input[type="number"], input[type="date"]');
|
||
inputFields.forEach(input => {
|
||
input.addEventListener('input', updateFilterCounter);
|
||
input.addEventListener('change', updateFilterCounter);
|
||
});
|
||
|
||
const selectFields = form.querySelectorAll('select');
|
||
selectFields.forEach(select => {
|
||
select.addEventListener('change', updateFilterCounter);
|
||
});
|
||
|
||
const checkboxFields = form.querySelectorAll('input[type="checkbox"]');
|
||
checkboxFields.forEach(checkbox => {
|
||
checkbox.addEventListener('change', updateFilterCounter);
|
||
});
|
||
}
|
||
|
||
// Update counter when offcanvas is shown
|
||
const offcanvasElement = document.getElementById('offcanvasFilters');
|
||
if (offcanvasElement) {
|
||
offcanvasElement.addEventListener('show.bs.offcanvas', updateFilterCounter);
|
||
}
|
||
|
||
// Initialize column visibility
|
||
setTimeout(initColumnVisibility, 100);
|
||
|
||
// Update the selected sources table when the offcanvas is shown
|
||
const selectedSourcesOffcanvas = document.getElementById('selectedSourcesOffcanvas');
|
||
if (selectedSourcesOffcanvas) {
|
||
selectedSourcesOffcanvas.addEventListener('show.bs.offcanvas', function () {
|
||
populateSelectedSourcesTable();
|
||
});
|
||
}
|
||
});
|
||
|
||
// Function to populate the selected sources table in the offcanvas
|
||
function populateSelectedSourcesTable() {
|
||
const tableBody = document.getElementById('selected-sources-table-body');
|
||
const noDataDiv = document.getElementById('selectedSourcesNoData');
|
||
const table = tableBody.closest('.table-responsive');
|
||
const offcanvasCounter = document.getElementById('selectedSourcesOffcanvasCounter');
|
||
|
||
if (!tableBody) return;
|
||
|
||
// Clear existing rows
|
||
tableBody.innerHTML = '';
|
||
|
||
if (!window.selectedSources || window.selectedSources.length === 0) {
|
||
// Show no data message
|
||
if (table) table.style.display = 'none';
|
||
if (noDataDiv) noDataDiv.style.display = 'block';
|
||
if (offcanvasCounter) offcanvasCounter.textContent = '0';
|
||
return;
|
||
}
|
||
|
||
// Hide no data message and show table
|
||
if (table) table.style.display = 'block';
|
||
if (noDataDiv) noDataDiv.style.display = 'none';
|
||
if (offcanvasCounter) offcanvasCounter.textContent = window.selectedSources.length;
|
||
|
||
// Add rows for each selected source
|
||
window.selectedSources.forEach((source, index) => {
|
||
const row = document.createElement('tr');
|
||
row.innerHTML = `
|
||
<td class="text-center">
|
||
<input type="checkbox" class="form-check-input selected-source-checkbox" value="${source.id}">
|
||
</td>
|
||
<td>${source.id}</td>
|
||
<td>${source.name}</td>
|
||
<td>${source.satellite}</td>
|
||
<td>${source.info}</td>
|
||
<td>${source.ownership}</td>
|
||
<td>${source.coords_average}</td>
|
||
<td class="text-center">${source.objitem_count}</td>
|
||
`;
|
||
tableBody.appendChild(row);
|
||
});
|
||
}
|
||
|
||
// Function to remove selected sources from the list
|
||
function removeSelectedSources() {
|
||
const checkboxes = document.querySelectorAll('#selected-sources-table-body .selected-source-checkbox:checked');
|
||
if (checkboxes.length === 0) {
|
||
alert('Пожалуйста, выберите хотя бы один источник для удаления из списка');
|
||
return;
|
||
}
|
||
|
||
// Get IDs of sources to remove
|
||
const idsToRemove = Array.from(checkboxes).map(checkbox => checkbox.value);
|
||
|
||
// Remove sources from the selectedSources array
|
||
window.selectedSources = window.selectedSources.filter(source => !idsToRemove.includes(source.id));
|
||
|
||
// Save selected sources to localStorage
|
||
saveSelectedSourcesToStorage();
|
||
|
||
// Update the counter and table
|
||
updateSelectedSourcesCounter();
|
||
populateSelectedSourcesTable();
|
||
}
|
||
|
||
// Function to show selected sources on map
|
||
function showSelectedSourcesOnMap() {
|
||
if (!window.selectedSources || window.selectedSources.length === 0) {
|
||
alert('Список источников пуст');
|
||
return;
|
||
}
|
||
|
||
const selectedIds = window.selectedSources.map(source => source.id);
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const polygonParam = urlParams.get('polygon');
|
||
|
||
let url = '{% url "mainapp:show_sources_map" %}' + '?ids=' + selectedIds.join(',');
|
||
if (polygonParam) {
|
||
url += '&polygon=' + encodeURIComponent(polygonParam);
|
||
}
|
||
|
||
window.open(url, '_blank');
|
||
}
|
||
|
||
// Function to show playback animation for selected sources
|
||
function showPlaybackAnimation() {
|
||
if (!window.selectedSources || window.selectedSources.length === 0) {
|
||
alert('Список источников пуст');
|
||
return;
|
||
}
|
||
|
||
// Check if any source has points
|
||
const sourcesWithPoints = window.selectedSources.filter(source => parseInt(source.objitem_count) > 0);
|
||
if (sourcesWithPoints.length === 0) {
|
||
alert('Выбранные источники не содержат точек ГЛ');
|
||
return;
|
||
}
|
||
|
||
const selectedIds = window.selectedSources.map(source => source.id);
|
||
const url = '{% url "mainapp:multi_sources_playback_map" %}' + '?ids=' + selectedIds.join(',');
|
||
|
||
window.open(url, '_blank');
|
||
}
|
||
|
||
// Function to merge selected sources
|
||
function mergeSelectedSources() {
|
||
if (!window.selectedSources || window.selectedSources.length < 2) {
|
||
alert('Для объединения необходимо выбрать минимум 2 источника');
|
||
return;
|
||
}
|
||
|
||
// Show merge modal
|
||
const modal = new bootstrap.Modal(document.getElementById('mergeSourcesModal'));
|
||
|
||
// Populate target source info
|
||
const targetSource = window.selectedSources[0];
|
||
document.getElementById('targetSourceInfo').innerHTML = `
|
||
<strong>ID:</strong> ${targetSource.id}<br>
|
||
<strong>Имя:</strong> ${targetSource.name}<br>
|
||
<strong>Спутник:</strong> ${targetSource.satellite}<br>
|
||
<strong>Количество точек:</strong> ${targetSource.objitem_count}
|
||
`;
|
||
|
||
// Populate sources to merge list
|
||
const sourcesToMergeList = document.getElementById('sourcesToMergeList');
|
||
sourcesToMergeList.innerHTML = '';
|
||
for (let i = 1; i < window.selectedSources.length; i++) {
|
||
const source = window.selectedSources[i];
|
||
const li = document.createElement('li');
|
||
li.className = 'list-group-item';
|
||
li.innerHTML = `
|
||
<strong>ID ${source.id}:</strong> ${source.name}
|
||
<span class="badge bg-secondary">${source.objitem_count} точек</span>
|
||
`;
|
||
sourcesToMergeList.appendChild(li);
|
||
}
|
||
|
||
modal.show();
|
||
}
|
||
|
||
// Function to confirm merge
|
||
function confirmMerge() {
|
||
const infoId = document.getElementById('mergeInfoSelect').value;
|
||
const ownershipId = document.getElementById('mergeOwnershipSelect').value;
|
||
const note = document.getElementById('mergeNoteTextarea').value;
|
||
|
||
if (!infoId) {
|
||
alert('Пожалуйста, выберите тип объекта');
|
||
return;
|
||
}
|
||
|
||
if (!ownershipId) {
|
||
alert('Пожалуйста, выберите принадлежность объекта');
|
||
return;
|
||
}
|
||
|
||
// Prepare data
|
||
const sourceIds = window.selectedSources.map(source => source.id);
|
||
|
||
// Get CSRF token
|
||
function getCookie(name) {
|
||
let cookieValue = null;
|
||
if (document.cookie && document.cookie !== '') {
|
||
const cookies = document.cookie.split(';');
|
||
for (let i = 0; i < cookies.length; i++) {
|
||
const cookie = cookies[i].trim();
|
||
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
return cookieValue;
|
||
}
|
||
const csrftoken = getCookie('csrftoken');
|
||
|
||
// Send AJAX request
|
||
fetch('{% url "mainapp:merge_sources" %}', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRFToken': csrftoken
|
||
},
|
||
body: JSON.stringify({
|
||
source_ids: sourceIds,
|
||
info_id: infoId,
|
||
ownership_id: ownershipId,
|
||
note: note
|
||
})
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
alert(data.message);
|
||
|
||
// Clear selected sources
|
||
window.selectedSources = [];
|
||
saveSelectedSourcesToStorage();
|
||
updateSelectedSourcesCounter();
|
||
|
||
// Close modal
|
||
const modal = bootstrap.Modal.getInstance(document.getElementById('mergeSourcesModal'));
|
||
modal.hide();
|
||
|
||
// Reload page
|
||
location.reload();
|
||
} else {
|
||
alert('Ошибка: ' + (data.error || 'Неизвестная ошибка'));
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
alert('Произошла ошибка при объединении источников');
|
||
});
|
||
}
|
||
|
||
// Function to toggle all checkboxes in the selected sources table
|
||
function toggleAllSelectedSources(checkbox) {
|
||
const checkboxes = document.querySelectorAll('#selected-sources-table-body .selected-source-checkbox');
|
||
checkboxes.forEach(cb => {
|
||
cb.checked = checkbox.checked;
|
||
});
|
||
}
|
||
|
||
// Show source details in modal
|
||
function showSourceDetails(sourceId) {
|
||
// Update modal title
|
||
document.getElementById('modalSourceId').textContent = sourceId;
|
||
|
||
// Show loading spinner, hide content and error
|
||
document.getElementById('modalLoadingSpinner').style.display = 'block';
|
||
document.getElementById('modalContent').style.display = 'none';
|
||
document.getElementById('modalNoData').style.display = 'none';
|
||
document.getElementById('modalErrorMessage').style.display = 'none';
|
||
|
||
// Open modal
|
||
const modal = new bootstrap.Modal(document.getElementById('sourceDetailsModal'));
|
||
modal.show();
|
||
|
||
// Build URL with filter parameters
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const geoDateFrom = urlParams.get('geo_date_from');
|
||
const geoDateTo = urlParams.get('geo_date_to');
|
||
|
||
let apiUrl = '/api/source/' + sourceId + '/objitems/';
|
||
const params = new URLSearchParams();
|
||
if (geoDateFrom) {
|
||
params.append('geo_date_from', geoDateFrom);
|
||
}
|
||
if (geoDateTo) {
|
||
params.append('geo_date_to', geoDateTo);
|
||
}
|
||
if (params.toString()) {
|
||
apiUrl += '?' + params.toString();
|
||
}
|
||
|
||
// Fetch data from API
|
||
fetch(apiUrl)
|
||
.then(response => {
|
||
if (!response.ok) {
|
||
if (response.status === 404) {
|
||
throw new Error('Объект не найден');
|
||
} else {
|
||
throw new Error('Ошибка при загрузке данных');
|
||
}
|
||
}
|
||
return response.json();
|
||
})
|
||
.then(data => {
|
||
// Hide loading spinner
|
||
document.getElementById('modalLoadingSpinner').style.display = 'none';
|
||
|
||
// Show marks if available
|
||
if (data.marks && data.marks.length > 0) {
|
||
document.getElementById('marksSection').style.display = 'block';
|
||
document.getElementById('marksCount').textContent = data.marks.length;
|
||
|
||
const marksTableBody = document.getElementById('marksTableBody');
|
||
marksTableBody.innerHTML = '';
|
||
|
||
data.marks.forEach(mark => {
|
||
const row = document.createElement('tr');
|
||
|
||
let markBadge = '<span class="badge bg-secondary">-</span>';
|
||
if (mark.mark === true) {
|
||
markBadge = '<span class="badge bg-success">Есть</span>';
|
||
} else if (mark.mark === false) {
|
||
markBadge = '<span class="badge bg-danger">Нет</span>';
|
||
}
|
||
|
||
row.innerHTML = '<td class="text-center">' + markBadge + '</td>' +
|
||
'<td>' + mark.timestamp + '</td>' +
|
||
'<td>' + mark.created_by + '</td>';
|
||
marksTableBody.appendChild(row);
|
||
});
|
||
} else {
|
||
document.getElementById('marksSection').style.display = 'none';
|
||
}
|
||
|
||
if (data.objitems && data.objitems.length > 0) {
|
||
// Show content
|
||
document.getElementById('modalContent').style.display = 'block';
|
||
document.getElementById('objitemCount').textContent = data.objitems.length;
|
||
|
||
// Populate table
|
||
const tbody = document.getElementById('objitemTableBody');
|
||
tbody.innerHTML = '';
|
||
|
||
data.objitems.forEach(objitem => {
|
||
const row = document.createElement('tr');
|
||
|
||
// Build transponder cell
|
||
let transponderCell = '-';
|
||
if (objitem.has_transponder) {
|
||
transponderCell = '<a href="#" class="text-decoration-underline" ' +
|
||
'onclick="showTransponderModal(' + objitem.transponder_id + '); return false;" ' +
|
||
'title="Показать данные транспондера">' +
|
||
objitem.transponder_info +
|
||
'</a>';
|
||
}
|
||
|
||
// Build LyngSat cell
|
||
let lyngsatCell = '-';
|
||
if (objitem.has_lyngsat) {
|
||
lyngsatCell = '<a href="#" class="text-primary text-decoration-none" ' +
|
||
'onclick="showLyngsatModal(' + objitem.lyngsat_id + '); return false;">' +
|
||
'<i class="bi bi-tv"></i> ТВ' +
|
||
'</a>';
|
||
}
|
||
|
||
// Build Sigma cell
|
||
let sigmaCell = '-';
|
||
if (objitem.has_sigma) {
|
||
sigmaCell = '<a href="#" class="text-info text-decoration-none" ' +
|
||
'onclick="showSigmaParameterModal(' + objitem.parameter_id + '); return false;" ' +
|
||
'title="' + objitem.sigma_info + '">' +
|
||
'<i class="bi bi-graph-up"></i> ' + objitem.sigma_info +
|
||
'</a>';
|
||
}
|
||
|
||
// Build satellite cell with link
|
||
let satelliteCell = objitem.satellite_name;
|
||
if (objitem.satellite_id) {
|
||
satelliteCell = '<a href="#" class="text-decoration-underline" ' +
|
||
'onclick="showSatelliteModal(' + objitem.satellite_id + '); return false;">' +
|
||
objitem.satellite_name +
|
||
'</a>';
|
||
}
|
||
|
||
row.innerHTML = '<td class="text-center">' +
|
||
'<input type="checkbox" class="form-check-input modal-item-checkbox" value="' + objitem.id + '">' +
|
||
'</td>' +
|
||
'<td class="text-center">' + objitem.id + '</td>' +
|
||
'<td>' + objitem.name + '</td>' +
|
||
'<td>' + satelliteCell + '</td>' +
|
||
'<td>' + transponderCell + '</td>' +
|
||
'<td>' + objitem.frequency + '</td>' +
|
||
'<td>' + objitem.freq_range + '</td>' +
|
||
'<td>' + objitem.polarization + '</td>' +
|
||
'<td>' + objitem.bod_velocity + '</td>' +
|
||
'<td>' + objitem.modulation + '</td>' +
|
||
'<td>' + objitem.snr + '</td>' +
|
||
'<td>' + objitem.geo_timestamp + '</td>' +
|
||
'<td>' + objitem.geo_location + '</td>' +
|
||
'<td>' + objitem.geo_coords + '</td>' +
|
||
'<td>' + objitem.updated_at + '</td>' +
|
||
'<td>' + objitem.updated_by + '</td>' +
|
||
'<td>' + objitem.created_at + '</td>' +
|
||
'<td>' + objitem.created_by + '</td>' +
|
||
'<td>' + objitem.comment + '</td>' +
|
||
'<td>' + objitem.is_average + '</td>' +
|
||
'<td>' + objitem.standard + '</td>' +
|
||
'<td>' + lyngsatCell + '</td>' +
|
||
'<td>' + sigmaCell + '</td>' +
|
||
'<td>' + objitem.mirrors + '</td>';
|
||
tbody.appendChild(row);
|
||
});
|
||
|
||
// Setup modal select-all checkbox
|
||
setupModalSelectAll();
|
||
|
||
// Initialize column visibility after DOM update
|
||
// Use requestAnimationFrame to ensure DOM is rendered
|
||
requestAnimationFrame(() => {
|
||
setTimeout(() => {
|
||
initModalColumnVisibility();
|
||
}, 50);
|
||
});
|
||
} else {
|
||
// Show no data message
|
||
document.getElementById('modalNoData').style.display = 'block';
|
||
}
|
||
})
|
||
.catch(error => {
|
||
// Hide loading spinner
|
||
document.getElementById('modalLoadingSpinner').style.display = 'none';
|
||
|
||
// Show error message
|
||
const errorDiv = document.getElementById('modalErrorMessage');
|
||
errorDiv.textContent = error.message;
|
||
errorDiv.style.display = 'block';
|
||
});
|
||
}
|
||
|
||
// Setup select-all functionality for modal
|
||
function setupModalSelectAll() {
|
||
const modalSelectAll = document.getElementById('modal-select-all');
|
||
const modalItemCheckboxes = document.querySelectorAll('.modal-item-checkbox');
|
||
|
||
if (modalSelectAll && modalItemCheckboxes.length > 0) {
|
||
// Remove old event listeners by cloning
|
||
const newModalSelectAll = modalSelectAll.cloneNode(true);
|
||
modalSelectAll.parentNode.replaceChild(newModalSelectAll, modalSelectAll);
|
||
|
||
newModalSelectAll.addEventListener('change', function() {
|
||
const checkboxes = document.querySelectorAll('.modal-item-checkbox');
|
||
checkboxes.forEach(checkbox => {
|
||
checkbox.checked = newModalSelectAll.checked;
|
||
});
|
||
});
|
||
|
||
modalItemCheckboxes.forEach(checkbox => {
|
||
checkbox.addEventListener('change', function() {
|
||
const allCheckboxes = document.querySelectorAll('.modal-item-checkbox');
|
||
const allChecked = Array.from(allCheckboxes).every(cb => cb.checked);
|
||
document.getElementById('modal-select-all').checked = allChecked;
|
||
});
|
||
});
|
||
}
|
||
}
|
||
|
||
// Modal column visibility functions with localStorage support
|
||
function getModalColumnVisibilityKey() {
|
||
return 'sourceListModalColumnVisibility';
|
||
}
|
||
|
||
function saveModalColumnVisibility() {
|
||
const columnCheckboxes = document.querySelectorAll('.modal-column-toggle');
|
||
const visibility = {};
|
||
columnCheckboxes.forEach(checkbox => {
|
||
const columnIndex = checkbox.getAttribute('data-column');
|
||
visibility[columnIndex] = checkbox.checked;
|
||
});
|
||
localStorage.setItem(getModalColumnVisibilityKey(), JSON.stringify(visibility));
|
||
}
|
||
|
||
function loadModalColumnVisibility() {
|
||
const saved = localStorage.getItem(getModalColumnVisibilityKey());
|
||
if (saved) {
|
||
return JSON.parse(saved);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// Function to toggle modal column visibility
|
||
function toggleModalColumn(checkbox) {
|
||
const columnIndex = parseInt(checkbox.getAttribute('data-column'));
|
||
|
||
// Get the specific tbody for objitems
|
||
const tbody = document.getElementById('objitemTableBody');
|
||
if (!tbody) return;
|
||
|
||
// Get the parent table
|
||
const table = tbody.closest('table');
|
||
if (!table) return;
|
||
|
||
// Get all rows and toggle specific cell in each
|
||
const rows = table.querySelectorAll('tr');
|
||
rows.forEach(row => {
|
||
const cells = row.children;
|
||
if (cells[columnIndex]) {
|
||
if (checkbox.checked) {
|
||
cells[columnIndex].style.removeProperty('display');
|
||
} else {
|
||
cells[columnIndex].style.setProperty('display', 'none', 'important');
|
||
}
|
||
}
|
||
});
|
||
|
||
// Save state after toggle
|
||
saveModalColumnVisibility();
|
||
}
|
||
|
||
// Function to toggle all modal columns
|
||
function toggleAllModalColumns(selectAllCheckbox) {
|
||
const columnCheckboxes = document.querySelectorAll('.modal-column-toggle');
|
||
columnCheckboxes.forEach(checkbox => {
|
||
checkbox.checked = selectAllCheckbox.checked;
|
||
toggleModalColumn(checkbox);
|
||
});
|
||
}
|
||
|
||
// Helper function to toggle modal column without saving
|
||
function toggleModalColumnWithoutSave(checkbox) {
|
||
const columnIndex = parseInt(checkbox.getAttribute('data-column'));
|
||
|
||
const tbody = document.getElementById('objitemTableBody');
|
||
if (!tbody) return;
|
||
|
||
const table = tbody.closest('table');
|
||
if (!table) return;
|
||
|
||
const rows = table.querySelectorAll('tr');
|
||
rows.forEach(row => {
|
||
const cells = row.children;
|
||
if (cells[columnIndex]) {
|
||
if (checkbox.checked) {
|
||
cells[columnIndex].style.removeProperty('display');
|
||
} else {
|
||
cells[columnIndex].style.setProperty('display', 'none', 'important');
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Initialize modal column visibility
|
||
function initModalColumnVisibility() {
|
||
const savedVisibility = loadModalColumnVisibility();
|
||
|
||
if (savedVisibility) {
|
||
// Restore saved state
|
||
const columnCheckboxes = document.querySelectorAll('.modal-column-toggle');
|
||
columnCheckboxes.forEach(checkbox => {
|
||
const columnIndex = checkbox.getAttribute('data-column');
|
||
if (savedVisibility.hasOwnProperty(columnIndex)) {
|
||
checkbox.checked = savedVisibility[columnIndex];
|
||
toggleModalColumnWithoutSave(checkbox);
|
||
}
|
||
});
|
||
} else {
|
||
// Default state: hide columns by default: Создано (16), Кем(созд) (17), Комментарий (18), Усреднённое (19), Стандарт (20), Sigma (22)
|
||
const columnsToHide = [16, 17, 18, 19, 20, 22];
|
||
|
||
const tbody = document.getElementById('objitemTableBody');
|
||
if (!tbody) {
|
||
console.log('objitemTableBody not found');
|
||
return;
|
||
}
|
||
|
||
const table = tbody.closest('table');
|
||
if (!table) {
|
||
console.log('Table not found');
|
||
return;
|
||
}
|
||
|
||
// Update checkboxes and hide columns
|
||
columnsToHide.forEach(columnIndex => {
|
||
const checkbox = document.querySelector(`.modal-column-toggle[data-column="${columnIndex}"]`);
|
||
if (checkbox) {
|
||
checkbox.checked = false;
|
||
}
|
||
|
||
const rows = table.querySelectorAll('tr');
|
||
rows.forEach(row => {
|
||
const cells = row.children;
|
||
if (cells[columnIndex]) {
|
||
cells[columnIndex].style.setProperty('display', 'none', 'important');
|
||
}
|
||
});
|
||
});
|
||
|
||
// Save initial state
|
||
saveModalColumnVisibility();
|
||
}
|
||
}
|
||
|
||
// Function to show LyngSat modal
|
||
function showLyngsatModal(lyngsatId) {
|
||
const modal = new bootstrap.Modal(document.getElementById('lyngsatModal'));
|
||
modal.show();
|
||
|
||
const modalBody = document.getElementById('lyngsatModalBody');
|
||
modalBody.innerHTML = '<div class="text-center py-4"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Загрузка...</span></div></div>';
|
||
|
||
fetch('/api/lyngsat/' + lyngsatId + '/')
|
||
.then(response => {
|
||
if (!response.ok) {
|
||
throw new Error('Ошибка загрузки данных');
|
||
}
|
||
return response.json();
|
||
})
|
||
.then(data => {
|
||
let html = '<div class="container-fluid"><div class="row g-3">' +
|
||
'<div class="col-md-6"><div class="card h-100">' +
|
||
'<div class="card-header bg-light"><strong><i class="bi bi-info-circle"></i> Основная информация</strong></div>' +
|
||
'<div class="card-body"><table class="table table-sm table-borderless mb-0"><tbody>' +
|
||
'<tr><td class="text-muted" style="width: 40%;">Спутник:</td><td><strong>' + data.satellite + '</strong></td></tr>' +
|
||
'<tr><td class="text-muted">Частота:</td><td><strong>' + data.frequency + ' МГц</strong></td></tr>' +
|
||
'<tr><td class="text-muted">Поляризация:</td><td><span class="badge bg-info">' + data.polarization + '</span></td></tr>' +
|
||
'<tr><td class="text-muted">Канал:</td><td>' + data.channel_info + '</td></tr>' +
|
||
'</tbody></table></div></div></div>' +
|
||
'<div class="col-md-6"><div class="card h-100">' +
|
||
'<div class="card-header bg-light"><strong><i class="bi bi-gear"></i> Технические параметры</strong></div>' +
|
||
'<div class="card-body"><table class="table table-sm table-borderless mb-0"><tbody>' +
|
||
'<tr><td class="text-muted" style="width: 40%;">Модуляция:</td><td><span class="badge bg-secondary">' + data.modulation + '</span></td></tr>' +
|
||
'<tr><td class="text-muted">Стандарт:</td><td><span class="badge bg-secondary">' + data.standard + '</span></td></tr>' +
|
||
'<tr><td class="text-muted">Сим. скорость:</td><td><strong>' + data.sym_velocity + ' БОД</strong></td></tr>' +
|
||
'<tr><td class="text-muted">FEC:</td><td>' + data.fec + '</td></tr>' +
|
||
'</tbody></table></div></div></div>' +
|
||
'<div class="col-12"><div class="card">' +
|
||
'<div class="card-header bg-light"><strong><i class="bi bi-clock-history"></i> Дополнительная информация</strong></div>' +
|
||
'<div class="card-body"><div class="row">' +
|
||
'<div class="col-md-6"><p class="mb-2"><span class="text-muted">Последнее обновление:</span><br><strong>' + data.last_update + '</strong></p></div>' +
|
||
'<div class="col-md-6">' + (data.url ? '<p class="mb-2"><span class="text-muted">Ссылка на объект:</span><br>' +
|
||
'<a href="' + data.url + '" target="_blank" class="btn btn-sm btn-outline-primary">' +
|
||
'<i class="bi bi-link-45deg"></i> Открыть на LyngSat</a></p>' : '') +
|
||
'</div></div></div></div></div></div></div>';
|
||
modalBody.innerHTML = html;
|
||
})
|
||
.catch(error => {
|
||
modalBody.innerHTML = '<div class="alert alert-danger" role="alert">' +
|
||
'<i class="bi bi-exclamation-triangle"></i> ' + error.message + '</div>';
|
||
});
|
||
}
|
||
|
||
// Function to show transponder modal
|
||
function showTransponderModal(transponderId) {
|
||
const modal = new bootstrap.Modal(document.getElementById('transponderModal'));
|
||
modal.show();
|
||
|
||
const modalBody = document.getElementById('transponderModalBody');
|
||
modalBody.innerHTML = '<div class="text-center py-4"><div class="spinner-border text-success" role="status"><span class="visually-hidden">Загрузка...</span></div></div>';
|
||
|
||
fetch('/api/transponder/' + transponderId + '/')
|
||
.then(response => {
|
||
if (!response.ok) {
|
||
throw new Error('Ошибка загрузки данных транспондера');
|
||
}
|
||
return response.json();
|
||
})
|
||
.then(data => {
|
||
let html = '<div class="container-fluid"><div class="row g-3">' +
|
||
'<div class="col-md-6"><div class="card h-100">' +
|
||
'<div class="card-header bg-light"><strong><i class="bi bi-info-circle"></i> Основная информация</strong></div>' +
|
||
'<div class="card-body"><table class="table table-sm table-borderless mb-0"><tbody>' +
|
||
'<tr><td class="text-muted" style="width: 40%;">Название:</td><td><strong>' + (data.name || '-') + '</strong></td></tr>' +
|
||
'<tr><td class="text-muted">Спутник:</td><td><strong>' + data.satellite + '</strong></td></tr>' +
|
||
'<tr><td class="text-muted">Зона покрытия:</td><td>' + (data.zone_name || '-') + '</td></tr>' +
|
||
'<tr><td class="text-muted">Поляризация:</td><td><span class="badge bg-info">' + data.polarization + '</span></td></tr>' +
|
||
'</tbody></table></div></div></div>' +
|
||
'<div class="col-md-6"><div class="card h-100">' +
|
||
'<div class="card-header bg-light"><strong><i class="bi bi-broadcast"></i> Частотные параметры</strong></div>' +
|
||
'<div class="card-body"><table class="table table-sm table-borderless mb-0"><tbody>' +
|
||
'<tr><td class="text-muted" style="width: 40%;">Downlink:</td><td><strong>' + data.downlink + ' МГц</strong></td></tr>' +
|
||
'<tr><td class="text-muted">Uplink:</td><td><strong>' + (data.uplink || '-') + (data.uplink ? ' МГц' : '') + '</strong></td></tr>' +
|
||
'<tr><td class="text-muted">Полоса:</td><td><strong>' + data.frequency_range + ' МГц</strong></td></tr>' +
|
||
'<tr><td class="text-muted">Перенос:</td><td>' + (data.transfer || '-') + (data.transfer ? ' МГц' : '') + '</td></tr>' +
|
||
'<tr><td class="text-muted">ОСШ:</td><td><strong>' + (data.snr || '-') + (data.snr ? ' дБ' : '') + '</strong></td></tr>' +
|
||
'</tbody></table></div></div></div>' +
|
||
'<div class="col-12"><div class="card">' +
|
||
'<div class="card-header bg-light"><strong><i class="bi bi-clock-history"></i> Метаданные</strong></div>' +
|
||
'<div class="card-body"><div class="row">' +
|
||
'<div class="col-md-6"><p class="mb-2"><span class="text-muted">Дата создания:</span><br><strong>' + (data.created_at || '-') + '</strong></p></div>' +
|
||
'<div class="col-md-6"><p class="mb-2"><span class="text-muted">Создан пользователем:</span><br><strong>' + (data.created_by || '-') + '</strong></p></div>' +
|
||
'</div></div></div></div></div></div>';
|
||
modalBody.innerHTML = html;
|
||
})
|
||
.catch(error => {
|
||
modalBody.innerHTML = '<div class="alert alert-danger" role="alert">' +
|
||
'<i class="bi bi-exclamation-triangle"></i> ' + error.message + '</div>';
|
||
});
|
||
}
|
||
</script>
|
||
|
||
<!-- LyngSat Data Modal -->
|
||
<div class="modal fade" id="lyngsatModal" tabindex="-1" aria-labelledby="lyngsatModalLabel" aria-hidden="true">
|
||
<div class="modal-dialog modal-lg">
|
||
<div class="modal-content">
|
||
<div class="modal-header bg-primary text-white">
|
||
<h5 class="modal-title" id="lyngsatModalLabel">
|
||
<i class="bi bi-tv"></i> Данные объекта LyngSat
|
||
</h5>
|
||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
|
||
aria-label="Закрыть"></button>
|
||
</div>
|
||
<div class="modal-body" id="lyngsatModalBody">
|
||
<div class="text-center py-4">
|
||
<div class="spinner-border text-primary" role="status">
|
||
<span class="visually-hidden">Загрузка...</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Transponder Data Modal -->
|
||
<div class="modal fade" id="transponderModal" tabindex="-1" aria-labelledby="transponderModalLabel" aria-hidden="true">
|
||
<div class="modal-dialog modal-lg">
|
||
<div class="modal-content">
|
||
<div class="modal-header bg-success text-white">
|
||
<h5 class="modal-title" id="transponderModalLabel">
|
||
<i class="bi bi-broadcast"></i> Данные транспондера
|
||
</h5>
|
||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
||
</div>
|
||
<div class="modal-body" id="transponderModalBody">
|
||
<div class="text-center py-4">
|
||
<div class="spinner-border text-success" role="status">
|
||
<span class="visually-hidden">Загрузка...</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Include the sigma parameter modal component -->
|
||
{% include 'mainapp/components/_sigma_parameter_modal.html' %}
|
||
|
||
<!-- Include the satellite modal component -->
|
||
{% include 'mainapp/components/_satellite_modal.html' %}
|
||
|
||
<!-- Selected Sources Offcanvas -->
|
||
<div class="offcanvas offcanvas-end" tabindex="-1" id="selectedSourcesOffcanvas" aria-labelledby="selectedSourcesOffcanvasLabel" style="width: 80%;">
|
||
<div class="offcanvas-header">
|
||
<h5 class="offcanvas-title" id="selectedSourcesOffcanvasLabel">
|
||
Выбранные источники
|
||
<span class="badge bg-info" id="selectedSourcesOffcanvasCounter">0</span>
|
||
</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Закрыть"></button>
|
||
</div>
|
||
<div class="offcanvas-body">
|
||
<div class="mb-3">
|
||
<div class="btn-group" role="group">
|
||
<button type="button" class="btn btn-outline-primary btn-sm" onclick="showSelectedSourcesOnMap()">
|
||
<i class="bi bi-map"></i> Карта
|
||
</button>
|
||
<button type="button" class="btn btn-outline-info btn-sm" onclick="showPlaybackAnimation()" title="Анимация движения объектов">
|
||
<i class="bi bi-play-circle"></i> Анимация
|
||
</button>
|
||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
||
<button type="button" class="btn btn-outline-success btn-sm" onclick="mergeSelectedSources()">
|
||
<i class="bi bi-union"></i> Объединить
|
||
</button>
|
||
{% endif %}
|
||
<button type="button" class="btn btn-outline-danger btn-sm" onclick="removeSelectedSources()">
|
||
<i class="bi bi-trash"></i> Удалить из списка
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="table-responsive" style="max-height: 70vh; overflow-y: auto;">
|
||
<table class="table table-striped table-hover table-sm table-bordered">
|
||
<thead class="table-light sticky-top">
|
||
<tr>
|
||
<th class="text-center" style="width: 3%;">
|
||
<input type="checkbox" id="select-all-sources" class="form-check-input" onchange="toggleAllSelectedSources(this)">
|
||
</th>
|
||
<th class="text-center" style="min-width: 60px;">ID</th>
|
||
<th style="min-width: 150px;">Имя</th>
|
||
<th style="min-width: 120px;">Спутник</th>
|
||
<th style="min-width: 120px;">Тип объекта</th>
|
||
<th style="min-width: 150px;">Принадлежность</th>
|
||
<th style="min-width: 150px;">Координаты ГЛ</th>
|
||
<th class="text-center" style="min-width: 100px;">Кол-во точек</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="selected-sources-table-body">
|
||
<!-- Data will be loaded here via JavaScript -->
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div id="selectedSourcesNoData" class="text-center text-muted py-4" style="display: none;">
|
||
Список пуст. Добавьте источники из основной таблицы.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Merge Sources Modal -->
|
||
<div class="modal fade" id="mergeSourcesModal" tabindex="-1" aria-labelledby="mergeSourcesModalLabel" aria-hidden="true">
|
||
<div class="modal-dialog modal-lg">
|
||
<div class="modal-content">
|
||
<div class="modal-header bg-success text-white">
|
||
<h5 class="modal-title" id="mergeSourcesModalLabel">
|
||
<i class="bi bi-union"></i> Объединение источников
|
||
</h5>
|
||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="alert alert-info">
|
||
<i class="bi bi-info-circle"></i>
|
||
<strong>Внимание:</strong> Все точки из выбранных источников будут присвоены первому источнику в списке.
|
||
Остальные источники будут удалены.
|
||
</div>
|
||
|
||
<div class="card mb-3">
|
||
<div class="card-header bg-light">
|
||
<strong>Целевой источник (все точки будут присвоены ему):</strong>
|
||
</div>
|
||
<div class="card-body" id="targetSourceInfo">
|
||
<!-- Target source info will be populated here -->
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card mb-3">
|
||
<div class="card-header bg-light">
|
||
<strong>Источники для объединения (будут удалены):</strong>
|
||
</div>
|
||
<div class="card-body">
|
||
<ul class="list-group" id="sourcesToMergeList">
|
||
<!-- Sources to merge will be populated here -->
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="mergeInfoSelect" class="form-label">Тип объекта <span class="text-danger">*</span></label>
|
||
<select class="form-select" id="mergeInfoSelect" required>
|
||
<option value="">Выберите тип объекта</option>
|
||
{% for info in object_infos %}
|
||
<option value="{{ info.id }}">{{ info.name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="mergeOwnershipSelect" class="form-label">Принадлежность объекта <span class="text-danger">*</span></label>
|
||
<select class="form-select" id="mergeOwnershipSelect" required>
|
||
<option value="">Выберите принадлежность</option>
|
||
{% for ownership in object_ownerships %}
|
||
<option value="{{ ownership.id }}">{{ ownership.name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="mergeNoteTextarea" class="form-label">Примечание</label>
|
||
<textarea class="form-control" id="mergeNoteTextarea" rows="3" placeholder="Введите примечание (необязательно)"></textarea>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||
<button type="button" class="btn btn-success" onclick="confirmMerge()">
|
||
<i class="bi bi-check-circle"></i> Объединить
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Polygon Filter Map Modal -->
|
||
<div class="modal fade" id="polygonFilterModal" tabindex="-1" aria-labelledby="polygonFilterModalLabel" aria-hidden="true">
|
||
<div class="modal-dialog modal-fullscreen">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="polygonFilterModalLabel">
|
||
<i class="bi bi-pentagon"></i> Нарисуйте полигон для фильтрации
|
||
</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
||
</div>
|
||
<div class="modal-body p-0" style="position: relative;">
|
||
<div id="polygonHelpAlert" class="alert alert-info m-2" style="position: absolute; top: 10px; left: 10px; z-index: 1000; max-width: 400px; opacity: 0.95;">
|
||
<button type="button" class="btn-close btn-sm float-end" onclick="document.getElementById('polygonHelpAlert').style.display='none'"></button>
|
||
<small>
|
||
<strong>Инструкция:</strong>
|
||
<ul class="mb-0 ps-3">
|
||
<li>Используйте инструменты справа для рисования полигона или прямоугольника</li>
|
||
<li>Кликайте по карте для создания вершин полигона</li>
|
||
<li>Замкните полигон, кликнув на первую точку</li>
|
||
<li>Нажмите "Применить фильтр" для фильтрации источников</li>
|
||
</ul>
|
||
</small>
|
||
</div>
|
||
<div id="polygonFilterMap" style="height: calc(100vh - 120px); width: 100%;"></div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||
<button type="button" class="btn btn-danger" onclick="clearPolygonOnMap()">
|
||
<i class="bi bi-trash"></i> Очистить полигон
|
||
</button>
|
||
<button type="button" class="btn btn-primary" onclick="applyPolygonFilter()">
|
||
<i class="bi bi-check-circle"></i> Применить фильтр
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{% endblock %}
|