Поменял теханализ, улучшения по простбам
This commit is contained in:
@@ -0,0 +1,71 @@
|
|||||||
|
<!-- Frequency Plan Modal -->
|
||||||
|
<div class="modal fade" id="frequencyPlanModal" tabindex="-1" aria-labelledby="frequencyPlanModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-xl">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="frequencyPlanModalLabel">Частотный план</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-5">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Загрузка...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="modalFrequencyContent" style="display: none;">
|
||||||
|
<p class="text-muted">Визуализация транспондеров спутника по частотам. <span style="color: #0d6efd;">■</span> Downlink (синий), <span style="color: #fd7e14;">■</span> Uplink (оранжевый). Используйте колесико мыши для масштабирования, наведите курсор на полосу для подробной информации и связи с парным каналом.</p>
|
||||||
|
|
||||||
|
<div class="frequency-plan">
|
||||||
|
<div class="chart-controls">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary" id="modalResetZoom">
|
||||||
|
<i class="bi bi-arrow-clockwise"></i> Сбросить масштаб
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="frequency-chart-container">
|
||||||
|
<canvas id="modalFrequencyChart"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<p><strong>Всего транспондеров:</strong> <span id="modalTransponderCount">0</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="modalNoData" style="display: none;" class="text-center text-muted py-5">
|
||||||
|
<p>Нет данных о транспондерах для этого спутника</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.frequency-plan {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frequency-chart-container {
|
||||||
|
position: relative;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 20px;
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-controls button {
|
||||||
|
padding: 5px 15px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -53,9 +53,9 @@
|
|||||||
onclick="showSelectedOnMap()">
|
onclick="showSelectedOnMap()">
|
||||||
<i class="bi bi-map"></i> Карта
|
<i class="bi bi-map"></i> Карта
|
||||||
</button>
|
</button>
|
||||||
<a href="{% url 'mainapp:tech_analyze_entry' %}" class="btn btn-info btn-sm" title="Тех. анализ">
|
<!-- <a href="{% url 'mainapp:tech_analyze_entry' %}" class="btn btn-info btn-sm" title="Тех. анализ">
|
||||||
<i class="bi bi-clipboard-data"></i> Тех. анализ
|
<i class="bi bi-clipboard-data"></i> Тех. анализ
|
||||||
</a>
|
</a> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Items per page select moved here -->
|
<!-- Items per page select moved here -->
|
||||||
|
|||||||
@@ -275,11 +275,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
layout: "fitDataStretch",
|
layout: "fitDataStretch",
|
||||||
height: "500px",
|
height: "500px",
|
||||||
placeholder: "Нет данных. Выберите спутник и диапазон дат, затем нажмите 'Загрузить данные'.",
|
placeholder: "Нет данных. Выберите спутник и диапазон дат, затем нажмите 'Загрузить данные'.",
|
||||||
|
initialSort: [
|
||||||
|
{column: "frequency", dir: "asc"}
|
||||||
|
],
|
||||||
columns: [
|
columns: [
|
||||||
{title: "Источник", field: "source_name", minWidth: 180, widthGrow: 2},
|
{title: "Источник", field: "source_name", minWidth: 180, widthGrow: 2},
|
||||||
{title: "Групп", field: "groups_count", minWidth: 70, hozAlign: "center"},
|
{title: "Групп", field: "groups_count", minWidth: 70, hozAlign: "center"},
|
||||||
{title: "Точек", field: "total_points", minWidth: 70, hozAlign: "center"},
|
{title: "Точек", field: "total_points", minWidth: 70, hozAlign: "center"},
|
||||||
{title: "Частота", field: "frequency", minWidth: 100},
|
{title: "Частота", field: "frequency", minWidth: 100, sorter: "number"},
|
||||||
{title: "Модуляция", field: "modulation", minWidth: 90},
|
{title: "Модуляция", field: "modulation", minWidth: 90},
|
||||||
{title: "Зеркала", field: "mirrors", minWidth: 130},
|
{title: "Зеркала", field: "mirrors", minWidth: 130},
|
||||||
{
|
{
|
||||||
@@ -624,14 +627,20 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
'ОСШ': group.snr,
|
'ОСШ': group.snr,
|
||||||
'Зеркала': group.mirrors,
|
'Зеркала': group.mirrors,
|
||||||
'Усреднённые координаты': group.avg_coordinates,
|
'Усреднённые координаты': group.avg_coordinates,
|
||||||
'Тип усреднения': group.avg_type || 'ГК',
|
// 'Тип усреднения': group.avg_type || 'ГК',
|
||||||
'Медианное время': group.avg_time || '-',
|
'Время': group.avg_time || '-',
|
||||||
'Кол-во точек': group.valid_points_count,
|
'Кол-во точек': group.valid_points_count
|
||||||
'Интервал': group.interval_label
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Sort by frequency
|
||||||
|
summaryData.sort((a, b) => {
|
||||||
|
const freqA = parseFloat(a['Частота, МГц']) || 0;
|
||||||
|
const freqB = parseFloat(b['Частота, МГц']) || 0;
|
||||||
|
return freqA - freqB;
|
||||||
|
});
|
||||||
|
|
||||||
const allPointsData = [];
|
const allPointsData = [];
|
||||||
allSourcesData.forEach(source => {
|
allSourcesData.forEach(source => {
|
||||||
source.groups.forEach(group => {
|
source.groups.forEach(group => {
|
||||||
@@ -651,13 +660,19 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
'Координаты точки': point.coordinates,
|
'Координаты точки': point.coordinates,
|
||||||
'Усреднённые координаты': group.avg_coordinates,
|
'Усреднённые координаты': group.avg_coordinates,
|
||||||
'Расстояние от среднего, км': point.distance_from_avg,
|
'Расстояние от среднего, км': point.distance_from_avg,
|
||||||
'Статус': point.is_outlier ? 'Выброс' : 'OK',
|
'Статус': point.is_outlier ? 'Выброс' : 'OK'
|
||||||
'Интервал': group.interval_label
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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();
|
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(summaryData), "Усреднение");
|
||||||
XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(allPointsData), "Все точки");
|
XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(allPointsData), "Все точки");
|
||||||
@@ -737,41 +752,41 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
"tags": {"layers": [], "creator": CREATOR_ID}
|
"tags": {"layers": [], "creator": CREATOR_ID}
|
||||||
});
|
});
|
||||||
|
|
||||||
group.points.forEach(point => {
|
// group.points.forEach(point => {
|
||||||
if (point.is_outlier) return;
|
// if (point.is_outlier) return;
|
||||||
|
|
||||||
const pointCoord = point.coord_tuple;
|
// const pointCoord = point.coord_tuple;
|
||||||
const pointCaption = `${point.name || '-'} - ${point.timestamp || '-'}`;
|
// const pointCaption = `${point.name || '-'} - ${point.timestamp || '-'}`;
|
||||||
const pointSourceId = generateUUID();
|
// const pointSourceId = generateUUID();
|
||||||
|
|
||||||
result.push({
|
// result.push({
|
||||||
"tacticObjectType": "source",
|
// "tacticObjectType": "source",
|
||||||
"captionPosition": "right",
|
// "captionPosition": "right",
|
||||||
"id": pointSourceId,
|
// "id": pointSourceId,
|
||||||
"icon": {"type": "circle", "color": sourceColor},
|
// "icon": {"type": "circle", "color": sourceColor},
|
||||||
"caption": pointCaption,
|
// "caption": pointCaption,
|
||||||
"name": pointCaption,
|
// "name": pointCaption,
|
||||||
"customActions": [],
|
// "customActions": [],
|
||||||
"trackBehavior": {},
|
// "trackBehavior": {},
|
||||||
"bearingStyle": {"color": sourceColor, "thickness": 2, "dash": "solid", "border": null},
|
// "bearingStyle": {"color": sourceColor, "thickness": 2, "dash": "solid", "border": null},
|
||||||
"bearingBehavior": {},
|
// "bearingBehavior": {},
|
||||||
"tags": {"creator": CREATOR_ID}
|
// "tags": {"creator": CREATOR_ID}
|
||||||
});
|
// });
|
||||||
|
|
||||||
result.push({
|
// result.push({
|
||||||
"tacticObjectType": "position",
|
// "tacticObjectType": "position",
|
||||||
"id": generateUUID(),
|
// "id": generateUUID(),
|
||||||
"parentId": pointSourceId,
|
// "parentId": pointSourceId,
|
||||||
"timeStamp": point.timestamp_unix || (Date.now() / 1000),
|
// "timeStamp": point.timestamp_unix || (Date.now() / 1000),
|
||||||
"latitude": pointCoord[1],
|
// "latitude": pointCoord[1],
|
||||||
"altitude": 0,
|
// "altitude": 0,
|
||||||
"longitude": pointCoord[0],
|
// "longitude": pointCoord[0],
|
||||||
"caption": "",
|
// "caption": "",
|
||||||
"tooltip": "",
|
// "tooltip": "",
|
||||||
"customActions": [],
|
// "customActions": [],
|
||||||
"tags": {"layers": [], "creator": CREATOR_ID}
|
// "tags": {"layers": [], "creator": CREATOR_ID}
|
||||||
});
|
// });
|
||||||
});
|
// });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -303,6 +303,7 @@
|
|||||||
<td>{{ satellite.created_at|date:"d.m.Y H:i" }}</td>
|
<td>{{ satellite.created_at|date:"d.m.Y H:i" }}</td>
|
||||||
<td>{{ satellite.updated_at|date:"d.m.Y H:i" }}</td>
|
<td>{{ satellite.updated_at|date:"d.m.Y H:i" }}</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
|
<div class="d-flex gap-1 justify-content-center">
|
||||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
||||||
<a href="{% url 'mainapp:satellite_update' satellite.id %}"
|
<a href="{% url 'mainapp:satellite_update' satellite.id %}"
|
||||||
class="btn btn-sm btn-outline-warning"
|
class="btn btn-sm btn-outline-warning"
|
||||||
@@ -314,6 +315,14 @@
|
|||||||
<i class="bi bi-pencil"></i>
|
<i class="bi bi-pencil"></i>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if satellite.transponder_count > 0 %}
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-info"
|
||||||
|
onclick="showFrequencyPlan({{ satellite.id }})"
|
||||||
|
title="Частотный план">
|
||||||
|
<i class="bi bi-bar-chart"></i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
@@ -330,6 +339,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% include 'mainapp/components/_frequency_plan_modal.html' %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
@@ -526,6 +537,607 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
offcanvasElement.addEventListener('show.bs.offcanvas', updateFilterCounter);
|
offcanvasElement.addEventListener('show.bs.offcanvas', updateFilterCounter);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Frequency Plan Modal functionality
|
||||||
|
let modalCanvas, modalCtx, modalContainer;
|
||||||
|
let modalZoomLevelUL = 1;
|
||||||
|
let modalZoomLevelDL = 1;
|
||||||
|
let modalPanOffsetUL = 0;
|
||||||
|
let modalPanOffsetDL = 0;
|
||||||
|
let modalIsDragging = false;
|
||||||
|
let modalDragStartX = 0;
|
||||||
|
let modalDragStartOffsetUL = 0;
|
||||||
|
let modalDragStartOffsetDL = 0;
|
||||||
|
let modalDragArea = null;
|
||||||
|
let modalHoveredTransponder = null;
|
||||||
|
let modalTransponderRects = [];
|
||||||
|
let modalTranspondersData = [];
|
||||||
|
|
||||||
|
let modalMinFreqUL, modalMaxFreqUL, modalFreqRangeUL;
|
||||||
|
let modalMinFreqDL, modalMaxFreqDL, modalFreqRangeDL;
|
||||||
|
let modalOriginalMinFreqUL, modalOriginalMaxFreqUL, modalOriginalFreqRangeUL;
|
||||||
|
let modalOriginalMinFreqDL, modalOriginalMaxFreqDL, modalOriginalFreqRangeDL;
|
||||||
|
|
||||||
|
let modalUplinkStartY, modalUplinkHeight, modalDownlinkStartY, modalDownlinkHeight;
|
||||||
|
|
||||||
|
function showFrequencyPlan(satelliteId) {
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('frequencyPlanModal'));
|
||||||
|
|
||||||
|
// Reset modal state
|
||||||
|
document.getElementById('modalLoadingSpinner').style.display = 'block';
|
||||||
|
document.getElementById('modalFrequencyContent').style.display = 'none';
|
||||||
|
document.getElementById('modalNoData').style.display = 'none';
|
||||||
|
|
||||||
|
modal.show();
|
||||||
|
|
||||||
|
// Fetch transponder data
|
||||||
|
fetch(`/api/satellite/${satelliteId}/transponders/`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
document.getElementById('modalLoadingSpinner').style.display = 'none';
|
||||||
|
|
||||||
|
if (data.transponders && data.transponders.length > 0) {
|
||||||
|
modalTranspondersData = data.transponders;
|
||||||
|
document.getElementById('modalTransponderCount').textContent = data.count;
|
||||||
|
document.getElementById('modalFrequencyContent').style.display = 'block';
|
||||||
|
|
||||||
|
// Initialize chart after modal is shown
|
||||||
|
setTimeout(() => {
|
||||||
|
initializeModalFrequencyChart();
|
||||||
|
}, 100);
|
||||||
|
} else {
|
||||||
|
document.getElementById('modalNoData').style.display = 'block';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error fetching transponder data:', error);
|
||||||
|
document.getElementById('modalLoadingSpinner').style.display = 'none';
|
||||||
|
document.getElementById('modalNoData').style.display = 'block';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeModalFrequencyChart() {
|
||||||
|
if (!modalTranspondersData || modalTranspondersData.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
modalCanvas = document.getElementById('modalFrequencyChart');
|
||||||
|
if (!modalCanvas) return;
|
||||||
|
|
||||||
|
modalContainer = modalCanvas.parentElement;
|
||||||
|
modalCtx = modalCanvas.getContext('2d');
|
||||||
|
|
||||||
|
// Calculate frequency ranges
|
||||||
|
modalMinFreqUL = Infinity;
|
||||||
|
modalMaxFreqUL = -Infinity;
|
||||||
|
modalMinFreqDL = Infinity;
|
||||||
|
modalMaxFreqDL = -Infinity;
|
||||||
|
|
||||||
|
modalTranspondersData.forEach(t => {
|
||||||
|
const dlStartFreq = t.downlink - (t.frequency_range / 2);
|
||||||
|
const dlEndFreq = t.downlink + (t.frequency_range / 2);
|
||||||
|
modalMinFreqDL = Math.min(modalMinFreqDL, dlStartFreq);
|
||||||
|
modalMaxFreqDL = Math.max(modalMaxFreqDL, dlEndFreq);
|
||||||
|
|
||||||
|
if (t.uplink) {
|
||||||
|
const ulStartFreq = t.uplink - (t.frequency_range / 2);
|
||||||
|
const ulEndFreq = t.uplink + (t.frequency_range / 2);
|
||||||
|
modalMinFreqUL = Math.min(modalMinFreqUL, ulStartFreq);
|
||||||
|
modalMaxFreqUL = Math.max(modalMaxFreqUL, ulEndFreq);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add padding
|
||||||
|
const paddingDL = (modalMaxFreqDL - modalMinFreqDL) * 0.04;
|
||||||
|
modalMinFreqDL -= paddingDL;
|
||||||
|
modalMaxFreqDL += paddingDL;
|
||||||
|
|
||||||
|
if (modalMaxFreqUL !== -Infinity) {
|
||||||
|
const paddingUL = (modalMaxFreqUL - modalMinFreqUL) * 0.04;
|
||||||
|
modalMinFreqUL -= paddingUL;
|
||||||
|
modalMaxFreqUL += paddingUL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store original values
|
||||||
|
modalOriginalMinFreqDL = modalMinFreqDL;
|
||||||
|
modalOriginalMaxFreqDL = modalMaxFreqDL;
|
||||||
|
modalOriginalFreqRangeDL = modalMaxFreqDL - modalMinFreqDL;
|
||||||
|
modalFreqRangeDL = modalOriginalFreqRangeDL;
|
||||||
|
|
||||||
|
modalOriginalMinFreqUL = modalMinFreqUL;
|
||||||
|
modalOriginalMaxFreqUL = modalMaxFreqUL;
|
||||||
|
modalOriginalFreqRangeUL = modalMaxFreqUL - modalMinFreqUL;
|
||||||
|
modalFreqRangeUL = modalOriginalFreqRangeUL;
|
||||||
|
|
||||||
|
// Reset zoom and pan
|
||||||
|
modalZoomLevelUL = 1;
|
||||||
|
modalZoomLevelDL = 1;
|
||||||
|
modalPanOffsetUL = 0;
|
||||||
|
modalPanOffsetDL = 0;
|
||||||
|
|
||||||
|
// Setup event listeners
|
||||||
|
modalCanvas.addEventListener('wheel', handleModalWheel, { passive: false });
|
||||||
|
modalCanvas.addEventListener('mousedown', handleModalMouseDown);
|
||||||
|
modalCanvas.addEventListener('mousemove', handleModalMouseMove);
|
||||||
|
modalCanvas.addEventListener('mouseup', handleModalMouseUp);
|
||||||
|
modalCanvas.addEventListener('mouseleave', handleModalMouseLeave);
|
||||||
|
|
||||||
|
renderModalChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderModalChart() {
|
||||||
|
if (!modalCanvas || !modalCtx) return;
|
||||||
|
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
const rect = modalContainer.getBoundingClientRect();
|
||||||
|
const width = rect.width;
|
||||||
|
const height = rect.height;
|
||||||
|
|
||||||
|
modalCanvas.width = width * dpr;
|
||||||
|
modalCanvas.height = height * dpr;
|
||||||
|
modalCanvas.style.width = width + 'px';
|
||||||
|
modalCanvas.style.height = height + 'px';
|
||||||
|
modalCtx.scale(dpr, dpr);
|
||||||
|
|
||||||
|
modalCtx.clearRect(0, 0, width, height);
|
||||||
|
|
||||||
|
const leftMargin = 60;
|
||||||
|
const rightMargin = 20;
|
||||||
|
const topMargin = 60;
|
||||||
|
const middleMargin = 60;
|
||||||
|
const bottomMargin = 40;
|
||||||
|
const chartWidth = width - leftMargin - rightMargin;
|
||||||
|
const availableHeight = height - topMargin - middleMargin - bottomMargin;
|
||||||
|
|
||||||
|
modalUplinkHeight = availableHeight * 0.48;
|
||||||
|
modalDownlinkHeight = availableHeight * 0.48;
|
||||||
|
|
||||||
|
// Group by polarization
|
||||||
|
const polarizationGroups = {};
|
||||||
|
modalTranspondersData.forEach(t => {
|
||||||
|
let pol = t.polarization || '-';
|
||||||
|
pol = pol.charAt(0).toUpperCase();
|
||||||
|
if (!polarizationGroups[pol]) {
|
||||||
|
polarizationGroups[pol] = [];
|
||||||
|
}
|
||||||
|
polarizationGroups[pol].push(t);
|
||||||
|
});
|
||||||
|
|
||||||
|
const polarizations = Object.keys(polarizationGroups);
|
||||||
|
const rowHeightUL = modalUplinkHeight / polarizations.length;
|
||||||
|
const rowHeightDL = modalDownlinkHeight / polarizations.length;
|
||||||
|
|
||||||
|
// Calculate visible ranges
|
||||||
|
const visibleFreqRangeUL = modalFreqRangeUL / modalZoomLevelUL;
|
||||||
|
const centerFreqUL = (modalMinFreqUL + modalMaxFreqUL) / 2;
|
||||||
|
const visibleMinFreqUL = centerFreqUL - visibleFreqRangeUL / 2 + modalPanOffsetUL;
|
||||||
|
const visibleMaxFreqUL = centerFreqUL + visibleFreqRangeUL / 2 + modalPanOffsetUL;
|
||||||
|
|
||||||
|
const visibleFreqRangeDL = modalFreqRangeDL / modalZoomLevelDL;
|
||||||
|
const centerFreqDL = (modalMinFreqDL + modalMaxFreqDL) / 2;
|
||||||
|
const visibleMinFreqDL = centerFreqDL - visibleFreqRangeDL / 2 + modalPanOffsetDL;
|
||||||
|
const visibleMaxFreqDL = centerFreqDL + visibleFreqRangeDL / 2 + modalPanOffsetDL;
|
||||||
|
|
||||||
|
modalUplinkStartY = topMargin;
|
||||||
|
modalDownlinkStartY = topMargin + modalUplinkHeight + middleMargin;
|
||||||
|
|
||||||
|
// Draw UPLINK axis
|
||||||
|
modalCtx.strokeStyle = '#dee2e6';
|
||||||
|
modalCtx.lineWidth = 1;
|
||||||
|
modalCtx.beginPath();
|
||||||
|
modalCtx.moveTo(leftMargin, modalUplinkStartY);
|
||||||
|
modalCtx.lineTo(width - rightMargin, modalUplinkStartY);
|
||||||
|
modalCtx.stroke();
|
||||||
|
|
||||||
|
modalCtx.fillStyle = '#6c757d';
|
||||||
|
modalCtx.font = '11px sans-serif';
|
||||||
|
modalCtx.textAlign = 'center';
|
||||||
|
|
||||||
|
const numTicks = 10;
|
||||||
|
for (let i = 0; i <= numTicks; i++) {
|
||||||
|
const freq = visibleMinFreqUL + (visibleMaxFreqUL - visibleMinFreqUL) * i / numTicks;
|
||||||
|
const x = leftMargin + chartWidth * i / numTicks;
|
||||||
|
|
||||||
|
modalCtx.beginPath();
|
||||||
|
modalCtx.moveTo(x, modalUplinkStartY);
|
||||||
|
modalCtx.lineTo(x, modalUplinkStartY - 5);
|
||||||
|
modalCtx.stroke();
|
||||||
|
|
||||||
|
modalCtx.strokeStyle = 'rgba(0, 0, 0, 0.05)';
|
||||||
|
modalCtx.beginPath();
|
||||||
|
modalCtx.moveTo(x, modalUplinkStartY);
|
||||||
|
modalCtx.lineTo(x, modalUplinkStartY + modalUplinkHeight);
|
||||||
|
modalCtx.stroke();
|
||||||
|
modalCtx.strokeStyle = '#dee2e6';
|
||||||
|
|
||||||
|
modalCtx.fillText(freq.toFixed(1), x, modalUplinkStartY - 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
modalCtx.fillStyle = '#000';
|
||||||
|
modalCtx.font = 'bold 12px sans-serif';
|
||||||
|
modalCtx.textAlign = 'center';
|
||||||
|
modalCtx.fillText('Uplink Частота (МГц)', width / 2, modalUplinkStartY - 25);
|
||||||
|
|
||||||
|
// Draw DOWNLINK axis
|
||||||
|
modalCtx.strokeStyle = '#dee2e6';
|
||||||
|
modalCtx.lineWidth = 1;
|
||||||
|
modalCtx.beginPath();
|
||||||
|
modalCtx.moveTo(leftMargin, modalDownlinkStartY);
|
||||||
|
modalCtx.lineTo(width - rightMargin, modalDownlinkStartY);
|
||||||
|
modalCtx.stroke();
|
||||||
|
|
||||||
|
modalCtx.fillStyle = '#6c757d';
|
||||||
|
modalCtx.font = '11px sans-serif';
|
||||||
|
modalCtx.textAlign = 'center';
|
||||||
|
|
||||||
|
for (let i = 0; i <= numTicks; i++) {
|
||||||
|
const freq = visibleMinFreqDL + (visibleMaxFreqDL - visibleMinFreqDL) * i / numTicks;
|
||||||
|
const x = leftMargin + chartWidth * i / numTicks;
|
||||||
|
|
||||||
|
modalCtx.beginPath();
|
||||||
|
modalCtx.moveTo(x, modalDownlinkStartY);
|
||||||
|
modalCtx.lineTo(x, modalDownlinkStartY - 5);
|
||||||
|
modalCtx.stroke();
|
||||||
|
|
||||||
|
modalCtx.strokeStyle = 'rgba(0, 0, 0, 0.05)';
|
||||||
|
modalCtx.beginPath();
|
||||||
|
modalCtx.moveTo(x, modalDownlinkStartY);
|
||||||
|
modalCtx.lineTo(x, modalDownlinkStartY + modalDownlinkHeight);
|
||||||
|
modalCtx.stroke();
|
||||||
|
modalCtx.strokeStyle = '#dee2e6';
|
||||||
|
|
||||||
|
modalCtx.fillText(freq.toFixed(1), x, modalDownlinkStartY - 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
modalCtx.fillStyle = '#000';
|
||||||
|
modalCtx.font = 'bold 12px sans-serif';
|
||||||
|
modalCtx.textAlign = 'center';
|
||||||
|
modalCtx.fillText('Downlink Частота (МГц)', width / 2, modalDownlinkStartY - 25);
|
||||||
|
|
||||||
|
modalCtx.save();
|
||||||
|
modalCtx.translate(15, height / 2);
|
||||||
|
modalCtx.rotate(-Math.PI / 2);
|
||||||
|
modalCtx.textAlign = 'center';
|
||||||
|
modalCtx.fillText('Поляризация', 0, 0);
|
||||||
|
modalCtx.restore();
|
||||||
|
|
||||||
|
modalTransponderRects = [];
|
||||||
|
|
||||||
|
// Draw transponders
|
||||||
|
polarizations.forEach((pol, index) => {
|
||||||
|
const group = polarizationGroups[pol];
|
||||||
|
const downlinkColor = '#0d6efd';
|
||||||
|
const uplinkColor = '#fd7e14';
|
||||||
|
|
||||||
|
const uplinkY = modalUplinkStartY + index * rowHeightUL;
|
||||||
|
const uplinkBarHeight = rowHeightUL * 0.8;
|
||||||
|
const uplinkBarY = uplinkY + (rowHeightUL - uplinkBarHeight) / 2;
|
||||||
|
|
||||||
|
const downlinkY = modalDownlinkStartY + index * rowHeightDL;
|
||||||
|
const downlinkBarHeight = rowHeightDL * 0.8;
|
||||||
|
const downlinkBarY = downlinkY + (rowHeightDL - downlinkBarHeight) / 2;
|
||||||
|
|
||||||
|
modalCtx.fillStyle = '#000';
|
||||||
|
modalCtx.font = 'bold 14px sans-serif';
|
||||||
|
modalCtx.textAlign = 'center';
|
||||||
|
modalCtx.fillText(pol, leftMargin - 25, uplinkBarY + uplinkBarHeight / 2 + 4);
|
||||||
|
modalCtx.fillText(pol, leftMargin - 25, downlinkBarY + downlinkBarHeight / 2 + 4);
|
||||||
|
|
||||||
|
if (index < polarizations.length - 1) {
|
||||||
|
modalCtx.strokeStyle = '#adb5bd';
|
||||||
|
modalCtx.lineWidth = 1;
|
||||||
|
modalCtx.beginPath();
|
||||||
|
modalCtx.moveTo(leftMargin, uplinkY + rowHeightUL);
|
||||||
|
modalCtx.lineTo(width - rightMargin, uplinkY + rowHeightUL);
|
||||||
|
modalCtx.stroke();
|
||||||
|
|
||||||
|
modalCtx.beginPath();
|
||||||
|
modalCtx.moveTo(leftMargin, downlinkY + rowHeightDL);
|
||||||
|
modalCtx.lineTo(width - rightMargin, downlinkY + rowHeightDL);
|
||||||
|
modalCtx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw uplink transponders
|
||||||
|
group.forEach(t => {
|
||||||
|
if (!t.uplink) return;
|
||||||
|
|
||||||
|
const startFreq = t.uplink - (t.frequency_range / 2);
|
||||||
|
const endFreq = t.uplink + (t.frequency_range / 2);
|
||||||
|
|
||||||
|
if (endFreq < visibleMinFreqUL || startFreq > visibleMaxFreqUL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const x1 = leftMargin + ((startFreq - visibleMinFreqUL) / (visibleMaxFreqUL - visibleMinFreqUL)) * chartWidth;
|
||||||
|
const x2 = leftMargin + ((endFreq - visibleMinFreqUL) / (visibleMaxFreqUL - visibleMinFreqUL)) * chartWidth;
|
||||||
|
const barWidth = x2 - x1;
|
||||||
|
|
||||||
|
if (barWidth < 1) return;
|
||||||
|
|
||||||
|
const isHovered = modalHoveredTransponder && modalHoveredTransponder.transponder.name === t.name;
|
||||||
|
|
||||||
|
modalCtx.fillStyle = uplinkColor;
|
||||||
|
modalCtx.fillRect(x1, uplinkBarY, barWidth, uplinkBarHeight);
|
||||||
|
|
||||||
|
modalCtx.strokeStyle = isHovered ? '#000' : '#fff';
|
||||||
|
modalCtx.lineWidth = isHovered ? 3 : 1;
|
||||||
|
modalCtx.strokeRect(x1, uplinkBarY, barWidth, uplinkBarHeight);
|
||||||
|
|
||||||
|
if (barWidth > 40) {
|
||||||
|
modalCtx.fillStyle = '#fff';
|
||||||
|
modalCtx.font = isHovered ? 'bold 10px sans-serif' : '9px sans-serif';
|
||||||
|
modalCtx.textAlign = 'center';
|
||||||
|
modalCtx.fillText(t.name, x1 + barWidth / 2, uplinkBarY + uplinkBarHeight / 2 + 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
modalTransponderRects.push({
|
||||||
|
x: x1,
|
||||||
|
y: uplinkBarY,
|
||||||
|
width: barWidth,
|
||||||
|
height: uplinkBarHeight,
|
||||||
|
transponder: t,
|
||||||
|
type: 'uplink',
|
||||||
|
centerX: x1 + barWidth / 2
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Draw downlink transponders
|
||||||
|
group.forEach(t => {
|
||||||
|
const startFreq = t.downlink - (t.frequency_range / 2);
|
||||||
|
const endFreq = t.downlink + (t.frequency_range / 2);
|
||||||
|
|
||||||
|
if (endFreq < visibleMinFreqDL || startFreq > visibleMaxFreqDL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const x1 = leftMargin + ((startFreq - visibleMinFreqDL) / (visibleMaxFreqDL - visibleMinFreqDL)) * chartWidth;
|
||||||
|
const x2 = leftMargin + ((endFreq - visibleMinFreqDL) / (visibleMaxFreqDL - visibleMinFreqDL)) * chartWidth;
|
||||||
|
const barWidth = x2 - x1;
|
||||||
|
|
||||||
|
if (barWidth < 1) return;
|
||||||
|
|
||||||
|
const isHovered = modalHoveredTransponder && modalHoveredTransponder.transponder.name === t.name;
|
||||||
|
|
||||||
|
modalCtx.fillStyle = downlinkColor;
|
||||||
|
modalCtx.fillRect(x1, downlinkBarY, barWidth, downlinkBarHeight);
|
||||||
|
|
||||||
|
modalCtx.strokeStyle = isHovered ? '#000' : '#fff';
|
||||||
|
modalCtx.lineWidth = isHovered ? 3 : 1;
|
||||||
|
modalCtx.strokeRect(x1, downlinkBarY, barWidth, downlinkBarHeight);
|
||||||
|
|
||||||
|
if (barWidth > 40) {
|
||||||
|
modalCtx.fillStyle = '#fff';
|
||||||
|
modalCtx.font = isHovered ? 'bold 10px sans-serif' : '9px sans-serif';
|
||||||
|
modalCtx.textAlign = 'center';
|
||||||
|
modalCtx.fillText(t.name, x1 + barWidth / 2, downlinkBarY + downlinkBarHeight / 2 + 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
modalTransponderRects.push({
|
||||||
|
x: x1,
|
||||||
|
y: downlinkBarY,
|
||||||
|
width: barWidth,
|
||||||
|
height: downlinkBarHeight,
|
||||||
|
transponder: t,
|
||||||
|
type: 'downlink',
|
||||||
|
centerX: x1 + barWidth / 2
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (modalHoveredTransponder) {
|
||||||
|
drawModalConnectionLine(modalHoveredTransponder);
|
||||||
|
drawModalTooltip(modalHoveredTransponder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawModalConnectionLine(rectInfo) {
|
||||||
|
const t = rectInfo.transponder;
|
||||||
|
if (!t.uplink) return;
|
||||||
|
|
||||||
|
const downlinkRect = modalTransponderRects.find(r => r.transponder.name === t.name && r.type === 'downlink');
|
||||||
|
const uplinkRect = modalTransponderRects.find(r => r.transponder.name === t.name && r.type === 'uplink');
|
||||||
|
|
||||||
|
if (!downlinkRect || !uplinkRect) return;
|
||||||
|
|
||||||
|
const x1 = downlinkRect.centerX;
|
||||||
|
const y1 = downlinkRect.y + downlinkRect.height;
|
||||||
|
const x2 = uplinkRect.centerX;
|
||||||
|
const y2 = uplinkRect.y;
|
||||||
|
|
||||||
|
modalCtx.save();
|
||||||
|
modalCtx.strokeStyle = '#ffc107';
|
||||||
|
modalCtx.lineWidth = 2;
|
||||||
|
modalCtx.setLineDash([5, 3]);
|
||||||
|
modalCtx.globalAlpha = 0.8;
|
||||||
|
|
||||||
|
modalCtx.beginPath();
|
||||||
|
modalCtx.moveTo(x1, y1);
|
||||||
|
modalCtx.lineTo(x2, y2);
|
||||||
|
modalCtx.stroke();
|
||||||
|
|
||||||
|
modalCtx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawModalTooltip(rectInfo) {
|
||||||
|
const t = rectInfo.transponder;
|
||||||
|
const isUplink = rectInfo.type === 'uplink';
|
||||||
|
const freq = isUplink ? t.uplink : t.downlink;
|
||||||
|
const startFreq = freq - (t.frequency_range / 2);
|
||||||
|
const endFreq = freq + (t.frequency_range / 2);
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
t.name,
|
||||||
|
'Тип: ' + (isUplink ? 'Uplink' : 'Downlink'),
|
||||||
|
'Диапазон: ' + startFreq.toFixed(3) + ' - ' + endFreq.toFixed(3) + ' МГц',
|
||||||
|
'Центр: ' + freq.toFixed(3) + ' МГц',
|
||||||
|
'Полоса: ' + t.frequency_range.toFixed(3) + ' МГц',
|
||||||
|
'Поляризация: ' + t.polarization,
|
||||||
|
'Зона: ' + t.zone_name
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isUplink && t.downlink && t.uplink) {
|
||||||
|
const conversion = t.downlink - t.uplink;
|
||||||
|
lines.push('Перенос: ' + conversion.toFixed(3) + ' МГц');
|
||||||
|
}
|
||||||
|
|
||||||
|
modalCtx.font = '12px sans-serif';
|
||||||
|
const padding = 10;
|
||||||
|
const lineHeight = 16;
|
||||||
|
let maxWidth = 0;
|
||||||
|
lines.forEach(line => {
|
||||||
|
const width = modalCtx.measureText(line).width;
|
||||||
|
maxWidth = Math.max(maxWidth, width);
|
||||||
|
});
|
||||||
|
|
||||||
|
const tooltipWidth = maxWidth + padding * 2;
|
||||||
|
const tooltipHeight = lines.length * lineHeight + padding * 2;
|
||||||
|
|
||||||
|
const mouseX = rectInfo._mouseX || modalCanvas.width / 2;
|
||||||
|
const mouseY = rectInfo._mouseY || modalCanvas.height / 2;
|
||||||
|
let tooltipX = mouseX + 15;
|
||||||
|
let tooltipY = mouseY - tooltipHeight - 15;
|
||||||
|
|
||||||
|
if (tooltipX + tooltipWidth > modalCanvas.width) {
|
||||||
|
tooltipX = mouseX - tooltipWidth - 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tooltipY < 0) {
|
||||||
|
tooltipY = mouseY + 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
modalCtx.fillStyle = 'rgba(0, 0, 0, 0.9)';
|
||||||
|
modalCtx.fillRect(tooltipX, tooltipY, tooltipWidth, tooltipHeight);
|
||||||
|
|
||||||
|
modalCtx.fillStyle = '#fff';
|
||||||
|
modalCtx.font = 'bold 12px sans-serif';
|
||||||
|
modalCtx.textAlign = 'left';
|
||||||
|
modalCtx.fillText(lines[0], tooltipX + padding, tooltipY + padding + 12);
|
||||||
|
|
||||||
|
modalCtx.font = '11px sans-serif';
|
||||||
|
for (let i = 1; i < lines.length; i++) {
|
||||||
|
modalCtx.fillText(lines[i], tooltipX + padding, tooltipY + padding + 12 + i * lineHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleModalWheel(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const rect = modalCanvas.getBoundingClientRect();
|
||||||
|
const mouseY = e.clientY - rect.top;
|
||||||
|
|
||||||
|
const isUplinkArea = mouseY < (modalUplinkStartY + modalUplinkHeight);
|
||||||
|
|
||||||
|
const delta = e.deltaY > 0 ? 0.9 : 1.1;
|
||||||
|
|
||||||
|
if (isUplinkArea) {
|
||||||
|
const newZoom = Math.max(1, Math.min(20, modalZoomLevelUL * delta));
|
||||||
|
if (newZoom !== modalZoomLevelUL) {
|
||||||
|
modalZoomLevelUL = newZoom;
|
||||||
|
|
||||||
|
const maxPan = (modalOriginalFreqRangeUL * (modalZoomLevelUL - 1)) / (2 * modalZoomLevelUL);
|
||||||
|
modalPanOffsetUL = Math.max(-maxPan, Math.min(maxPan, modalPanOffsetUL));
|
||||||
|
|
||||||
|
renderModalChart();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const newZoom = Math.max(1, Math.min(20, modalZoomLevelDL * delta));
|
||||||
|
if (newZoom !== modalZoomLevelDL) {
|
||||||
|
modalZoomLevelDL = newZoom;
|
||||||
|
|
||||||
|
const maxPan = (modalOriginalFreqRangeDL * (modalZoomLevelDL - 1)) / (2 * modalZoomLevelDL);
|
||||||
|
modalPanOffsetDL = Math.max(-maxPan, Math.min(maxPan, modalPanOffsetDL));
|
||||||
|
|
||||||
|
renderModalChart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleModalMouseDown(e) {
|
||||||
|
const rect = modalCanvas.getBoundingClientRect();
|
||||||
|
const mouseY = e.clientY - rect.top;
|
||||||
|
|
||||||
|
modalDragArea = mouseY < (modalUplinkStartY + modalUplinkHeight) ? 'uplink' : 'downlink';
|
||||||
|
|
||||||
|
modalIsDragging = true;
|
||||||
|
modalDragStartX = e.clientX;
|
||||||
|
modalDragStartOffsetUL = modalPanOffsetUL;
|
||||||
|
modalDragStartOffsetDL = modalPanOffsetDL;
|
||||||
|
modalCanvas.style.cursor = 'grabbing';
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleModalMouseMove(e) {
|
||||||
|
const rect = modalCanvas.getBoundingClientRect();
|
||||||
|
const mouseX = e.clientX - rect.left;
|
||||||
|
const mouseY = e.clientY - rect.top;
|
||||||
|
|
||||||
|
if (modalIsDragging) {
|
||||||
|
const dx = e.clientX - modalDragStartX;
|
||||||
|
|
||||||
|
if (modalDragArea === 'uplink') {
|
||||||
|
const freqPerPixel = (modalFreqRangeUL / modalZoomLevelUL) / (rect.width - 80);
|
||||||
|
modalPanOffsetUL = modalDragStartOffsetUL - dx * freqPerPixel;
|
||||||
|
|
||||||
|
const maxPan = (modalOriginalFreqRangeUL * (modalZoomLevelUL - 1)) / (2 * modalZoomLevelUL);
|
||||||
|
modalPanOffsetUL = Math.max(-maxPan, Math.min(maxPan, modalPanOffsetUL));
|
||||||
|
} else {
|
||||||
|
const freqPerPixel = (modalFreqRangeDL / modalZoomLevelDL) / (rect.width - 80);
|
||||||
|
modalPanOffsetDL = modalDragStartOffsetDL - dx * freqPerPixel;
|
||||||
|
|
||||||
|
const maxPan = (modalOriginalFreqRangeDL * (modalZoomLevelDL - 1)) / (2 * modalZoomLevelDL);
|
||||||
|
modalPanOffsetDL = Math.max(-maxPan, Math.min(maxPan, modalPanOffsetDL));
|
||||||
|
}
|
||||||
|
|
||||||
|
renderModalChart();
|
||||||
|
} else {
|
||||||
|
let found = null;
|
||||||
|
for (const tr of modalTransponderRects) {
|
||||||
|
if (mouseX >= tr.x && mouseX <= tr.x + tr.width &&
|
||||||
|
mouseY >= tr.y && mouseY <= tr.y + tr.height) {
|
||||||
|
found = tr;
|
||||||
|
found._mouseX = mouseX;
|
||||||
|
found._mouseY = mouseY;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (found !== modalHoveredTransponder) {
|
||||||
|
modalHoveredTransponder = found;
|
||||||
|
modalCanvas.style.cursor = found ? 'pointer' : 'default';
|
||||||
|
renderModalChart();
|
||||||
|
} else if (found) {
|
||||||
|
found._mouseX = mouseX;
|
||||||
|
found._mouseY = mouseY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleModalMouseUp() {
|
||||||
|
modalIsDragging = false;
|
||||||
|
modalCanvas.style.cursor = modalHoveredTransponder ? 'pointer' : 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleModalMouseLeave() {
|
||||||
|
modalIsDragging = false;
|
||||||
|
modalHoveredTransponder = null;
|
||||||
|
modalCanvas.style.cursor = 'default';
|
||||||
|
renderModalChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetModalZoom() {
|
||||||
|
modalZoomLevelUL = 1;
|
||||||
|
modalZoomLevelDL = 1;
|
||||||
|
modalPanOffsetUL = 0;
|
||||||
|
modalPanOffsetDL = 0;
|
||||||
|
renderModalChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup reset button
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const resetBtn = document.getElementById('modalResetZoom');
|
||||||
|
if (resetBtn) {
|
||||||
|
resetBtn.addEventListener('click', resetModalZoom);
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -79,9 +79,9 @@
|
|||||||
<i class="bi bi-plus-circle"></i> Создать
|
<i class="bi bi-plus-circle"></i> Создать
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% url 'mainapp:data_entry' %}" class="btn btn-info btn-sm" title="Ввод данных точек спутников">
|
<!-- <a href="{% url 'mainapp:data_entry' %}" class="btn btn-info btn-sm" title="Ввод данных точек спутников">
|
||||||
Передача точек
|
Передача точек
|
||||||
</a>
|
</a> -->
|
||||||
<a href="{% url 'mainapp:load_excel_data' %}" class="btn btn-primary btn-sm" title="Загрузка данных из Excel">
|
<a href="{% url 'mainapp:load_excel_data' %}" class="btn btn-primary btn-sm" title="Загрузка данных из Excel">
|
||||||
<i class="bi bi-file-earmark-excel"></i> Excel
|
<i class="bi bi-file-earmark-excel"></i> Excel
|
||||||
</a>
|
</a>
|
||||||
@@ -101,6 +101,9 @@
|
|||||||
<a href="{% url 'mainapp:points_averaging' %}" class="btn btn-warning btn-sm" title="Усреднение точек">
|
<a href="{% url 'mainapp:points_averaging' %}" class="btn btn-warning btn-sm" title="Усреднение точек">
|
||||||
<i class="bi bi-calculator"></i> Усреднение
|
<i class="bi bi-calculator"></i> Усреднение
|
||||||
</a>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Add to List Button -->
|
<!-- Add to List Button -->
|
||||||
@@ -157,7 +160,7 @@
|
|||||||
<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="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="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="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="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>
|
<li><label class="dropdown-item"><input type="checkbox" class="column-toggle" data-column="16" checked onchange="toggleColumn(this)"> Действия</label></li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
@@ -496,7 +499,7 @@
|
|||||||
{% include 'mainapp/components/_sort_header.html' with field='updated_at' label='Обновлено' current_sort=sort %}
|
{% include 'mainapp/components/_sort_header.html' with field='updated_at' label='Обновлено' current_sort=sort %}
|
||||||
</th>
|
</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: 150px;">Последний сигнал</th> -->
|
||||||
<th scope="col" class="text-center" style="min-width: 150px;">Действия</th>
|
<th scope="col" class="text-center" style="min-width: 150px;">Действия</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -539,7 +542,7 @@
|
|||||||
<td>{{ source.created_at|date:"d.m.Y H:i" }}</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.updated_at|date:"d.m.Y H:i" }}</td>
|
||||||
<td>{{ source.confirm_at|date:"d.m.Y H:i"|default:"-" }}</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>{{ source.last_signal_at|date:"d.m.Y H:i"|default:"-" }}</td> -->
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group" role="group">
|
||||||
{% if source.objitem_count > 0 %}
|
{% if source.objitem_count > 0 %}
|
||||||
|
|||||||
@@ -53,6 +53,18 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<!-- Toast Container -->
|
||||||
|
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 9999;">
|
||||||
|
<div id="saveToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
||||||
|
<div class="toast-header">
|
||||||
|
<strong class="me-auto" id="toastTitle">Уведомление</strong>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Закрыть"></button>
|
||||||
|
</div>
|
||||||
|
<div class="toast-body" id="toastBody">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="data-entry-container">
|
<div class="data-entry-container">
|
||||||
<h2>Тех. анализ - Ввод данных</h2>
|
<h2>Тех. анализ - Ввод данных</h2>
|
||||||
|
|
||||||
@@ -67,6 +79,11 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-8 mb-3 d-flex align-items-end gap-2">
|
||||||
|
<a href="{% url 'mainapp:tech_analyze_list' %}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-list"></i> Список данных
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
<!-- <div class="col-md-8 mb-3">
|
<!-- <div class="col-md-8 mb-3">
|
||||||
<div class="alert alert-info mb-0">
|
<div class="alert alert-info mb-0">
|
||||||
<i class="bi bi-info-circle"></i>
|
<i class="bi bi-info-circle"></i>
|
||||||
@@ -228,16 +245,43 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Helper function to show toast
|
||||||
|
function showToast(title, message, type = 'info') {
|
||||||
|
const toastEl = document.getElementById('saveToast');
|
||||||
|
const toastTitle = document.getElementById('toastTitle');
|
||||||
|
const toastBody = document.getElementById('toastBody');
|
||||||
|
const toastHeader = toastEl.querySelector('.toast-header');
|
||||||
|
|
||||||
|
// Remove previous background classes
|
||||||
|
toastHeader.classList.remove('bg-success', 'bg-danger', 'bg-warning', 'text-white');
|
||||||
|
|
||||||
|
// Add appropriate background class
|
||||||
|
if (type === 'success') {
|
||||||
|
toastHeader.classList.add('bg-success', 'text-white');
|
||||||
|
} else if (type === 'error') {
|
||||||
|
toastHeader.classList.add('bg-danger', 'text-white');
|
||||||
|
} else if (type === 'warning') {
|
||||||
|
toastHeader.classList.add('bg-warning');
|
||||||
|
}
|
||||||
|
|
||||||
|
toastTitle.textContent = title;
|
||||||
|
toastBody.innerHTML = message;
|
||||||
|
|
||||||
|
const toast = new bootstrap.Toast(toastEl, { delay: 5000 });
|
||||||
|
toast.show();
|
||||||
|
}
|
||||||
|
|
||||||
// Delete selected rows
|
// Delete selected rows
|
||||||
document.getElementById('delete-selected').addEventListener('click', function() {
|
document.getElementById('delete-selected').addEventListener('click', function() {
|
||||||
const selectedRows = table.getSelectedRows();
|
const selectedRows = table.getSelectedRows();
|
||||||
if (selectedRows.length === 0) {
|
if (selectedRows.length === 0) {
|
||||||
alert('Выберите строки для удаления');
|
showToast('Внимание', 'Выберите строки для удаления', 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (confirm(`Удалить ${selectedRows.length} строк(и)?`)) {
|
if (confirm(`Удалить ${selectedRows.length} строк(и)?`)) {
|
||||||
selectedRows.forEach(row => row.delete());
|
selectedRows.forEach(row => row.delete());
|
||||||
|
showToast('Успешно', `Удалено строк: ${selectedRows.length}`, 'success');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -246,21 +290,21 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const satelliteId = document.getElementById('satellite-select').value;
|
const satelliteId = document.getElementById('satellite-select').value;
|
||||||
|
|
||||||
if (!satelliteId) {
|
if (!satelliteId) {
|
||||||
alert('Пожалуйста, выберите спутник');
|
showToast('Внимание', 'Пожалуйста, выберите спутник', 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = table.getData();
|
const data = table.getData();
|
||||||
|
|
||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
alert('Нет данных для сохранения');
|
showToast('Внимание', 'Нет данных для сохранения', 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate that all rows have names
|
// Validate that all rows have names
|
||||||
const emptyNames = data.filter(row => !row.name || row.name.trim() === '');
|
const emptyNames = data.filter(row => !row.name || row.name.trim() === '');
|
||||||
if (emptyNames.length > 0) {
|
if (emptyNames.length > 0) {
|
||||||
alert('Все строки должны иметь имя');
|
showToast('Внимание', 'Все строки должны иметь имя', 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,27 +328,27 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
let message = `Успешно сохранено!\n`;
|
let message = `<strong>Успешно сохранено!</strong><br>`;
|
||||||
message += `Создано: ${result.created}\n`;
|
message += `Создано: ${result.created}<br>`;
|
||||||
message += `Обновлено: ${result.updated}\n`;
|
message += `Обновлено: ${result.updated}<br>`;
|
||||||
message += `Всего: ${result.total}`;
|
message += `Всего: ${result.total}`;
|
||||||
|
|
||||||
if (result.errors && result.errors.length > 0) {
|
if (result.errors && result.errors.length > 0) {
|
||||||
message += `\n\nОшибки:\n${result.errors.join('\n')}`;
|
message += `<br><br><strong>Ошибки:</strong><br>${result.errors.join('<br>')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
alert(message);
|
showToast('Сохранение завершено', message, 'success');
|
||||||
|
|
||||||
// Clear table after successful save
|
// Clear table after successful save
|
||||||
if (!result.errors || result.errors.length === 0) {
|
if (!result.errors || result.errors.length === 0) {
|
||||||
table.clearData();
|
table.clearData();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
alert('Ошибка: ' + (result.error || 'Неизвестная ошибка'));
|
showToast('Ошибка', result.error || 'Неизвестная ошибка', 'error');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
alert('Произошла ошибка при сохранении данных');
|
showToast('Ошибка', 'Произошла ошибка при сохранении данных', 'error');
|
||||||
} finally {
|
} finally {
|
||||||
// Re-enable button
|
// Re-enable button
|
||||||
this.disabled = false;
|
this.disabled = false;
|
||||||
|
|||||||
518
dbapp/mainapp/templates/mainapp/tech_analyze_list.html
Normal file
518
dbapp/mainapp/templates/mainapp/tech_analyze_list.html
Normal file
@@ -0,0 +1,518 @@
|
|||||||
|
{% extends 'mainapp/base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Тех. анализ - Список{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<link href="{% static 'tabulator/css/tabulator_bootstrap5.min.css' %}" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
.sticky-top {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
#tech-analyze-table {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
#tech-analyze-table .tabulator-header {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
#tech-analyze-table .tabulator-cell {
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: normal;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
#tech-analyze-table .tabulator-row {
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
</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 или имени...">
|
||||||
|
<button type="button" class="btn btn-outline-primary" onclick="performSearch()">Найти</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" onclick="clearSearch()">Очистить</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action buttons -->
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a href="{% url 'mainapp:tech_analyze_entry' %}" class="btn btn-success btn-sm" title="Ввод данных">
|
||||||
|
<i class="bi bi-plus-circle"></i> Ввод данных
|
||||||
|
</a>
|
||||||
|
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
||||||
|
<button type="button" class="btn btn-danger btn-sm" title="Удалить выбранные" onclick="deleteSelected()">
|
||||||
|
<i class="bi bi-trash"></i> Удалить
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
<button type="button" class="btn btn-info btn-sm" title="Привязать к существующим точкам" onclick="showLinkModal()">
|
||||||
|
<i class="bi bi-link-45deg"></i> Привязать к точкам
|
||||||
|
</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>
|
||||||
|
</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">
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
|
<!-- 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">
|
||||||
|
<div id="tech-analyze-table"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Link to Points Modal -->
|
||||||
|
<div class="modal fade" id="linkToPointsModal" tabindex="-1" aria-labelledby="linkToPointsModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="linkToPointsModalLabel">
|
||||||
|
<i class="bi bi-link-45deg"></i> Привязать к существующим точкам
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="bi bi-info-circle"></i>
|
||||||
|
Будут обновлены только точки с отсутствующими данными (модуляция "-", символьная скорость -1 или 0, стандарт "-").
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="linkSatelliteSelect" class="form-label">Выберите спутник <span class="text-danger">*</span></label>
|
||||||
|
<select class="form-select" id="linkSatelliteSelect" required>
|
||||||
|
<option value="">Выберите спутник</option>
|
||||||
|
{% for satellite in satellites %}
|
||||||
|
<option value="{{ satellite.id }}">{{ satellite.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="linkResultMessage" class="alert" style="display: none;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="confirmLinkBtn" onclick="confirmLink(event)">
|
||||||
|
<i class="bi bi-check-circle"></i> Привязать
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Include the satellite modal component -->
|
||||||
|
{% include 'mainapp/components/_satellite_modal.html' %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script src="{% static 'tabulator/js/tabulator.min.js' %}"></script>
|
||||||
|
<script>
|
||||||
|
// Helper function to 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 Tabulator
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const ajaxParams = {};
|
||||||
|
for (const [key, value] of urlParams.entries()) {
|
||||||
|
if (ajaxParams[key]) {
|
||||||
|
if (!Array.isArray(ajaxParams[key])) {
|
||||||
|
ajaxParams[key] = [ajaxParams[key]];
|
||||||
|
}
|
||||||
|
ajaxParams[key].push(value);
|
||||||
|
} else {
|
||||||
|
ajaxParams[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const table = new Tabulator("#tech-analyze-table", {
|
||||||
|
ajaxURL: "{% url 'mainapp:tech_analyze_api' %}",
|
||||||
|
ajaxParams: ajaxParams,
|
||||||
|
pagination: true,
|
||||||
|
paginationMode: "remote",
|
||||||
|
paginationSize: {{ items_per_page }},
|
||||||
|
paginationSizeSelector: [25, 50, 100, 200, 500],
|
||||||
|
layout: "fitDataStretch",
|
||||||
|
height: "70vh",
|
||||||
|
placeholder: "Нет данных для отображения",
|
||||||
|
rowHeight: null, // Автоматическая высота строк
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
formatter: "rowSelection",
|
||||||
|
titleFormatter: "rowSelection",
|
||||||
|
hozAlign: "center",
|
||||||
|
headerSort: false,
|
||||||
|
width: 40,
|
||||||
|
cellClick: function(e, cell) {
|
||||||
|
cell.getRow().toggleSelect();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{title: "ID", field: "id", width: 80, hozAlign: "center"},
|
||||||
|
{
|
||||||
|
title: "Имя",
|
||||||
|
field: "name",
|
||||||
|
minWidth: 250,
|
||||||
|
widthGrow: 3,
|
||||||
|
formatter: function(cell) {
|
||||||
|
return '<div style="white-space: normal; word-wrap: break-word; padding: 4px 0;">' +
|
||||||
|
(cell.getValue() || '-') + '</div>';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Спутник",
|
||||||
|
field: "satellite_name",
|
||||||
|
minWidth: 120,
|
||||||
|
widthGrow: 1,
|
||||||
|
formatter: function(cell) {
|
||||||
|
const data = cell.getData();
|
||||||
|
if (data.satellite_id) {
|
||||||
|
return '<a href="#" class="text-decoration-underline" onclick="showSatelliteModal(' + data.satellite_id + '); return false;">' +
|
||||||
|
(data.satellite_name || '-') + '</a>';
|
||||||
|
}
|
||||||
|
return data.satellite_name || '-';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{title: "Частота, МГц", field: "frequency", width: 120, hozAlign: "right", formatter: function(cell) {
|
||||||
|
const val = cell.getValue();
|
||||||
|
return val && val !== 0 ? val.toFixed(3) : '-';
|
||||||
|
}},
|
||||||
|
{title: "Полоса, МГц", field: "freq_range", width: 120, hozAlign: "right", formatter: function(cell) {
|
||||||
|
const val = cell.getValue();
|
||||||
|
return val && val !== 0 ? val.toFixed(3) : '-';
|
||||||
|
}},
|
||||||
|
{title: "Сим. скорость, БОД", field: "bod_velocity", width: 150, hozAlign: "right", formatter: function(cell) {
|
||||||
|
const val = cell.getValue();
|
||||||
|
return val && val !== 0 ? val.toFixed(0) : '-';
|
||||||
|
}},
|
||||||
|
{title: "Поляризация", field: "polarization_name", width: 120},
|
||||||
|
{title: "Модуляция", field: "modulation_name", width: 120},
|
||||||
|
{title: "Стандарт", field: "standard_name", width: 120},
|
||||||
|
{
|
||||||
|
title: "Примечание",
|
||||||
|
field: "note",
|
||||||
|
minWidth: 150,
|
||||||
|
widthGrow: 2,
|
||||||
|
formatter: function(cell) {
|
||||||
|
return '<div style="white-space: normal; word-wrap: break-word; padding: 4px 0;">' +
|
||||||
|
(cell.getValue() || '-') + '</div>';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Создано",
|
||||||
|
field: "created_at",
|
||||||
|
width: 140,
|
||||||
|
formatter: function(cell) {
|
||||||
|
const val = cell.getValue();
|
||||||
|
if (!val) return '-';
|
||||||
|
try {
|
||||||
|
const date = new Date(val);
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0');
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||||
|
return day + '.' + month + '.' + year + ' ' + hours + ':' + minutes;
|
||||||
|
} catch (e) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Обновлено",
|
||||||
|
field: "updated_at",
|
||||||
|
width: 140,
|
||||||
|
formatter: function(cell) {
|
||||||
|
const val = cell.getValue();
|
||||||
|
if (!val) return '-';
|
||||||
|
try {
|
||||||
|
const date = new Date(val);
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0');
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||||
|
return day + '.' + month + '.' + year + ' ' + hours + ':' + minutes;
|
||||||
|
} catch (e) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
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');
|
||||||
|
window.location.search = urlParams.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Enter key in search input
|
||||||
|
document.getElementById('toolbar-search').addEventListener('keypress', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
performSearch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
let filterCount = 0;
|
||||||
|
|
||||||
|
// Count selected satellites
|
||||||
|
const satelliteSelect = document.querySelector('select[name="satellite_id"]');
|
||||||
|
if (satelliteSelect) {
|
||||||
|
const selectedOptions = Array.from(satelliteSelect.selectedOptions).filter(function(opt) { return opt.selected; });
|
||||||
|
if (selectedOptions.length > 0) {
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete selected items
|
||||||
|
function deleteSelected() {
|
||||||
|
const selectedRows = table.getSelectedRows();
|
||||||
|
|
||||||
|
if (selectedRows.length === 0) {
|
||||||
|
alert('Пожалуйста, выберите хотя бы одну запись для удаления');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm('Удалить ' + selectedRows.length + ' записей?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedIds = selectedRows.map(function(row) { return row.getData().id; });
|
||||||
|
|
||||||
|
fetch('{% url "mainapp:tech_analyze_delete" %}', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': getCookie('csrftoken')
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
ids: selectedIds
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(function(response) { return response.json(); })
|
||||||
|
.then(function(data) {
|
||||||
|
if (data.success) {
|
||||||
|
table.replaceData();
|
||||||
|
} else {
|
||||||
|
alert('Ошибка: ' + (data.error || 'Неизвестная ошибка'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function(error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Произошла ошибка при удалении записей');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show link modal
|
||||||
|
function showLinkModal() {
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('linkToPointsModal'));
|
||||||
|
document.getElementById('linkResultMessage').style.display = 'none';
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm link
|
||||||
|
function confirmLink(event) {
|
||||||
|
const satelliteId = document.getElementById('linkSatelliteSelect').value;
|
||||||
|
const resultDiv = document.getElementById('linkResultMessage');
|
||||||
|
|
||||||
|
if (!satelliteId) {
|
||||||
|
resultDiv.className = 'alert alert-warning';
|
||||||
|
resultDiv.textContent = 'Пожалуйста, выберите спутник';
|
||||||
|
resultDiv.style.display = 'block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
const btn = document.getElementById('confirmLinkBtn');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Обработка...';
|
||||||
|
resultDiv.style.display = 'none';
|
||||||
|
|
||||||
|
fetch('{% url "mainapp:tech_analyze_link_existing" %}', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': getCookie('csrftoken')
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
satellite_id: satelliteId
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(function(response) { return response.json(); })
|
||||||
|
.then(function(data) {
|
||||||
|
if (data.success) {
|
||||||
|
resultDiv.className = 'alert alert-success';
|
||||||
|
resultDiv.innerHTML = '<strong>Привязка завершена!</strong><br>' +
|
||||||
|
'Обновлено точек: ' + data.updated + '<br>' +
|
||||||
|
'Пропущено: ' + data.skipped + '<br>' +
|
||||||
|
'Всего обработано: ' + data.total;
|
||||||
|
|
||||||
|
if (data.errors && data.errors.length > 0) {
|
||||||
|
resultDiv.innerHTML += '<br><br><strong>Ошибки:</strong><br>' + data.errors.join('<br>');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resultDiv.className = 'alert alert-danger';
|
||||||
|
resultDiv.textContent = 'Ошибка: ' + (data.error || 'Неизвестная ошибка');
|
||||||
|
}
|
||||||
|
resultDiv.style.display = 'block';
|
||||||
|
})
|
||||||
|
.catch(function(error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
resultDiv.className = 'alert alert-danger';
|
||||||
|
resultDiv.textContent = 'Произошла ошибка при привязке точек';
|
||||||
|
resultDiv.style.display = 'block';
|
||||||
|
})
|
||||||
|
.finally(function() {
|
||||||
|
// Re-enable button
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = '<i class="bi bi-check-circle"></i> Привязать';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize on page load
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// 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 selectFields = form.querySelectorAll('select');
|
||||||
|
selectFields.forEach(function(select) {
|
||||||
|
select.addEventListener('change', updateFilterCounter);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update counter when offcanvas is shown
|
||||||
|
const offcanvasElement = document.getElementById('offcanvasFilters');
|
||||||
|
if (offcanvasElement) {
|
||||||
|
offcanvasElement.addEventListener('show.bs.offcanvas', updateFilterCounter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set search value from URL
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const searchQuery = urlParams.get('search');
|
||||||
|
if (searchQuery) {
|
||||||
|
document.getElementById('toolbar-search').value = searchQuery;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -36,6 +36,7 @@ from .views import (
|
|||||||
ObjItemUpdateView,
|
ObjItemUpdateView,
|
||||||
ProcessKubsatView,
|
ProcessKubsatView,
|
||||||
SatelliteDataAPIView,
|
SatelliteDataAPIView,
|
||||||
|
SatelliteTranspondersAPIView,
|
||||||
SatelliteListView,
|
SatelliteListView,
|
||||||
SatelliteCreateView,
|
SatelliteCreateView,
|
||||||
SatelliteUpdateView,
|
SatelliteUpdateView,
|
||||||
@@ -60,7 +61,14 @@ from .views import (
|
|||||||
custom_logout,
|
custom_logout,
|
||||||
)
|
)
|
||||||
from .views.marks import ObjectMarksListView, AddObjectMarkView, UpdateObjectMarkView
|
from .views.marks import ObjectMarksListView, AddObjectMarkView, UpdateObjectMarkView
|
||||||
from .views.tech_analyze import tech_analyze_entry, tech_analyze_save
|
from .views.tech_analyze import (
|
||||||
|
TechAnalyzeEntryView,
|
||||||
|
TechAnalyzeSaveView,
|
||||||
|
LinkExistingPointsView,
|
||||||
|
TechAnalyzeListView,
|
||||||
|
TechAnalyzeDeleteView,
|
||||||
|
TechAnalyzeAPIView,
|
||||||
|
)
|
||||||
from .views.points_averaging import PointsAveragingView, PointsAveragingAPIView, RecalculateGroupAPIView
|
from .views.points_averaging import PointsAveragingView, PointsAveragingAPIView, RecalculateGroupAPIView
|
||||||
|
|
||||||
app_name = 'mainapp'
|
app_name = 'mainapp'
|
||||||
@@ -108,6 +116,7 @@ urlpatterns = [
|
|||||||
path('api/source/<int:source_id>/objitems/', SourceObjItemsAPIView.as_view(), name='source_objitems_api'),
|
path('api/source/<int:source_id>/objitems/', SourceObjItemsAPIView.as_view(), name='source_objitems_api'),
|
||||||
path('api/transponder/<int:transponder_id>/', TransponderDataAPIView.as_view(), name='transponder_data_api'),
|
path('api/transponder/<int:transponder_id>/', TransponderDataAPIView.as_view(), name='transponder_data_api'),
|
||||||
path('api/satellite/<int:satellite_id>/', SatelliteDataAPIView.as_view(), name='satellite_data_api'),
|
path('api/satellite/<int:satellite_id>/', SatelliteDataAPIView.as_view(), name='satellite_data_api'),
|
||||||
|
path('api/satellite/<int:satellite_id>/transponders/', SatelliteTranspondersAPIView.as_view(), name='satellite_transponders_api'),
|
||||||
path('api/geo-points/', GeoPointsAPIView.as_view(), name='geo_points_api'),
|
path('api/geo-points/', GeoPointsAPIView.as_view(), name='geo_points_api'),
|
||||||
path('api/multi-sources-playback/', MultiSourcesPlaybackDataAPIView.as_view(), name='multi_sources_playback_api'),
|
path('api/multi-sources-playback/', MultiSourcesPlaybackDataAPIView.as_view(), name='multi_sources_playback_api'),
|
||||||
path('kubsat-excel/', ProcessKubsatView.as_view(), name='kubsat_excel'),
|
path('kubsat-excel/', ProcessKubsatView.as_view(), name='kubsat_excel'),
|
||||||
@@ -128,8 +137,12 @@ urlpatterns = [
|
|||||||
path('kubsat/export/', KubsatExportView.as_view(), name='kubsat_export'),
|
path('kubsat/export/', KubsatExportView.as_view(), name='kubsat_export'),
|
||||||
path('data-entry/', DataEntryView.as_view(), name='data_entry'),
|
path('data-entry/', DataEntryView.as_view(), name='data_entry'),
|
||||||
path('api/search-objitem/', SearchObjItemAPIView.as_view(), name='search_objitem_api'),
|
path('api/search-objitem/', SearchObjItemAPIView.as_view(), name='search_objitem_api'),
|
||||||
path('tech-analyze/', tech_analyze_entry, name='tech_analyze_entry'),
|
path('tech-analyze/', TechAnalyzeEntryView.as_view(), name='tech_analyze_entry'),
|
||||||
path('tech-analyze/save/', tech_analyze_save, name='tech_analyze_save'),
|
path('tech-analyze/list/', TechAnalyzeListView.as_view(), name='tech_analyze_list'),
|
||||||
|
path('tech-analyze/save/', TechAnalyzeSaveView.as_view(), name='tech_analyze_save'),
|
||||||
|
path('tech-analyze/delete/', TechAnalyzeDeleteView.as_view(), name='tech_analyze_delete'),
|
||||||
|
path('tech-analyze/link-existing/', LinkExistingPointsView.as_view(), name='tech_analyze_link_existing'),
|
||||||
|
path('api/tech-analyze/', TechAnalyzeAPIView.as_view(), name='tech_analyze_api'),
|
||||||
path('points-averaging/', PointsAveragingView.as_view(), name='points_averaging'),
|
path('points-averaging/', PointsAveragingView.as_view(), name='points_averaging'),
|
||||||
path('api/points-averaging/', PointsAveragingAPIView.as_view(), name='points_averaging_api'),
|
path('api/points-averaging/', PointsAveragingAPIView.as_view(), name='points_averaging_api'),
|
||||||
path('api/points-averaging/recalculate/', RecalculateGroupAPIView.as_view(), name='points_averaging_recalculate'),
|
path('api/points-averaging/recalculate/', RecalculateGroupAPIView.as_view(), name='points_averaging_recalculate'),
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from .api import (
|
|||||||
GetLocationsView,
|
GetLocationsView,
|
||||||
LyngsatDataAPIView,
|
LyngsatDataAPIView,
|
||||||
SatelliteDataAPIView,
|
SatelliteDataAPIView,
|
||||||
|
SatelliteTranspondersAPIView,
|
||||||
SigmaParameterDataAPIView,
|
SigmaParameterDataAPIView,
|
||||||
SourceObjItemsAPIView,
|
SourceObjItemsAPIView,
|
||||||
LyngsatTaskStatusAPIView,
|
LyngsatTaskStatusAPIView,
|
||||||
@@ -96,6 +97,7 @@ __all__ = [
|
|||||||
'GetLocationsView',
|
'GetLocationsView',
|
||||||
'LyngsatDataAPIView',
|
'LyngsatDataAPIView',
|
||||||
'SatelliteDataAPIView',
|
'SatelliteDataAPIView',
|
||||||
|
'SatelliteTranspondersAPIView',
|
||||||
'SigmaParameterDataAPIView',
|
'SigmaParameterDataAPIView',
|
||||||
'SourceObjItemsAPIView',
|
'SourceObjItemsAPIView',
|
||||||
'LyngsatTaskStatusAPIView',
|
'LyngsatTaskStatusAPIView',
|
||||||
|
|||||||
@@ -723,3 +723,43 @@ class MultiSourcesPlaybackDataAPIView(LoginRequiredMixin, View):
|
|||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return JsonResponse({'error': str(e)}, status=500)
|
return JsonResponse({'error': str(e)}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class SatelliteTranspondersAPIView(LoginRequiredMixin, View):
|
||||||
|
"""API endpoint for getting transponders for a satellite."""
|
||||||
|
|
||||||
|
def get(self, request, satellite_id):
|
||||||
|
from mapsapp.models import Transponders
|
||||||
|
|
||||||
|
try:
|
||||||
|
transponders = Transponders.objects.filter(
|
||||||
|
sat_id=satellite_id
|
||||||
|
).select_related('polarization').order_by('downlink')
|
||||||
|
|
||||||
|
if not transponders.exists():
|
||||||
|
return JsonResponse({
|
||||||
|
'satellite_id': satellite_id,
|
||||||
|
'transponders': [],
|
||||||
|
'count': 0
|
||||||
|
})
|
||||||
|
|
||||||
|
transponders_data = []
|
||||||
|
for t in transponders:
|
||||||
|
transponders_data.append({
|
||||||
|
'id': t.id,
|
||||||
|
'name': t.name or '-',
|
||||||
|
'downlink': float(t.downlink) if t.downlink else 0,
|
||||||
|
'uplink': float(t.uplink) if t.uplink else None,
|
||||||
|
'frequency_range': float(t.frequency_range) if t.frequency_range else 0,
|
||||||
|
'polarization': t.polarization.name if t.polarization else '-',
|
||||||
|
'zone_name': t.zone_name or '-',
|
||||||
|
})
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'satellite_id': satellite_id,
|
||||||
|
'transponders': transponders_data,
|
||||||
|
'count': len(transponders_data)
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({'error': str(e)}, status=500)
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib import messages
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.db import transaction
|
||||||
|
from django.db.models import Q
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
from django.views import View
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
from django.db import transaction
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from ..models import (
|
from ..models import (
|
||||||
@@ -11,14 +15,19 @@ from ..models import (
|
|||||||
Polarization,
|
Polarization,
|
||||||
Modulation,
|
Modulation,
|
||||||
Standard,
|
Standard,
|
||||||
|
ObjItem,
|
||||||
|
Parameter,
|
||||||
)
|
)
|
||||||
|
from ..mixins import RoleRequiredMixin
|
||||||
|
from ..utils import parse_pagination_params
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
class TechAnalyzeEntryView(LoginRequiredMixin, View):
|
||||||
def tech_analyze_entry(request):
|
|
||||||
"""
|
"""
|
||||||
Представление для ввода данных технического анализа.
|
Представление для ввода данных технического анализа.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
satellites = Satellite.objects.all().order_by('name')
|
satellites = Satellite.objects.all().order_by('name')
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
@@ -28,12 +37,12 @@ def tech_analyze_entry(request):
|
|||||||
return render(request, 'mainapp/tech_analyze_entry.html', context)
|
return render(request, 'mainapp/tech_analyze_entry.html', context)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
class TechAnalyzeSaveView(LoginRequiredMixin, View):
|
||||||
@require_http_methods(["POST"])
|
|
||||||
def tech_analyze_save(request):
|
|
||||||
"""
|
"""
|
||||||
API endpoint для сохранения данных технического анализа.
|
API endpoint для сохранения данных технического анализа.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
try:
|
try:
|
||||||
data = json.loads(request.body)
|
data = json.loads(request.body)
|
||||||
satellite_id = data.get('satellite_id')
|
satellite_id = data.get('satellite_id')
|
||||||
@@ -165,3 +174,256 @@ def tech_analyze_save(request):
|
|||||||
'success': False,
|
'success': False,
|
||||||
'error': str(e)
|
'error': str(e)
|
||||||
}, status=500)
|
}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class LinkExistingPointsView(LoginRequiredMixin, View):
|
||||||
|
"""
|
||||||
|
API endpoint для привязки существующих точек к данным теханализа.
|
||||||
|
|
||||||
|
Алгоритм:
|
||||||
|
1. Получить все ObjItem для выбранного спутника
|
||||||
|
2. Для каждого ObjItem:
|
||||||
|
- Извлечь имя источника
|
||||||
|
- Найти соответствующую запись TechAnalyze по имени и спутнику
|
||||||
|
- Если найдена и данные отсутствуют в Parameter:
|
||||||
|
* Обновить модуляцию (если "-")
|
||||||
|
* Обновить символьную скорость (если -1.0 или None)
|
||||||
|
* Обновить стандарт (если "-")
|
||||||
|
"""
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
data = json.loads(request.body)
|
||||||
|
satellite_id = data.get('satellite_id')
|
||||||
|
|
||||||
|
if not satellite_id:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Не выбран спутник'
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
try:
|
||||||
|
satellite = Satellite.objects.get(id=satellite_id)
|
||||||
|
except Satellite.DoesNotExist:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Спутник не найден'
|
||||||
|
}, status=404)
|
||||||
|
|
||||||
|
# Получаем все ObjItem для данного спутника
|
||||||
|
objitems = ObjItem.objects.filter(
|
||||||
|
parameter_obj__id_satellite=satellite
|
||||||
|
).select_related('parameter_obj', 'parameter_obj__modulation', 'parameter_obj__standard')
|
||||||
|
|
||||||
|
updated_count = 0
|
||||||
|
skipped_count = 0
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
for objitem in objitems:
|
||||||
|
try:
|
||||||
|
if not objitem.parameter_obj:
|
||||||
|
skipped_count += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
parameter = objitem.parameter_obj
|
||||||
|
source_name = objitem.name
|
||||||
|
|
||||||
|
# Проверяем, нужно ли обновлять данные
|
||||||
|
needs_update = (
|
||||||
|
(parameter.modulation and parameter.modulation.name == "-") or
|
||||||
|
parameter.bod_velocity is None or
|
||||||
|
parameter.bod_velocity == -1.0 or
|
||||||
|
parameter.bod_velocity == 0 or
|
||||||
|
(parameter.standard and parameter.standard.name == "-")
|
||||||
|
)
|
||||||
|
|
||||||
|
if not needs_update:
|
||||||
|
skipped_count += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Ищем данные в TechAnalyze по имени и спутнику
|
||||||
|
tech_analyze = TechAnalyze.objects.filter(
|
||||||
|
name=source_name,
|
||||||
|
satellite=satellite
|
||||||
|
).select_related('modulation', 'standard').first()
|
||||||
|
|
||||||
|
if not tech_analyze:
|
||||||
|
skipped_count += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Обновляем данные
|
||||||
|
updated = False
|
||||||
|
|
||||||
|
# Обновляем модуляцию
|
||||||
|
if parameter.modulation and parameter.modulation.name == "-" and tech_analyze.modulation:
|
||||||
|
parameter.modulation = tech_analyze.modulation
|
||||||
|
updated = True
|
||||||
|
|
||||||
|
# Обновляем символьную скорость
|
||||||
|
if (parameter.bod_velocity is None or parameter.bod_velocity == -1.0 or parameter.bod_velocity == 0) and \
|
||||||
|
tech_analyze.bod_velocity and tech_analyze.bod_velocity > 0:
|
||||||
|
parameter.bod_velocity = tech_analyze.bod_velocity
|
||||||
|
updated = True
|
||||||
|
|
||||||
|
# Обновляем стандарт
|
||||||
|
if parameter.standard and parameter.standard.name == "-" and tech_analyze.standard:
|
||||||
|
parameter.standard = tech_analyze.standard
|
||||||
|
updated = True
|
||||||
|
|
||||||
|
if updated:
|
||||||
|
parameter.save()
|
||||||
|
updated_count += 1
|
||||||
|
else:
|
||||||
|
skipped_count += 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(f"ObjItem {objitem.id}: {str(e)}")
|
||||||
|
|
||||||
|
response_data = {
|
||||||
|
'success': True,
|
||||||
|
'updated': updated_count,
|
||||||
|
'skipped': skipped_count,
|
||||||
|
'total': objitems.count(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
response_data['errors'] = errors
|
||||||
|
|
||||||
|
return JsonResponse(response_data)
|
||||||
|
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Неверный формат данных'
|
||||||
|
}, status=400)
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': str(e)
|
||||||
|
}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class TechAnalyzeListView(LoginRequiredMixin, View):
|
||||||
|
"""
|
||||||
|
Представление для отображения списка данных технического анализа.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
# Получаем список спутников для фильтра
|
||||||
|
satellites = Satellite.objects.all().order_by('name')
|
||||||
|
|
||||||
|
# Получаем параметры из URL для передачи в шаблон
|
||||||
|
search_query = request.GET.get('search', '').strip()
|
||||||
|
satellite_ids = request.GET.getlist('satellite_id')
|
||||||
|
items_per_page = int(request.GET.get('items_per_page', 50))
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'satellites': satellites,
|
||||||
|
'selected_satellites': [int(sid) for sid in satellite_ids if sid],
|
||||||
|
'search_query': search_query,
|
||||||
|
'items_per_page': items_per_page,
|
||||||
|
'available_items_per_page': [25, 50, 100, 200, 500],
|
||||||
|
'full_width_page': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, 'mainapp/tech_analyze_list.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
class TechAnalyzeDeleteView(LoginRequiredMixin, RoleRequiredMixin, View):
|
||||||
|
"""
|
||||||
|
API endpoint для удаления выбранных записей теханализа.
|
||||||
|
"""
|
||||||
|
allowed_roles = ['admin', 'moderator']
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
data = json.loads(request.body)
|
||||||
|
ids = data.get('ids', [])
|
||||||
|
|
||||||
|
if not ids:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Не выбраны записи для удаления'
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
# Удаляем записи
|
||||||
|
deleted_count, _ = TechAnalyze.objects.filter(id__in=ids).delete()
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'success': True,
|
||||||
|
'deleted': deleted_count,
|
||||||
|
'message': f'Удалено записей: {deleted_count}'
|
||||||
|
})
|
||||||
|
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Неверный формат данных'
|
||||||
|
}, status=400)
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': str(e)
|
||||||
|
}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class TechAnalyzeAPIView(LoginRequiredMixin, View):
|
||||||
|
"""
|
||||||
|
API endpoint для получения данных теханализа в формате для Tabulator.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
# Получаем параметры фильтрации
|
||||||
|
search_query = request.GET.get('search', '').strip()
|
||||||
|
satellite_ids = request.GET.getlist('satellite_id')
|
||||||
|
|
||||||
|
# Получаем параметры пагинации от Tabulator
|
||||||
|
page = int(request.GET.get('page', 1))
|
||||||
|
size = int(request.GET.get('size', 50))
|
||||||
|
|
||||||
|
# Базовый queryset
|
||||||
|
tech_analyzes = TechAnalyze.objects.select_related(
|
||||||
|
'satellite', 'polarization', 'modulation', 'standard', 'created_by', 'updated_by'
|
||||||
|
).order_by('-created_at')
|
||||||
|
|
||||||
|
# Применяем фильтры
|
||||||
|
if search_query:
|
||||||
|
tech_analyzes = tech_analyzes.filter(
|
||||||
|
Q(name__icontains=search_query) |
|
||||||
|
Q(id__icontains=search_query)
|
||||||
|
)
|
||||||
|
|
||||||
|
if satellite_ids:
|
||||||
|
tech_analyzes = tech_analyzes.filter(satellite_id__in=satellite_ids)
|
||||||
|
|
||||||
|
# Пагинация
|
||||||
|
paginator = Paginator(tech_analyzes, size)
|
||||||
|
page_obj = paginator.get_page(page)
|
||||||
|
|
||||||
|
# Формируем данные для Tabulator
|
||||||
|
results = []
|
||||||
|
for item in page_obj:
|
||||||
|
results.append({
|
||||||
|
'id': item.id,
|
||||||
|
'name': item.name or '',
|
||||||
|
'satellite_id': item.satellite.id if item.satellite else None,
|
||||||
|
'satellite_name': item.satellite.name if item.satellite else '-',
|
||||||
|
'frequency': float(item.frequency) if item.frequency else 0,
|
||||||
|
'freq_range': float(item.freq_range) if item.freq_range else 0,
|
||||||
|
'bod_velocity': float(item.bod_velocity) if item.bod_velocity else 0,
|
||||||
|
'polarization_name': item.polarization.name if item.polarization else '-',
|
||||||
|
'modulation_name': item.modulation.name if item.modulation else '-',
|
||||||
|
'standard_name': item.standard.name if item.standard else '-',
|
||||||
|
'note': item.note or '',
|
||||||
|
'created_at': item.created_at.isoformat() if item.created_at else None,
|
||||||
|
'updated_at': item.updated_at.isoformat() if item.updated_at else None,
|
||||||
|
})
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'last_page': paginator.num_pages,
|
||||||
|
'data': results,
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user