diff --git a/dbapp/mainapp/urls.py b/dbapp/mainapp/urls.py
index 750caff..0808daf 100644
--- a/dbapp/mainapp/urls.py
+++ b/dbapp/mainapp/urls.py
@@ -22,6 +22,7 @@ urlpatterns = [
path('vch-link/', views.LinkVchSigmaView.as_view(), name='link_vch_sigma'),
path('link-lyngsat/', views.LinkLyngsatSourcesView.as_view(), name='link_lyngsat'),
path('api/lyngsat//', views.LyngsatDataAPIView.as_view(), name='lyngsat_data_api'),
+ path('api/sigma-parameter//', views.SigmaParameterDataAPIView.as_view(), name='sigma_parameter_data_api'),
path('kubsat-excel/', views.ProcessKubsatView.as_view(), name='kubsat_excel'),
path('object/create/', views.ObjItemCreateView.as_view(), name='objitem_create'),
path('object//edit/', views.ObjItemUpdateView.as_view(), name='objitem_update'),
diff --git a/dbapp/mainapp/utils.py b/dbapp/mainapp/utils.py
index f815cb8..2942aef 100644
--- a/dbapp/mainapp/utils.py
+++ b/dbapp/mainapp/utils.py
@@ -447,27 +447,96 @@ def get_vch_load_from_html(file, sat: Satellite) -> None:
sigma_load.save()
+def get_frequency_tolerance_percent(freq_range_mhz: float) -> float:
+ """
+ Определяет процент погрешности центральной частоты в зависимости от полосы частот.
+
+ Args:
+ freq_range_mhz (float): Полоса частот в МГц
+
+ Returns:
+ float: Процент погрешности для центральной частоты
+
+ Диапазоны:
+ - 0 - 0.5 МГц (0 - 500 кГц): 0.1%
+ - 0.5 - 1.5 МГц (500 кГц - 1.5 МГц): 0.5%
+ - 1.5 - 5 МГц: 1%
+ - 5 - 10 МГц: 2%
+ - > 10 МГц: 5%
+ """
+ if freq_range_mhz < 0.5:
+ return 0.005
+ elif freq_range_mhz < 1.5:
+ return 0.01
+ elif freq_range_mhz < 5.0:
+ return 0.02
+ elif freq_range_mhz < 10.0:
+ return 0.05
+ else:
+ return 0.1
+
+
def compare_and_link_vch_load(
sat_id: Satellite, eps_freq: float, eps_frange: float, ku_range: float
):
- item_obj = ObjItem.objects.filter(parameters_obj__id_satellite=sat_id)
- vch_sigma = SigmaParameter.objects.filter(id_satellite=sat_id)
+ """
+ Привязывает SigmaParameter к Parameter на основе совпадения параметров.
+
+ Погрешность центральной частоты определяется автоматически в зависимости от полосы частот:
+ - 0-500 кГц: 0.1%
+ - 500 кГц-1.5 МГц: 0.5%
+ - 1.5-5 МГц: 1%
+ - 5-10 МГц: 2%
+ - >10 МГц: 5%
+
+ Args:
+ sat_id (Satellite): Спутник для фильтрации
+ eps_freq (float): Не используется (оставлен для обратной совместимости)
+ eps_frange (float): Погрешность полосы частот в процентах
+ ku_range (float): Не используется (оставлен для обратной совместимости)
+
+ Returns:
+ tuple: (количество объектов, количество привязок)
+ """
+ # Получаем все ObjItem с Parameter для данного спутника
+ item_obj = ObjItem.objects.filter(
+ parameter_obj__id_satellite=sat_id
+ ).select_related('parameter_obj', 'parameter_obj__polarization')
+
+ vch_sigma = SigmaParameter.objects.filter(
+ id_satellite=sat_id
+ ).select_related('polarization')
+
link_count = 0
- obj_count = len(item_obj)
- for idx, obj in enumerate(item_obj):
- vch_load = obj.parameters_obj.get()
- if vch_load.frequency == -1.0:
+ obj_count = item_obj.count()
+
+ for obj in item_obj:
+ vch_load = obj.parameter_obj
+
+ # Пропускаем объекты с некорректной частотой
+ if not vch_load or vch_load.frequency == -1.0:
continue
+
+ # Определяем погрешность частоты на основе полосы
+ freq_tolerance_percent = get_frequency_tolerance_percent(vch_load.freq_range)
+
+ # Вычисляем допустимое отклонение частоты в МГц
+ freq_tolerance_mhz = vch_load.frequency * freq_tolerance_percent / 100
+
+ # Вычисляем допустимое отклонение полосы в МГц
+ frange_tolerance_mhz = vch_load.freq_range * eps_frange / 100
+
for sigma in vch_sigma:
- if (
- abs(sigma.transfer_frequency - vch_load.frequency) <= eps_freq
- and abs(sigma.freq_range - vch_load.freq_range)
- <= vch_load.freq_range * eps_frange / 100
- and sigma.polarization == vch_load.polarization
- ):
+ # Проверяем совпадение по всем параметрам
+ freq_match = abs(sigma.transfer_frequency - vch_load.frequency) <= freq_tolerance_mhz
+ frange_match = abs(sigma.freq_range - vch_load.freq_range) <= frange_tolerance_mhz
+ pol_match = sigma.polarization == vch_load.polarization
+
+ if freq_match and frange_match and pol_match:
sigma.parameter = vch_load
sigma.save()
link_count += 1
+
return obj_count, link_count
diff --git a/dbapp/mainapp/views.py b/dbapp/mainapp/views.py
index 7e0e599..3b31bea 100644
--- a/dbapp/mainapp/views.py
+++ b/dbapp/mainapp/views.py
@@ -362,12 +362,13 @@ class LinkVchSigmaView(LoginRequiredMixin, FormView):
form_class = VchLinkForm
def form_valid(self, form):
- freq = form.cleaned_data["value1"]
+ # value1 больше не используется - погрешность частоты определяется автоматически
freq_range = form.cleaned_data["value2"]
- # ku_range = float(form.cleaned_data['ku_range'])
sat_id = form.cleaned_data["sat_choice"]
- # print(freq, freq_range, ku_range, sat_id.pk)
- count_all, link_count = compare_and_link_vch_load(sat_id, freq, freq_range, 1)
+
+ # Передаём 0 для eps_freq и ku_range, так как они не используются
+ count_all, link_count = compare_and_link_vch_load(sat_id, 0, freq_range, 0)
+
messages.success(
self.request, f"Привязано {link_count} из {count_all} объектов"
)
@@ -479,6 +480,84 @@ class LyngsatDataAPIView(LoginRequiredMixin, View):
return JsonResponse({'error': str(e)}, status=500)
+class SigmaParameterDataAPIView(LoginRequiredMixin, View):
+ """API для получения данных SigmaParameter"""
+
+ def get(self, request, parameter_id):
+ from .models import Parameter
+
+ try:
+ parameter = Parameter.objects.select_related(
+ 'id_satellite',
+ 'polarization',
+ 'modulation',
+ 'standard'
+ ).prefetch_related(
+ 'sigma_parameter__mark',
+ 'sigma_parameter__id_satellite',
+ 'sigma_parameter__polarization',
+ 'sigma_parameter__modulation',
+ 'sigma_parameter__standard'
+ ).get(id=parameter_id)
+
+ # Получаем все связанные SigmaParameter
+ sigma_params = parameter.sigma_parameter.all()
+
+ sigma_data = []
+ for sigma in sigma_params:
+ # Получаем отметки
+ marks = []
+ for mark in sigma.mark.all().order_by('-timestamp'):
+ mark_str = '+' if mark.mark else '-'
+ date_str = '-'
+ if mark.timestamp:
+ local_time = timezone.localtime(mark.timestamp)
+ date_str = local_time.strftime("%d.%m.%Y %H:%M")
+ marks.append({
+ 'mark': mark_str,
+ 'date': date_str
+ })
+
+ # Форматируем даты начала и окончания
+ datetime_begin_str = '-'
+ if sigma.datetime_begin:
+ local_time = timezone.localtime(sigma.datetime_begin)
+ datetime_begin_str = local_time.strftime("%d.%m.%Y %H:%M")
+
+ datetime_end_str = '-'
+ if sigma.datetime_end:
+ local_time = timezone.localtime(sigma.datetime_end)
+ datetime_end_str = local_time.strftime("%d.%m.%Y %H:%M")
+
+ sigma_data.append({
+ 'id': sigma.id,
+ 'satellite': sigma.id_satellite.name if sigma.id_satellite else '-',
+ 'frequency': f"{sigma.frequency:.3f}" if sigma.frequency else '-',
+ 'transfer_frequency': f"{sigma.transfer_frequency:.3f}" if sigma.transfer_frequency else '-',
+ 'freq_range': f"{sigma.freq_range:.3f}" if sigma.freq_range else '-',
+ 'polarization': sigma.polarization.name if sigma.polarization else '-',
+ 'modulation': sigma.modulation.name if sigma.modulation else '-',
+ 'standard': sigma.standard.name if sigma.standard else '-',
+ 'bod_velocity': f"{sigma.bod_velocity:.0f}" if sigma.bod_velocity else '-',
+ 'snr': f"{sigma.snr:.1f}" if sigma.snr is not None else '-',
+ 'power': f"{sigma.power:.1f}" if sigma.power is not None else '-',
+ 'status': sigma.status or '-',
+ 'packets': 'Да' if sigma.packets else 'Нет' if sigma.packets is not None else '-',
+ 'datetime_begin': datetime_begin_str,
+ 'datetime_end': datetime_end_str,
+ 'marks': marks
+ })
+
+ return JsonResponse({
+ 'parameter_id': parameter.id,
+ 'sigma_parameters': sigma_data
+ })
+ except Parameter.DoesNotExist:
+ return JsonResponse({'error': 'Parameter не найден'}, status=404)
+ except Exception as e:
+ return JsonResponse({'error': str(e)}, status=500)
+
+
class ProcessKubsatView(LoginRequiredMixin, FormMessageMixin, FormView):
template_name = "mainapp/process_kubsat.html"
form_class = NewEventForm
@@ -585,6 +664,10 @@ class ObjItemListView(LoginRequiredMixin, View):
"parameter_obj__modulation",
"parameter_obj__standard",
)
+ .prefetch_related(
+ "parameter_obj__sigma_parameter",
+ "parameter_obj__sigma_parameter__polarization",
+ )
.filter(parameter_obj__id_satellite_id__in=selected_satellites)
)
else:
@@ -598,6 +681,9 @@ class ObjItemListView(LoginRequiredMixin, View):
"parameter_obj__polarization",
"parameter_obj__modulation",
"parameter_obj__standard",
+ ).prefetch_related(
+ "parameter_obj__sigma_parameter",
+ "parameter_obj__sigma_parameter__polarization",
)
if freq_min is not None and freq_min.strip() != "":
@@ -872,6 +958,23 @@ class ObjItemListView(LoginRequiredMixin, View):
# Check if LyngSat source is linked
source_type = "ТВ" if obj.lyngsat_source else "-"
+ # Check if SigmaParameter is linked
+ has_sigma = False
+ sigma_info = "-"
+ if param:
+ sigma_count = param.sigma_parameter.count()
+ if sigma_count > 0:
+ has_sigma = True
+ # Get first sigma parameter for preview
+ first_sigma = param.sigma_parameter.first()
+ if first_sigma:
+ sigma_freq = f"{first_sigma.frequency:.3f}" if first_sigma.frequency else "-"
+ sigma_range = f"{first_sigma.freq_range:.3f}" if first_sigma.freq_range else "-"
+ sigma_pol = first_sigma.polarization.name if first_sigma.polarization else "-"
+ # Сокращаем поляризацию
+ sigma_pol_short = sigma_pol[0] if sigma_pol and sigma_pol != "-" else "-"
+ sigma_info = f"{sigma_freq}/{sigma_range}/{sigma_pol_short}"
+
processed_objects.append(
{
"id": obj.id,
@@ -896,6 +999,8 @@ class ObjItemListView(LoginRequiredMixin, View):
"is_average": is_average,
"source_type": source_type,
"standard": standard_name,
+ "has_sigma": has_sigma,
+ "sigma_info": sigma_info,
"obj": obj,
}
)
diff --git a/dbapp/mapsapp/views.py b/dbapp/mapsapp/views.py
index 3ba973d..6d12a87 100644
--- a/dbapp/mapsapp/views.py
+++ b/dbapp/mapsapp/views.py
@@ -22,31 +22,38 @@ from .utils import get_band_names
class CesiumMapView(LoginRequiredMixin, TemplateView):
"""
Представление для отображения 3D карты с использованием Cesium.
-
+
Отображает спутники и их зоны покрытия на интерактивной 3D карте.
"""
- template_name = 'mapsapp/map3d.html'
+
+ template_name = "mapsapp/map3d.html"
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(**kwargs)
# Оптимизированный запрос - загружаем только необходимые поля
- context['sats'] = Satellite.objects.filter(
- parameters__objitems__isnull=False
- ).distinct().only('id', 'name').order_by('name')
+ # Фильтруем спутники, у которых есть параметры с привязанными объектами
+ context["sats"] = (
+ Satellite.objects.filter(parameters__objitem__isnull=False)
+ .distinct()
+ .only("id", "name")
+ .order_by("name")
+ )
return context
+
class GetFootprintsView(LoginRequiredMixin, View):
"""
API для получения зон покрытия (footprints) спутника.
-
+
Возвращает список названий зон покрытия для указанного спутника.
"""
+
def get(self, request, sat_id):
try:
# Оптимизированный запрос - загружаем только поле name
- sat_name = Satellite.objects.only('name').get(id=sat_id).name
+ sat_name = Satellite.objects.only("name").get(id=sat_id).name
footprint_names = get_band_names(sat_name)
-
+
return JsonResponse(footprint_names, safe=False)
except Satellite.DoesNotExist:
return JsonResponse({"error": "Спутник не найден"}, status=404)
@@ -57,14 +64,15 @@ class GetFootprintsView(LoginRequiredMixin, View):
class TileProxyView(View):
"""
Прокси для загрузки тайлов карты покрытия спутников.
-
+
Кэширует тайлы на 7 дней для улучшения производительности.
"""
+
# Константы
TILE_BASE_URL = "https://static.satbeams.com/tiles"
CACHE_DURATION = 60 * 60 * 24 * 7 # 7 дней
REQUEST_TIMEOUT = 10 # секунд
-
+
@method_decorator(require_GET)
@method_decorator(cache_page(CACHE_DURATION))
def dispatch(self, *args, **kwargs):
@@ -72,7 +80,7 @@ class TileProxyView(View):
def get(self, request, footprint_name, z, x, y):
# Валидация имени footprint
- if not footprint_name.replace('-', '').replace('_', '').isalnum():
+ if not footprint_name.replace("-", "").replace("_", "").isalnum():
return HttpResponse("Invalid footprint name", status=400)
url = f"{self.TILE_BASE_URL}/{footprint_name}/{z}/{x}/{y}.png"
@@ -80,7 +88,7 @@ class TileProxyView(View):
try:
resp = requests.get(url, timeout=self.REQUEST_TIMEOUT)
if resp.status_code == 200:
- response = HttpResponse(resp.content, content_type='image/png')
+ response = HttpResponse(resp.content, content_type="image/png")
response["Access-Control-Allow-Origin"] = "*"
response["Cache-Control"] = f"public, max-age={self.CACHE_DURATION}"
return response
@@ -90,27 +98,38 @@ class TileProxyView(View):
return HttpResponse("Request timeout", status=504)
except requests.RequestException as e:
return HttpResponse(f"Proxy error: {e}", status=500)
-
+
+
class LeafletMapView(LoginRequiredMixin, TemplateView):
"""
Представление для отображения 2D карты с использованием Leaflet.
-
+
Отображает спутники и транспондеры на интерактивной 2D карте.
"""
- template_name = 'mapsapp/map2d.html'
+
+ template_name = "mapsapp/map2d.html"
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(**kwargs)
# Оптимизированные запросы - загружаем только необходимые поля
- context['sats'] = Satellite.objects.filter(
- parameters__objitems__isnull=False
- ).distinct().only('id', 'name').order_by('name')
-
- context['trans'] = Transponders.objects.select_related(
- 'sat_id', 'polarization'
+ # Фильтруем спутники, у которых есть параметры с привязанными объектами
+ context["sats"] = (
+ Satellite.objects.filter(parameters__objitem__isnull=False)
+ .distinct()
+ .only("id", "name")
+ .order_by("name")
+ )
+
+ context["trans"] = Transponders.objects.select_related(
+ "sat_id", "polarization"
).only(
- 'id', 'name', 'sat_id__name', 'polarization__name',
- 'downlink', 'frequency_range', 'zone_name'
+ "id",
+ "name",
+ "sat_id__name",
+ "polarization__name",
+ "downlink",
+ "frequency_range",
+ "zone_name",
)
return context
@@ -118,20 +137,22 @@ class LeafletMapView(LoginRequiredMixin, TemplateView):
class GetTransponderOnSatIdView(LoginRequiredMixin, View):
"""
API для получения транспондеров спутника.
-
+
Возвращает список транспондеров для указанного спутника с оптимизированными запросами.
"""
+
def get(self, request, sat_id):
# Оптимизированный запрос с select_related и only
- trans = Transponders.objects.filter(
- sat_id=sat_id
- ).select_related('polarization').only(
- 'name', 'downlink', 'frequency_range',
- 'zone_name', 'polarization__name'
+ trans = (
+ Transponders.objects.filter(sat_id=sat_id)
+ .select_related("polarization")
+ .only(
+ "name", "downlink", "frequency_range", "zone_name", "polarization__name"
+ )
)
-
+
if not trans.exists():
- return JsonResponse({'error': 'Объектов не найдено'}, status=404)
+ return JsonResponse({"error": "Объектов не найдено"}, status=404)
# Используем list comprehension для лучшей производительности
output = [
@@ -140,9 +161,9 @@ class GetTransponderOnSatIdView(LoginRequiredMixin, View):
"frequency": tran.downlink,
"frequency_range": tran.frequency_range,
"zone_name": tran.zone_name,
- "polarization": tran.polarization.name if tran.polarization else "-"
+ "polarization": tran.polarization.name if tran.polarization else "-",
}
for tran in trans
]
- return JsonResponse(output, safe=False)
\ No newline at end of file
+ return JsonResponse(output, safe=False)