Начал с усреднениями

This commit is contained in:
2025-11-28 00:18:04 +03:00
parent 908e11879d
commit d521b6baad
5 changed files with 1345 additions and 0 deletions

View File

@@ -40,6 +40,9 @@
<li class="nav-item">
<a class="nav-link" href="{% url 'mainapp:kubsat' %}">Кубсат</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'mainapp:points_averaging' %}">Усреднение</a>
</li>
<!-- <li class="nav-item">
<a class="nav-link" href="{% url 'mapsapp:3dmap' %}">3D карта</a>
</li> -->

View File

@@ -0,0 +1,849 @@
{% extends "mainapp/base.html" %}
{% load static %}
{% block title %}Усреднение точек{% endblock %}
{% block extra_css %}
<link href="{% static 'tabulator/css/tabulator_bootstrap5.min.css' %}" rel="stylesheet">
<style>
.averaging-container {
padding: 20px;
}
.form-section {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.table-section {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
#result-table {
margin-top: 20px;
font-size: 12px;
}
#result-table .tabulator-header {
font-size: 12px;
white-space: normal;
word-wrap: break-word;
}
#result-table .tabulator-header .tabulator-col {
white-space: normal;
word-wrap: break-word;
height: auto;
min-height: 40px;
}
#result-table .tabulator-header .tabulator-col-content {
white-space: normal;
word-wrap: break-word;
padding: 6px 4px;
}
#result-table .tabulator-cell {
font-size: 12px;
padding: 6px 4px;
}
.btn-group-custom {
margin-top: 15px;
}
.outlier-row {
background-color: #ffcccc !important;
}
.outlier-warning {
color: #dc3545;
font-weight: bold;
}
.group-header {
background-color: #e9ecef;
padding: 10px;
margin-bottom: 10px;
border-radius: 4px;
}
.points-detail-table {
margin-top: 10px;
font-size: 11px;
}
.modal-xl {
max-width: 95%;
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255,255,255,0.8);
display: none;
justify-content: center;
align-items: center;
z-index: 9999;
}
.loading-overlay.active {
display: flex;
}
/* Цвета для групп */
.group-color-0 { background-color: #cce5ff !important; }
.group-color-1 { background-color: #d4edda !important; }
.group-color-2 { background-color: #fff3cd !important; }
.group-color-3 { background-color: #f8d7da !important; }
.group-color-4 { background-color: #d1ecf1 !important; }
.group-color-5 { background-color: #e2d5f1 !important; }
.group-color-6 { background-color: #ffeeba !important; }
.group-color-7 { background-color: #c3e6cb !important; }
.group-color-8 { background-color: #bee5eb !important; }
.group-color-9 { background-color: #f5c6cb !important; }
.all-points-section {
margin-top: 20px;
}
#all-points-table {
margin-top: 10px;
font-size: 11px;
}
#all-points-table .tabulator-header {
font-size: 11px;
}
#all-points-table .tabulator-cell {
font-size: 11px;
padding: 4px 3px;
}
.legend-item {
display: inline-flex;
align-items: center;
margin-right: 15px;
margin-bottom: 5px;
}
.legend-color {
width: 20px;
height: 20px;
margin-right: 5px;
border: 1px solid #ccc;
border-radius: 3px;
}
.legend-container {
padding: 10px;
background: #f8f9fa;
border-radius: 4px;
margin-bottom: 10px;
max-height: 100px;
overflow-y: auto;
}
</style>
{% endblock %}
{% block content %}
<div class="loading-overlay" id="loading-overlay">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
</div>
<div class="averaging-container">
<h2>Усреднение точек спутников</h2>
<div class="form-section">
<div class="row">
<div class="col-md-3 mb-3">
<label for="satellite-select" class="form-label">Спутник</label>
<select id="satellite-select" class="form-select">
<option value="">Выберите спутник</option>
{% for satellite in satellites %}
<option value="{{ satellite.id }}">{{ satellite.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-3 mb-3">
<label for="date-from" class="form-label">Дата с</label>
<input type="date" id="date-from" class="form-control">
</div>
<div class="col-md-3 mb-3">
<label for="date-to" class="form-label">Дата по</label>
<input type="date" id="date-to" class="form-control">
</div>
<div class="col-md-3 mb-3 d-flex align-items-end">
<button id="btn-process" class="btn btn-primary w-100">
<i class="bi bi-play-fill"></i> Добавить
</button>
</div>
</div>
</div>
<div class="table-section">
<div class="d-flex justify-content-between align-items-center">
<div>
<h5>Результаты усреднения <span id="group-count" class="badge bg-primary">0</span></h5>
</div>
<div class="btn-group-custom">
<button id="export-xlsx" class="btn btn-success" disabled>
<i class="bi bi-file-earmark-excel"></i> Сохранить в Excel
</button>
<button id="clear-table" class="btn btn-danger ms-2">
<i class="bi bi-trash"></i> Очистить таблицу
</button>
</div>
</div>
<div id="result-table"></div>
<!-- All points section -->
<div class="all-points-section" id="all-points-section" style="display: none;">
<hr>
<div class="d-flex justify-content-between align-items-center">
<h5>Все точки, участвующие в усреднении <span id="all-points-count" class="badge bg-secondary">0</span></h5>
<button id="toggle-all-points" class="btn btn-outline-primary btn-sm">
<i class="bi bi-eye"></i> Показать/Скрыть
</button>
</div>
<div id="all-points-wrapper" style="display: none;">
<div class="legend-container" id="legend-container"></div>
<div id="all-points-table"></div>
</div>
</div>
</div>
</div>
<!-- Modal for group details -->
<div class="modal fade" id="groupDetailsModal" tabindex="-1" aria-labelledby="groupDetailsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="groupDetailsModalLabel">Детали группы</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="group-alert-container"></div>
<div class="card mb-3">
<div class="card-header bg-primary text-white">
<i class="bi bi-geo-alt"></i> Усреднённый результат
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4">
<strong>Усреднённые координаты:</strong>
<span id="modal-avg-coords" class="fs-5 text-primary"></span>
</div>
<div class="col-md-2">
<strong>Всего точек:</strong>
<span id="modal-total-points" class="badge bg-secondary"></span>
</div>
<div class="col-md-2">
<strong>Валидных:</strong>
<span id="modal-valid-points" class="badge bg-success"></span>
</div>
<div class="col-md-2">
<strong>Выбросов:</strong>
<span id="modal-outliers-count" class="badge bg-danger"></span>
</div>
</div>
</div>
</div>
<div id="group-points-container"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
<button type="button" class="btn btn-warning" id="btn-average-all" style="display: none;">
<i class="bi bi-calculator"></i> Усреднить все точки
</button>
<button type="button" class="btn btn-primary" id="btn-average-valid" style="display: none;">
<i class="bi bi-check-circle"></i> Усреднить без выбросов
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{% static 'tabulator/js/tabulator.min.js' %}"></script>
<script src="{% static 'sheetjs/xlsx.full.min.js' %}"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
let allGroupsData = [];
let currentGroupIndex = null;
let allPointsTable = null;
// Color palette for groups
const groupColors = [
'#cce5ff', '#d4edda', '#fff3cd', '#f8d7da', '#d1ecf1',
'#e2d5f1', '#ffeeba', '#c3e6cb', '#bee5eb', '#f5c6cb'
];
// Initialize Tabulator
const table = new Tabulator("#result-table", {
layout: "fitDataStretch",
height: "500px",
placeholder: "Нет данных. Выберите спутник и диапазон дат, затем нажмите 'Добавить'.",
headerWordWrap: true,
columns: [
{title: "Объект наблюдения", field: "source_name", minWidth: 180, widthGrow: 2},
{title: "Интервал", field: "interval_label", minWidth: 150, widthGrow: 1.5},
{title: "Частота, МГц", field: "frequency", minWidth: 100, widthGrow: 1},
{title: "Полоса, МГц", field: "freq_range", minWidth: 100, widthGrow: 1},
{title: "Символьная скорость, БОД", field: "bod_velocity", minWidth: 120, widthGrow: 1.5},
{title: "Модуляция", field: "modulation", minWidth: 100, widthGrow: 1},
{title: "ОСШ", field: "snr", minWidth: 70, widthGrow: 0.8},
{title: "Зеркала", field: "mirrors", minWidth: 130, widthGrow: 1.5},
{title: "Усреднённые координаты", field: "avg_coordinates", minWidth: 150, widthGrow: 2},
{title: "Кол-во точек", field: "total_points", minWidth: 80, widthGrow: 0.8, hozAlign: "center"},
{
title: "Статус",
field: "status",
minWidth: 120,
widthGrow: 1,
formatter: function(cell, formatterParams, onRendered) {
const data = cell.getRow().getData();
if (data.has_outliers) {
return `<span class="outlier-warning"><i class="bi bi-exclamation-triangle"></i> Выбросы (${data.outliers_count})</span>`;
}
return '<span class="text-success"><i class="bi bi-check-circle"></i> OK</span>';
}
},
{
title: "Действия",
field: "actions",
minWidth: 100,
widthGrow: 1,
hozAlign: "center",
formatter: function(cell, formatterParams, onRendered) {
const data = cell.getRow().getData();
const btnClass = data.has_outliers ? 'btn-warning' : 'btn-info';
return `<button class="btn btn-sm ${btnClass} btn-view-details" title="Просмотр точек"><i class="bi bi-eye"></i></button>`;
},
cellClick: function(e, cell) {
const data = cell.getRow().getData();
if (e.target.closest('.btn-view-details')) {
showGroupDetails(data._groupIndex);
}
}
}
],
data: [],
rowFormatter: function(row) {
const data = row.getData();
if (data.has_outliers) {
row.getElement().style.backgroundColor = "#fff3cd";
}
}
});
// Update group count
function updateGroupCount() {
document.getElementById('group-count').textContent = allGroupsData.length;
document.getElementById('export-xlsx').disabled = allGroupsData.length === 0;
}
// Show loading overlay
function showLoading() {
document.getElementById('loading-overlay').classList.add('active');
}
// Hide loading overlay
function hideLoading() {
document.getElementById('loading-overlay').classList.remove('active');
}
// Process button click
document.getElementById('btn-process').addEventListener('click', async function() {
const satelliteId = document.getElementById('satellite-select').value;
const dateFrom = document.getElementById('date-from').value;
const dateTo = document.getElementById('date-to').value;
if (!satelliteId) {
alert('Выберите спутник');
return;
}
if (!dateFrom || !dateTo) {
alert('Укажите диапазон дат');
return;
}
showLoading();
try {
const params = new URLSearchParams({
satellite_id: satelliteId,
date_from: dateFrom,
date_to: dateTo
});
const response = await fetch(`/api/points-averaging/?${params.toString()}`);
const data = await response.json();
if (!response.ok) {
alert(data.error || 'Ошибка при обработке данных');
return;
}
// Add groups to table
const startIndex = allGroupsData.length;
data.groups.forEach((group, idx) => {
group._groupIndex = startIndex + idx;
allGroupsData.push(group);
});
// Update table
table.setData(allGroupsData.map(g => ({
...g,
status: g.has_outliers ? 'outliers' : 'ok'
})));
updateGroupCount();
updateAllPointsTable();
} catch (error) {
console.error('Error:', error);
alert('Произошла ошибка при обработке данных');
} finally {
hideLoading();
}
});
// Update all points table
function updateAllPointsTable() {
if (allGroupsData.length === 0) {
document.getElementById('all-points-section').style.display = 'none';
return;
}
document.getElementById('all-points-section').style.display = 'block';
// Collect all points with group info
const allPoints = [];
allGroupsData.forEach((group, groupIdx) => {
group.points.forEach(point => {
allPoints.push({
...point,
group_name: group.source_name,
group_interval: group.interval_label,
group_index: groupIdx,
group_color: groupColors[groupIdx % groupColors.length],
avg_coordinates: group.avg_coordinates
});
});
});
document.getElementById('all-points-count').textContent = allPoints.length;
// Build legend
let legendHtml = '';
allGroupsData.forEach((group, idx) => {
const color = groupColors[idx % groupColors.length];
legendHtml += `
<span class="legend-item">
<span class="legend-color" style="background-color: ${color};"></span>
<small>${group.source_name} - ${group.interval_label}</small>
</span>
`;
});
document.getElementById('legend-container').innerHTML = legendHtml;
// Initialize or update all points table
if (!allPointsTable) {
allPointsTable = new Tabulator("#all-points-table", {
layout: "fitDataStretch",
height: "400px",
placeholder: "Нет точек",
headerWordWrap: true,
columns: [
{title: "ID", field: "id", minWidth: 60, widthGrow: 0.5},
{title: "Группа", field: "group_name", minWidth: 150, widthGrow: 1.5},
{title: "Интервал", field: "group_interval", minWidth: 140, widthGrow: 1.2},
{title: "Имя точки", field: "name", minWidth: 150, widthGrow: 1.5},
{title: "Частота", field: "frequency", minWidth: 80, widthGrow: 0.8},
{title: "Полоса", field: "freq_range", minWidth: 80, widthGrow: 0.8},
{title: "Символьная скорость", field: "bod_velocity", minWidth: 100, widthGrow: 1},
{title: "Модуляция", field: "modulation", minWidth: 80, widthGrow: 0.8},
{title: "ОСШ", field: "snr", minWidth: 50, widthGrow: 0.5},
{title: "Дата/Время", field: "timestamp", minWidth: 120, widthGrow: 1},
{title: "Зеркала", field: "mirrors", minWidth: 100, widthGrow: 1},
{title: "Местоположение", field: "location", minWidth: 100, widthGrow: 1},
{title: "Координаты точки", field: "coordinates", minWidth: 130, widthGrow: 1.2},
{title: "Усреднённые коорд.", field: "avg_coordinates", minWidth: 130, widthGrow: 1.2},
{title: "Расст. от среднего, км", field: "distance_from_avg", minWidth: 100, widthGrow: 0.8, hozAlign: "right"},
{
title: "Статус",
field: "is_outlier",
minWidth: 80,
widthGrow: 0.7,
formatter: function(cell) {
return cell.getValue()
? '<span class="badge bg-danger">Выброс</span>'
: '<span class="badge bg-success">OK</span>';
}
}
],
data: allPoints,
rowFormatter: function(row) {
const data = row.getData();
row.getElement().style.backgroundColor = data.group_color;
if (data.is_outlier) {
row.getElement().style.border = '2px solid #dc3545';
}
}
});
} else {
allPointsTable.setData(allPoints);
}
}
// Toggle all points visibility
document.getElementById('toggle-all-points').addEventListener('click', function() {
const wrapper = document.getElementById('all-points-wrapper');
if (wrapper.style.display === 'none') {
wrapper.style.display = 'block';
this.innerHTML = '<i class="bi bi-eye-slash"></i> Скрыть';
} else {
wrapper.style.display = 'none';
this.innerHTML = '<i class="bi bi-eye"></i> Показать/Скрыть';
}
});
// Show group details modal
function showGroupDetails(groupIndex) {
currentGroupIndex = groupIndex;
const group = allGroupsData[groupIndex];
if (!group) return;
// Update modal title
document.getElementById('groupDetailsModalLabel').textContent =
`Детали группы: ${group.source_name} - ${group.interval_label}`;
// Update summary info
document.getElementById('modal-avg-coords').textContent = group.avg_coordinates;
document.getElementById('modal-total-points').textContent = group.total_points;
document.getElementById('modal-valid-points').textContent = group.valid_points_count;
document.getElementById('modal-outliers-count').textContent = group.outliers_count;
// Show/hide alert and buttons based on outliers
const alertContainer = document.getElementById('group-alert-container');
const btnAverageAll = document.getElementById('btn-average-all');
const btnAverageValid = document.getElementById('btn-average-valid');
if (group.has_outliers) {
alertContainer.innerHTML = `
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle"></i>
<strong>Внимание!</strong> Обнаружены точки на расстоянии более 56 км от среднего значения.
Они выделены красным цветом в таблице ниже.
</div>
`;
btnAverageAll.style.display = 'inline-block';
btnAverageValid.style.display = 'inline-block';
} else {
alertContainer.innerHTML = `
<div class="alert alert-success">
<i class="bi bi-check-circle"></i>
Все точки находятся в пределах 56 км от среднего значения.
</div>
`;
btnAverageAll.style.display = 'none';
btnAverageValid.style.display = 'none';
}
// Build points table - first valid points, then outliers
let html = `
<h6 class="text-success"><i class="bi bi-check-circle"></i> Валидные точки (${group.valid_points_count})</h6>
<table class="table table-sm table-bordered points-detail-table">
<thead class="table-success">
<tr>
<th>ID</th>
<th>Имя</th>
<th>Частота</th>
<th>Полоса</th>
<th>Дата/Время</th>
<th>Координаты</th>
<th>Расстояние от среднего, км</th>
</tr>
</thead>
<tbody>
`;
// Valid points
group.points.forEach((point, idx) => {
if (!point.is_outlier) {
html += `
<tr class="table-success" data-point-index="${idx}">
<td>${point.id}</td>
<td>${point.name}</td>
<td>${point.frequency}</td>
<td>${point.freq_range}</td>
<td>${point.timestamp}</td>
<td>${point.coordinates}</td>
<td>${point.distance_from_avg}</td>
</tr>
`;
}
});
html += '</tbody></table>';
// Outliers section (if any)
if (group.has_outliers) {
html += `
<h6 class="text-danger mt-4"><i class="bi bi-exclamation-triangle"></i> Выбросы - точки за пределами 56 км (${group.outliers_count})</h6>
<table class="table table-sm table-bordered points-detail-table">
<thead class="table-danger">
<tr>
<th>ID</th>
<th>Имя</th>
<th>Частота</th>
<th>Полоса</th>
<th>Дата/Время</th>
<th>Координаты</th>
<th>Расстояние от среднего, км</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
`;
group.points.forEach((point, idx) => {
if (point.is_outlier) {
html += `
<tr class="table-danger" data-point-index="${idx}">
<td>${point.id}</td>
<td>${point.name}</td>
<td>${point.frequency}</td>
<td>${point.freq_range}</td>
<td>${point.timestamp}</td>
<td>${point.coordinates}</td>
<td><strong>${point.distance_from_avg}</strong></td>
<td>
<button class="btn btn-sm btn-danger btn-remove-point" data-index="${idx}" title="Удалить точку">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
`;
}
});
html += '</tbody></table>';
}
document.getElementById('group-points-container').innerHTML = html;
// Add event listeners for remove buttons
document.querySelectorAll('.btn-remove-point').forEach(btn => {
btn.addEventListener('click', function() {
const pointIndex = parseInt(this.dataset.index);
removePointFromGroup(groupIndex, pointIndex);
});
});
// Show modal
const modal = new bootstrap.Modal(document.getElementById('groupDetailsModal'));
modal.show();
}
// Remove point from group and recalculate
async function removePointFromGroup(groupIndex, pointIndex) {
const group = allGroupsData[groupIndex];
if (!group) return;
// Check if this is the last point
if (group.points.length <= 1) {
alert('Нельзя удалить последнюю точку. Удалите всю группу из основной таблицы.');
return;
}
// Remove point
group.points.splice(pointIndex, 1);
// Recalculate with all remaining points
await recalculateGroup(groupIndex, true);
}
// Recalculate group
async function recalculateGroup(groupIndex, includeAll) {
const group = allGroupsData[groupIndex];
if (!group) {
hideLoading();
return;
}
// Check if there are points to process
if (!group.points || group.points.length === 0) {
alert('Нет точек для пересчёта');
hideLoading();
return;
}
showLoading();
try {
const response = await fetch('/api/points-averaging/recalculate/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken')
},
body: JSON.stringify({
points: group.points,
include_all: includeAll
})
});
const data = await response.json();
if (!response.ok) {
alert(data.error || 'Ошибка при пересчёте');
hideLoading();
return;
}
// Update group data
group.avg_coordinates = data.avg_coordinates;
group.avg_coord_tuple = data.avg_coord_tuple;
group.total_points = data.total_points;
group.valid_points_count = data.valid_points_count;
group.outliers_count = data.outliers_count;
group.has_outliers = data.has_outliers;
group.points = data.points;
// Update table
table.setData(allGroupsData.map(g => ({
...g,
status: g.has_outliers ? 'outliers' : 'ok'
})));
// Update all points table
updateAllPointsTable();
// Update modal if open
if (currentGroupIndex === groupIndex) {
showGroupDetails(groupIndex);
}
} catch (error) {
console.error('Error:', error);
alert('Произошла ошибка при пересчёте');
} finally {
hideLoading();
}
}
// Average all points button
document.getElementById('btn-average-all').addEventListener('click', function() {
if (currentGroupIndex !== null) {
recalculateGroup(currentGroupIndex, true);
}
});
// Average valid points button
document.getElementById('btn-average-valid').addEventListener('click', function() {
if (currentGroupIndex !== null) {
recalculateGroup(currentGroupIndex, false);
}
});
// Export to Excel
document.getElementById('export-xlsx').addEventListener('click', function() {
if (allGroupsData.length === 0) {
alert('Нет данных для экспорта');
return;
}
// Prepare summary data for export
const summaryData = allGroupsData.map(group => ({
'Объект наблюдения': group.source_name,
'Интервал': group.interval_label,
'Частота, МГц': group.frequency,
'Полоса, МГц': group.freq_range,
'Символьная скорость, БОД': group.bod_velocity,
'Модуляция': group.modulation,
'ОСШ': group.snr,
'Зеркала': group.mirrors,
'Усреднённые координаты': group.avg_coordinates,
'Кол-во точек': group.total_points,
'Выбросов': group.outliers_count,
'Статус': group.has_outliers ? 'Есть выбросы' : 'OK'
}));
// Prepare all points data for export
const allPointsData = [];
allGroupsData.forEach((group, groupIdx) => {
group.points.forEach(point => {
allPointsData.push({
'Группа': group.source_name,
'Интервал': group.interval_label,
'ID точки': point.id,
'Имя точки': point.name,
'Частота, МГц': point.frequency,
'Полоса, МГц': point.freq_range,
'Символьная скорость, БОД': point.bod_velocity,
'Модуляция': point.modulation,
'ОСШ': point.snr,
'Дата/Время': point.timestamp,
'Зеркала': point.mirrors,
'Местоположение': point.location,
'Координаты точки': point.coordinates,
'Усреднённые координаты': group.avg_coordinates,
'Расстояние от среднего, км': point.distance_from_avg,
'Статус': point.is_outlier ? 'Выброс' : 'OK'
});
});
});
// Create workbook with two sheets
const wb = XLSX.utils.book_new();
// Summary sheet
const wsSummary = XLSX.utils.json_to_sheet(summaryData);
XLSX.utils.book_append_sheet(wb, wsSummary, "Усреднение");
// All points sheet
const wsPoints = XLSX.utils.json_to_sheet(allPointsData);
XLSX.utils.book_append_sheet(wb, wsPoints, "Все точки");
// Generate filename with date
const now = new Date();
const dateStr = now.toISOString().slice(0, 10);
const filename = `averaging_${dateStr}.xlsx`;
// Download
XLSX.writeFile(wb, filename);
});
// Clear table
document.getElementById('clear-table').addEventListener('click', function() {
if (confirm('Вы уверены, что хотите очистить таблицу?')) {
allGroupsData = [];
table.clearData();
updateGroupCount();
if (allPointsTable) {
allPointsTable.clearData();
}
document.getElementById('all-points-section').style.display = 'none';
document.getElementById('all-points-wrapper').style.display = 'none';
document.getElementById('toggle-all-points').innerHTML = '<i class="bi bi-eye"></i> Показать/Скрыть';
}
});
// 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;
}
// Initialize
updateGroupCount();
});
</script>
{% endblock %}