Страница с Кубсатами

This commit is contained in:
2025-11-19 17:36:39 +03:00
parent 4d7cc9f667
commit 66e1929978
12 changed files with 1429 additions and 159 deletions

View File

@@ -49,6 +49,10 @@ from .map import (
ShowSourceAveragingStepsMapView,
ClusterTestView,
)
from .kubsat import (
KubsatView,
KubsatExportView,
)
__all__ = [
# Base
@@ -102,4 +106,7 @@ __all__ = [
'ShowSourceWithPointsMapView',
'ShowSourceAveragingStepsMapView',
'ClusterTestView',
# Kubsat
'KubsatView',
'KubsatExportView',
]

View File

@@ -0,0 +1,330 @@
"""
Представления для страницы Кубсат с фильтрацией и экспортом в Excel
"""
from datetime import datetime
from io import BytesIO
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.gis.geos import Point
from django.db.models import Count, Q
from django.http import HttpResponse
from django.views.generic import FormView
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment
from mainapp.forms import KubsatFilterForm
from mainapp.models import Source, ObjItem
from mainapp.utils import calculate_mean_coords
class KubsatView(LoginRequiredMixin, FormView):
"""Страница Кубсат с фильтрами и таблицей источников"""
template_name = 'mainapp/kubsat.html'
form_class = KubsatFilterForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['full_width_page'] = True
# Если форма была отправлена, применяем фильтры
if self.request.GET:
form = self.form_class(self.request.GET)
if form.is_valid():
sources = self.apply_filters(form.cleaned_data)
# Определяем, какие источники подходят по дате
date_from = form.cleaned_data.get('date_from')
date_to = form.cleaned_data.get('date_to')
# Добавляем информацию о соответствии дате для каждого источника
sources_with_date_info = []
for source in sources:
source_data = {
'source': source,
'matches_date': False,
'objitems_data': []
}
# Проверяем каждый ObjItem
for objitem in source.source_objitems.all():
objitem_matches_date = False
geo_date = None
if hasattr(objitem, 'geo_obj') and objitem.geo_obj and objitem.geo_obj.timestamp:
geo_date = objitem.geo_obj.timestamp.date()
# Проверяем попадание в диапазон дат
if date_from and date_to:
objitem_matches_date = date_from <= geo_date <= date_to
elif date_from:
objitem_matches_date = geo_date >= date_from
elif date_to:
objitem_matches_date = geo_date <= date_to
else:
objitem_matches_date = True # Нет фильтра по дате
source_data['objitems_data'].append({
'objitem': objitem,
'matches_date': objitem_matches_date,
'geo_date': geo_date
})
# Если хотя бы одна точка подходит по дате, весь источник подходит
if objitem_matches_date:
source_data['matches_date'] = True
sources_with_date_info.append(source_data)
context['sources_with_date_info'] = sources_with_date_info
context['form'] = form
return context
def apply_filters(self, filters):
"""Применяет фильтры к queryset Source"""
queryset = Source.objects.select_related('info').prefetch_related(
'source_objitems__parameter_obj__id_satellite',
'source_objitems__parameter_obj__polarization',
'source_objitems__parameter_obj__modulation',
'source_objitems__transponder__sat_id'
).annotate(objitem_count=Count('source_objitems'))
# Фильтр по спутникам
if filters.get('satellites'):
queryset = queryset.filter(
source_objitems__parameter_obj__id_satellite__in=filters['satellites']
).distinct()
# Фильтр по полосе спутника (пока не реализован полностью)
if filters.get('band'):
pass # TODO: реализовать фильтр по band
# Фильтр по поляризации
if filters.get('polarization'):
queryset = queryset.filter(
source_objitems__parameter_obj__polarization__in=filters['polarization']
).distinct()
# Фильтр по центральной частоте
if filters.get('frequency_min'):
queryset = queryset.filter(
source_objitems__parameter_obj__frequency__gte=filters['frequency_min']
)
if filters.get('frequency_max'):
queryset = queryset.filter(
source_objitems__parameter_obj__frequency__lte=filters['frequency_max']
)
# Фильтр по полосе частот
if filters.get('freq_range_min'):
queryset = queryset.filter(
source_objitems__parameter_obj__freq_range__gte=filters['freq_range_min']
)
if filters.get('freq_range_max'):
queryset = queryset.filter(
source_objitems__parameter_obj__freq_range__lte=filters['freq_range_max']
)
# Фильтр по модуляции
if filters.get('modulation'):
queryset = queryset.filter(
source_objitems__parameter_obj__modulation__in=filters['modulation']
).distinct()
# Фильтр по типу объекта
if filters.get('object_type'):
queryset = queryset.filter(info__in=filters['object_type'])
# Фильтр по количеству ObjItem
objitem_count = filters.get('objitem_count')
if objitem_count == '1':
queryset = queryset.filter(objitem_count=1)
elif objitem_count == '2+':
queryset = queryset.filter(objitem_count__gte=2)
# Фиктивные фильтры (пока не применяются)
# has_plans, success_1, success_2, date_from, date_to
return queryset.distinct()
class KubsatExportView(LoginRequiredMixin, FormView):
"""Экспорт отфильтрованных данных в Excel"""
form_class = KubsatFilterForm
def post(self, request, *args, **kwargs):
# Получаем список ID точек (ObjItem) из POST
objitem_ids = request.POST.getlist('objitem_ids')
if not objitem_ids:
return HttpResponse("Нет данных для экспорта", status=400)
# Получаем ObjItem с их источниками
objitems = ObjItem.objects.filter(id__in=objitem_ids).select_related(
'source',
'source__info',
'parameter_obj__id_satellite',
'parameter_obj__polarization',
'transponder__sat_id',
'geo_obj'
).prefetch_related('geo_obj__mirrors')
# Группируем ObjItem по Source для расчета инкрементального среднего
sources_objitems = {}
for objitem in objitems:
if objitem.source:
if objitem.source.id not in sources_objitems:
sources_objitems[objitem.source.id] = {
'source': objitem.source,
'objitems': []
}
sources_objitems[objitem.source.id]['objitems'].append(objitem)
# Создаем Excel файл
wb = Workbook()
ws = wb.active
ws.title = "Кубсат"
# Заголовки
headers = [
'Дата',
'Широта, град',
'Долгота, град',
'Высота, м',
'Местоположение',
'ИСЗ',
'Прямой канал, МГц',
'Обратный канал, МГц',
'Перенос',
'Получено координат, раз',
'Дата',
'Зеркала',
'СКО, км',
'Примечание',
'Оператор'
]
# Стиль заголовков
for col_num, header in enumerate(headers, 1):
cell = ws.cell(row=1, column=col_num, value=header)
cell.font = Font(bold=True)
cell.alignment = Alignment(horizontal='center', vertical='center')
# Заполняем данные
current_date = datetime.now().strftime('%d.%m.%Y')
operator_name = f"{request.user.first_name} {request.user.last_name}" if request.user.first_name else request.user.username
row_num = 2
for source_id, data in sources_objitems.items():
source = data['source']
objitems_list = data['objitems']
# Рассчитываем инкрементальное среднее координат из оставшихся точек
average_coords = None
for objitem in objitems_list:
if hasattr(objitem, 'geo_obj') and objitem.geo_obj and objitem.geo_obj.coords:
coord = (objitem.geo_obj.coords.x, objitem.geo_obj.coords.y)
if average_coords is None:
# Первая точка
average_coords = coord
else:
# Инкрементальное усреднение
average_coords, _ = calculate_mean_coords(average_coords, coord)
# Если нет координат из geo_obj, берем из source
if average_coords is None:
coords = source.coords_kupsat or source.coords_average or source.coords_valid or source.coords_reference
if coords:
average_coords = (coords.x, coords.y)
latitude = average_coords[1] if average_coords else ''
longitude = average_coords[0] if average_coords else ''
# Получаем местоположение из первого ObjItem с geo_obj
location = ''
for objitem in objitems_list:
if hasattr(objitem, 'geo_obj') and objitem.geo_obj and objitem.geo_obj.location:
location = objitem.geo_obj.location
break
# Получаем данные спутника и частоты
satellite_info = ''
reverse_channel = ''
direct_channel = ''
transfer = ''
for objitem in objitems_list:
if hasattr(objitem, 'parameter_obj') and objitem.parameter_obj:
param = objitem.parameter_obj
if param.id_satellite:
sat_name = param.id_satellite.name
norad = f"({param.id_satellite.norad})" if param.id_satellite.norad else ""
satellite_info = f"{sat_name} {norad}"
if param.frequency:
reverse_channel = param.frequency
if objitem.transponder and objitem.transponder.transfer:
transfer = objitem.transponder.transfer
if param.frequency:
direct_channel = param.frequency + objitem.transponder.transfer
break
objitem_count = len(objitems_list)
# Зеркала
mirrors = []
for objitem in objitems_list:
if hasattr(objitem, 'geo_obj') and objitem.geo_obj:
for mirror in objitem.geo_obj.mirrors.all():
if mirror.name not in mirrors:
mirrors.append(mirror.name)
mirrors_str = '\n'.join(mirrors)
# Записываем строку
ws.cell(row=row_num, column=1, value=current_date)
ws.cell(row=row_num, column=2, value=latitude)
ws.cell(row=row_num, column=3, value=longitude)
ws.cell(row=row_num, column=4, value=0) # Высота всегда 0
ws.cell(row=row_num, column=5, value=location)
ws.cell(row=row_num, column=6, value=satellite_info)
ws.cell(row=row_num, column=7, value=direct_channel)
ws.cell(row=row_num, column=8, value=reverse_channel)
ws.cell(row=row_num, column=9, value=transfer)
ws.cell(row=row_num, column=10, value=objitem_count)
ws.cell(row=row_num, column=11, value='-') # Дата (пока не заполняется)
ws.cell(row=row_num, column=12, value=mirrors_str)
ws.cell(row=row_num, column=13, value='') # СКО не заполняется
ws.cell(row=row_num, column=14, value='') # Примечание не заполняется
ws.cell(row=row_num, column=15, value=operator_name)
row_num += 1
# Автоширина колонок
for column in ws.columns:
max_length = 0
column_letter = column[0].column_letter
for cell in column:
try:
if len(str(cell.value)) > max_length:
max_length = len(str(cell.value))
except:
pass
adjusted_width = min(max_length + 2, 50)
ws.column_dimensions[column_letter].width = adjusted_width
# Сохраняем в BytesIO
output = BytesIO()
wb.save(output)
output.seek(0)
# Возвращаем файл
response = HttpResponse(
output.read(),
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
)
response['Content-Disposition'] = f'attachment; filename="kubsat_{datetime.now().strftime("%Y%m%d_%H%M%S")}.xlsx"'
return response

View File

@@ -8,7 +8,7 @@ from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.gis.geos import Point, Polygon as GEOSPolygon
from django.core.paginator import Paginator
from django.db.models import Count, Q
from django.db.models import Count, Prefetch, Q
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
@@ -234,10 +234,109 @@ class SourceListView(LoginRequiredMixin, View):
objitem_filter_q &= Q(source_objitems__geo_obj__coords__within=polygon_geom)
has_objitem_filter = True
# Build filtered objitems queryset for prefetch
from ..models import ObjItem
filtered_objitems_qs = ObjItem.objects.select_related(
'parameter_obj',
'parameter_obj__id_satellite',
'parameter_obj__polarization',
'parameter_obj__modulation',
'parameter_obj__standard',
'geo_obj',
'lyngsat_source',
'lyngsat_source__id_satellite',
'lyngsat_source__polarization',
'lyngsat_source__modulation',
'lyngsat_source__standard',
'transponder',
'created_by',
'created_by__user',
'updated_by',
'updated_by__user',
).prefetch_related(
'geo_obj__mirrors',
)
# Apply the same filters to prefetch queryset
if search_by_name:
filtered_objitems_qs = filtered_objitems_qs.filter(name__icontains=search_query)
if geo_date_from:
try:
geo_date_from_obj = datetime.strptime(geo_date_from, "%Y-%m-%d")
filtered_objitems_qs = filtered_objitems_qs.filter(geo_obj__timestamp__gte=geo_date_from_obj)
except (ValueError, TypeError):
pass
if geo_date_to:
try:
from datetime import timedelta
geo_date_to_obj = datetime.strptime(geo_date_to, "%Y-%m-%d")
geo_date_to_obj = geo_date_to_obj + timedelta(days=1)
filtered_objitems_qs = filtered_objitems_qs.filter(geo_obj__timestamp__lt=geo_date_to_obj)
except (ValueError, TypeError):
pass
if selected_satellites:
filtered_objitems_qs = filtered_objitems_qs.filter(parameter_obj__id_satellite_id__in=selected_satellites)
if selected_polarizations:
filtered_objitems_qs = filtered_objitems_qs.filter(parameter_obj__polarization_id__in=selected_polarizations)
if selected_modulations:
filtered_objitems_qs = filtered_objitems_qs.filter(parameter_obj__modulation_id__in=selected_modulations)
if freq_min:
try:
freq_min_val = float(freq_min)
filtered_objitems_qs = filtered_objitems_qs.filter(parameter_obj__frequency__gte=freq_min_val)
except (ValueError, TypeError):
pass
if freq_max:
try:
freq_max_val = float(freq_max)
filtered_objitems_qs = filtered_objitems_qs.filter(parameter_obj__frequency__lte=freq_max_val)
except (ValueError, TypeError):
pass
if freq_range_min:
try:
freq_range_min_val = float(freq_range_min)
filtered_objitems_qs = filtered_objitems_qs.filter(parameter_obj__freq_range__gte=freq_range_min_val)
except (ValueError, TypeError):
pass
if freq_range_max:
try:
freq_range_max_val = float(freq_range_max)
filtered_objitems_qs = filtered_objitems_qs.filter(parameter_obj__freq_range__lte=freq_range_max_val)
except (ValueError, TypeError):
pass
if bod_velocity_min:
try:
bod_velocity_min_val = float(bod_velocity_min)
filtered_objitems_qs = filtered_objitems_qs.filter(parameter_obj__bod_velocity__gte=bod_velocity_min_val)
except (ValueError, TypeError):
pass
if bod_velocity_max:
try:
bod_velocity_max_val = float(bod_velocity_max)
filtered_objitems_qs = filtered_objitems_qs.filter(parameter_obj__bod_velocity__lte=bod_velocity_max_val)
except (ValueError, TypeError):
pass
if snr_min:
try:
snr_min_val = float(snr_min)
filtered_objitems_qs = filtered_objitems_qs.filter(parameter_obj__snr__gte=snr_min_val)
except (ValueError, TypeError):
pass
if snr_max:
try:
snr_max_val = float(snr_max)
filtered_objitems_qs = filtered_objitems_qs.filter(parameter_obj__snr__lte=snr_max_val)
except (ValueError, TypeError):
pass
if selected_mirrors:
filtered_objitems_qs = filtered_objitems_qs.filter(geo_obj__mirrors__id__in=selected_mirrors)
if polygon_geom:
filtered_objitems_qs = filtered_objitems_qs.filter(geo_obj__coords__within=polygon_geom)
# Get all Source objects with query optimization
# Using annotate to count ObjItems efficiently (single query with GROUP BY)
# Using select_related for ForeignKey/OneToOne relationships to avoid N+1 queries
# Using prefetch_related for reverse ForeignKey and ManyToMany relationships
# Using Prefetch with filtered queryset to avoid N+1 queries in display loop
sources = Source.objects.select_related(
'info', # ForeignKey to ObjectInfo
'created_by', # ForeignKey to CustomUser
@@ -245,25 +344,8 @@ class SourceListView(LoginRequiredMixin, View):
'updated_by', # ForeignKey to CustomUser
'updated_by__user', # OneToOne to User
).prefetch_related(
# Prefetch related objitems with their nested relationships
'source_objitems',
'source_objitems__parameter_obj',
'source_objitems__parameter_obj__id_satellite',
'source_objitems__parameter_obj__polarization',
'source_objitems__parameter_obj__modulation',
'source_objitems__parameter_obj__standard',
'source_objitems__geo_obj',
'source_objitems__geo_obj__mirrors',
'source_objitems__lyngsat_source',
'source_objitems__lyngsat_source__id_satellite',
'source_objitems__lyngsat_source__polarization',
'source_objitems__lyngsat_source__modulation',
'source_objitems__lyngsat_source__standard',
'source_objitems__transponder',
'source_objitems__created_by',
'source_objitems__created_by__user',
'source_objitems__updated_by',
'source_objitems__updated_by__user',
# Use Prefetch with filtered queryset
Prefetch('source_objitems', queryset=filtered_objitems_qs, to_attr='filtered_objitems'),
# Prefetch marks with their relationships
'marks',
'marks__created_by',
@@ -525,76 +607,8 @@ class SourceListView(LoginRequiredMixin, View):
coords_valid_str = format_coords_display(source.coords_valid)
coords_reference_str = format_coords_display(source.coords_reference)
# Filter objitems for display (to get satellites and lyngsat info)
objitems_to_display = source.source_objitems.all()
# Apply the same filters as in the count annotation
if geo_date_from:
try:
geo_date_from_obj = datetime.strptime(geo_date_from, "%Y-%m-%d")
objitems_to_display = objitems_to_display.filter(geo_obj__timestamp__gte=geo_date_from_obj)
except (ValueError, TypeError):
pass
if geo_date_to:
try:
from datetime import timedelta
geo_date_to_obj = datetime.strptime(geo_date_to, "%Y-%m-%d")
geo_date_to_obj = geo_date_to_obj + timedelta(days=1)
objitems_to_display = objitems_to_display.filter(geo_obj__timestamp__lt=geo_date_to_obj)
except (ValueError, TypeError):
pass
if selected_satellites:
objitems_to_display = objitems_to_display.filter(parameter_obj__id_satellite_id__in=selected_satellites)
if selected_polarizations:
objitems_to_display = objitems_to_display.filter(parameter_obj__polarization_id__in=selected_polarizations)
if selected_modulations:
objitems_to_display = objitems_to_display.filter(parameter_obj__modulation_id__in=selected_modulations)
if freq_min:
try:
objitems_to_display = objitems_to_display.filter(parameter_obj__frequency__gte=float(freq_min))
except (ValueError, TypeError):
pass
if freq_max:
try:
objitems_to_display = objitems_to_display.filter(parameter_obj__frequency__lte=float(freq_max))
except (ValueError, TypeError):
pass
if freq_range_min:
try:
objitems_to_display = objitems_to_display.filter(parameter_obj__freq_range__gte=float(freq_range_min))
except (ValueError, TypeError):
pass
if freq_range_max:
try:
objitems_to_display = objitems_to_display.filter(parameter_obj__freq_range__lte=float(freq_range_max))
except (ValueError, TypeError):
pass
if bod_velocity_min:
try:
objitems_to_display = objitems_to_display.filter(parameter_obj__bod_velocity__gte=float(bod_velocity_min))
except (ValueError, TypeError):
pass
if bod_velocity_max:
try:
objitems_to_display = objitems_to_display.filter(parameter_obj__bod_velocity__lte=float(bod_velocity_max))
except (ValueError, TypeError):
pass
if snr_min:
try:
objitems_to_display = objitems_to_display.filter(parameter_obj__snr__gte=float(snr_min))
except (ValueError, TypeError):
pass
if snr_max:
try:
objitems_to_display = objitems_to_display.filter(parameter_obj__snr__lte=float(snr_max))
except (ValueError, TypeError):
pass
if selected_mirrors:
objitems_to_display = objitems_to_display.filter(geo_obj__mirrors__id__in=selected_mirrors)
if search_by_name:
objitems_to_display = objitems_to_display.filter(name__icontains=search_query)
if polygon_geom:
objitems_to_display = objitems_to_display.filter(geo_obj__coords__within=polygon_geom)
# Use pre-filtered objitems from Prefetch
objitems_to_display = source.filtered_objitems
# Use annotated count (consistent with filtering)
objitem_count = source.objitem_count