1305 lines
46 KiB
Python
1305 lines
46 KiB
Python
# Django imports
|
||
from django.contrib.auth.models import User
|
||
from django.contrib.gis.db import models as gis
|
||
from django.contrib.gis.db.models import functions
|
||
from django.core.exceptions import ValidationError
|
||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||
from django.db import models
|
||
from django.db.models import ExpressionWrapper, F
|
||
from django.utils import timezone
|
||
|
||
|
||
def get_default_polarization():
|
||
obj, created = Polarization.objects.get_or_create(name="-")
|
||
return obj.id
|
||
|
||
|
||
def get_default_modulation():
|
||
obj, created = Modulation.objects.get_or_create(name="-")
|
||
return obj.id
|
||
|
||
|
||
def get_default_standard():
|
||
obj, created = Standard.objects.get_or_create(name="-")
|
||
return obj.id
|
||
|
||
|
||
class CustomUser(models.Model):
|
||
"""
|
||
Расширенная модель пользователя с ролями.
|
||
|
||
Добавляет систему ролей к стандартной модели User Django.
|
||
"""
|
||
|
||
ROLE_CHOICES = [
|
||
("admin", "Администратор"),
|
||
("moderator", "Модератор"),
|
||
("user", "Пользователь"),
|
||
]
|
||
|
||
# Связи
|
||
user = models.OneToOneField(
|
||
User,
|
||
on_delete=models.CASCADE,
|
||
verbose_name="Пользователь",
|
||
help_text="Связанный пользователь Django",
|
||
)
|
||
|
||
# Основные поля
|
||
role = models.CharField(
|
||
max_length=20,
|
||
choices=ROLE_CHOICES,
|
||
default="user",
|
||
verbose_name="Роль пользователя",
|
||
db_index=True,
|
||
help_text="Роль пользователя в системе",
|
||
)
|
||
|
||
def __str__(self):
|
||
return (
|
||
f"{self.user.first_name} {self.user.last_name}"
|
||
if self.user.first_name and self.user.last_name
|
||
else self.user.username
|
||
)
|
||
|
||
class Meta:
|
||
verbose_name = "Пользователь"
|
||
verbose_name_plural = "Пользователи"
|
||
ordering = ["user__username"]
|
||
|
||
class ObjectInfo(models.Model):
|
||
name = models.CharField(
|
||
max_length=255,
|
||
unique=True,
|
||
verbose_name="Тип объекта",
|
||
help_text="Информация о типе объекта",
|
||
)
|
||
|
||
def __str__(self):
|
||
return self.name
|
||
|
||
class Meta:
|
||
verbose_name = "Тип объекта"
|
||
verbose_name_plural = "Типы объектов"
|
||
ordering = ["name"]
|
||
|
||
|
||
class ObjectOwnership(models.Model):
|
||
"""
|
||
Модель принадлежности объекта.
|
||
"""
|
||
name = models.CharField(
|
||
max_length=255,
|
||
unique=True,
|
||
verbose_name="Принадлежность",
|
||
help_text="Принадлежность объекта",
|
||
)
|
||
|
||
def __str__(self):
|
||
return self.name
|
||
|
||
class Meta:
|
||
verbose_name = "Принадлежность объекта"
|
||
verbose_name_plural = "Принадлежности объектов"
|
||
ordering = ["name"]
|
||
|
||
|
||
class ObjectMark(models.Model):
|
||
"""
|
||
Модель отметки о наличии объекта.
|
||
|
||
Используется для фиксации моментов времени когда объект был обнаружен или отсутствовал.
|
||
"""
|
||
|
||
# Основные поля
|
||
mark = models.BooleanField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Наличие объекта",
|
||
help_text="True - объект обнаружен, False - объект отсутствует",
|
||
)
|
||
timestamp = models.DateTimeField(
|
||
auto_now_add=True,
|
||
verbose_name="Время",
|
||
db_index=True,
|
||
help_text="Время фиксации отметки",
|
||
)
|
||
source = models.ForeignKey(
|
||
'Source',
|
||
on_delete=models.CASCADE,
|
||
related_name="marks",
|
||
verbose_name="Источник",
|
||
help_text="Связанный источник",
|
||
)
|
||
created_by = models.ForeignKey(
|
||
CustomUser,
|
||
on_delete=models.SET_NULL,
|
||
related_name="marks_created",
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Создан пользователем",
|
||
help_text="Пользователь, создавший отметку",
|
||
)
|
||
|
||
def can_edit(self):
|
||
"""Проверка возможности редактирования отметки (в течение 5 минут)"""
|
||
from datetime import timedelta
|
||
if not self.timestamp:
|
||
return False
|
||
time_diff = timezone.now() - self.timestamp
|
||
return time_diff < timedelta(minutes=5)
|
||
|
||
def can_add_new_mark_for_object(self):
|
||
"""Проверка возможности добавления новой отметки для объекта (прошло 5 минут с последней)"""
|
||
from datetime import timedelta
|
||
if not self.timestamp:
|
||
return True
|
||
time_diff = timezone.now() - self.timestamp
|
||
return time_diff >= timedelta(minutes=5)
|
||
|
||
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"]
|
||
|
||
|
||
# Для обратной совместимости с SigmaParameter
|
||
# 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="Время фиксации отметки",
|
||
# )
|
||
|
||
# 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 Polarization(models.Model):
|
||
"""
|
||
Модель поляризации сигнала.
|
||
|
||
Определяет тип поляризации спутникового сигнала (H, V, L, R и т.д.).
|
||
"""
|
||
|
||
# Основные поля
|
||
name = models.CharField(
|
||
max_length=20,
|
||
unique=True,
|
||
verbose_name="Поляризация",
|
||
db_index=True,
|
||
help_text="Тип поляризации (H - горизонтальная, V - вертикальная, L - левая круговая, R - правая круговая)",
|
||
)
|
||
|
||
def __str__(self):
|
||
return self.name
|
||
|
||
class Meta:
|
||
verbose_name = "Поляризация"
|
||
verbose_name_plural = "Поляризация"
|
||
ordering = ["name"]
|
||
|
||
|
||
class Modulation(models.Model):
|
||
"""
|
||
Модель типа модуляции сигнала.
|
||
|
||
Определяет схему модуляции (QPSK, 8PSK, 16APSK и т.д.).
|
||
"""
|
||
|
||
# Основные поля
|
||
name = models.CharField(
|
||
max_length=20,
|
||
unique=True,
|
||
verbose_name="Модуляция",
|
||
db_index=True,
|
||
help_text="Тип модуляции сигнала (QPSK, 8PSK, 16APSK и т.д.)",
|
||
)
|
||
|
||
def __str__(self):
|
||
return self.name
|
||
|
||
class Meta:
|
||
verbose_name = "Модуляция"
|
||
verbose_name_plural = "Модуляции"
|
||
ordering = ["name"]
|
||
|
||
|
||
class Standard(models.Model):
|
||
"""
|
||
Модель стандарта передачи данных.
|
||
|
||
Определяет стандарт передачи (DVB-S, DVB-S2, DVB-S2X и т.д.).
|
||
"""
|
||
|
||
# Основные поля
|
||
name = models.CharField(
|
||
max_length=80,
|
||
unique=True,
|
||
verbose_name="Стандарт",
|
||
db_index=True,
|
||
help_text="Стандарт передачи данных (DVB-S, DVB-S2, DVB-S2X и т.д.)",
|
||
)
|
||
|
||
def __str__(self):
|
||
return self.name
|
||
|
||
class Meta:
|
||
verbose_name = "Стандарт"
|
||
verbose_name_plural = "Стандарты"
|
||
ordering = ["name"]
|
||
|
||
|
||
class Band(models.Model):
|
||
name = models.CharField(
|
||
max_length=50,
|
||
unique=True,
|
||
verbose_name="Название",
|
||
help_text="Название диапазона",
|
||
)
|
||
border_start = models.FloatField(
|
||
blank=True, null=True, verbose_name="Нижняя граница диапазона, МГц"
|
||
)
|
||
border_end = models.FloatField(
|
||
blank=True, null=True, verbose_name="Верхняя граница диапазона, МГц"
|
||
)
|
||
|
||
def __str__(self):
|
||
return self.name
|
||
|
||
class Meta:
|
||
verbose_name = "Диапазон"
|
||
verbose_name_plural = "Диапазоны"
|
||
ordering = ["name"]
|
||
|
||
|
||
class Satellite(models.Model):
|
||
"""
|
||
Модель спутника.
|
||
|
||
Представляет спутник связи с его основными характеристиками.
|
||
"""
|
||
# PLACES = [
|
||
# ("kr", "КР"),
|
||
# ("dv", "ДВ")
|
||
# ]
|
||
# Основные поля
|
||
name = models.CharField(
|
||
max_length=100,
|
||
unique=True,
|
||
verbose_name="Имя спутника",
|
||
db_index=True,
|
||
help_text="Название спутника",
|
||
)
|
||
alternative_name = models.CharField(
|
||
max_length=100,
|
||
blank=True,
|
||
null=True,
|
||
verbose_name="Альтернативное имя",
|
||
db_index=True,
|
||
help_text="Альтернативное название спутника",
|
||
)
|
||
# location_place = models.CharField(
|
||
# max_length=30,
|
||
# choices=PLACES,
|
||
# null=True,
|
||
# default="kr",
|
||
# verbose_name="Комплекс",
|
||
# help_text="К какому комплексу принадлежит спутник",
|
||
# )
|
||
norad = models.IntegerField(
|
||
blank=True,
|
||
null=True,
|
||
verbose_name="NORAD ID",
|
||
help_text="Идентификатор NORAD для отслеживания спутника",
|
||
)
|
||
international_code = models.CharField(
|
||
max_length=50,
|
||
blank=True,
|
||
null=True,
|
||
verbose_name="Международный код",
|
||
help_text="Международный идентификатор спутника (например, 2011-074A)",
|
||
)
|
||
band = models.ManyToManyField(
|
||
Band,
|
||
related_name="bands",
|
||
verbose_name="Диапазоны",
|
||
blank=True,
|
||
help_text="Диапазоны работы спутника",
|
||
)
|
||
undersat_point = models.FloatField(
|
||
blank=True,
|
||
null=True,
|
||
verbose_name="Подспутниковая точка, градусы",
|
||
help_text="Подспутниковая точка в градусах. Восточное полушарие с +, западное с -",
|
||
)
|
||
url = models.URLField(
|
||
blank=True,
|
||
null=True,
|
||
verbose_name="Ссылка на источник",
|
||
help_text="Ссылка на сайт, где можно проверить информацию",
|
||
)
|
||
comment = models.TextField(
|
||
blank=True,
|
||
null=True,
|
||
verbose_name="Комментарий",
|
||
help_text="Любой возможный комменатрий",
|
||
)
|
||
launch_date = models.DateField(
|
||
blank=True,
|
||
null=True,
|
||
verbose_name="Дата запуска",
|
||
help_text="Дата запуска спутника",
|
||
)
|
||
|
||
created_at = models.DateTimeField(
|
||
auto_now_add=True,
|
||
verbose_name="Дата создания",
|
||
help_text="Дата и время создания записи",
|
||
)
|
||
created_by = models.ForeignKey(
|
||
CustomUser,
|
||
on_delete=models.SET_NULL,
|
||
related_name="satellite_created",
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Создан пользователем",
|
||
help_text="Пользователь, создавший запись",
|
||
)
|
||
updated_at = models.DateTimeField(
|
||
auto_now=True,
|
||
verbose_name="Дата последнего изменения",
|
||
help_text="Дата и время последнего изменения",
|
||
)
|
||
updated_by = models.ForeignKey(
|
||
CustomUser,
|
||
on_delete=models.SET_NULL,
|
||
related_name="satellite_updated",
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Изменен пользователем",
|
||
help_text="Пользователь, последним изменивший запись",
|
||
)
|
||
|
||
def __str__(self):
|
||
return self.name
|
||
|
||
class Meta:
|
||
verbose_name = "Спутник"
|
||
verbose_name_plural = "Спутники"
|
||
ordering = ["name"]
|
||
|
||
|
||
class ObjItemQuerySet(models.QuerySet):
|
||
"""Custom QuerySet для модели ObjItem с оптимизированными запросами"""
|
||
|
||
def with_related(self):
|
||
"""Оптимизирует запросы, загружая связанные объекты"""
|
||
return self.select_related(
|
||
"geo_obj",
|
||
"updated_by__user",
|
||
"created_by__user",
|
||
"lyngsat_source",
|
||
"parameter_obj",
|
||
"parameter_obj__id_satellite",
|
||
"parameter_obj__polarization",
|
||
"parameter_obj__modulation",
|
||
"parameter_obj__standard",
|
||
)
|
||
|
||
def recent(self, days=30):
|
||
"""Возвращает объекты, созданные за последние N дней"""
|
||
from datetime import timedelta
|
||
|
||
cutoff_date = timezone.now() - timedelta(days=days)
|
||
return self.filter(created_at__gte=cutoff_date)
|
||
|
||
def by_user(self, user):
|
||
"""Возвращает объекты, созданные указанным пользователем"""
|
||
return self.filter(created_by=user)
|
||
|
||
|
||
class ObjItemManager(models.Manager):
|
||
"""Custom Manager для модели ObjItem"""
|
||
|
||
def get_queryset(self):
|
||
return ObjItemQuerySet(self.model, using=self._db)
|
||
|
||
def with_related(self):
|
||
"""Возвращает queryset с предзагруженными связанными объектами"""
|
||
return self.get_queryset().with_related()
|
||
|
||
def recent(self, days=30):
|
||
"""Возвращает недавно созданные объекты"""
|
||
return self.get_queryset().recent(days)
|
||
|
||
def by_user(self, user):
|
||
"""Возвращает объекты пользователя"""
|
||
return self.get_queryset().by_user(user)
|
||
|
||
|
||
class TechAnalyze(models.Model):
|
||
"""
|
||
Модель технического анализа сигнала.
|
||
|
||
Хранит информацию о технических параметрах сигнала для анализа.
|
||
"""
|
||
|
||
# Основные поля
|
||
name = models.CharField(
|
||
max_length=255,
|
||
unique=True,
|
||
verbose_name="Имя",
|
||
db_index=True,
|
||
help_text="Уникальное название для технического анализа",
|
||
)
|
||
satellite = models.ForeignKey(
|
||
Satellite,
|
||
on_delete=models.PROTECT,
|
||
related_name="tech_analyzes",
|
||
verbose_name="Спутник",
|
||
help_text="Спутник, к которому относится анализ",
|
||
)
|
||
polarization = models.ForeignKey(
|
||
Polarization,
|
||
default=get_default_polarization,
|
||
on_delete=models.SET_DEFAULT,
|
||
related_name="tech_analyze_polarizations",
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Поляризация",
|
||
)
|
||
frequency = models.FloatField(
|
||
default=0,
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Частота, МГц",
|
||
db_index=True,
|
||
help_text="Центральная частота сигнала",
|
||
)
|
||
freq_range = models.FloatField(
|
||
default=0,
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Полоса частот, МГц",
|
||
help_text="Полоса частот сигнала",
|
||
)
|
||
bod_velocity = models.FloatField(
|
||
default=0,
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Символьная скорость, БОД",
|
||
help_text="Символьная скорость",
|
||
)
|
||
modulation = models.ForeignKey(
|
||
Modulation,
|
||
default=get_default_modulation,
|
||
on_delete=models.SET_DEFAULT,
|
||
related_name="tech_analyze_modulations",
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Модуляция",
|
||
)
|
||
standard = models.ForeignKey(
|
||
Standard,
|
||
default=get_default_standard,
|
||
on_delete=models.SET_DEFAULT,
|
||
related_name="tech_analyze_standards",
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Стандарт",
|
||
)
|
||
note = models.TextField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Примечание",
|
||
help_text="Дополнительные примечания",
|
||
)
|
||
|
||
# Метаданные
|
||
created_at = models.DateTimeField(
|
||
auto_now_add=True,
|
||
verbose_name="Дата создания",
|
||
help_text="Дата и время создания записи",
|
||
)
|
||
created_by = models.ForeignKey(
|
||
CustomUser,
|
||
on_delete=models.SET_NULL,
|
||
related_name="tech_analyze_created",
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Создан пользователем",
|
||
help_text="Пользователь, создавший запись",
|
||
)
|
||
updated_at = models.DateTimeField(
|
||
auto_now=True,
|
||
verbose_name="Дата последнего изменения",
|
||
help_text="Дата и время последнего изменения",
|
||
)
|
||
updated_by = models.ForeignKey(
|
||
CustomUser,
|
||
on_delete=models.SET_NULL,
|
||
related_name="tech_analyze_updated",
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Изменен пользователем",
|
||
help_text="Пользователь, последним изменивший запись",
|
||
)
|
||
|
||
def __str__(self):
|
||
return f"{self.name} ({self.satellite.name if self.satellite else '-'})"
|
||
|
||
class Meta:
|
||
verbose_name = "Тех. анализ"
|
||
verbose_name_plural = "Тех. анализы"
|
||
ordering = ["-created_at"]
|
||
|
||
|
||
class Source(models.Model):
|
||
"""
|
||
Модель источника сигнала.
|
||
"""
|
||
|
||
info = models.ForeignKey(
|
||
ObjectInfo,
|
||
on_delete=models.SET_NULL,
|
||
related_name="source_info",
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Тип объекта",
|
||
help_text="Тип объекта",
|
||
)
|
||
ownership = models.ForeignKey(
|
||
'ObjectOwnership',
|
||
on_delete=models.SET_NULL,
|
||
related_name="source_ownership",
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Принадлежность объекта",
|
||
help_text="Принадлежность объекта (страна, организация и т.д.)",
|
||
)
|
||
note = models.TextField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Примечание",
|
||
help_text="Дополнительное описание объекта",
|
||
)
|
||
confirm_at = models.DateTimeField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Дата подтверждения",
|
||
help_text="Дата и время добавления последней полученной точки ГЛ",
|
||
)
|
||
last_signal_at = models.DateTimeField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Последний сигнал",
|
||
help_text="Дата и время последней отметки о наличии сигнала",
|
||
)
|
||
|
||
coords_average = gis.PointField(
|
||
srid=4326,
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Координаты ГЛ",
|
||
help_text="Усреднённые координаты, полученные от в ходе геолокации (WGS84)",
|
||
)
|
||
coords_kupsat = gis.PointField(
|
||
srid=4326,
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Координаты Кубсата",
|
||
help_text="Координаты, полученные от кубсата (WGS84)",
|
||
)
|
||
coords_valid = gis.PointField(
|
||
srid=4326,
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Координаты оперативников",
|
||
help_text="Координаты, предоставленные оперативным отделом (WGS84)",
|
||
)
|
||
coords_reference = gis.PointField(
|
||
srid=4326,
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Координаты справочные",
|
||
help_text="Координаты, ещё кем-то проверенные (WGS84)",
|
||
)
|
||
|
||
created_at = models.DateTimeField(
|
||
auto_now_add=True,
|
||
verbose_name="Дата создания",
|
||
help_text="Дата и время создания записи",
|
||
)
|
||
created_by = models.ForeignKey(
|
||
CustomUser,
|
||
on_delete=models.SET_NULL,
|
||
related_name="source_created",
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Создан пользователем",
|
||
help_text="Пользователь, создавший запись",
|
||
)
|
||
updated_at = models.DateTimeField(
|
||
auto_now=True,
|
||
verbose_name="Дата последнего изменения",
|
||
help_text="Дата и время последнего изменения",
|
||
)
|
||
updated_by = models.ForeignKey(
|
||
CustomUser,
|
||
on_delete=models.SET_NULL,
|
||
related_name="source_updated",
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Изменен пользователем",
|
||
help_text="Пользователь, последним изменивший запись",
|
||
)
|
||
|
||
def update_coords_average(self, new_coord_tuple):
|
||
"""
|
||
Обновляет coords_average в зависимости от типа объекта (info).
|
||
|
||
Логика:
|
||
- Если info == "Подвижные": coords_average = последняя добавленная координата
|
||
- Иначе (Стационарные и др.): coords_average = инкрементальное среднее
|
||
|
||
Args:
|
||
new_coord_tuple: кортеж (longitude, latitude) новой координаты
|
||
"""
|
||
from django.contrib.gis.geos import Point
|
||
from .utils import calculate_mean_coords
|
||
|
||
# Если тип объекта "Подвижные" - просто устанавливаем последнюю координату
|
||
if self.info and self.info.name == "Подвижные":
|
||
self.coords_average = Point(new_coord_tuple, srid=4326)
|
||
else:
|
||
# Для стационарных объектов - вычисляем среднее
|
||
if self.coords_average:
|
||
# Есть предыдущее среднее - вычисляем новое среднее
|
||
current_coord = (self.coords_average.x, self.coords_average.y)
|
||
new_avg, _ = calculate_mean_coords(current_coord, new_coord_tuple)
|
||
self.coords_average = Point(new_avg, srid=4326)
|
||
else:
|
||
# Первая координата - просто устанавливаем её
|
||
self.coords_average = Point(new_coord_tuple, srid=4326)
|
||
|
||
def get_last_geo_coords(self):
|
||
"""
|
||
Получает координаты последней добавленной точки ГЛ для этого источника.
|
||
Сортировка по ID (последняя добавленная в базу).
|
||
|
||
Returns:
|
||
tuple: (longitude, latitude) или None если точек нет
|
||
"""
|
||
# Получаем последний ObjItem для этого Source (по ID)
|
||
last_objitem = self.source_objitems.filter(
|
||
geo_obj__coords__isnull=False
|
||
).select_related('geo_obj').order_by('-id').first()
|
||
|
||
if last_objitem and last_objitem.geo_obj and last_objitem.geo_obj.coords:
|
||
return (last_objitem.geo_obj.coords.x, last_objitem.geo_obj.coords.y)
|
||
|
||
return None
|
||
|
||
def update_confirm_at(self):
|
||
"""
|
||
Обновляет дату confirm_at на дату создания последней добавленной точки ГЛ.
|
||
"""
|
||
last_objitem = self.source_objitems.order_by('-created_at').first()
|
||
if last_objitem:
|
||
self.confirm_at = last_objitem.created_at
|
||
|
||
def update_last_signal_at(self):
|
||
"""
|
||
Обновляет дату last_signal_at на дату последней отметки о наличии сигнала (mark=True).
|
||
"""
|
||
last_signal_mark = self.marks.filter(mark=True).order_by('-timestamp').first()
|
||
if last_signal_mark:
|
||
self.last_signal_at = last_signal_mark.timestamp
|
||
else:
|
||
self.last_signal_at = None
|
||
|
||
def save(self, *args, **kwargs):
|
||
"""
|
||
Переопределенный метод save для автоматического обновления coords_average
|
||
при изменении типа объекта.
|
||
"""
|
||
from django.contrib.gis.geos import Point
|
||
|
||
# Проверяем, изменился ли тип объекта
|
||
if self.pk: # Объект уже существует
|
||
try:
|
||
old_instance = Source.objects.get(pk=self.pk)
|
||
old_info = old_instance.info
|
||
new_info = self.info
|
||
|
||
# Если тип изменился на "Подвижные"
|
||
if new_info and new_info.name == "Подвижные" and (not old_info or old_info.name != "Подвижные"):
|
||
# Устанавливаем координату последней точки
|
||
last_coords = self.get_last_geo_coords()
|
||
if last_coords:
|
||
self.coords_average = Point(last_coords, srid=4326)
|
||
|
||
# Если тип изменился с "Подвижные" на что-то другое
|
||
elif old_info and old_info.name == "Подвижные" and (not new_info or new_info.name != "Подвижные"):
|
||
# Пересчитываем среднюю координату по всем точкам
|
||
self._recalculate_average_coords()
|
||
|
||
except Source.DoesNotExist:
|
||
pass
|
||
|
||
super().save(*args, **kwargs)
|
||
|
||
def _recalculate_average_coords(self):
|
||
"""
|
||
Пересчитывает среднюю координату по всем точкам источника.
|
||
Используется при переключении с "Подвижные" на "Стационарные".
|
||
|
||
Сортировка по ID (порядок добавления в базу), инкрементальное усреднение
|
||
как в функциях импорта.
|
||
"""
|
||
from django.contrib.gis.geos import Point
|
||
from .utils import calculate_mean_coords
|
||
|
||
# Получаем все точки для этого источника, сортируем по ID (порядок добавления)
|
||
objitems = self.source_objitems.filter(
|
||
geo_obj__coords__isnull=False
|
||
).select_related('geo_obj').order_by('id')
|
||
|
||
if not objitems.exists():
|
||
return
|
||
|
||
# Вычисляем среднюю координату инкрементально (как в функциях импорта)
|
||
coords_average = None
|
||
for objitem in objitems:
|
||
if objitem.geo_obj and objitem.geo_obj.coords:
|
||
coord = (objitem.geo_obj.coords.x, objitem.geo_obj.coords.y)
|
||
if coords_average is None:
|
||
# Первая точка - просто устанавливаем её
|
||
coords_average = coord
|
||
else:
|
||
# Последующие точки - вычисляем среднее между текущим средним и новой точкой
|
||
coords_average, _ = calculate_mean_coords(coords_average, coord)
|
||
|
||
if coords_average:
|
||
self.coords_average = Point(coords_average, srid=4326)
|
||
|
||
class Meta:
|
||
verbose_name = "Источник"
|
||
verbose_name_plural = "Источники"
|
||
|
||
|
||
class ObjItem(models.Model):
|
||
"""
|
||
Модель точки ГЛ.
|
||
|
||
Центральная модель, объединяющая информацию о ВЧ параметрах, геолокации.
|
||
"""
|
||
|
||
# Основные поля
|
||
name = models.CharField(
|
||
null=True,
|
||
blank=True,
|
||
max_length=100,
|
||
verbose_name="Имя объекта",
|
||
db_index=True,
|
||
help_text="Название объекта/источника сигнала",
|
||
)
|
||
source = models.ForeignKey(
|
||
Source,
|
||
on_delete=models.CASCADE,
|
||
null=True,
|
||
verbose_name="ИРИ",
|
||
related_name="source_objitems",
|
||
)
|
||
transponder = models.ForeignKey(
|
||
"mapsapp.Transponders",
|
||
on_delete=models.SET_NULL,
|
||
related_name="transponder_objitems",
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Транспондер",
|
||
help_text="Транспондер, с помощью которого была получена точка",
|
||
)
|
||
is_automatic = models.BooleanField(
|
||
default=False,
|
||
verbose_name="Автоматическая",
|
||
db_index=True,
|
||
help_text="Если True, точка не добавляется к объектам (Source), а хранится отдельно",
|
||
)
|
||
|
||
# Метаданные
|
||
created_at = models.DateTimeField(
|
||
auto_now_add=True,
|
||
verbose_name="Дата создания",
|
||
help_text="Дата и время создания записи",
|
||
)
|
||
created_by = models.ForeignKey(
|
||
CustomUser,
|
||
on_delete=models.SET_NULL,
|
||
related_name="objitems_created",
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Создан пользователем",
|
||
help_text="Пользователь, создавший запись",
|
||
)
|
||
updated_at = models.DateTimeField(
|
||
auto_now=True,
|
||
verbose_name="Дата последнего изменения",
|
||
help_text="Дата и время последнего изменения",
|
||
)
|
||
updated_by = models.ForeignKey(
|
||
CustomUser,
|
||
on_delete=models.SET_NULL,
|
||
related_name="objitems_updated",
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Изменен пользователем",
|
||
help_text="Пользователь, последним изменивший запись",
|
||
)
|
||
lyngsat_source = models.ForeignKey(
|
||
"lyngsatapp.LyngSat",
|
||
on_delete=models.SET_NULL,
|
||
related_name="objitems",
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Источник LyngSat",
|
||
help_text="Связанный источник из базы LyngSat (ТВ)",
|
||
)
|
||
|
||
# Custom manager
|
||
objects = ObjItemManager()
|
||
|
||
def __str__(self):
|
||
return f"Объект {self.name}" if self.name else f"Объект #{self.pk}"
|
||
|
||
class Meta:
|
||
verbose_name = "Объект"
|
||
verbose_name_plural = "Объекты"
|
||
ordering = ["-updated_at"]
|
||
indexes = [
|
||
models.Index(fields=["name"]),
|
||
models.Index(fields=["-updated_at"]),
|
||
models.Index(fields=["-created_at"]),
|
||
]
|
||
|
||
|
||
class Parameter(models.Model):
|
||
id_satellite = models.ForeignKey(
|
||
Satellite,
|
||
on_delete=models.PROTECT,
|
||
related_name="parameters",
|
||
verbose_name="Спутник",
|
||
null=True,
|
||
)
|
||
polarization = models.ForeignKey(
|
||
Polarization,
|
||
default=get_default_polarization,
|
||
on_delete=models.SET_DEFAULT,
|
||
related_name="polarizations",
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Поляризация",
|
||
)
|
||
frequency = models.FloatField(
|
||
default=0,
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Частота, МГц",
|
||
db_index=True,
|
||
# validators=[MinValueValidator(0), MaxValueValidator(50000)],
|
||
help_text="Центральная частота сигнала",
|
||
)
|
||
freq_range = models.FloatField(
|
||
default=0,
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Полоса частот, МГц",
|
||
# validators=[MinValueValidator(0), MaxValueValidator(1000)],
|
||
help_text="Полоса частот сигнала",
|
||
)
|
||
bod_velocity = models.FloatField(
|
||
default=0,
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Символьная скорость, БОД",
|
||
# validators=[MinValueValidator(0)],
|
||
help_text="Символьная скорость должна быть положительной",
|
||
)
|
||
modulation = models.ForeignKey(
|
||
Modulation,
|
||
default=get_default_modulation,
|
||
on_delete=models.SET_DEFAULT,
|
||
related_name="modulations",
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Модуляция",
|
||
)
|
||
snr = models.FloatField(
|
||
default=0,
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="ОСШ",
|
||
# validators=[MinValueValidator(-50), MaxValueValidator(100)],
|
||
help_text="Отношение сигнал/шум",
|
||
)
|
||
standard = models.ForeignKey(
|
||
Standard,
|
||
default=get_default_standard,
|
||
on_delete=models.SET_DEFAULT,
|
||
related_name="standards",
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Стандарт",
|
||
)
|
||
# id_user_add = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, related_name="parameter_added", verbose_name="Пользователь", null=True, blank=True)
|
||
objitem = models.OneToOneField(
|
||
ObjItem,
|
||
on_delete=models.CASCADE,
|
||
related_name="parameter_obj",
|
||
verbose_name="Объект",
|
||
null=True,
|
||
blank=True,
|
||
help_text="Связанный объект",
|
||
)
|
||
# id_sigma_parameter = models.ManyToManyField(SigmaParameter, on_delete=models.SET_NULL, related_name="sigma_parameter", verbose_name="ВЧ с sigma", null=True, blank=True)
|
||
# id_sigma_parameter = models.ManyToManyField(SigmaParameter, verbose_name="ВЧ с sigma", null=True, blank=True)
|
||
|
||
def clean(self):
|
||
"""Валидация на уровне модели"""
|
||
super().clean()
|
||
|
||
# Проверка что частота больше полосы частот
|
||
if self.frequency and self.freq_range:
|
||
if self.freq_range > self.frequency:
|
||
raise ValidationError(
|
||
{"freq_range": "Полоса частот не может быть больше частоты"}
|
||
)
|
||
|
||
# Проверка что символьная скорость соответствует полосе частот
|
||
if self.bod_velocity and self.freq_range:
|
||
if self.bod_velocity > self.freq_range * 1000000: # Конвертация МГц в Гц
|
||
raise ValidationError(
|
||
{
|
||
"bod_velocity": "Символьная скорость не может превышать полосу частот"
|
||
}
|
||
)
|
||
|
||
def __str__(self):
|
||
polarization_name = self.polarization.name if self.polarization else "-"
|
||
modulation_name = self.modulation.name if self.modulation else "-"
|
||
return f"Источник-{self.frequency}:{self.freq_range} МГц:{polarization_name}:{modulation_name}"
|
||
|
||
class Meta:
|
||
verbose_name = "ВЧ загрузка"
|
||
verbose_name_plural = "ВЧ загрузки"
|
||
indexes = [
|
||
models.Index(fields=["id_satellite", "frequency"]),
|
||
models.Index(fields=["frequency", "polarization"]),
|
||
]
|
||
# constraints = [
|
||
# models.UniqueConstraint(
|
||
# fields=[
|
||
# 'polarization', 'frequency', 'freq_range',
|
||
# 'bod_velocity', 'modulation', 'snr', 'standard'
|
||
# ],
|
||
# name='unique_parameter_combination'
|
||
# )
|
||
# ]
|
||
|
||
|
||
class SigmaParameter(models.Model):
|
||
TRANSFERS = [(-1.0, "-"), (9750.0, "9750 МГц"), (10750.0, "10750 МГц")]
|
||
|
||
id_satellite = models.ForeignKey(
|
||
Satellite,
|
||
on_delete=models.PROTECT,
|
||
related_name="sigmapar_sat",
|
||
verbose_name="Спутник",
|
||
)
|
||
transfer = models.FloatField(
|
||
choices=TRANSFERS,
|
||
default=-1.0,
|
||
verbose_name="Перенос по частоте",
|
||
help_text="Выберите перенос по частоте",
|
||
)
|
||
status = models.CharField(
|
||
max_length=20,
|
||
blank=True,
|
||
null=True,
|
||
verbose_name="Статус",
|
||
help_text="Статус измерения",
|
||
)
|
||
frequency = models.FloatField(
|
||
default=0,
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Частота, МГц",
|
||
db_index=True,
|
||
# validators=[MinValueValidator(0), MaxValueValidator(50000)],
|
||
help_text="Центральная частота сигнала",
|
||
)
|
||
transfer_frequency = models.GeneratedField(
|
||
expression=ExpressionWrapper(
|
||
F("frequency") + F("transfer"), output_field=models.FloatField()
|
||
),
|
||
output_field=models.FloatField(),
|
||
db_persist=True,
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Частота в Ku, МГц",
|
||
)
|
||
freq_range = models.FloatField(
|
||
default=0,
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Полоса частот, МГц",
|
||
# validators=[MinValueValidator(0), MaxValueValidator(1000)],
|
||
help_text="Полоса частот",
|
||
)
|
||
power = models.FloatField(
|
||
default=0,
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Мощность, дБм",
|
||
# validators=[MinValueValidator(-100), MaxValueValidator(100)],
|
||
help_text="Мощность сигнала",
|
||
)
|
||
bod_velocity = models.FloatField(
|
||
default=0,
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Символьная скорость, БОД",
|
||
# validators=[MinValueValidator(0)],
|
||
help_text="Символьная скорость должна быть положительной",
|
||
)
|
||
polarization = models.ForeignKey(
|
||
Polarization,
|
||
default=get_default_polarization,
|
||
on_delete=models.SET_DEFAULT,
|
||
related_name="polarizations_sigma",
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Поляризация",
|
||
)
|
||
modulation = models.ForeignKey(
|
||
Modulation,
|
||
default=get_default_modulation,
|
||
on_delete=models.SET_DEFAULT,
|
||
related_name="modulations_sigma",
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Модуляция",
|
||
)
|
||
snr = models.FloatField(
|
||
default=0,
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="ОСШ, Дб",
|
||
validators=[MinValueValidator(-50), MaxValueValidator(100)],
|
||
help_text="Отношение сигнал/шум в диапазоне от -50 до 100 дБ",
|
||
)
|
||
standard = models.ForeignKey(
|
||
Standard,
|
||
default=get_default_standard,
|
||
on_delete=models.SET_DEFAULT,
|
||
related_name="standards_sigma",
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Стандарт",
|
||
)
|
||
packets = models.BooleanField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Пакетность",
|
||
help_text="Наличие пакетной передачи",
|
||
)
|
||
datetime_begin = models.DateTimeField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Время начала измерения",
|
||
help_text="Дата и время начала измерения",
|
||
)
|
||
datetime_end = models.DateTimeField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Время окончания измерения",
|
||
help_text="Дата и время окончания измерения",
|
||
)
|
||
# mark = models.ManyToManyField(SigmaParMark, verbose_name="Отметка", blank=True)
|
||
parameter = models.ForeignKey(
|
||
Parameter,
|
||
on_delete=models.SET_NULL,
|
||
related_name="sigma_parameter",
|
||
verbose_name="ВЧ",
|
||
null=True,
|
||
blank=True,
|
||
)
|
||
|
||
def clean(self):
|
||
"""Валидация на уровне модели"""
|
||
super().clean()
|
||
|
||
# Проверка что время окончания больше времени начала
|
||
if self.datetime_begin and self.datetime_end:
|
||
if self.datetime_end < self.datetime_begin:
|
||
raise ValidationError(
|
||
{"datetime_end": "Время окончания должно быть позже времени начала"}
|
||
)
|
||
|
||
# Проверка что частота больше полосы частот
|
||
if self.frequency and self.freq_range:
|
||
if self.freq_range > self.frequency:
|
||
raise ValidationError(
|
||
{"freq_range": "Полоса частот не может быть больше частоты"}
|
||
)
|
||
|
||
def __str__(self):
|
||
modulation_name = self.modulation.name if self.modulation else "-"
|
||
return f"Sigma-{self.frequency}:{self.freq_range} МГц:{modulation_name}"
|
||
|
||
class Meta:
|
||
verbose_name = "ВЧ sigma"
|
||
verbose_name_plural = "ВЧ sigma"
|
||
|
||
|
||
class Geo(models.Model):
|
||
"""
|
||
Модель геолокационных данных.
|
||
|
||
Хранит информацию о местоположении источника сигнала, включая координаты,
|
||
данные от различных источников (геолокация, кубсат, оперативники) и расстояния между ними.
|
||
"""
|
||
|
||
# Основные поля
|
||
timestamp = models.DateTimeField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Время",
|
||
db_index=True,
|
||
help_text="Время фиксации геолокации",
|
||
)
|
||
location = models.CharField(
|
||
max_length=255,
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Местоположение",
|
||
help_text="Текстовое описание местоположения",
|
||
)
|
||
comment = models.CharField(
|
||
max_length=255,
|
||
blank=True,
|
||
verbose_name="Комментарий",
|
||
help_text="Дополнительные комментарии",
|
||
)
|
||
is_average = models.BooleanField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Усреднённое",
|
||
help_text="Является ли координата усредненной",
|
||
)
|
||
|
||
# Координаты
|
||
coords = gis.PointField(
|
||
srid=4326,
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Координата геолокации",
|
||
help_text="Основные координаты геолокации (WGS84)",
|
||
)
|
||
|
||
# Вычисляемые поля - расстояния
|
||
# distance_coords_kup = models.GeneratedField(
|
||
# expression=functions.Distance("coords", "coords_kupsat") / 1000,
|
||
# output_field=models.FloatField(),
|
||
# db_persist=True,
|
||
# null=True,
|
||
# blank=True,
|
||
# verbose_name="Расстояние между кубсатом и гео, км",
|
||
# )
|
||
# distance_coords_valid = models.GeneratedField(
|
||
# expression=functions.Distance("coords", "coords_valid") / 1000,
|
||
# output_field=models.FloatField(),
|
||
# db_persist=True,
|
||
# null=True,
|
||
# blank=True,
|
||
# verbose_name="Расстояние между гео и оперативным отделом, км",
|
||
# )
|
||
# distance_kup_valid = models.GeneratedField(
|
||
# expression=functions.Distance("coords_valid", "coords_kupsat") / 1000,
|
||
# output_field=models.FloatField(),
|
||
# db_persist=True,
|
||
# null=True,
|
||
# blank=True,
|
||
# verbose_name="Расстояние между кубсатом и оперативным отделом, км",
|
||
# )
|
||
|
||
# Связи
|
||
mirrors = models.ManyToManyField(
|
||
Satellite,
|
||
related_name="geo_mirrors",
|
||
verbose_name="Зеркала",
|
||
blank=True,
|
||
help_text="Спутники-зеркала, использованные для приема",
|
||
)
|
||
objitem = models.OneToOneField(
|
||
ObjItem,
|
||
on_delete=models.CASCADE,
|
||
verbose_name="Объект",
|
||
related_name="geo_obj",
|
||
null=True,
|
||
help_text="Связанный объект",
|
||
)
|
||
|
||
def __str__(self):
|
||
if self.coords:
|
||
longitude = self.coords.coords[0]
|
||
latitude = self.coords.coords[1]
|
||
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||
location_str = f", {self.location}" if self.location else ""
|
||
return f"{lat} {lon}{location_str}"
|
||
return f"Гео #{self.pk}"
|
||
|
||
class Meta:
|
||
verbose_name = "Гео"
|
||
verbose_name_plural = "Гео"
|
||
ordering = ["-timestamp"]
|
||
indexes = [
|
||
models.Index(fields=["-timestamp"]),
|
||
models.Index(fields=["location"]),
|
||
]
|
||
constraints = [
|
||
models.UniqueConstraint(
|
||
fields=["timestamp", "coords"], name="unique_geo_combination"
|
||
)
|
||
]
|