diff --git a/dbapp/mainapp/admin.py b/dbapp/mainapp/admin.py index 2f2e89e..da66777 100644 --- a/dbapp/mainapp/admin.py +++ b/dbapp/mainapp/admin.py @@ -23,14 +23,12 @@ from .models import ( Polarization, Modulation, Standard, - SigmaParMark, ObjectMark, ObjectInfo, ObjectOwnership, SigmaParameter, Parameter, Satellite, - Mirror, Geo, ObjItem, CustomUser, @@ -359,14 +357,14 @@ class ObjectMarkAdmin(BaseAdmin): autocomplete_fields = ("source",) -@admin.register(SigmaParMark) -class SigmaParMarkAdmin(BaseAdmin): - """Админ-панель для модели SigmaParMark.""" +# @admin.register(SigmaParMark) +# class SigmaParMarkAdmin(BaseAdmin): +# """Админ-панель для модели SigmaParMark.""" - list_display = ("mark", "timestamp") - search_fields = ("mark",) - ordering = ("-timestamp",) - list_filter = (("timestamp", DateRangeQuickSelectListFilterBuilder()),) +# list_display = ("mark", "timestamp") +# search_fields = ("mark",) +# ordering = ("-timestamp",) +# list_filter = (("timestamp", DateRangeQuickSelectListFilterBuilder()),) @admin.register(Polarization) @@ -417,7 +415,6 @@ class ObjectOwnershipAdmin(BaseAdmin): class SigmaParameterInline(admin.StackedInline): model = SigmaParameter extra = 0 - autocomplete_fields = ["mark"] readonly_fields = ( "datetime_begin", "datetime_end", @@ -561,7 +558,6 @@ class SigmaParameterAdmin(ImportExportActionModelAdmin, BaseAdmin): "modulation__name", "standard__name", ) - autocomplete_fields = ("mark",) ordering = ("-frequency",) def get_queryset(self, request): @@ -589,13 +585,13 @@ class SatelliteAdmin(BaseAdmin): readonly_fields = ("created_at", "created_by", "updated_at", "updated_by") -@admin.register(Mirror) -class MirrorAdmin(ImportExportActionModelAdmin, BaseAdmin): - """Админ-панель для модели Mirror с поддержкой импорта/экспорта.""" +# @admin.register(Mirror) +# class MirrorAdmin(ImportExportActionModelAdmin, BaseAdmin): +# """Админ-панель для модели Mirror с поддержкой импорта/экспорта.""" - list_display = ("name",) - search_fields = ("name",) - ordering = ("name",) +# list_display = ("name",) +# search_fields = ("name",) +# ordering = ("name",) @admin.register(Geo) diff --git a/dbapp/mainapp/models.py b/dbapp/mainapp/models.py index f956696..f36dddb 100644 --- a/dbapp/mainapp/models.py +++ b/dbapp/mainapp/models.py @@ -172,63 +172,63 @@ class ObjectMark(models.Model): # Для обратной совместимости с SigmaParameter -class SigmaParMark(models.Model): - """ - Модель отметки о наличии сигнала (для Sigma). +# class SigmaParMark(models.Model): +# """ +# Модель отметки о наличии сигнала (для Sigma). - Используется для фиксации моментов времени когда сигнал был обнаружен или потерян. - """ +# Используется для фиксации моментов времени когда сигнал был обнаружен или потерян. +# """ - # Основные поля - mark = models.BooleanField( - null=True, - blank=True, - verbose_name="Наличие сигнала", - help_text="True - сигнал обнаружен, False - сигнал отсутствует", - ) - timestamp = models.DateTimeField( - null=True, - blank=True, - verbose_name="Время", - db_index=True, - help_text="Время фиксации отметки", - ) +# # Основные поля +# mark = models.BooleanField( +# null=True, +# blank=True, +# verbose_name="Наличие сигнала", +# help_text="True - сигнал обнаружен, False - сигнал отсутствует", +# ) +# timestamp = models.DateTimeField( +# null=True, +# blank=True, +# verbose_name="Время", +# db_index=True, +# help_text="Время фиксации отметки", +# ) - def __str__(self): - if self.timestamp: - timestamp = self.timestamp.strftime("%d.%m.%Y %H:%M") - return f"+ {timestamp}" if self.mark else f"- {timestamp}" - return "Отметка без времени" +# def __str__(self): +# if self.timestamp: +# timestamp = self.timestamp.strftime("%d.%m.%Y %H:%M") +# return f"+ {timestamp}" if self.mark else f"- {timestamp}" +# return "Отметка без времени" - class Meta: - verbose_name = "Отметка сигнала" - verbose_name_plural = "Отметки сигналов" - ordering = ["-timestamp"] +# class Meta: +# verbose_name = "Отметка сигнала" +# verbose_name_plural = "Отметки сигналов" +# ordering = ["-timestamp"] -class Mirror(models.Model): - """ - Модель зеркала антенны. +# class Mirror(models.Model): +# """ +# Модель зеркала антенны. - Представляет физическое зеркало антенны для приема спутникового сигнала. - """ +# Представляет физическое зеркало антенны для приема спутникового сигнала. +# """ - # Основные поля - name = models.CharField( - max_length=30, - unique=True, - verbose_name="Имя зеркала", - db_index=True, - help_text="Уникальное название зеркала антенны", - ) +# # Основные поля +# name = models.CharField( +# max_length=30, +# unique=True, +# verbose_name="Имя зеркала", +# db_index=True, +# help_text="Уникальное название зеркала антенны", +# ) - def __str__(self): - return self.name +# def __str__(self): +# return self.name - class Meta: - verbose_name = "Зеркало" - verbose_name_plural = "Зеркала" - ordering = ["name"] +# class Meta: +# verbose_name = "Зеркало" +# verbose_name_plural = "Зеркала" +# ordering = ["name"] class Polarization(models.Model): @@ -1027,7 +1027,7 @@ class SigmaParameter(models.Model): verbose_name="Время окончания измерения", help_text="Дата и время окончания измерения", ) - mark = models.ManyToManyField(SigmaParMark, verbose_name="Отметка", blank=True) + # mark = models.ManyToManyField(SigmaParMark, verbose_name="Отметка", blank=True) parameter = models.ForeignKey( Parameter, on_delete=models.SET_NULL, diff --git a/dbapp/mainapp/templates/mainapp/satellite_form.html b/dbapp/mainapp/templates/mainapp/satellite_form.html index 98c8eb4..28a38d8 100644 --- a/dbapp/mainapp/templates/mainapp/satellite_form.html +++ b/dbapp/mainapp/templates/mainapp/satellite_form.html @@ -212,26 +212,26 @@

Частотный план

-

Визуализация транспондеров спутника по частотам (Downlink). Используйте колесико мыши для масштабирования, наведите курсор на полосу для подробной информации.

+

Визуализация транспондеров спутника по частотам. Downlink (синий), Uplink (оранжевый). Используйте колесико мыши для масштабирования, наведите курсор на полосу для подробной информации.

-
-
+

Всего транспондеров: {{ transponder_count }}

@@ -310,19 +310,28 @@ function initializeFrequencyChart() { container = canvas.parentElement; ctx = canvas.getContext('2d'); - // Calculate frequency range + // Calculate frequency range (including both downlink and uplink) minFreq = Infinity; maxFreq = -Infinity; transpondersData.forEach(t => { - const startFreq = t.downlink - (t.frequency_range / 2); - const endFreq = t.downlink + (t.frequency_range / 2); - minFreq = Math.min(minFreq, startFreq); - maxFreq = Math.max(maxFreq, endFreq); + // Downlink + const dlStartFreq = t.downlink - (t.frequency_range / 2); + const dlEndFreq = t.downlink + (t.frequency_range / 2); + minFreq = Math.min(minFreq, dlStartFreq); + maxFreq = Math.max(maxFreq, dlEndFreq); + + // Uplink (if exists) + if (t.uplink) { + const ulStartFreq = t.uplink - (t.frequency_range / 2); + const ulEndFreq = t.uplink + (t.frequency_range / 2); + minFreq = Math.min(minFreq, ulStartFreq); + maxFreq = Math.max(maxFreq, ulEndFreq); + } }); // Add 2% padding - const padding = (maxFreq - minFreq) * 0.02; + const padding = (maxFreq - minFreq) * 0.04; minFreq -= padding; maxFreq += padding; @@ -368,10 +377,12 @@ function renderChart() { const chartWidth = width - leftMargin - rightMargin; const chartHeight = height - topMargin - bottomMargin; - // Group transponders by polarization + // Group transponders by polarization (use first letter only) const polarizationGroups = {}; transpondersData.forEach(t => { - const pol = t.polarization || 'Другая'; + let pol = t.polarization || '-'; + // Take only first letter for abbreviation + pol = pol.charAt(0).toUpperCase(); if (!polarizationGroups[pol]) { polarizationGroups[pol] = []; } @@ -379,7 +390,8 @@ function renderChart() { }); const polarizations = Object.keys(polarizationGroups); - const rowHeight = chartHeight / polarizations.length; + // Each polarization gets 2 rows (downlink + uplink) + const rowHeight = chartHeight / (polarizations.length * 2); // Calculate visible frequency range with zoom and pan const visibleFreqRange = freqRange / zoomLevel; @@ -443,18 +455,41 @@ function renderChart() { // Draw transponders polarizations.forEach((pol, index) => { const group = polarizationGroups[pol]; - const color = getColor(pol); - const y = topMargin + index * rowHeight; - const barHeight = rowHeight * 0.7; - const barY = y + (rowHeight - barHeight) / 2; + const downlinkColor = '#0000ff'; //getColor(pol); + const uplinkColor = '#fd7e14'; - // Draw polarization label + // Downlink row + const downlinkY = topMargin + (index * 2) * rowHeight; + const downlinkBarHeight = rowHeight * 0.8; + const downlinkBarY = downlinkY + (rowHeight - downlinkBarHeight) / 2; + + // Uplink row + const uplinkY = topMargin + (index * 2 + 1) * rowHeight; + const uplinkBarHeight = rowHeight * 0.8; + const uplinkBarY = uplinkY + (rowHeight - uplinkBarHeight) / 2; + + // Draw polarization label (centered between downlink and uplink) ctx.fillStyle = '#000'; - ctx.font = 'bold 12px sans-serif'; - ctx.textAlign = 'right'; - ctx.fillText(pol, leftMargin - 10, barY + barHeight / 2 + 4); + ctx.font = 'bold 14px sans-serif'; + ctx.textAlign = 'center'; + const labelY = downlinkY + rowHeight; + ctx.fillText(pol, leftMargin - 25, labelY); - // Draw transponders + // Draw "DL" and "UL" labels + ctx.font = '10px sans-serif'; + ctx.fillStyle = '#666'; + ctx.fillText('DL', leftMargin - 5, downlinkBarY + downlinkBarHeight / 2 + 3); + ctx.fillText('UL', leftMargin - 5, uplinkBarY + uplinkBarHeight / 2 + 3); + + // Draw separator line between DL and UL + ctx.strokeStyle = '#dee2e6'; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(leftMargin, uplinkY); + ctx.lineTo(width - rightMargin, uplinkY); + ctx.stroke(); + + // Draw downlink transponders group.forEach(t => { const startFreq = t.downlink - (t.frequency_range / 2); const endFreq = t.downlink + (t.frequency_range / 2); @@ -464,6 +499,52 @@ function renderChart() { return; } + const x1 = leftMargin + ((startFreq - visibleMinFreq) / (visibleMaxFreq - visibleMinFreq)) * chartWidth; + const x2 = leftMargin + ((endFreq - visibleMinFreq) / (visibleMaxFreq - visibleMinFreq)) * chartWidth; + const barWidth = x2 - x1; + + if (barWidth < 1) return; + + // Draw downlink bar + ctx.fillStyle = downlinkColor; + ctx.fillRect(x1, downlinkBarY, barWidth, downlinkBarHeight); + + // Draw border + ctx.strokeStyle = '#fff'; + ctx.lineWidth = 1; + ctx.strokeRect(x1, downlinkBarY, barWidth, downlinkBarHeight); + + // Draw name if there's space + if (barWidth > 40) { + ctx.fillStyle = (pol === 'R') ? '#000' : '#fff'; + ctx.font = '9px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(t.name, x1 + barWidth / 2, downlinkBarY + downlinkBarHeight / 2 + 3); + } + + // Store for hover detection + transponderRects.push({ + x: x1, + y: downlinkBarY, + width: barWidth, + height: downlinkBarHeight, + transponder: t, + type: 'downlink' + }); + }); + + // Draw uplink transponders + group.forEach(t => { + if (!t.uplink) return; // Skip if no uplink data + + const startFreq = t.uplink - (t.frequency_range / 2); + const endFreq = t.uplink + (t.frequency_range / 2); + + // Check if transponder is visible + if (endFreq < visibleMinFreq || startFreq > visibleMaxFreq) { + return; + } + // Calculate position const x1 = leftMargin + ((startFreq - visibleMinFreq) / (visibleMaxFreq - visibleMinFreq)) * chartWidth; const x2 = leftMargin + ((endFreq - visibleMinFreq) / (visibleMaxFreq - visibleMinFreq)) * chartWidth; @@ -472,32 +553,44 @@ function renderChart() { // Skip if too small if (barWidth < 1) return; - // Draw bar - ctx.fillStyle = color; - ctx.fillRect(x1, barY, barWidth, barHeight); + // Draw uplink bar + ctx.fillStyle = uplinkColor; + ctx.fillRect(x1, uplinkBarY, barWidth, uplinkBarHeight); // Draw border ctx.strokeStyle = '#fff'; - ctx.lineWidth = 2; - ctx.strokeRect(x1, barY, barWidth, barHeight); + ctx.lineWidth = 1; + ctx.strokeRect(x1, uplinkBarY, barWidth, uplinkBarHeight); // Draw name if there's space if (barWidth > 40) { - ctx.fillStyle = (pol === 'R') ? '#000' : '#fff'; - ctx.font = '10px sans-serif'; + ctx.fillStyle = '#fff'; + ctx.font = '9px sans-serif'; ctx.textAlign = 'center'; - ctx.fillText(t.name, x1 + barWidth / 2, barY + barHeight / 2 + 3); + ctx.fillText(t.name, x1 + barWidth / 2, uplinkBarY + uplinkBarHeight / 2 + 3); } // Store for hover detection transponderRects.push({ x: x1, - y: barY, + y: uplinkBarY, width: barWidth, - height: barHeight, - transponder: t + height: uplinkBarHeight, + transponder: t, + type: 'uplink' }); }); + + // Draw separator line after each polarization group (except last) + if (index < polarizations.length - 1) { + const separatorY = topMargin + (index * 2 + 2) * rowHeight; + ctx.strokeStyle = '#adb5bd'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(leftMargin, separatorY); + ctx.lineTo(width - rightMargin, separatorY); + ctx.stroke(); + } }); // Draw hover tooltip @@ -506,19 +599,29 @@ function renderChart() { } } -function drawTooltip(t) { - const startFreq = t.downlink - (t.frequency_range / 2); - const endFreq = t.downlink + (t.frequency_range / 2); +function drawTooltip(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) + ' МГц', - 'Downlink: ' + t.downlink.toFixed(3) + ' МГц', + 'Центр: ' + freq.toFixed(3) + ' МГц', 'Полоса: ' + t.frequency_range.toFixed(3) + ' МГц', 'Поляризация: ' + t.polarization, 'Зона: ' + t.zone_name ]; + // Add frequency conversion info for uplink + if (isUplink && t.downlink && t.uplink) { + const conversion = t.downlink - t.uplink; + lines.push('Перенос: ' + conversion.toFixed(3) + ' МГц'); + } + // Calculate tooltip size ctx.font = '12px sans-serif'; const padding = 10; @@ -533,8 +636,8 @@ function drawTooltip(t) { const tooltipHeight = lines.length * lineHeight + padding * 2; // Position tooltip - const mouseX = hoveredTransponder._mouseX || canvas.width / 2; - const mouseY = hoveredTransponder._mouseY || canvas.height / 2; + const mouseX = rectInfo._mouseX || canvas.width / 2; + const mouseY = rectInfo._mouseY || canvas.height / 2; let tooltipX = mouseX + 15; let tooltipY = mouseY + 15; @@ -607,7 +710,7 @@ function handleMouseMove(e) { for (const tr of transponderRects) { if (mouseX >= tr.x && mouseX <= tr.x + tr.width && mouseY >= tr.y && mouseY <= tr.y + tr.height) { - found = tr.transponder; + found = tr; found._mouseX = mouseX; found._mouseY = mouseY; break; @@ -662,8 +765,8 @@ document.addEventListener('DOMContentLoaded', function() { // Control buttons document.getElementById('resetZoom').addEventListener('click', resetZoom); - document.getElementById('zoomIn').addEventListener('click', zoomIn); - document.getElementById('zoomOut').addEventListener('click', zoomOut); + // document.getElementById('zoomIn').addEventListener('click', zoomIn); + // document.getElementById('zoomOut').addEventListener('click', zoomOut); }); // Re-render on window resize diff --git a/dbapp/mainapp/utils.py b/dbapp/mainapp/utils.py index 3bb94d3..b432172 100644 --- a/dbapp/mainapp/utils.py +++ b/dbapp/mainapp/utils.py @@ -17,7 +17,6 @@ from mapsapp.models import Transponders from .models import ( CustomUser, Geo, - Mirror, Modulation, ObjItem, Parameter, diff --git a/dbapp/mainapp/views/satellite.py b/dbapp/mainapp/views/satellite.py index 5389439..26a56ec 100644 --- a/dbapp/mainapp/views/satellite.py +++ b/dbapp/mainapp/views/satellite.py @@ -259,6 +259,7 @@ class SatelliteUpdateView(RoleRequiredMixin, FormMessageMixin, UpdateView): 'id': t.id, 'name': t.name or f"TP-{t.id}", 'downlink': float(t.downlink), + 'uplink': float(t.uplink) if t.uplink else None, 'frequency_range': float(t.frequency_range), 'polarization': t.polarization.name if t.polarization else '-', 'zone_name': t.zone_name or '-',