From 889899080a228f9af8b08e7df49e5684c13c62a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=BE=D1=88=D0=BA=D0=B8=D0=BD=20=D0=A1=D0=B5=D1=80?= =?UTF-8?q?=D0=B3=D0=B5=D0=B9?= Date: Tue, 2 Dec 2025 14:56:29 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=BE=D0=BC=D0=B5=D0=BD=D1=8F=D0=BB=20?= =?UTF-8?q?=D1=82=D0=B5=D1=85=D0=B0=D0=BD=D0=B0=D0=BB=D0=B8=D0=B7,=20?= =?UTF-8?q?=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D0=BF=D1=80=D0=BE=D1=81=D1=82=D0=B1=D0=B0=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/_frequency_plan_modal.html | 71 ++ .../templates/mainapp/objitem_list.html | 4 +- .../templates/mainapp/points_averaging.html | 93 +-- .../templates/mainapp/satellite_list.html | 634 +++++++++++++++++- .../templates/mainapp/source_list.html | 13 +- .../templates/mainapp/tech_analyze_entry.html | 66 +- .../templates/mainapp/tech_analyze_list.html | 518 ++++++++++++++ dbapp/mainapp/urls.py | 19 +- dbapp/mainapp/views/__init__.py | 2 + dbapp/mainapp/views/api.py | 40 ++ dbapp/mainapp/views/tech_analyze.py | 530 +++++++++++---- 11 files changed, 1785 insertions(+), 205 deletions(-) create mode 100644 dbapp/mainapp/templates/mainapp/components/_frequency_plan_modal.html create mode 100644 dbapp/mainapp/templates/mainapp/tech_analyze_list.html 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 @@ + + + + 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 %} + +
    + + +
    + +
    +
    +
    +
    +
    +
    + + +
    +
    +
    Фильтры
    + +
    +
    +
    + +
    + +
    + + +
    + +
    + + +
    + + Сбросить +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + + + + +{% 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, + })