diff --git a/dbapp/mainapp/templates/mainapp/components/_frequency_plan_modal.html b/dbapp/mainapp/templates/mainapp/components/_frequency_plan_modal.html
new file mode 100644
index 0000000..459383d
--- /dev/null
+++ b/dbapp/mainapp/templates/mainapp/components/_frequency_plan_modal.html
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
Визуализация транспондеров спутника по частотам. ■ Downlink (синий), ■ Uplink (оранжевый). Используйте колесико мыши для масштабирования, наведите курсор на полосу для подробной информации и связи с парным каналом.
+
+
+
+
+ Сбросить масштаб
+
+
+
+
+
+
+
+
+
Всего транспондеров: 0
+
+
+
+
+
+
Нет данных о транспондерах для этого спутника
+
+
+
+
+
+
+
diff --git a/dbapp/mainapp/templates/mainapp/objitem_list.html b/dbapp/mainapp/templates/mainapp/objitem_list.html
index a12f2ce..9c159cc 100644
--- a/dbapp/mainapp/templates/mainapp/objitem_list.html
+++ b/dbapp/mainapp/templates/mainapp/objitem_list.html
@@ -53,9 +53,9 @@
onclick="showSelectedOnMap()">
Карта
-
+
diff --git a/dbapp/mainapp/templates/mainapp/points_averaging.html b/dbapp/mainapp/templates/mainapp/points_averaging.html
index 5e9c744..3dfe07f 100644
--- a/dbapp/mainapp/templates/mainapp/points_averaging.html
+++ b/dbapp/mainapp/templates/mainapp/points_averaging.html
@@ -275,11 +275,14 @@ document.addEventListener('DOMContentLoaded', function() {
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},
+ {title: "Частота", field: "frequency", minWidth: 100, sorter: "number"},
{title: "Модуляция", field: "modulation", minWidth: 90},
{title: "Зеркала", field: "mirrors", minWidth: 130},
{
@@ -624,14 +627,20 @@ document.addEventListener('DOMContentLoaded', function() {
'ОСШ': group.snr,
'Зеркала': group.mirrors,
'Усреднённые координаты': group.avg_coordinates,
- 'Тип усреднения': group.avg_type || 'ГК',
- 'Медианное время': group.avg_time || '-',
- 'Кол-во точек': group.valid_points_count,
- 'Интервал': group.interval_label
+ // 'Тип усреднения': 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 => {
@@ -651,13 +660,19 @@ document.addEventListener('DOMContentLoaded', function() {
'Координаты точки': point.coordinates,
'Усреднённые координаты': group.avg_coordinates,
'Расстояние от среднего, км': point.distance_from_avg,
- 'Статус': point.is_outlier ? 'Выброс' : 'OK',
- 'Интервал': group.interval_label
+ 'Статус': 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), "Все точки");
@@ -737,41 +752,41 @@ document.addEventListener('DOMContentLoaded', function() {
"tags": {"layers": [], "creator": CREATOR_ID}
});
- group.points.forEach(point => {
- if (point.is_outlier) return;
+ // group.points.forEach(point => {
+ // if (point.is_outlier) return;
- const pointCoord = point.coord_tuple;
- const pointCaption = `${point.name || '-'} - ${point.timestamp || '-'}`;
- const pointSourceId = generateUUID();
+ // 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": "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}
- });
- });
+ // 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}
+ // });
+ // });
});
});
diff --git a/dbapp/mainapp/templates/mainapp/satellite_list.html b/dbapp/mainapp/templates/mainapp/satellite_list.html
index 43f9989..29f29d2 100644
--- a/dbapp/mainapp/templates/mainapp/satellite_list.html
+++ b/dbapp/mainapp/templates/mainapp/satellite_list.html
@@ -303,17 +303,26 @@
{{ satellite.created_at|date:"d.m.Y H:i" }}
{{ satellite.updated_at|date:"d.m.Y H:i" }}
- {% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
-
-
-
- {% else %}
-
-
-
- {% endif %}
+
+ {% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
+
+
+
+ {% else %}
+
+
+
+ {% endif %}
+ {% if satellite.transponder_count > 0 %}
+
+
+
+ {% endif %}
+
{% empty %}
@@ -330,6 +339,8 @@
+{% include 'mainapp/components/_frequency_plan_modal.html' %}
+
{% endblock %}
{% block extra_js %}
@@ -526,6 +537,607 @@ document.addEventListener('DOMContentLoaded', function() {
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);
+ }
+});
{% endblock %}
diff --git a/dbapp/mainapp/templates/mainapp/source_list.html b/dbapp/mainapp/templates/mainapp/source_list.html
index 435c103..c270a19 100644
--- a/dbapp/mainapp/templates/mainapp/source_list.html
+++ b/dbapp/mainapp/templates/mainapp/source_list.html
@@ -79,9 +79,9 @@
Создать
{% endif %}
-
+
Excel
@@ -101,6 +101,9 @@
Усреднение
+
+ Тех. анализ
+
@@ -157,7 +160,7 @@
Создано
Обновлено
Дата подтверждения
- Последний сигнал
+
Действия
@@ -496,7 +499,7 @@
{% include 'mainapp/components/_sort_header.html' with field='updated_at' label='Обновлено' current_sort=sort %}
Дата подтверждения
- Последний сигнал
+
Действия
@@ -539,7 +542,7 @@
{{ source.created_at|date:"d.m.Y H:i" }}
{{ source.updated_at|date:"d.m.Y H:i" }}
{{ source.confirm_at|date:"d.m.Y H:i"|default:"-" }}
- {{ source.last_signal_at|date:"d.m.Y H:i"|default:"-" }}
+
{% if source.objitem_count > 0 %}
diff --git a/dbapp/mainapp/templates/mainapp/tech_analyze_entry.html b/dbapp/mainapp/templates/mainapp/tech_analyze_entry.html
index 4e2e056..8e88136 100644
--- a/dbapp/mainapp/templates/mainapp/tech_analyze_entry.html
+++ b/dbapp/mainapp/templates/mainapp/tech_analyze_entry.html
@@ -53,6 +53,18 @@
{% endblock %}
{% block content %}
+
+
+
Тех. анализ - Ввод данных
@@ -67,6 +79,11 @@
{% endfor %}
+
+
+
+
+
+
+
+
+
+
+ Найти
+ Очистить
+
+
+
+
+
+
+ Ввод данных
+
+ {% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
+
+ Удалить
+
+ {% endif %}
+
+ Привязать к точкам
+
+
+
+
+
+
+ Фильтры
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Будут обновлены только точки с отсутствующими данными (модуляция "-", символьная скорость -1 или 0, стандарт "-").
+
+
+
+ Выберите спутник *
+
+ Выберите спутник
+ {% for satellite in satellites %}
+ {{ satellite.name }}
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+
+{% include 'mainapp/components/_satellite_modal.html' %}
+
+{% endblock %}
+
+{% block extra_js %}
+
+
+{% endblock %}
diff --git a/dbapp/mainapp/urls.py b/dbapp/mainapp/urls.py
index 3f090ce..0e34213 100644
--- a/dbapp/mainapp/urls.py
+++ b/dbapp/mainapp/urls.py
@@ -36,6 +36,7 @@ from .views import (
ObjItemUpdateView,
ProcessKubsatView,
SatelliteDataAPIView,
+ SatelliteTranspondersAPIView,
SatelliteListView,
SatelliteCreateView,
SatelliteUpdateView,
@@ -60,7 +61,14 @@ from .views import (
custom_logout,
)
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
app_name = 'mainapp'
@@ -108,6 +116,7 @@ urlpatterns = [
path('api/source//objitems/', SourceObjItemsAPIView.as_view(), name='source_objitems_api'),
path('api/transponder//', TransponderDataAPIView.as_view(), name='transponder_data_api'),
path('api/satellite//', SatelliteDataAPIView.as_view(), name='satellite_data_api'),
+ path('api/satellite//transponders/', SatelliteTranspondersAPIView.as_view(), name='satellite_transponders_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('kubsat-excel/', ProcessKubsatView.as_view(), name='kubsat_excel'),
@@ -128,8 +137,12 @@ urlpatterns = [
path('kubsat/export/', KubsatExportView.as_view(), name='kubsat_export'),
path('data-entry/', DataEntryView.as_view(), name='data_entry'),
path('api/search-objitem/', SearchObjItemAPIView.as_view(), name='search_objitem_api'),
- path('tech-analyze/', tech_analyze_entry, name='tech_analyze_entry'),
- path('tech-analyze/save/', tech_analyze_save, name='tech_analyze_save'),
+ path('tech-analyze/', TechAnalyzeEntryView.as_view(), name='tech_analyze_entry'),
+ 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('api/points-averaging/', PointsAveragingAPIView.as_view(), name='points_averaging_api'),
path('api/points-averaging/recalculate/', RecalculateGroupAPIView.as_view(), name='points_averaging_recalculate'),
diff --git a/dbapp/mainapp/views/__init__.py b/dbapp/mainapp/views/__init__.py
index d76387e..6a3a7b3 100644
--- a/dbapp/mainapp/views/__init__.py
+++ b/dbapp/mainapp/views/__init__.py
@@ -22,6 +22,7 @@ from .api import (
GetLocationsView,
LyngsatDataAPIView,
SatelliteDataAPIView,
+ SatelliteTranspondersAPIView,
SigmaParameterDataAPIView,
SourceObjItemsAPIView,
LyngsatTaskStatusAPIView,
@@ -96,6 +97,7 @@ __all__ = [
'GetLocationsView',
'LyngsatDataAPIView',
'SatelliteDataAPIView',
+ 'SatelliteTranspondersAPIView',
'SigmaParameterDataAPIView',
'SourceObjItemsAPIView',
'LyngsatTaskStatusAPIView',
diff --git a/dbapp/mainapp/views/api.py b/dbapp/mainapp/views/api.py
index a6a177d..af037d2 100644
--- a/dbapp/mainapp/views/api.py
+++ b/dbapp/mainapp/views/api.py
@@ -723,3 +723,43 @@ class MultiSourcesPlaybackDataAPIView(LoginRequiredMixin, View):
})
except Exception as e:
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)
diff --git a/dbapp/mainapp/views/tech_analyze.py b/dbapp/mainapp/views/tech_analyze.py
index 05eb4f3..62ba809 100644
--- a/dbapp/mainapp/views/tech_analyze.py
+++ b/dbapp/mainapp/views/tech_analyze.py
@@ -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.shortcuts import render
+from django.views import View
from django.views.decorators.http import require_http_methods
-from django.db import transaction
import json
from ..models import (
@@ -11,157 +15,415 @@ from ..models import (
Polarization,
Modulation,
Standard,
+ ObjItem,
+ Parameter,
)
+from ..mixins import RoleRequiredMixin
+from ..utils import parse_pagination_params
-@login_required
-def tech_analyze_entry(request):
+class TechAnalyzeEntryView(LoginRequiredMixin, View):
"""
Представление для ввода данных технического анализа.
"""
- satellites = Satellite.objects.all().order_by('name')
- context = {
- 'satellites': satellites,
- }
-
- return render(request, 'mainapp/tech_analyze_entry.html', context)
+ def get(self, request):
+ satellites = Satellite.objects.all().order_by('name')
+
+ context = {
+ 'satellites': satellites,
+ }
+
+ return render(request, 'mainapp/tech_analyze_entry.html', context)
-@login_required
-@require_http_methods(["POST"])
-def tech_analyze_save(request):
+class TechAnalyzeSaveView(LoginRequiredMixin, View):
"""
API endpoint для сохранения данных технического анализа.
"""
- try:
- data = json.loads(request.body)
- satellite_id = data.get('satellite_id')
- rows = data.get('rows', [])
-
- if not satellite_id:
- return JsonResponse({
- 'success': False,
- 'error': 'Не выбран спутник'
- }, status=400)
-
- if not rows:
- return JsonResponse({
- 'success': False,
- 'error': 'Нет данных для сохранения'
- }, status=400)
-
+
+ def post(self, request):
try:
- satellite = Satellite.objects.get(id=satellite_id)
- except Satellite.DoesNotExist:
+ data = json.loads(request.body)
+ satellite_id = data.get('satellite_id')
+ rows = data.get('rows', [])
+
+ if not satellite_id:
+ return JsonResponse({
+ 'success': False,
+ 'error': 'Не выбран спутник'
+ }, status=400)
+
+ if not rows:
+ 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)
+
+ created_count = 0
+ updated_count = 0
+ errors = []
+
+ with transaction.atomic():
+ for idx, row in enumerate(rows, start=1):
+ try:
+ name = row.get('name', '').strip()
+ if not name:
+ errors.append(f"Строка {idx}: отсутствует имя")
+ continue
+
+ # Обработка поляризации
+ polarization_name = row.get('polarization', '').strip() or '-'
+ polarization, _ = Polarization.objects.get_or_create(name=polarization_name)
+
+ # Обработка модуляции
+ modulation_name = row.get('modulation', '').strip() or '-'
+ modulation, _ = Modulation.objects.get_or_create(name=modulation_name)
+
+ # Обработка стандарта
+ standard_name = row.get('standard', '').strip()
+ if standard_name.lower() == 'unknown':
+ standard_name = '-'
+ if not standard_name:
+ standard_name = '-'
+ standard, _ = Standard.objects.get_or_create(name=standard_name)
+
+ # Обработка числовых полей
+ frequency = row.get('frequency')
+ if frequency:
+ try:
+ frequency = float(str(frequency).replace(',', '.'))
+ except (ValueError, TypeError):
+ frequency = 0
+ else:
+ frequency = 0
+
+ freq_range = row.get('freq_range')
+ if freq_range:
+ try:
+ freq_range = float(str(freq_range).replace(',', '.'))
+ except (ValueError, TypeError):
+ freq_range = 0
+ else:
+ freq_range = 0
+
+ bod_velocity = row.get('bod_velocity')
+ if bod_velocity:
+ try:
+ bod_velocity = float(str(bod_velocity).replace(',', '.'))
+ except (ValueError, TypeError):
+ bod_velocity = 0
+ else:
+ bod_velocity = 0
+
+ note = row.get('note', '').strip()
+
+ # Создание или обновление записи
+ tech_analyze, created = TechAnalyze.objects.update_or_create(
+ name=name,
+ defaults={
+ 'satellite': satellite,
+ 'polarization': polarization,
+ 'frequency': frequency,
+ 'freq_range': freq_range,
+ 'bod_velocity': bod_velocity,
+ 'modulation': modulation,
+ 'standard': standard,
+ 'note': note,
+ 'updated_by': request.user.customuser if hasattr(request.user, 'customuser') else None,
+ }
+ )
+
+ if created:
+ tech_analyze.created_by = request.user.customuser if hasattr(request.user, 'customuser') else None
+ tech_analyze.save()
+ created_count += 1
+ else:
+ updated_count += 1
+
+ except Exception as e:
+ errors.append(f"Строка {idx}: {str(e)}")
+
+ response_data = {
+ 'success': True,
+ 'created': created_count,
+ 'updated': updated_count,
+ 'total': created_count + updated_count,
+ }
+
+ if errors:
+ response_data['errors'] = errors
+
+ return JsonResponse(response_data)
+
+ except json.JSONDecodeError:
return JsonResponse({
'success': False,
- 'error': 'Спутник не найден'
- }, status=404)
-
- created_count = 0
- updated_count = 0
- errors = []
-
- with transaction.atomic():
- for idx, row in enumerate(rows, start=1):
- try:
- name = row.get('name', '').strip()
- if not name:
- errors.append(f"Строка {idx}: отсутствует имя")
- continue
-
- # Обработка поляризации
- polarization_name = row.get('polarization', '').strip() or '-'
- polarization, _ = Polarization.objects.get_or_create(name=polarization_name)
-
- # Обработка модуляции
- modulation_name = row.get('modulation', '').strip() or '-'
- modulation, _ = Modulation.objects.get_or_create(name=modulation_name)
-
- # Обработка стандарта
- standard_name = row.get('standard', '').strip()
- if standard_name.lower() == 'unknown':
- standard_name = '-'
- if not standard_name:
- standard_name = '-'
- standard, _ = Standard.objects.get_or_create(name=standard_name)
-
- # Обработка числовых полей
- frequency = row.get('frequency')
- if frequency:
- try:
- frequency = float(str(frequency).replace(',', '.'))
- except (ValueError, TypeError):
- frequency = 0
- else:
- frequency = 0
-
- freq_range = row.get('freq_range')
- if freq_range:
- try:
- freq_range = float(str(freq_range).replace(',', '.'))
- except (ValueError, TypeError):
- freq_range = 0
- else:
- freq_range = 0
-
- bod_velocity = row.get('bod_velocity')
- if bod_velocity:
- try:
- bod_velocity = float(str(bod_velocity).replace(',', '.'))
- except (ValueError, TypeError):
- bod_velocity = 0
- else:
- bod_velocity = 0
-
- note = row.get('note', '').strip()
-
- # Создание или обновление записи
- tech_analyze, created = TechAnalyze.objects.update_or_create(
- name=name,
- defaults={
- 'satellite': satellite,
- 'polarization': polarization,
- 'frequency': frequency,
- 'freq_range': freq_range,
- 'bod_velocity': bod_velocity,
- 'modulation': modulation,
- 'standard': standard,
- 'note': note,
- 'updated_by': request.user.customuser if hasattr(request.user, 'customuser') else None,
- }
- )
-
- if created:
- tech_analyze.created_by = request.user.customuser if hasattr(request.user, 'customuser') else None
- tech_analyze.save()
- created_count += 1
- else:
- updated_count += 1
+ 'error': 'Неверный формат данных'
+ }, status=400)
+ except Exception as e:
+ return JsonResponse({
+ 'success': False,
+ 'error': str(e)
+ }, 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
- except Exception as e:
- errors.append(f"Строка {idx}: {str(e)}")
+ 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')
- response_data = {
- 'success': True,
- 'created': created_count,
- 'updated': updated_count,
- 'total': created_count + updated_count,
+ # Получаем параметры из 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,
}
- if errors:
- response_data['errors'] = errors
+ 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')
- return JsonResponse(response_data)
+ # Получаем параметры пагинации от 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,
+ })
- except json.JSONDecodeError:
return JsonResponse({
- 'success': False,
- 'error': 'Неверный формат данных'
- }, status=400)
- except Exception as e:
- return JsonResponse({
- 'success': False,
- 'error': str(e)
- }, status=500)
+ 'last_page': paginator.num_pages,
+ 'data': results,
+ })