811 lines
33 KiB
HTML
811 lines
33 KiB
HTML
{% 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);
|
||
}
|
||
#sources-table {
|
||
margin-top: 15px;
|
||
font-size: 12px;
|
||
}
|
||
#sources-table .tabulator-header {
|
||
font-size: 12px;
|
||
}
|
||
#sources-table .tabulator-cell {
|
||
font-size: 12px;
|
||
padding: 6px 4px;
|
||
}
|
||
.btn-group-custom {
|
||
margin-top: 15px;
|
||
}
|
||
.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;
|
||
}
|
||
.modal-xl {
|
||
max-width: 95%;
|
||
}
|
||
.group-card {
|
||
border: 1px solid #e9ecef;
|
||
border-radius: 6px;
|
||
margin-bottom: 10px;
|
||
background: #fff;
|
||
}
|
||
.group-header {
|
||
background: #f8f9fa;
|
||
padding: 10px 12px;
|
||
border-bottom: 1px solid #e9ecef;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
}
|
||
.group-header.has-outliers {
|
||
background: #fff3cd;
|
||
}
|
||
.group-info {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 15px;
|
||
align-items: center;
|
||
}
|
||
.group-info-item {
|
||
font-size: 13px;
|
||
}
|
||
.group-info-item strong {
|
||
color: #495057;
|
||
}
|
||
.group-actions {
|
||
display: flex;
|
||
gap: 5px;
|
||
}
|
||
.group-body {
|
||
padding: 10px;
|
||
}
|
||
.points-table {
|
||
font-size: 11px;
|
||
width: 100%;
|
||
}
|
||
.points-table th, .points-table td {
|
||
padding: 5px 6px;
|
||
border: 1px solid #dee2e6;
|
||
}
|
||
.points-table th {
|
||
background: #f8f9fa;
|
||
font-weight: 600;
|
||
}
|
||
.points-table tr.outlier {
|
||
background-color: #ffcccc !important;
|
||
}
|
||
.points-table tr.valid {
|
||
background-color: #d4edda !important;
|
||
}
|
||
.source-has-outliers {
|
||
background-color: #fff3cd !important;
|
||
}
|
||
</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="source-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="export-json" class="btn btn-info ms-2" disabled>
|
||
<i class="bi bi-filetype-json"></i> Сохранить в JSON
|
||
</button>
|
||
<button id="clear-all" class="btn btn-danger ms-2">
|
||
<i class="bi bi-trash"></i> Очистить всё
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="sources-table"></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-xl">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="sourceDetailsModalLabel">Детали объекта</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||
</div>
|
||
<div class="modal-body" id="modal-body-content">
|
||
<!-- Groups will be rendered here -->
|
||
</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 'tabulator/js/tabulator.min.js' %}"></script>
|
||
<script src="{% static 'sheetjs/xlsx.full.min.js' %}"></script>
|
||
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
let allSourcesData = [];
|
||
let currentSourceIdx = null;
|
||
let sourcesTable = null;
|
||
|
||
function showLoading() {
|
||
document.getElementById('loading-overlay').classList.add('active');
|
||
}
|
||
function hideLoading() {
|
||
document.getElementById('loading-overlay').classList.remove('active');
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
function generateUUID() {
|
||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||
const r = Math.random() * 16 | 0;
|
||
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||
return v.toString(16);
|
||
});
|
||
}
|
||
|
||
function updateCounts() {
|
||
document.getElementById('source-count').textContent = allSourcesData.length;
|
||
const hasData = allSourcesData.length > 0;
|
||
document.getElementById('export-xlsx').disabled = !hasData;
|
||
document.getElementById('export-json').disabled = !hasData;
|
||
}
|
||
|
||
// Prepare table data from sources
|
||
function getTableData() {
|
||
const data = [];
|
||
allSourcesData.forEach((source, sourceIdx) => {
|
||
const totalPoints = source.groups.reduce((sum, g) => sum + g.valid_points_count, 0);
|
||
const hasOutliers = source.groups.some(g => g.has_outliers);
|
||
|
||
// Get first group's params as representative
|
||
const firstGroup = source.groups[0] || {};
|
||
|
||
data.push({
|
||
_sourceIdx: sourceIdx,
|
||
source_name: source.source_name,
|
||
source_id: source.source_id,
|
||
groups_count: source.groups.length,
|
||
total_points: totalPoints,
|
||
has_outliers: hasOutliers,
|
||
frequency: firstGroup.frequency || '-',
|
||
modulation: firstGroup.modulation || '-',
|
||
mirrors: firstGroup.mirrors || '-',
|
||
});
|
||
});
|
||
return data;
|
||
}
|
||
|
||
// Initialize or update sources table
|
||
function updateSourcesTable() {
|
||
const data = getTableData();
|
||
|
||
if (!sourcesTable) {
|
||
sourcesTable = new Tabulator("#sources-table", {
|
||
layout: "fitDataStretch",
|
||
height: "500px",
|
||
placeholder: "Нет данных. Выберите спутник и диапазон дат, затем нажмите 'Загрузить данные'.",
|
||
initialSort: [
|
||
{column: "frequency", dir: "asc"}
|
||
],
|
||
columns: [
|
||
{title: "Объект", field: "source_name", minWidth: 180, widthGrow: 2},
|
||
{title: "Групп", field: "groups_count", minWidth: 70, hozAlign: "center"},
|
||
{title: "Точек", field: "total_points", minWidth: 70, hozAlign: "center"},
|
||
{title: "Частота", field: "frequency", minWidth: 100, sorter: "number"},
|
||
{title: "Модуляция", field: "modulation", minWidth: 90},
|
||
{title: "Зеркала", field: "mirrors", minWidth: 130},
|
||
{
|
||
title: "Действия",
|
||
field: "actions",
|
||
minWidth: 150,
|
||
hozAlign: "center",
|
||
formatter: function(cell) {
|
||
const data = cell.getRow().getData();
|
||
const outlierBadge = data.has_outliers ? '<span class="badge bg-warning me-1">!</span>' : '';
|
||
return `${outlierBadge}
|
||
<button class="btn btn-sm btn-primary btn-view-source" title="Открыть детали">
|
||
<i class="bi bi-eye"></i>
|
||
</button>
|
||
<button class="btn btn-sm btn-danger btn-delete-source ms-1" title="Удалить">
|
||
<i class="bi bi-trash"></i>
|
||
</button>`;
|
||
},
|
||
cellClick: function(e, cell) {
|
||
const data = cell.getRow().getData();
|
||
if (e.target.closest('.btn-view-source')) {
|
||
openSourceModal(data._sourceIdx);
|
||
} else if (e.target.closest('.btn-delete-source')) {
|
||
deleteSource(data._sourceIdx);
|
||
}
|
||
}
|
||
}
|
||
],
|
||
data: data,
|
||
rowFormatter: function(row) {
|
||
if (row.getData().has_outliers) {
|
||
row.getElement().classList.add('source-has-outliers');
|
||
}
|
||
}
|
||
});
|
||
} else {
|
||
sourcesTable.setData(data);
|
||
}
|
||
|
||
updateCounts();
|
||
}
|
||
|
||
// Delete source
|
||
function deleteSource(sourceIdx) {
|
||
//if (!confirm('Удалить этот объект со всеми группами?')) return;
|
||
allSourcesData.splice(sourceIdx, 1);
|
||
updateSourcesTable();
|
||
}
|
||
|
||
// Open source modal
|
||
function openSourceModal(sourceIdx) {
|
||
currentSourceIdx = sourceIdx;
|
||
const source = allSourcesData[sourceIdx];
|
||
if (!source) return;
|
||
|
||
document.getElementById('sourceDetailsModalLabel').textContent = `Объект: ${source.source_name}`;
|
||
renderModalContent();
|
||
|
||
const modal = new bootstrap.Modal(document.getElementById('sourceDetailsModal'));
|
||
modal.show();
|
||
}
|
||
|
||
// Render modal content
|
||
function renderModalContent() {
|
||
const source = allSourcesData[currentSourceIdx];
|
||
if (!source) return;
|
||
|
||
let html = '';
|
||
source.groups.forEach((group, groupIdx) => {
|
||
html += renderGroupCard(group, groupIdx);
|
||
});
|
||
|
||
if (source.groups.length === 0) {
|
||
html = '<div class="alert alert-info">Нет групп для отображения</div>';
|
||
}
|
||
|
||
document.getElementById('modal-body-content').innerHTML = html;
|
||
addModalEventListeners();
|
||
}
|
||
|
||
// Render group card
|
||
function renderGroupCard(group, groupIdx) {
|
||
const headerClass = group.has_outliers ? 'has-outliers' : '';
|
||
|
||
let pointsHtml = '';
|
||
group.points.forEach((point, pointIdx) => {
|
||
const rowClass = point.is_outlier ? 'outlier' : 'valid';
|
||
pointsHtml += `
|
||
<tr class="${rowClass}">
|
||
<td>${point.id}</td>
|
||
<td>${point.name}</td>
|
||
<td>${point.frequency}</td>
|
||
<td>${point.freq_range}</td>
|
||
<td>${point.bod_velocity}</td>
|
||
<td>${point.modulation}</td>
|
||
<td>${point.snr}</td>
|
||
<td>${point.timestamp}</td>
|
||
<td>${point.mirrors}</td>
|
||
<td>${point.coordinates}</td>
|
||
<td>${point.distance_from_avg}</td>
|
||
<td>
|
||
<button class="btn btn-sm btn-outline-danger btn-delete-point"
|
||
data-group-idx="${groupIdx}"
|
||
data-point-idx="${pointIdx}"
|
||
title="Удалить точку">
|
||
<i class="bi bi-x"></i>
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
});
|
||
|
||
return `
|
||
<div class="group-card" data-group-idx="${groupIdx}">
|
||
<div class="group-header ${headerClass}">
|
||
<div class="group-info">
|
||
<span class="group-info-item"><strong>Интервал:</strong> ${group.interval_label}</span>
|
||
<span class="group-info-item"><strong>Усреднённые координаты:</strong> ${group.avg_coordinates} <span class="badge bg-secondary">${group.avg_type || 'ГК'}</span></span>
|
||
<span class="group-info-item"><strong>Медианное время:</strong> ${group.avg_time}</span>
|
||
<span class="group-info-item"><strong>Точек:</strong> ${group.valid_points_count}/${group.total_points}</span>
|
||
${group.has_outliers ? `<span class="badge bg-warning">Выбросов: ${group.outliers_count}</span>` : ''}
|
||
</div>
|
||
<div class="group-actions">
|
||
<button class="btn btn-sm btn-primary btn-average-group" data-group-idx="${groupIdx}" title="Пересчитать усреднение">
|
||
<i class="bi bi-calculator"></i> Усреднить
|
||
</button>
|
||
${group.has_outliers ? `
|
||
<button class="btn btn-sm btn-warning btn-average-all" data-group-idx="${groupIdx}" title="Усреднить все точки">
|
||
<i class="bi bi-arrow-repeat"></i> Все точки
|
||
</button>
|
||
` : ''}
|
||
<button class="btn btn-sm btn-danger btn-delete-group" data-group-idx="${groupIdx}" title="Удалить группу">
|
||
<i class="bi bi-trash"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="group-body">
|
||
<table class="points-table">
|
||
<thead>
|
||
<tr>
|
||
<th>ID</th>
|
||
<th>Имя</th>
|
||
<th>Частота</th>
|
||
<th>Полоса</th>
|
||
<th>Симв. скорость</th>
|
||
<th>Модуляция</th>
|
||
<th>ОСШ</th>
|
||
<th>Дата/Время</th>
|
||
<th>Зеркала</th>
|
||
<th>Координаты</th>
|
||
<th>Расст., км</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>${pointsHtml}</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// Add event listeners for modal
|
||
function addModalEventListeners() {
|
||
document.querySelectorAll('.btn-delete-group').forEach(btn => {
|
||
btn.addEventListener('click', function() {
|
||
const groupIdx = parseInt(this.dataset.groupIdx);
|
||
deleteGroup(groupIdx);
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('.btn-delete-point').forEach(btn => {
|
||
btn.addEventListener('click', function() {
|
||
const groupIdx = parseInt(this.dataset.groupIdx);
|
||
const pointIdx = parseInt(this.dataset.pointIdx);
|
||
deletePoint(groupIdx, pointIdx);
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('.btn-average-group').forEach(btn => {
|
||
btn.addEventListener('click', function() {
|
||
const groupIdx = parseInt(this.dataset.groupIdx);
|
||
recalculateGroup(groupIdx, false);
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('.btn-average-all').forEach(btn => {
|
||
btn.addEventListener('click', function() {
|
||
const groupIdx = parseInt(this.dataset.groupIdx);
|
||
recalculateGroup(groupIdx, true);
|
||
});
|
||
});
|
||
}
|
||
|
||
// Delete group
|
||
function deleteGroup(groupIdx) {
|
||
if (!confirm('Удалить эту группу точек?')) return;
|
||
|
||
const source = allSourcesData[currentSourceIdx];
|
||
source.groups.splice(groupIdx, 1);
|
||
|
||
if (source.groups.length === 0) {
|
||
allSourcesData.splice(currentSourceIdx, 1);
|
||
bootstrap.Modal.getInstance(document.getElementById('sourceDetailsModal')).hide();
|
||
updateSourcesTable();
|
||
} else {
|
||
renderModalContent();
|
||
updateSourcesTable();
|
||
}
|
||
}
|
||
|
||
// Delete point
|
||
function deletePoint(groupIdx, pointIdx) {
|
||
const source = allSourcesData[currentSourceIdx];
|
||
const group = source.groups[groupIdx];
|
||
|
||
if (group.points.length <= 1) {
|
||
alert('Нельзя удалить последнюю точку. Удалите группу целиком.');
|
||
return;
|
||
}
|
||
|
||
if (!confirm('Удалить эту точку и пересчитать усреднение?')) return;
|
||
|
||
group.points.splice(pointIdx, 1);
|
||
group.total_points = group.points.length;
|
||
recalculateGroup(groupIdx, true);
|
||
}
|
||
|
||
// Recalculate group
|
||
async function recalculateGroup(groupIdx, includeAll) {
|
||
const source = allSourcesData[currentSourceIdx];
|
||
const group = source.groups[groupIdx];
|
||
|
||
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 || 'Ошибка при пересчёте');
|
||
return;
|
||
}
|
||
|
||
// Update group data
|
||
Object.assign(group, {
|
||
avg_coordinates: data.avg_coordinates,
|
||
avg_coord_tuple: data.avg_coord_tuple,
|
||
avg_type: data.avg_type,
|
||
total_points: data.total_points,
|
||
valid_points_count: data.valid_points_count,
|
||
outliers_count: data.outliers_count,
|
||
has_outliers: data.has_outliers,
|
||
mirrors: data.mirrors || group.mirrors,
|
||
avg_time: data.avg_time || group.avg_time,
|
||
points: data.points,
|
||
});
|
||
|
||
renderModalContent();
|
||
updateSourcesTable();
|
||
|
||
} catch (error) {
|
||
console.error('Error:', error);
|
||
alert('Произошла ошибка при пересчёте');
|
||
} finally {
|
||
hideLoading();
|
||
}
|
||
}
|
||
|
||
// 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; }
|
||
|
||
data.sources.forEach(source => {
|
||
const existingIdx = allSourcesData.findIndex(s => s.source_id === source.source_id);
|
||
if (existingIdx >= 0) {
|
||
source.groups.forEach(newGroup => {
|
||
const existingGroupIdx = allSourcesData[existingIdx].groups.findIndex(g => g.interval_key === newGroup.interval_key);
|
||
if (existingGroupIdx < 0) {
|
||
allSourcesData[existingIdx].groups.push(newGroup);
|
||
}
|
||
});
|
||
} else {
|
||
allSourcesData.push(source);
|
||
}
|
||
});
|
||
|
||
updateSourcesTable();
|
||
|
||
} catch (error) {
|
||
console.error('Error:', error);
|
||
alert('Произошла ошибка при обработке данных');
|
||
} finally {
|
||
hideLoading();
|
||
}
|
||
});
|
||
|
||
// Clear all
|
||
document.getElementById('clear-all').addEventListener('click', function() {
|
||
if (!confirm('Очистить все данные?')) return;
|
||
allSourcesData = [];
|
||
updateSourcesTable();
|
||
});
|
||
|
||
// Export to Excel
|
||
document.getElementById('export-xlsx').addEventListener('click', function() {
|
||
if (allSourcesData.length === 0) { alert('Нет данных для экспорта'); return; }
|
||
|
||
const summaryData = [];
|
||
allSourcesData.forEach(source => {
|
||
source.groups.forEach(group => {
|
||
summaryData.push({
|
||
'Объект': source.source_name,
|
||
'Частота, МГц': group.frequency,
|
||
'Полоса, МГц': group.freq_range,
|
||
'Символьная скорость, БОД': group.bod_velocity,
|
||
'Модуляция': group.modulation,
|
||
'ОСШ': group.snr,
|
||
'Зеркала': group.mirrors,
|
||
'Усреднённые координаты': group.avg_coordinates,
|
||
// 'Тип усреднения': group.avg_type || 'ГК',
|
||
'Время': group.avg_time || '-',
|
||
'Кол-во точек': group.valid_points_count
|
||
});
|
||
});
|
||
});
|
||
|
||
// Sort by frequency
|
||
summaryData.sort((a, b) => {
|
||
const freqA = parseFloat(a['Частота, МГц']) || 0;
|
||
const freqB = parseFloat(b['Частота, МГц']) || 0;
|
||
return freqA - freqB;
|
||
});
|
||
|
||
const allPointsData = [];
|
||
allSourcesData.forEach(source => {
|
||
source.groups.forEach(group => {
|
||
group.points.forEach(point => {
|
||
allPointsData.push({
|
||
'Объект': source.source_name,
|
||
'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'
|
||
});
|
||
});
|
||
});
|
||
});
|
||
|
||
// Sort by frequency
|
||
allPointsData.sort((a, b) => {
|
||
const freqA = parseFloat(a['Частота, МГц']) || 0;
|
||
const freqB = parseFloat(b['Частота, МГц']) || 0;
|
||
return freqA - freqB;
|
||
});
|
||
|
||
const wb = XLSX.utils.book_new();
|
||
XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(summaryData), "Усреднение");
|
||
XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(allPointsData), "Все точки");
|
||
|
||
const dateStr = new Date().toISOString().slice(0, 10);
|
||
XLSX.writeFile(wb, `averaging_${dateStr}.xlsx`);
|
||
});
|
||
|
||
|
||
// Export to JSON
|
||
document.getElementById('export-json').addEventListener('click', function() {
|
||
if (allSourcesData.length === 0) { alert('Нет данных для экспорта'); return; }
|
||
|
||
const CREATOR_ID = '6fd12c90-7f17-43d9-a03e-ee14e880f757';
|
||
|
||
const pathObject = {
|
||
"tacticObjectType": "path",
|
||
"captionPosition": "right",
|
||
"points": [
|
||
{"id": "b92b9cbb-dd27-49aa-bcb6-e89a147bc02c", "latitude": 57, "longitude": -13, "altitude": 0, "customActions": [], "tags": {"creator": CREATOR_ID}, "tacticObjectType": "point"},
|
||
{"id": "8e3666d4-4990-4cb9-9594-63ad06333489", "latitude": 57, "longitude": 64, "altitude": 0, "customActions": [], "tags": {"creator": CREATOR_ID}, "tacticObjectType": "point"},
|
||
{"id": "5f137485-d2fc-443d-8507-c936f02f3569", "latitude": 11, "longitude": 64, "altitude": 0, "customActions": [], "tags": {"creator": CREATOR_ID}, "tacticObjectType": "point"},
|
||
{"id": "0fb90df7-8eb0-49fa-9d00-336389171bf5", "latitude": 11, "longitude": -13, "altitude": 0, "customActions": [], "tags": {"creator": CREATOR_ID}, "tacticObjectType": "point"},
|
||
{"id": "3ef12637-585e-40a4-b0ee-8f1786c89ce6", "latitude": 57, "longitude": -13, "altitude": 0, "customActions": [], "tags": {"creator": CREATOR_ID}, "tacticObjectType": "point"}
|
||
],
|
||
"isCycle": false,
|
||
"id": "2f604051-4984-4c2f-8c4c-c0cb64008f5f",
|
||
"draggable": false, "selectable": false, "editable": false,
|
||
"caption": "Ограничение для работы с поверхностями",
|
||
"line": {"color": "rgb(148,0,211)", "thickness": 1, "dash": "solid", "border": null},
|
||
"customActions": [],
|
||
"tags": {"creator": CREATOR_ID}
|
||
};
|
||
|
||
const result = [pathObject];
|
||
|
||
const jsonSourceColors = [
|
||
"rgb(0,128,0)", "rgb(0,0,255)", "rgb(255,0,0)", "rgb(255,165,0)", "rgb(128,0,128)",
|
||
"rgb(0,128,128)", "rgb(255,20,147)", "rgb(139,69,19)", "rgb(0,100,0)", "rgb(70,130,180)"
|
||
];
|
||
|
||
allSourcesData.forEach((source, sourceIdx) => {
|
||
const sourceColor = jsonSourceColors[sourceIdx % jsonSourceColors.length];
|
||
|
||
source.groups.forEach(group => {
|
||
const avgCoord = group.avg_coord_tuple;
|
||
const avgLat = avgCoord[1];
|
||
const avgLon = avgCoord[0];
|
||
const avgCaption = `${source.source_name} (усредн) - ${group.avg_time || '-'}`;
|
||
const avgSourceId = generateUUID();
|
||
|
||
result.push({
|
||
"tacticObjectType": "source",
|
||
"captionPosition": "right",
|
||
"id": avgSourceId,
|
||
"icon": {"type": "triangle", "color": sourceColor},
|
||
"caption": avgCaption,
|
||
"name": avgCaption,
|
||
"customActions": [],
|
||
"trackBehavior": {},
|
||
"bearingStyle": {"color": sourceColor, "thickness": 2, "dash": "solid", "border": null},
|
||
"bearingBehavior": {},
|
||
"tags": {"creator": CREATOR_ID}
|
||
});
|
||
|
||
result.push({
|
||
"tacticObjectType": "position",
|
||
"id": generateUUID(),
|
||
"parentId": avgSourceId,
|
||
"timeStamp": Date.now() / 1000,
|
||
"latitude": avgLat,
|
||
"altitude": 0,
|
||
"longitude": avgLon,
|
||
"caption": "",
|
||
"tooltip": "",
|
||
"customActions": [],
|
||
"tags": {"layers": [], "creator": CREATOR_ID}
|
||
});
|
||
|
||
// group.points.forEach(point => {
|
||
// if (point.is_outlier) return;
|
||
|
||
// const pointCoord = point.coord_tuple;
|
||
// const pointCaption = `${point.name || '-'} - ${point.timestamp || '-'}`;
|
||
// const pointSourceId = generateUUID();
|
||
|
||
// result.push({
|
||
// "tacticObjectType": "source",
|
||
// "captionPosition": "right",
|
||
// "id": pointSourceId,
|
||
// "icon": {"type": "circle", "color": sourceColor},
|
||
// "caption": pointCaption,
|
||
// "name": pointCaption,
|
||
// "customActions": [],
|
||
// "trackBehavior": {},
|
||
// "bearingStyle": {"color": sourceColor, "thickness": 2, "dash": "solid", "border": null},
|
||
// "bearingBehavior": {},
|
||
// "tags": {"creator": CREATOR_ID}
|
||
// });
|
||
|
||
// result.push({
|
||
// "tacticObjectType": "position",
|
||
// "id": generateUUID(),
|
||
// "parentId": pointSourceId,
|
||
// "timeStamp": point.timestamp_unix || (Date.now() / 1000),
|
||
// "latitude": pointCoord[1],
|
||
// "altitude": 0,
|
||
// "longitude": pointCoord[0],
|
||
// "caption": "",
|
||
// "tooltip": "",
|
||
// "customActions": [],
|
||
// "tags": {"layers": [], "creator": CREATOR_ID}
|
||
// });
|
||
// });
|
||
});
|
||
});
|
||
|
||
const jsonString = JSON.stringify(result, null, 2);
|
||
const blob = new Blob(['\uFEFF' + jsonString], {type: 'application/json;charset=utf-8'});
|
||
const dateStr = new Date().toISOString().slice(0, 10);
|
||
|
||
const a = document.createElement('a');
|
||
a.href = URL.createObjectURL(blob);
|
||
a.download = `averaging_${dateStr}.json`;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
URL.revokeObjectURL(a.href);
|
||
});
|
||
|
||
// Initialize
|
||
updateSourcesTable();
|
||
});
|
||
</script>
|
||
{% endblock %}
|