Добавил транспондеры к ObjItem шаблону
This commit is contained in:
@@ -32,13 +32,13 @@ from .models import (
|
||||
ObjItem,
|
||||
CustomUser,
|
||||
Band,
|
||||
Source
|
||||
Source,
|
||||
)
|
||||
from .filters import (
|
||||
GeoKupDistanceFilter,
|
||||
GeoValidDistanceFilter,
|
||||
UniqueToggleFilter,
|
||||
HasSigmaParameterFilter
|
||||
HasSigmaParameterFilter,
|
||||
)
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ admin.site.unregister(Group)
|
||||
# Base Admin Classes
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class BaseAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Базовый класс для всех admin моделей.
|
||||
@@ -64,6 +65,7 @@ class BaseAdmin(admin.ModelAdmin):
|
||||
- Настройка количества элементов на странице
|
||||
- Автоматическое заполнение полей created_by и updated_by
|
||||
"""
|
||||
|
||||
save_on_top = True
|
||||
list_per_page = 50
|
||||
|
||||
@@ -79,12 +81,12 @@ class BaseAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
if not change:
|
||||
# При создании нового объекта устанавливаем created_by
|
||||
if hasattr(obj, 'created_by') and not obj.created_by_id:
|
||||
obj.created_by = getattr(request.user, 'customuser', None)
|
||||
if hasattr(obj, "created_by") and not obj.created_by_id:
|
||||
obj.created_by = getattr(request.user, "customuser", None)
|
||||
|
||||
# При любом сохранении обновляем updated_by
|
||||
if hasattr(obj, 'updated_by'):
|
||||
obj.updated_by = getattr(request.user, 'customuser', None)
|
||||
if hasattr(obj, "updated_by"):
|
||||
obj.updated_by = getattr(request.user, "customuser", None)
|
||||
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
@@ -92,7 +94,7 @@ class BaseAdmin(admin.ModelAdmin):
|
||||
class CustomUserInline(admin.StackedInline):
|
||||
model = CustomUser
|
||||
can_delete = False
|
||||
verbose_name_plural = 'Дополнительная информация пользователя'
|
||||
verbose_name_plural = "Дополнительная информация пользователя"
|
||||
|
||||
|
||||
class LocationForm(forms.ModelForm):
|
||||
@@ -105,13 +107,13 @@ class LocationForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Geo
|
||||
fields = '__all__'
|
||||
fields = "__all__"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.instance and self.instance.coords:
|
||||
self.fields['latitude_geo'].initial = self.instance.coords[1]
|
||||
self.fields['longitude_geo'].initial = self.instance.coords[0]
|
||||
self.fields["latitude_geo"].initial = self.instance.coords[1]
|
||||
self.fields["longitude_geo"].initial = self.instance.coords[0]
|
||||
# if self.instance and self.instance.coords_kupsat:
|
||||
# self.fields['latitude_kupsat'].initial = self.instance.coords_kupsat[1]
|
||||
# self.fields['longitude_kupsat'].initial = self.instance.coords_kupsat[0]
|
||||
@@ -122,8 +124,9 @@ class LocationForm(forms.ModelForm):
|
||||
def save(self, commit=True):
|
||||
instance = super().save(commit=False)
|
||||
from django.contrib.gis.geos import Point
|
||||
lat = self.cleaned_data.get('latitude_geo')
|
||||
lon = self.cleaned_data.get('longitude_geo')
|
||||
|
||||
lat = self.cleaned_data.get("latitude_geo")
|
||||
lon = self.cleaned_data.get("longitude_geo")
|
||||
if lat is not None and lon is not None:
|
||||
instance.coords = Point(lon, lat, srid=4326)
|
||||
|
||||
@@ -150,18 +153,28 @@ class GeoInline(admin.StackedInline):
|
||||
form = LocationForm
|
||||
# readonly_fields = ("distance_coords_kup", "distance_coords_valid", "distance_kup_valid")
|
||||
prefetch_related = ("mirrors",)
|
||||
autocomplete_fields = ('mirrors',)
|
||||
autocomplete_fields = ("mirrors",)
|
||||
fieldsets = (
|
||||
("Основная информация", {
|
||||
"fields": ("mirrors", "location",
|
||||
(
|
||||
"Основная информация",
|
||||
{
|
||||
"fields": (
|
||||
"mirrors",
|
||||
"location",
|
||||
# "distance_coords_kup",
|
||||
# "distance_coords_valid",
|
||||
# "distance_kup_valid",
|
||||
"timestamp", "comment",)
|
||||
}),
|
||||
("Координаты: геолокация", {
|
||||
"timestamp",
|
||||
"comment",
|
||||
)
|
||||
},
|
||||
),
|
||||
(
|
||||
"Координаты: геолокация",
|
||||
{
|
||||
"fields": ("longitude_geo", "latitude_geo", "coords"),
|
||||
}),
|
||||
},
|
||||
),
|
||||
# ("Координаты: Кубсат", {
|
||||
# "fields": ("longitude_kupsat", "latitude_kupsat", "coords_kupsat"),
|
||||
# }),
|
||||
@@ -174,6 +187,7 @@ class GeoInline(admin.StackedInline):
|
||||
class UserAdmin(BaseUserAdmin):
|
||||
inlines = [CustomUserInline]
|
||||
|
||||
|
||||
admin.site.register(User, UserAdmin)
|
||||
|
||||
|
||||
@@ -181,6 +195,7 @@ admin.site.register(User, UserAdmin)
|
||||
# Custom Admin Actions
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@admin.action(description="Показать выбранные на карте")
|
||||
def show_on_map(modeladmin, request, queryset):
|
||||
"""
|
||||
@@ -189,9 +204,9 @@ def show_on_map(modeladmin, request, queryset):
|
||||
Оптимизирован для работы с большим количеством объектов:
|
||||
использует values_list для получения только ID.
|
||||
"""
|
||||
selected_ids = queryset.values_list('id', flat=True)
|
||||
ids_str = ','.join(str(pk) for pk in selected_ids)
|
||||
return redirect(reverse('mainapp:admin_show_map') + f'?ids={ids_str}')
|
||||
selected_ids = queryset.values_list("id", flat=True)
|
||||
ids_str = ",".join(str(pk) for pk in selected_ids)
|
||||
return redirect(reverse("mainapp:admin_show_map") + f"?ids={ids_str}")
|
||||
|
||||
|
||||
@admin.action(description="Показать выбранные объекты на карте")
|
||||
@@ -202,9 +217,9 @@ def show_selected_on_map(modeladmin, request, queryset):
|
||||
Оптимизирован для работы с большим количеством объектов:
|
||||
использует values_list для получения только ID.
|
||||
"""
|
||||
selected_ids = queryset.values_list('id', flat=True)
|
||||
ids_str = ','.join(str(pk) for pk in selected_ids)
|
||||
return redirect(reverse('mainapp:show_selected_objects_map') + f'?ids={ids_str}')
|
||||
selected_ids = queryset.values_list("id", flat=True)
|
||||
ids_str = ",".join(str(pk) for pk in selected_ids)
|
||||
return redirect(reverse("mainapp:show_selected_objects_map") + f"?ids={ids_str}")
|
||||
|
||||
|
||||
@admin.action(description="Экспортировать выбранные объекты в CSV")
|
||||
@@ -220,39 +235,41 @@ def export_objects_to_csv(modeladmin, request, queryset):
|
||||
|
||||
# Оптимизируем queryset
|
||||
queryset = queryset.select_related(
|
||||
'geo_obj',
|
||||
'created_by__user',
|
||||
'updated_by__user',
|
||||
'parameter_obj',
|
||||
'parameter_obj__id_satellite',
|
||||
'parameter_obj__polarization',
|
||||
'parameter_obj__modulation'
|
||||
"geo_obj",
|
||||
"created_by__user",
|
||||
"updated_by__user",
|
||||
"parameter_obj",
|
||||
"parameter_obj__id_satellite",
|
||||
"parameter_obj__polarization",
|
||||
"parameter_obj__modulation",
|
||||
)
|
||||
|
||||
response = HttpResponse(content_type='text/csv; charset=utf-8')
|
||||
response['Content-Disposition'] = 'attachment; filename="objitems_export.csv"'
|
||||
response.write('\ufeff') # UTF-8 BOM для корректного отображения в Excel
|
||||
response = HttpResponse(content_type="text/csv; charset=utf-8")
|
||||
response["Content-Disposition"] = 'attachment; filename="objitems_export.csv"'
|
||||
response.write("\ufeff") # UTF-8 BOM для корректного отображения в Excel
|
||||
|
||||
writer = csv.writer(response)
|
||||
writer.writerow([
|
||||
'Название',
|
||||
'Спутник',
|
||||
'Частота (МГц)',
|
||||
'Полоса (МГц)',
|
||||
'Поляризация',
|
||||
'Модуляция',
|
||||
'ОСШ',
|
||||
'Координаты геолокации',
|
||||
'Координаты Кубсата',
|
||||
'Координаты оперативного отдела',
|
||||
'Расстояние Гео-Куб (км)',
|
||||
'Расстояние Гео-Опер (км)',
|
||||
'Дата создания',
|
||||
'Дата обновления'
|
||||
])
|
||||
writer.writerow(
|
||||
[
|
||||
"Название",
|
||||
"Спутник",
|
||||
"Частота (МГц)",
|
||||
"Полоса (МГц)",
|
||||
"Поляризация",
|
||||
"Модуляция",
|
||||
"ОСШ",
|
||||
"Координаты геолокации",
|
||||
"Координаты Кубсата",
|
||||
"Координаты оперативного отдела",
|
||||
"Расстояние Гео-Куб (км)",
|
||||
"Расстояние Гео-Опер (км)",
|
||||
"Дата создания",
|
||||
"Дата обновления",
|
||||
]
|
||||
)
|
||||
|
||||
for obj in queryset:
|
||||
param = getattr(obj, 'parameter_obj', None)
|
||||
param = getattr(obj, "parameter_obj", None)
|
||||
geo = obj.geo_obj
|
||||
|
||||
# Форматирование координат
|
||||
@@ -264,7 +281,8 @@ def export_objects_to_csv(modeladmin, request, queryset):
|
||||
lat_str = f"{lat}N" if lat > 0 else f"{abs(lat)}S"
|
||||
return f"{lat_str} {lon_str}"
|
||||
|
||||
writer.writerow([
|
||||
writer.writerow(
|
||||
[
|
||||
obj.name,
|
||||
param.id_satellite.name if param and param.id_satellite else "-",
|
||||
param.frequency if param else "-",
|
||||
@@ -275,11 +293,16 @@ def export_objects_to_csv(modeladmin, request, queryset):
|
||||
format_coords(geo) if geo and geo.coords else "-",
|
||||
format_coords(geo) if geo and geo.coords_kupsat else "-",
|
||||
format_coords(geo) if geo and geo.coords_valid else "-",
|
||||
round(geo.distance_coords_kup, 3) if geo and geo.distance_coords_kup else "-",
|
||||
round(geo.distance_coords_valid, 3) if geo and geo.distance_coords_valid else "-",
|
||||
round(geo.distance_coords_kup, 3)
|
||||
if geo and geo.distance_coords_kup
|
||||
else "-",
|
||||
round(geo.distance_coords_valid, 3)
|
||||
if geo and geo.distance_coords_valid
|
||||
else "-",
|
||||
obj.created_at.strftime("%d.%m.%Y %H:%M:%S") if obj.created_at else "-",
|
||||
obj.updated_at.strftime("%d.%m.%Y %H:%M:%S") if obj.updated_at else "-"
|
||||
])
|
||||
obj.updated_at.strftime("%d.%m.%Y %H:%M:%S") if obj.updated_at else "-",
|
||||
]
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
@@ -288,8 +311,10 @@ def export_objects_to_csv(modeladmin, request, queryset):
|
||||
# Inline Admin Classes
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class ParameterInline(admin.StackedInline):
|
||||
"""Inline для редактирования параметра объекта."""
|
||||
|
||||
model = Parameter
|
||||
extra = 0
|
||||
max_num = 1
|
||||
@@ -297,36 +322,37 @@ class ParameterInline(admin.StackedInline):
|
||||
verbose_name = "ВЧ загрузка"
|
||||
verbose_name_plural = "ВЧ загрузка"
|
||||
fields = (
|
||||
'id_satellite',
|
||||
'frequency',
|
||||
'freq_range',
|
||||
'polarization',
|
||||
'modulation',
|
||||
'bod_velocity',
|
||||
'snr',
|
||||
'standard'
|
||||
"id_satellite",
|
||||
"frequency",
|
||||
"freq_range",
|
||||
"polarization",
|
||||
"modulation",
|
||||
"bod_velocity",
|
||||
"snr",
|
||||
"standard",
|
||||
)
|
||||
autocomplete_fields = ('id_satellite', 'polarization', 'modulation', 'standard')
|
||||
autocomplete_fields = ("id_satellite", "polarization", "modulation", "standard")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Admin Classes
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@admin.register(SigmaParMark)
|
||||
class SigmaParMarkAdmin(BaseAdmin):
|
||||
"""Админ-панель для модели SigmaParMark."""
|
||||
|
||||
list_display = ("mark", "timestamp")
|
||||
search_fields = ("mark",)
|
||||
ordering = ("-timestamp",)
|
||||
list_filter = (
|
||||
("timestamp", DateRangeQuickSelectListFilterBuilder()),
|
||||
)
|
||||
list_filter = (("timestamp", DateRangeQuickSelectListFilterBuilder()),)
|
||||
|
||||
|
||||
@admin.register(Polarization)
|
||||
class PolarizationAdmin(BaseAdmin):
|
||||
"""Админ-панель для модели Polarization."""
|
||||
|
||||
list_display = ("name",)
|
||||
search_fields = ("name",)
|
||||
ordering = ("name",)
|
||||
@@ -335,6 +361,7 @@ class PolarizationAdmin(BaseAdmin):
|
||||
@admin.register(Modulation)
|
||||
class ModulationAdmin(BaseAdmin):
|
||||
"""Админ-панель для модели Modulation."""
|
||||
|
||||
list_display = ("name",)
|
||||
search_fields = ("name",)
|
||||
ordering = ("name",)
|
||||
@@ -343,6 +370,7 @@ class ModulationAdmin(BaseAdmin):
|
||||
@admin.register(Standard)
|
||||
class StandardAdmin(BaseAdmin):
|
||||
"""Админ-панель для модели Standard."""
|
||||
|
||||
list_display = ("name",)
|
||||
search_fields = ("name",)
|
||||
ordering = ("name",)
|
||||
@@ -351,11 +379,12 @@ class StandardAdmin(BaseAdmin):
|
||||
class SigmaParameterInline(admin.StackedInline):
|
||||
model = SigmaParameter
|
||||
extra = 0
|
||||
autocomplete_fields = ['mark']
|
||||
autocomplete_fields = ["mark"]
|
||||
readonly_fields = (
|
||||
"datetime_begin",
|
||||
"datetime_end",
|
||||
)
|
||||
|
||||
def has_add_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
@@ -370,6 +399,7 @@ class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
||||
- Предоставляет фильтры по основным характеристикам
|
||||
- Поддерживает импорт/экспорт данных
|
||||
"""
|
||||
|
||||
list_display = (
|
||||
"id_satellite",
|
||||
"frequency",
|
||||
@@ -380,10 +410,16 @@ class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
||||
"snr",
|
||||
"standard",
|
||||
"related_objitem",
|
||||
"sigma_parameter"
|
||||
"sigma_parameter",
|
||||
)
|
||||
list_display_links = ("frequency", "id_satellite")
|
||||
list_select_related = ("polarization", "modulation", "standard", "id_satellite", "objitem")
|
||||
list_select_related = (
|
||||
"polarization",
|
||||
"modulation",
|
||||
"standard",
|
||||
"id_satellite",
|
||||
"objitem",
|
||||
)
|
||||
|
||||
list_filter = (
|
||||
HasSigmaParameterFilter,
|
||||
@@ -415,9 +451,10 @@ class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
||||
|
||||
def related_objitem(self, obj):
|
||||
"""Отображает связанный ObjItem."""
|
||||
if hasattr(obj, 'objitem') and obj.objitem:
|
||||
if hasattr(obj, "objitem") and obj.objitem:
|
||||
return obj.objitem.name
|
||||
return "-"
|
||||
|
||||
related_objitem.short_description = "Объект"
|
||||
related_objitem.admin_order_field = "objitem__name"
|
||||
|
||||
@@ -427,6 +464,7 @@ class ParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
||||
if sigma_obj:
|
||||
return f"{sigma_obj[0].frequency}: {sigma_obj[0].freq_range}"
|
||||
return "-"
|
||||
|
||||
sigma_parameter.short_description = "ВЧ sigma"
|
||||
|
||||
|
||||
@@ -440,6 +478,7 @@ class SigmaParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
||||
- Предоставляет фильтры по основным характеристикам
|
||||
- Поддерживает импорт/экспорт данных
|
||||
"""
|
||||
|
||||
list_display = (
|
||||
"id_satellite",
|
||||
"frequency",
|
||||
@@ -454,14 +493,16 @@ class SigmaParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
||||
"datetime_end",
|
||||
)
|
||||
list_display_links = ("id_satellite",)
|
||||
list_select_related = ("modulation", "standard", "id_satellite", "parameter", "polarization")
|
||||
|
||||
readonly_fields = (
|
||||
"datetime_begin",
|
||||
"datetime_end",
|
||||
"transfer_frequency"
|
||||
list_select_related = (
|
||||
"modulation",
|
||||
"standard",
|
||||
"id_satellite",
|
||||
"parameter",
|
||||
"polarization",
|
||||
)
|
||||
|
||||
readonly_fields = ("datetime_begin", "datetime_end", "transfer_frequency")
|
||||
|
||||
list_filter = (
|
||||
("id_satellite__name", MultiSelectDropdownFilter),
|
||||
("modulation__name", MultiSelectDropdownFilter),
|
||||
@@ -494,7 +535,15 @@ class SigmaParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
||||
@admin.register(Satellite)
|
||||
class SatelliteAdmin(BaseAdmin):
|
||||
"""Админ-панель для модели Satellite."""
|
||||
list_display = ("name", "norad", "undersat_point", "launch_date", "created_at", "updated_at")
|
||||
|
||||
list_display = (
|
||||
"name",
|
||||
"norad",
|
||||
"undersat_point",
|
||||
"launch_date",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
search_fields = ("name", "norad")
|
||||
ordering = ("name",)
|
||||
filter_horizontal = ("band",)
|
||||
@@ -505,6 +554,7 @@ class SatelliteAdmin(BaseAdmin):
|
||||
@admin.register(Mirror)
|
||||
class MirrorAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
||||
"""Админ-панель для модели Mirror с поддержкой импорта/экспорта."""
|
||||
|
||||
list_display = ("name",)
|
||||
search_fields = ("name",)
|
||||
ordering = ("name",)
|
||||
@@ -521,21 +571,30 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
||||
- Поддерживает импорт/экспорт данных
|
||||
- Интегрирована с Leaflet для отображения на карте
|
||||
"""
|
||||
|
||||
form = LocationForm
|
||||
|
||||
# readonly_fields = ("distance_coords_kup", "distance_coords_valid", "distance_kup_valid")
|
||||
|
||||
fieldsets = (
|
||||
("Основная информация", {
|
||||
"fields": ("mirrors", "location",
|
||||
(
|
||||
"Основная информация",
|
||||
{
|
||||
"fields": (
|
||||
"mirrors",
|
||||
"location",
|
||||
# "distance_coords_kup",
|
||||
# "distance_coords_valid",
|
||||
# "distance_kup_valid",
|
||||
"timestamp", "comment", "transponder")
|
||||
}),
|
||||
("Координаты: геолокация", {
|
||||
"fields": ("longitude_geo", "latitude_geo", "coords")
|
||||
}),
|
||||
"timestamp",
|
||||
"comment",
|
||||
)
|
||||
},
|
||||
),
|
||||
(
|
||||
"Координаты: геолокация",
|
||||
{"fields": ("longitude_geo", "latitude_geo", "coords")},
|
||||
),
|
||||
# ("Координаты: Кубсат", {
|
||||
# "fields": ("longitude_kupsat", "latitude_kupsat", "coords_kupsat")
|
||||
# }),
|
||||
@@ -557,7 +616,6 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
||||
|
||||
list_filter = (
|
||||
("mirrors", MultiSelectRelatedDropdownFilter),
|
||||
("transponder", MultiSelectRelatedDropdownFilter),
|
||||
"is_average",
|
||||
("location", MultiSelectDropdownFilter),
|
||||
("timestamp", DateRangeQuickSelectListFilterBuilder()),
|
||||
@@ -566,7 +624,6 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
||||
search_fields = (
|
||||
"mirrors__name",
|
||||
"location",
|
||||
"transponder__name",
|
||||
)
|
||||
|
||||
autocomplete_fields = ("mirrors",)
|
||||
@@ -574,18 +631,21 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
||||
actions = [show_on_map]
|
||||
|
||||
settings_overrides = {
|
||||
'DEFAULT_CENTER': (55.7558, 37.6173),
|
||||
'DEFAULT_ZOOM': 12,
|
||||
"DEFAULT_CENTER": (55.7558, 37.6173),
|
||||
"DEFAULT_ZOOM": 12,
|
||||
}
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Оптимизированный queryset с prefetch_related для mirrors."""
|
||||
qs = super().get_queryset(request)
|
||||
return qs.prefetch_related("mirrors", "transponder")
|
||||
return qs.prefetch_related(
|
||||
"mirrors",
|
||||
)
|
||||
|
||||
def mirrors_names(self, obj):
|
||||
"""Отображает список зеркал через запятую."""
|
||||
return ", ".join(m.name for m in obj.mirrors.all())
|
||||
|
||||
mirrors_names.short_description = "Зеркала"
|
||||
|
||||
def formatted_timestamp(self, obj):
|
||||
@@ -594,6 +654,7 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
||||
return ""
|
||||
local_time = timezone.localtime(obj.timestamp)
|
||||
return local_time.strftime("%d.%m.%Y %H:%M:%S")
|
||||
|
||||
formatted_timestamp.short_description = "Дата и время"
|
||||
formatted_timestamp.admin_order_field = "timestamp"
|
||||
|
||||
@@ -606,6 +667,7 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
||||
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||
return f"{lat} {lon}"
|
||||
|
||||
geo_coords.short_description = "Координаты геолокации"
|
||||
|
||||
# def kupsat_coords(self, obj):
|
||||
@@ -631,8 +693,6 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
||||
# valid_coords.short_description = "Координаты оперативного отдела"
|
||||
|
||||
|
||||
|
||||
|
||||
@admin.register(ObjItem)
|
||||
class ObjItemAdmin(BaseAdmin):
|
||||
"""
|
||||
@@ -644,6 +704,7 @@ class ObjItemAdmin(BaseAdmin):
|
||||
- Поддерживает поиск по имени, координатам и частоте
|
||||
- Включает кастомные actions для отображения на карте
|
||||
"""
|
||||
|
||||
list_display = (
|
||||
"name",
|
||||
"sat_name",
|
||||
@@ -671,7 +732,7 @@ class ObjItemAdmin(BaseAdmin):
|
||||
"parameter_obj__id_satellite",
|
||||
"parameter_obj__polarization",
|
||||
"parameter_obj__modulation",
|
||||
"parameter_obj__standard"
|
||||
"parameter_obj__standard",
|
||||
)
|
||||
|
||||
list_filter = (
|
||||
@@ -701,13 +762,14 @@ class ObjItemAdmin(BaseAdmin):
|
||||
readonly_fields = ("created_at", "created_by", "updated_at", "updated_by")
|
||||
|
||||
fieldsets = (
|
||||
("Основная информация", {
|
||||
"fields": ("name",)
|
||||
}),
|
||||
("Метаданные", {
|
||||
("Основная информация", {"fields": ("name",)}),
|
||||
(
|
||||
"Метаданные",
|
||||
{
|
||||
"fields": ("created_at", "created_by", "updated_at", "updated_by"),
|
||||
"classes": ("collapse",)
|
||||
}),
|
||||
"classes": ("collapse",),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
def get_queryset(self, request):
|
||||
@@ -725,23 +787,25 @@ class ObjItemAdmin(BaseAdmin):
|
||||
"parameter_obj__id_satellite",
|
||||
"parameter_obj__polarization",
|
||||
"parameter_obj__modulation",
|
||||
"parameter_obj__standard"
|
||||
"parameter_obj__standard",
|
||||
)
|
||||
|
||||
def sat_name(self, obj):
|
||||
"""Отображает название спутника из связанного параметра."""
|
||||
if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
|
||||
if hasattr(obj, "parameter_obj") and obj.parameter_obj:
|
||||
if obj.parameter_obj.id_satellite:
|
||||
return obj.parameter_obj.id_satellite.name
|
||||
return "-"
|
||||
|
||||
sat_name.short_description = "Спутник"
|
||||
sat_name.admin_order_field = "parameter_obj__id_satellite__name"
|
||||
|
||||
def freq(self, obj):
|
||||
"""Отображает частоту из связанного параметра."""
|
||||
if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
|
||||
if hasattr(obj, "parameter_obj") and obj.parameter_obj:
|
||||
return obj.parameter_obj.frequency
|
||||
return "-"
|
||||
|
||||
freq.short_description = "Частота, МГц"
|
||||
freq.admin_order_field = "parameter_obj__frequency"
|
||||
|
||||
@@ -771,40 +835,45 @@ class ObjItemAdmin(BaseAdmin):
|
||||
|
||||
def pol(self, obj):
|
||||
"""Отображает поляризацию из связанного параметра."""
|
||||
if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
|
||||
if hasattr(obj, "parameter_obj") and obj.parameter_obj:
|
||||
if obj.parameter_obj.polarization:
|
||||
return obj.parameter_obj.polarization.name
|
||||
return "-"
|
||||
|
||||
pol.short_description = "Поляризация"
|
||||
|
||||
def freq_range(self, obj):
|
||||
"""Отображает полосу частот из связанного параметра."""
|
||||
if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
|
||||
if hasattr(obj, "parameter_obj") and obj.parameter_obj:
|
||||
return obj.parameter_obj.freq_range
|
||||
return "-"
|
||||
|
||||
freq_range.short_description = "Полоса, МГц"
|
||||
freq_range.admin_order_field = "parameter_obj__freq_range"
|
||||
|
||||
def bod_velocity(self, obj):
|
||||
"""Отображает символьную скорость из связанного параметра."""
|
||||
if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
|
||||
if hasattr(obj, "parameter_obj") and obj.parameter_obj:
|
||||
return obj.parameter_obj.bod_velocity
|
||||
return "-"
|
||||
|
||||
bod_velocity.short_description = "Сим. v, БОД"
|
||||
|
||||
def modulation(self, obj):
|
||||
"""Отображает модуляцию из связанного параметра."""
|
||||
if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
|
||||
if hasattr(obj, "parameter_obj") and obj.parameter_obj:
|
||||
if obj.parameter_obj.modulation:
|
||||
return obj.parameter_obj.modulation.name
|
||||
return "-"
|
||||
|
||||
modulation.short_description = "Модуляция"
|
||||
|
||||
def snr(self, obj):
|
||||
"""Отображает отношение сигнал/шум из связанного параметра."""
|
||||
if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
|
||||
if hasattr(obj, "parameter_obj") and obj.parameter_obj:
|
||||
return obj.parameter_obj.snr
|
||||
return "-"
|
||||
|
||||
snr.short_description = "ОСШ"
|
||||
|
||||
def geo_coords(self, obj):
|
||||
@@ -817,6 +886,7 @@ class ObjItemAdmin(BaseAdmin):
|
||||
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||
return f"{lat} {lon}"
|
||||
|
||||
geo_coords.short_description = "Координаты геолокации"
|
||||
geo_coords.admin_order_field = "geo_obj__coords"
|
||||
|
||||
@@ -830,6 +900,7 @@ class ObjItemAdmin(BaseAdmin):
|
||||
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||
return f"{lat} {lon}"
|
||||
|
||||
kupsat_coords.short_description = "Координаты Кубсата"
|
||||
|
||||
def valid_coords(self, obj):
|
||||
@@ -842,12 +913,14 @@ class ObjItemAdmin(BaseAdmin):
|
||||
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||
return f"{lat} {lon}"
|
||||
|
||||
valid_coords.short_description = "Координаты оперативного отдела"
|
||||
|
||||
|
||||
@admin.register(Band)
|
||||
class BandAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
||||
"""Админ-панель для модели Band."""
|
||||
|
||||
list_display = ("name", "border_start", "border_end")
|
||||
search_fields = ("name",)
|
||||
ordering = ("name",)
|
||||
@@ -855,6 +928,7 @@ class BandAdmin(ImportExportActionModelAdmin, BaseAdmin):
|
||||
|
||||
class ObjItemInline(admin.TabularInline):
|
||||
"""Inline для отображения объектов ObjItem в Source."""
|
||||
|
||||
model = ObjItem
|
||||
fk_name = "source"
|
||||
extra = 0
|
||||
@@ -862,22 +936,36 @@ class ObjItemInline(admin.TabularInline):
|
||||
verbose_name = "Объект"
|
||||
verbose_name_plural = "Объекты"
|
||||
|
||||
fields = ("name", "get_geo_coords", "get_satellite", "get_frequency", "get_polarization", "updated_at")
|
||||
readonly_fields = ("name", "get_geo_coords", "get_satellite", "get_frequency", "get_polarization", "updated_at")
|
||||
fields = (
|
||||
"name",
|
||||
"get_geo_coords",
|
||||
"get_satellite",
|
||||
"get_frequency",
|
||||
"get_polarization",
|
||||
"updated_at",
|
||||
)
|
||||
readonly_fields = (
|
||||
"name",
|
||||
"get_geo_coords",
|
||||
"get_satellite",
|
||||
"get_frequency",
|
||||
"get_polarization",
|
||||
"updated_at",
|
||||
)
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Оптимизированный queryset с предзагрузкой связанных объектов."""
|
||||
qs = super().get_queryset(request)
|
||||
return qs.select_related(
|
||||
'geo_obj',
|
||||
'parameter_obj',
|
||||
'parameter_obj__id_satellite',
|
||||
'parameter_obj__polarization'
|
||||
"geo_obj",
|
||||
"parameter_obj",
|
||||
"parameter_obj__id_satellite",
|
||||
"parameter_obj__polarization",
|
||||
)
|
||||
|
||||
def get_geo_coords(self, obj):
|
||||
"""Отображает координаты из связанной модели Geo."""
|
||||
if not obj or not hasattr(obj, 'geo_obj'):
|
||||
if not obj or not hasattr(obj, "geo_obj"):
|
||||
return "-"
|
||||
geo = obj.geo_obj
|
||||
if not geo or not geo.coords:
|
||||
@@ -887,27 +975,39 @@ class ObjItemInline(admin.TabularInline):
|
||||
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||
return f"{lat} {lon}"
|
||||
|
||||
get_geo_coords.short_description = "Координаты"
|
||||
|
||||
def get_satellite(self, obj):
|
||||
"""Отображает спутник из связанного параметра."""
|
||||
if hasattr(obj, 'parameter_obj') and obj.parameter_obj and obj.parameter_obj.id_satellite:
|
||||
if (
|
||||
hasattr(obj, "parameter_obj")
|
||||
and obj.parameter_obj
|
||||
and obj.parameter_obj.id_satellite
|
||||
):
|
||||
return obj.parameter_obj.id_satellite.name
|
||||
return "-"
|
||||
|
||||
get_satellite.short_description = "Спутник"
|
||||
|
||||
def get_frequency(self, obj):
|
||||
"""Отображает частоту из связанного параметра."""
|
||||
if hasattr(obj, 'parameter_obj') and obj.parameter_obj:
|
||||
if hasattr(obj, "parameter_obj") and obj.parameter_obj:
|
||||
return obj.parameter_obj.frequency
|
||||
return "-"
|
||||
|
||||
get_frequency.short_description = "Частота, МГц"
|
||||
|
||||
def get_polarization(self, obj):
|
||||
"""Отображает поляризацию из связанного параметра."""
|
||||
if hasattr(obj, 'parameter_obj') and obj.parameter_obj and obj.parameter_obj.polarization:
|
||||
if (
|
||||
hasattr(obj, "parameter_obj")
|
||||
and obj.parameter_obj
|
||||
and obj.parameter_obj.polarization
|
||||
):
|
||||
return obj.parameter_obj.polarization.name
|
||||
return "-"
|
||||
|
||||
get_polarization.short_description = "Поляризация"
|
||||
|
||||
def has_add_permission(self, request, obj=None):
|
||||
@@ -917,6 +1017,7 @@ class ObjItemInline(admin.TabularInline):
|
||||
@admin.register(Source)
|
||||
class SourceAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
||||
"""Админ-панель для модели Source."""
|
||||
|
||||
list_display = ("id", "created_at", "updated_at")
|
||||
list_filter = (
|
||||
("created_at", DateRangeQuickSelectListFilterBuilder()),
|
||||
@@ -927,11 +1028,15 @@ class SourceAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin, BaseAdmin):
|
||||
inlines = [ObjItemInline]
|
||||
|
||||
fieldsets = (
|
||||
("Координаты: геолокация", {
|
||||
"fields": ("coords_kupsat", "coords_valid", "coords_reference")
|
||||
}),
|
||||
("Метаданные", {
|
||||
(
|
||||
"Координаты: геолокация",
|
||||
{"fields": ("coords_kupsat", "coords_valid", "coords_reference")},
|
||||
),
|
||||
(
|
||||
"Метаданные",
|
||||
{
|
||||
"fields": ("created_at", "created_by", "updated_at", "updated_by"),
|
||||
"classes": ("collapse",)
|
||||
}),
|
||||
"classes": ("collapse",),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@ from .models import (
|
||||
Parameter,
|
||||
Polarization,
|
||||
Satellite,
|
||||
Source,
|
||||
Standard,
|
||||
)
|
||||
from .widgets import CheckboxSelectMultipleWidget
|
||||
@@ -305,8 +306,8 @@ class GeoForm(forms.ModelForm):
|
||||
"is_average": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||
"mirrors": CheckboxSelectMultipleWidget(
|
||||
attrs={
|
||||
'id': 'id_geo-mirrors',
|
||||
'placeholder': 'Выберите спутники...',
|
||||
"id": "id_geo-mirrors",
|
||||
"placeholder": "Выберите спутники...",
|
||||
}
|
||||
),
|
||||
}
|
||||
@@ -372,3 +373,160 @@ class ObjItemForm(forms.ModelForm):
|
||||
)
|
||||
|
||||
return name
|
||||
|
||||
|
||||
class SourceForm(forms.ModelForm):
|
||||
"""Form for editing Source model with 4 coordinate fields."""
|
||||
|
||||
# Координаты ГЛ (coords_average)
|
||||
average_latitude = forms.FloatField(
|
||||
required=False,
|
||||
widget=forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.000001", "placeholder": "Широта"}
|
||||
),
|
||||
label="Широта ГЛ",
|
||||
)
|
||||
average_longitude = forms.FloatField(
|
||||
required=False,
|
||||
widget=forms.NumberInput(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
"step": "0.000001",
|
||||
"placeholder": "Долгота",
|
||||
}
|
||||
),
|
||||
label="Долгота ГЛ",
|
||||
)
|
||||
|
||||
# Координаты Кубсата (coords_kupsat)
|
||||
kupsat_latitude = forms.FloatField(
|
||||
required=False,
|
||||
widget=forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.000001", "placeholder": "Широта"}
|
||||
),
|
||||
label="Широта Кубсата",
|
||||
)
|
||||
kupsat_longitude = forms.FloatField(
|
||||
required=False,
|
||||
widget=forms.NumberInput(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
"step": "0.000001",
|
||||
"placeholder": "Долгота",
|
||||
}
|
||||
),
|
||||
label="Долгота Кубсата",
|
||||
)
|
||||
|
||||
# Координаты оперативников (coords_valid)
|
||||
valid_latitude = forms.FloatField(
|
||||
required=False,
|
||||
widget=forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.000001", "placeholder": "Широта"}
|
||||
),
|
||||
label="Широта оперативников",
|
||||
)
|
||||
valid_longitude = forms.FloatField(
|
||||
required=False,
|
||||
widget=forms.NumberInput(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
"step": "0.000001",
|
||||
"placeholder": "Долгота",
|
||||
}
|
||||
),
|
||||
label="Долгота оперативников",
|
||||
)
|
||||
|
||||
# Координаты справочные (coords_reference)
|
||||
reference_latitude = forms.FloatField(
|
||||
required=False,
|
||||
widget=forms.NumberInput(
|
||||
attrs={"class": "form-control", "step": "0.000001", "placeholder": "Широта"}
|
||||
),
|
||||
label="Широта справочные",
|
||||
)
|
||||
reference_longitude = forms.FloatField(
|
||||
required=False,
|
||||
widget=forms.NumberInput(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
"step": "0.000001",
|
||||
"placeholder": "Долгота",
|
||||
}
|
||||
),
|
||||
label="Долгота справочные",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Source
|
||||
fields = [] # Все поля обрабатываются вручную
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Заполняем поля координат из instance
|
||||
if self.instance and self.instance.pk:
|
||||
if self.instance.coords_average:
|
||||
self.fields[
|
||||
"average_longitude"
|
||||
].initial = self.instance.coords_average.x
|
||||
self.fields["average_latitude"].initial = self.instance.coords_average.y
|
||||
|
||||
if self.instance.coords_kupsat:
|
||||
self.fields["kupsat_longitude"].initial = self.instance.coords_kupsat.x
|
||||
self.fields["kupsat_latitude"].initial = self.instance.coords_kupsat.y
|
||||
|
||||
if self.instance.coords_valid:
|
||||
self.fields["valid_longitude"].initial = self.instance.coords_valid.x
|
||||
self.fields["valid_latitude"].initial = self.instance.coords_valid.y
|
||||
|
||||
if self.instance.coords_reference:
|
||||
self.fields[
|
||||
"reference_longitude"
|
||||
].initial = self.instance.coords_reference.x
|
||||
self.fields[
|
||||
"reference_latitude"
|
||||
].initial = self.instance.coords_reference.y
|
||||
|
||||
def save(self, commit=True):
|
||||
from django.contrib.gis.geos import Point
|
||||
|
||||
instance = super().save(commit=False)
|
||||
|
||||
# Обработка coords_average
|
||||
avg_lat = self.cleaned_data.get("average_latitude")
|
||||
avg_lng = self.cleaned_data.get("average_longitude")
|
||||
if avg_lat is not None and avg_lng is not None:
|
||||
instance.coords_average = Point(avg_lng, avg_lat, srid=4326)
|
||||
else:
|
||||
instance.coords_average = None
|
||||
|
||||
# Обработка coords_kupsat
|
||||
kup_lat = self.cleaned_data.get("kupsat_latitude")
|
||||
kup_lng = self.cleaned_data.get("kupsat_longitude")
|
||||
if kup_lat is not None and kup_lng is not None:
|
||||
instance.coords_kupsat = Point(kup_lng, kup_lat, srid=4326)
|
||||
else:
|
||||
instance.coords_kupsat = None
|
||||
|
||||
# Обработка coords_valid
|
||||
val_lat = self.cleaned_data.get("valid_latitude")
|
||||
val_lng = self.cleaned_data.get("valid_longitude")
|
||||
if val_lat is not None and val_lng is not None:
|
||||
instance.coords_valid = Point(val_lng, val_lat, srid=4326)
|
||||
else:
|
||||
instance.coords_valid = None
|
||||
|
||||
# Обработка coords_reference
|
||||
ref_lat = self.cleaned_data.get("reference_latitude")
|
||||
ref_lng = self.cleaned_data.get("reference_longitude")
|
||||
if ref_lat is not None and ref_lng is not None:
|
||||
instance.coords_reference = Point(ref_lng, ref_lat, srid=4326)
|
||||
else:
|
||||
instance.coords_reference = None
|
||||
|
||||
if commit:
|
||||
instance.save()
|
||||
|
||||
return instance
|
||||
|
||||
@@ -22,24 +22,25 @@
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=0 column_label="Выбрать" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=1 column_label="Имя" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=2 column_label="Спутник" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=3 column_label="Част, МГц" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=4 column_label="Полоса, МГц" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=5 column_label="Поляризация" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=6 column_label="Сим. V" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=7 column_label="Модул" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=8 column_label="ОСШ" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=9 column_label="Время ГЛ" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=10 column_label="Местоположение" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=11 column_label="Геолокация" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=12 column_label="Обновлено" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=13 column_label="Кем (обновление)" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=14 column_label="Создано" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=15 column_label="Кем (создание)" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=16 column_label="Комментарий" checked=False %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=17 column_label="Усреднённое" checked=False %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=18 column_label="Стандарт" checked=False %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=19 column_label="Тип источника" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=20 column_label="Sigma" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=21 column_label="Зеркала" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=3 column_label="Транспондер" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=4 column_label="Част, МГц" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=5 column_label="Полоса, МГц" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=6 column_label="Поляризация" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=7 column_label="Сим. V" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=8 column_label="Модул" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=9 column_label="ОСШ" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=10 column_label="Время ГЛ" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=11 column_label="Местоположение" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=12 column_label="Геолокация" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=13 column_label="Обновлено" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=14 column_label="Кем (обновление)" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=15 column_label="Создано" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=16 column_label="Кем (создание)" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=17 column_label="Комментарий" checked=False %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=18 column_label="Усреднённое" checked=False %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=19 column_label="Стандарт" checked=False %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=20 column_label="Тип источника" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=21 column_label="Sigma" checked=True %}
|
||||
{% include 'mainapp/components/_column_toggle_item.html' with column_index=22 column_label="Зеркала" checked=True %}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -32,6 +32,7 @@
|
||||
</th>
|
||||
<th scope="col">Имя</th>
|
||||
<th scope="col">Спутник</th>
|
||||
<th scope="col">Транспондер</th>
|
||||
<th scope="col">Част, МГц</th>
|
||||
<th scope="col">Полоса, МГц</th>
|
||||
<th scope="col">Поляризация</th>
|
||||
@@ -41,8 +42,6 @@
|
||||
<th scope="col">Время ГЛ</th>
|
||||
<th scope="col">Местоположение</th>
|
||||
<th scope="col">Геолокация</th>
|
||||
<th scope="col">Кубсат</th>
|
||||
<th scope="col">Опер. отд</th>
|
||||
<th scope="col">Обновлено</th>
|
||||
<th scope="col">Кем(обн)</th>
|
||||
<th scope="col">Создано</th>
|
||||
|
||||
@@ -70,7 +70,8 @@
|
||||
|
||||
<!-- Filter Toggle Button -->
|
||||
<div>
|
||||
<button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvasFilters" aria-controls="offcanvasFilters">
|
||||
<button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="offcanvas"
|
||||
data-bs-target="#offcanvasFilters" aria-controls="offcanvasFilters">
|
||||
<i class="bi bi-funnel"></i> Фильтры
|
||||
<span id="filterCounter" class="badge bg-danger" style="display: none;">0</span>
|
||||
</button>
|
||||
@@ -85,7 +86,8 @@
|
||||
|
||||
<!-- Selected Items Counter Button -->
|
||||
<div>
|
||||
<button class="btn btn-outline-info btn-sm" type="button" data-bs-toggle="offcanvas" data-bs-target="#selectedItemsOffcanvas" aria-controls="selectedItemsOffcanvas">
|
||||
<button class="btn btn-outline-info btn-sm" type="button" data-bs-toggle="offcanvas"
|
||||
data-bs-target="#selectedItemsOffcanvas" aria-controls="selectedItemsOffcanvas">
|
||||
<i class="bi bi-list-check"></i> Список
|
||||
<span id="selectedCounter" class="badge bg-info" style="display: none;">0</span>
|
||||
</button>
|
||||
@@ -202,52 +204,19 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Kubsat Coordinates Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Координаты Кубсата:</label>
|
||||
<div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_kupsat" id="has_kupsat_1"
|
||||
value="1" {% if has_kupsat == '1' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_kupsat_1">Есть</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_kupsat" id="has_kupsat_0"
|
||||
value="0" {% if has_kupsat == '0' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_kupsat_0">Нет</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Valid Coordinates Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Координаты опер. отдела:</label>
|
||||
<div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_valid" id="has_valid_1"
|
||||
value="1" {% if has_valid == '1' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_valid_1">Есть</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_valid" id="has_valid_0"
|
||||
value="0" {% if has_valid == '0' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_valid_0">Нет</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Source Type Filter -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Тип источника:</label>
|
||||
<div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_source_type" id="has_source_type_1"
|
||||
value="1" {% if has_source_type == '1' %}checked{% endif %}>
|
||||
<input class="form-check-input" type="checkbox" name="has_source_type"
|
||||
id="has_source_type_1" value="1" {% if has_source_type == '1' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_source_type_1">Есть (ТВ)</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_source_type" id="has_source_type_0"
|
||||
value="0" {% if has_source_type == '0' %}checked{% endif %}>
|
||||
<input class="form-check-input" type="checkbox" name="has_source_type"
|
||||
id="has_source_type_0" value="0" {% if has_source_type == '0' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_source_type_0">Нет</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -258,13 +227,13 @@
|
||||
<label class="form-label">Sigma:</label>
|
||||
<div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_sigma" id="has_sigma_1"
|
||||
value="1" {% if has_sigma == '1' %}checked{% endif %}>
|
||||
<input class="form-check-input" type="checkbox" name="has_sigma" id="has_sigma_1" value="1"
|
||||
{% if has_sigma == '1' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_sigma_1">Есть</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="has_sigma" id="has_sigma_0"
|
||||
value="0" {% if has_sigma == '0' %}checked{% endif %}>
|
||||
<input class="form-check-input" type="checkbox" name="has_sigma" id="has_sigma_0" value="0"
|
||||
{% if has_sigma == '0' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="has_sigma_0">Нет</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -289,8 +258,8 @@
|
||||
</div>
|
||||
<input type="date" name="date_from" id="date_from" class="form-control form-control-sm mb-1"
|
||||
placeholder="От" value="{{ date_from|default:'' }}">
|
||||
<input type="date" name="date_to" id="date_to" class="form-control form-control-sm"
|
||||
placeholder="До" value="{{ date_to|default:'' }}">
|
||||
<input type="date" name="date_to" id="date_to" class="form-control form-control-sm" placeholder="До"
|
||||
value="{{ date_to|default:'' }}">
|
||||
</div>
|
||||
|
||||
<!-- Apply Filters and Reset Buttons -->
|
||||
@@ -316,6 +285,7 @@
|
||||
</th>
|
||||
{% include 'mainapp/components/_table_header.html' with label="Имя" field="name" sort=sort %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Спутник" field="satellite" sort=sort %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Транспондер" field="" sortable=False %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Част, МГц" field="frequency" sort=sort %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Полоса, МГц" field="freq_range" sort=sort %}
|
||||
{% include 'mainapp/components/_table_header.html' with label="Поляризация" field="polarization" sort=sort %}
|
||||
@@ -341,11 +311,22 @@
|
||||
{% for item in processed_objects %}
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" class="form-check-input item-checkbox"
|
||||
value="{{ item.id }}">
|
||||
<input type="checkbox" class="form-check-input item-checkbox" value="{{ item.id }}">
|
||||
</td>
|
||||
<td><a href="{% if item.obj.id %}{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}{% url 'mainapp:objitem_update' item.obj.id %}?{{ request.GET.urlencode }}{% else %}{% url 'mainapp:objitem_detail' item.obj.id %}?{{ request.GET.urlencode }}{% endif %}{% endif %}">{{ item.name }}</a></td>
|
||||
<td>
|
||||
<a href="{% if item.obj.id %}{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}{% url 'mainapp:objitem_update' item.obj.id %}?{{ request.GET.urlencode }}{% else %}{% url 'mainapp:objitem_detail' item.obj.id %}?{{ request.GET.urlencode }}{% endif %}{% endif %}">{{ item.name }}</a></td>
|
||||
<td>{{ item.satellite_name }}</td>
|
||||
<td>
|
||||
{% if item.obj.transponder %}
|
||||
<a href="#" class="text-success text-decoration-none"
|
||||
onclick="showTransponderModal({{ item.obj.transponder.id }}); return false;"
|
||||
title="Показать данные транспондера">
|
||||
<i class="bi bi-broadcast"></i> {{ item.obj.transponder.downlink }}:{{ item.obj.transponder.frequency_range }}
|
||||
</a>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ item.frequency }}</td>
|
||||
<td>{{ item.freq_range }}</td>
|
||||
<td>{{ item.polarization }}</td>
|
||||
@@ -364,7 +345,8 @@
|
||||
<td>{{ item.standard }}</td>
|
||||
<td>
|
||||
{% if item.obj.lyngsat_source %}
|
||||
<a href="#" class="text-primary text-decoration-none" onclick="showLyngsatModal({{ item.obj.lyngsat_source.id }}); return false;">
|
||||
<a href="#" class="text-primary text-decoration-none"
|
||||
onclick="showLyngsatModal({{ item.obj.lyngsat_source.id }}); return false;">
|
||||
<i class="bi bi-tv"></i> ТВ
|
||||
</a>
|
||||
{% else %}
|
||||
@@ -373,7 +355,9 @@
|
||||
</td>
|
||||
<td>
|
||||
{% if item.has_sigma %}
|
||||
<a href="#" class="text-info text-decoration-none" onclick="showSigmaParameterModal({{ item.obj.parameter_obj.id }}); return false;" title="{{ item.sigma_info }}">
|
||||
<a href="#" class="text-info text-decoration-none"
|
||||
onclick="showSigmaParameterModal({{ item.obj.parameter_obj.id }}); return false;"
|
||||
title="{{ item.sigma_info }}">
|
||||
<i class="bi bi-graph-up"></i> {{ item.sigma_info }}
|
||||
</a>
|
||||
{% else %}
|
||||
@@ -720,8 +704,8 @@
|
||||
|
||||
// Initialize column visibility - hide creation columns by default
|
||||
function initColumnVisibility() {
|
||||
const creationDateCheckbox = document.querySelector('input[data-column="14"]');
|
||||
const creationUserCheckbox = document.querySelector('input[data-column="15"]');
|
||||
const creationDateCheckbox = document.querySelector('input[data-column="15"]');
|
||||
const creationUserCheckbox = document.querySelector('input[data-column="16"]');
|
||||
if (creationDateCheckbox) {
|
||||
creationDateCheckbox.checked = false;
|
||||
toggleColumn(creationDateCheckbox);
|
||||
@@ -733,9 +717,9 @@
|
||||
}
|
||||
|
||||
// Hide comment, is_average, and standard columns by default
|
||||
const commentCheckbox = document.querySelector('input[data-column="16"]');
|
||||
const isAverageCheckbox = document.querySelector('input[data-column="17"]');
|
||||
const standardCheckbox = document.querySelector('input[data-column="18"]');
|
||||
const commentCheckbox = document.querySelector('input[data-column="17"]');
|
||||
const isAverageCheckbox = document.querySelector('input[data-column="18"]');
|
||||
const standardCheckbox = document.querySelector('input[data-column="19"]');
|
||||
|
||||
if (commentCheckbox) {
|
||||
commentCheckbox.checked = false;
|
||||
@@ -904,22 +888,21 @@
|
||||
id: itemId,
|
||||
name: row.cells[1].textContent,
|
||||
satellite: row.cells[2].textContent,
|
||||
frequency: row.cells[3].textContent,
|
||||
freq_range: row.cells[4].textContent,
|
||||
polarization: row.cells[5].textContent,
|
||||
bod_velocity: row.cells[6].textContent,
|
||||
modulation: row.cells[7].textContent,
|
||||
snr: row.cells[8].textContent,
|
||||
geo_timestamp: row.cells[9].textContent,
|
||||
geo_location: row.cells[10].textContent,
|
||||
geo_coords: row.cells[11].textContent,
|
||||
kupsat_coords: row.cells[12].textContent,
|
||||
valid_coords: row.cells[13].textContent,
|
||||
updated_at: row.cells[12].textContent,
|
||||
updated_by: row.cells[13].textContent,
|
||||
created_at: row.cells[14].textContent,
|
||||
created_by: row.cells[15].textContent,
|
||||
mirrors: row.cells[21].textContent
|
||||
transponder: row.cells[3].textContent,
|
||||
frequency: row.cells[4].textContent,
|
||||
freq_range: row.cells[5].textContent,
|
||||
polarization: row.cells[6].textContent,
|
||||
bod_velocity: row.cells[7].textContent,
|
||||
modulation: row.cells[8].textContent,
|
||||
snr: row.cells[9].textContent,
|
||||
geo_timestamp: row.cells[10].textContent,
|
||||
geo_location: row.cells[11].textContent,
|
||||
geo_coords: row.cells[12].textContent,
|
||||
updated_at: row.cells[13].textContent,
|
||||
updated_by: row.cells[14].textContent,
|
||||
created_at: row.cells[15].textContent,
|
||||
created_by: row.cells[16].textContent,
|
||||
mirrors: row.cells[22].textContent
|
||||
};
|
||||
|
||||
window.selectedItems.push(rowData);
|
||||
@@ -966,6 +949,7 @@
|
||||
</td>
|
||||
<td>${item.name}</td>
|
||||
<td>${item.satellite}</td>
|
||||
<td>${item.transponder}</td>
|
||||
<td>${item.frequency}</td>
|
||||
<td>${item.freq_range}</td>
|
||||
<td>${item.polarization}</td>
|
||||
@@ -975,8 +959,6 @@
|
||||
<td>${item.geo_timestamp}</td>
|
||||
<td>${item.geo_location}</td>
|
||||
<td>${item.geo_coords}</td>
|
||||
<td>${item.kupsat_coords}</td>
|
||||
<td>${item.valid_coords}</td>
|
||||
<td>${item.updated_at}</td>
|
||||
<td>${item.updated_by}</td>
|
||||
<td>${item.created_at}</td>
|
||||
@@ -1056,7 +1038,8 @@
|
||||
<h5 class="modal-title" id="lyngsatModalLabel">
|
||||
<i class="bi bi-tv"></i> Данные источника LyngSat
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
|
||||
aria-label="Закрыть"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="lyngsatModalBody">
|
||||
<div class="text-center py-4">
|
||||
@@ -1202,5 +1185,128 @@ function showLyngsatModal(lyngsatId) {
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
// Function to show transponder modal
|
||||
function showTransponderModal(transponderId) {
|
||||
const modal = new bootstrap.Modal(document.getElementById('transponderModal'));
|
||||
modal.show();
|
||||
|
||||
const modalBody = document.getElementById('transponderModalBody');
|
||||
modalBody.innerHTML = `
|
||||
<div class="text-center py-4">
|
||||
<div class="spinner-border text-success" role="status">
|
||||
<span class="visually-hidden">Загрузка...</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
fetch(`/api/transponder/${transponderId}/`)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Ошибка загрузки данных транспондера');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
let html = `
|
||||
<div class="container-fluid">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header bg-light">
|
||||
<strong><i class="bi bi-info-circle"></i> Основная информация</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm table-borderless mb-0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-muted" style="width: 40%;">Название:</td>
|
||||
<td><strong>${data.name || '-'}</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted">Спутник:</td>
|
||||
<td><strong>${data.satellite}</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted">Зона покрытия:</td>
|
||||
<td>${data.zone_name || '-'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted">Поляризация:</td>
|
||||
<td><span class="badge bg-info">${data.polarization}</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header bg-light">
|
||||
<strong><i class="bi bi-broadcast"></i> Частотные параметры</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm table-borderless mb-0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-muted" style="width: 40%;">Downlink:</td>
|
||||
<td><strong>${data.downlink} МГц</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted">Uplink:</td>
|
||||
<td><strong>${data.uplink || '-'} ${data.uplink ? 'МГц' : ''}</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted">Полоса:</td>
|
||||
<td><strong>${data.frequency_range} МГц</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted">Перенос:</td>
|
||||
<td>${data.transfer || '-'} ${data.transfer ? 'МГц' : ''}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
modalBody.innerHTML = html;
|
||||
})
|
||||
.catch(error => {
|
||||
modalBody.innerHTML = `
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<i class="bi bi-exclamation-triangle"></i> ${error.message}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Transponder Data Modal -->
|
||||
<div class="modal fade" id="transponderModal" tabindex="-1" aria-labelledby="transponderModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-success text-white">
|
||||
<h5 class="modal-title" id="transponderModalLabel">
|
||||
<i class="bi bi-broadcast"></i> Данные транспондера
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="transponderModalBody">
|
||||
<div class="text-center py-4">
|
||||
<div class="spinner-border text-success" role="status">
|
||||
<span class="visually-hidden">Загрузка...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,10 +1,74 @@
|
||||
{% extends "mapsapp/map2d_base.html" %}
|
||||
{% extends "mainapp/base.html" %}
|
||||
{% load static %}
|
||||
{% block title %}Карта выбранных объектов{% endblock title %}
|
||||
|
||||
{% block extra_css %}
|
||||
<!-- Leaflet CSS -->
|
||||
<link href="{% static 'leaflet/leaflet.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'leaflet-measure/leaflet-measure.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'leaflet-tree/L.Control.Layers.Tree.css' %}" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
#map {
|
||||
position: fixed;
|
||||
top: 56px; /* Высота navbar */
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="map"></div>
|
||||
{% endblock content %}
|
||||
|
||||
{% block extra_js %}
|
||||
<!-- Leaflet JavaScript -->
|
||||
<script src="{% static 'leaflet/leaflet.js' %}"></script>
|
||||
<script src="{% static 'leaflet-measure/leaflet-measure.ru.js' %}"></script>
|
||||
<script src="{% static 'leaflet-tree/L.Control.Layers.Tree.js' %}"></script>
|
||||
|
||||
<script>
|
||||
// Цвета для стандартных маркеров (из leaflet-color-markers)
|
||||
// Инициализация карты
|
||||
let map = L.map('map').setView([55.75, 37.62], 5);
|
||||
L.control.scale({
|
||||
imperial: false,
|
||||
metric: true
|
||||
}).addTo(map);
|
||||
map.attributionControl.setPrefix(false);
|
||||
|
||||
const street = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
});
|
||||
street.addTo(map);
|
||||
|
||||
const satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
||||
attribution: 'Tiles © Esri'
|
||||
});
|
||||
|
||||
const street_local = L.tileLayer('http://127.0.0.1:8080/styles/basic-preview/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: 'Local Tiles'
|
||||
});
|
||||
|
||||
const baseLayers = {
|
||||
"Улицы": street,
|
||||
"Спутник": satellite,
|
||||
"Локально": street_local
|
||||
};
|
||||
|
||||
L.control.layers(baseLayers).addTo(map);
|
||||
map.setMaxZoom(18);
|
||||
map.setMinZoom(0);
|
||||
L.control.measure({ primaryLengthUnit: 'kilometers' }).addTo(map);
|
||||
|
||||
// Цвета для маркеров
|
||||
var markerColors = ['red', 'green', 'orange', 'violet', 'grey', 'black', 'blue'];
|
||||
var getColorIcon = function(color) {
|
||||
return L.icon({
|
||||
@@ -19,47 +83,52 @@
|
||||
|
||||
var overlays = [];
|
||||
|
||||
// Создаём слои для каждого объекта
|
||||
{% for group in groups %}
|
||||
var groupIndex = {{ forloop.counter0 }};
|
||||
var groupName = '{{ group.name|escapejs }}';
|
||||
var colorName = markerColors[groupIndex % markerColors.length];
|
||||
var groupIcon = getColorIcon(colorName);
|
||||
|
||||
var groupLayer = L.layerGroup();
|
||||
|
||||
var subgroup = [];
|
||||
{% for point_data in group.points %}
|
||||
var pointName = "{{ group.name|escapejs }}";
|
||||
|
||||
var marker = L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}], {
|
||||
icon: groupIcon
|
||||
}).bindPopup(pointName + '<br>' + "{{ point_data.frequency|escapejs }}");
|
||||
|
||||
groupLayer.addLayer(marker);
|
||||
|
||||
subgroup.push({
|
||||
label: "{{ forloop.counter }} - {{ point_data.frequency }}",
|
||||
label: "{{ forloop.counter }} - {{ point_data.frequency|escapejs }}",
|
||||
layer: marker
|
||||
});
|
||||
{% endfor %}
|
||||
|
||||
overlays.push({
|
||||
label: '{{ group.name|escapejs }}',
|
||||
label: groupName,
|
||||
selectAllCheckbox: true,
|
||||
children: subgroup,
|
||||
layer: groupLayer
|
||||
});
|
||||
{% endfor %}
|
||||
|
||||
// Create the layer control with a custom container that includes a select all checkbox
|
||||
var layerControl = L.control.layers.tree(baseLayers, overlays, {
|
||||
// Корневая группа
|
||||
const rootGroup = {
|
||||
label: "Все точки",
|
||||
selectAllCheckbox: true,
|
||||
children: overlays,
|
||||
layer: L.layerGroup()
|
||||
};
|
||||
|
||||
// Создаём tree control
|
||||
const layerControl = L.control.layers.tree(baseLayers, [rootGroup], {
|
||||
collapsed: false,
|
||||
autoZIndex: true
|
||||
});
|
||||
|
||||
// Add the layer control to the map
|
||||
layerControl.addTo(map);
|
||||
|
||||
// Calculate map bounds to fit all markers
|
||||
// Подгоняем карту под все маркеры
|
||||
{% if groups %}
|
||||
var groupBounds = L.featureGroup([]);
|
||||
{% for group in groups %}
|
||||
@@ -67,40 +136,7 @@
|
||||
groupBounds.addLayer(L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}]));
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
map.fitBounds(groupBounds.getBounds().pad(0.1)); // Add some padding
|
||||
{% else %}
|
||||
map.setView([55.75, 37.62], 5); // Default view if no markers
|
||||
map.fitBounds(groupBounds.getBounds().pad(0.1));
|
||||
{% endif %}
|
||||
|
||||
// Add a "Select All" checkbox functionality for all overlays
|
||||
setTimeout(function() {
|
||||
// Create a custom "select all" checkbox
|
||||
var selectAllContainer = document.createElement('div');
|
||||
selectAllContainer.className = 'leaflet-control-layers-select-all';
|
||||
selectAllContainer.style.padding = '5px';
|
||||
selectAllContainer.style.borderBottom = '1px solid #ccc';
|
||||
selectAllContainer.style.marginBottom = '5px';
|
||||
selectAllContainer.innerHTML = '<label><input type="checkbox" id="select-all-overlays" checked> Показать все точки</label>';
|
||||
|
||||
// Insert the checkbox at the top of the layer control
|
||||
var layerControlContainer = document.querySelector('.leaflet-control-layers-list');
|
||||
if (layerControlContainer) {
|
||||
layerControlContainer.insertBefore(selectAllContainer, layerControlContainer.firstChild);
|
||||
}
|
||||
|
||||
// Add event listener to the "select all" checkbox
|
||||
document.getElementById('select-all-overlays').addEventListener('change', function() {
|
||||
var isChecked = this.checked;
|
||||
|
||||
// Iterate through all overlays and toggle visibility
|
||||
for (var i = 0; i < overlays.length; i++) {
|
||||
if (isChecked) {
|
||||
map.addLayer(overlays[i].layer);
|
||||
} else {
|
||||
map.removeLayer(overlays[i].layer);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 500); // Slight delay to ensure the tree control has been rendered
|
||||
</script>
|
||||
{% endblock extra_js %}
|
||||
63
dbapp/mainapp/templates/mainapp/source_confirm_delete.html
Normal file
63
dbapp/mainapp/templates/mainapp/source_confirm_delete.html
Normal file
@@ -0,0 +1,63 @@
|
||||
{% extends 'mainapp/base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Удалить источник #{{ object.id }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<h4 class="mb-0">Подтверждение удаления</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<i class="bi bi-exclamation-triangle-fill"></i>
|
||||
<strong>Внимание!</strong> Вы собираетесь удалить источник.
|
||||
</div>
|
||||
|
||||
<h5>Информация об источнике:</h5>
|
||||
<ul class="list-group mb-3">
|
||||
<li class="list-group-item">
|
||||
<strong>ID:</strong> {{ object.id }}
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>Дата создания:</strong>
|
||||
{% if object.created_at %}{{ object.created_at|date:"d.m.Y H:i" }}{% else %}-{% endif %}
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>Создан пользователем:</strong>
|
||||
{% if object.created_by %}{{ object.created_by }}{% else %}-{% endif %}
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>Привязанных объектов:</strong>
|
||||
<span class="badge bg-primary">{{ objitems_count }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{% if objitems_count > 0 %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<i class="bi bi-exclamation-circle-fill"></i>
|
||||
<strong>Важно!</strong> При удалении источника будут также удалены все {{ objitems_count }} привязанных объектов!
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<p class="text-muted">Это действие нельзя отменить. Вы уверены, что хотите продолжить?</p>
|
||||
|
||||
<form method="post" class="d-inline">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="bi bi-trash"></i> Да, удалить
|
||||
</button>
|
||||
<a href="{% url 'mainapp:source_update' object.id %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}"
|
||||
class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Отмена
|
||||
</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
652
dbapp/mainapp/templates/mainapp/source_form.html
Normal file
652
dbapp/mainapp/templates/mainapp/source_form.html
Normal file
@@ -0,0 +1,652 @@
|
||||
{% extends 'mainapp/base.html' %}
|
||||
{% load static %}
|
||||
{% load static leaflet_tags %}
|
||||
{% load l10n %}
|
||||
|
||||
{% block title %}Редактировать источник #{{ object.id }}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.form-section {
|
||||
margin-bottom: 2rem;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.25rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.form-section-header {
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
padding-bottom: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.btn-action {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.readonly-field {
|
||||
background-color: #f8f9fa;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.coord-group {
|
||||
border: 1px solid #dee2e6;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.coord-group-header {
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
#map {
|
||||
height: 500px;
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.map-container {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.map-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.map-control-btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: 1px solid #ced4da;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.map-control-btn.active {
|
||||
background-color: #e9ecef;
|
||||
border-color: #dee2e6;
|
||||
}
|
||||
|
||||
.map-control-btn.edit {
|
||||
background-color: #fff3cd;
|
||||
border-color: #ffeeba;
|
||||
}
|
||||
|
||||
.map-control-btn.save {
|
||||
background-color: #d1ecf1;
|
||||
border-color: #bee5eb;
|
||||
}
|
||||
|
||||
.map-control-btn.cancel {
|
||||
background-color: #f8d7da;
|
||||
border-color: #f5c6cb;
|
||||
}
|
||||
|
||||
.leaflet-marker-icon {
|
||||
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.3));
|
||||
}
|
||||
|
||||
.objitems-table {
|
||||
width: 100%;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.objitems-table th {
|
||||
background-color: #f8f9fa;
|
||||
font-weight: 600;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.objitems-table td {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.objitems-table tbody tr:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.objitem-link {
|
||||
color: #0d6efd;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.objitem-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-3">
|
||||
<div class="row mb-3">
|
||||
<div class="col-12 d-flex justify-content-between align-items-center">
|
||||
<h2>Редактировать источник #{{ object.id }}</h2>
|
||||
<div>
|
||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
||||
<button type="submit" form="source-form" class="btn btn-primary btn-action">Сохранить</button>
|
||||
<a href="{% url 'mainapp:source_delete' object.id %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}"
|
||||
class="btn btn-danger btn-action">Удалить</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'mainapp:home' %}{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}"
|
||||
class="btn btn-secondary btn-action">Назад</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" id="source-form">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Основная информация -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-header">
|
||||
<h4>Основная информация</h4>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">ID источника:</label>
|
||||
<div class="readonly-field">{{ object.id }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Дата создания:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.created_at %}{{ object.created_at|date:"d.m.Y H:i" }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Создан пользователем:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.created_by %}{{ object.created_by }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Дата последнего изменения:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.updated_at %}{{ object.updated_at|date:"d.m.Y H:i" }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Изменен пользователем:</label>
|
||||
<div class="readonly-field">
|
||||
{% if object.updated_by %}{{ object.updated_by }}{% else %}-{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Блок с картой -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-header">
|
||||
<h4>Карта</h4>
|
||||
</div>
|
||||
<div class="map-container">
|
||||
<div id="map"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Координаты -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-header">
|
||||
<h4>Координаты</h4>
|
||||
</div>
|
||||
|
||||
<!-- Координаты ГЛ -->
|
||||
<div class="coord-group">
|
||||
<div class="coord-group-header">Координаты ГЛ (усреднённые)</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="id_average_latitude" class="form-label">Широта:</label>
|
||||
{{ form.average_latitude }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="id_average_longitude" class="form-label">Долгота:</label>
|
||||
{{ form.average_longitude }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Координаты Кубсата -->
|
||||
<div class="coord-group">
|
||||
<div class="coord-group-header">Координаты Кубсата</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="id_kupsat_latitude" class="form-label">Широта:</label>
|
||||
{{ form.kupsat_latitude }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="id_kupsat_longitude" class="form-label">Долгота:</label>
|
||||
{{ form.kupsat_longitude }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Координаты оперативников -->
|
||||
<div class="coord-group">
|
||||
<div class="coord-group-header">Координаты оперативников</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="id_valid_latitude" class="form-label">Широта:</label>
|
||||
{{ form.valid_latitude }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="id_valid_longitude" class="form-label">Долгота:</label>
|
||||
{{ form.valid_longitude }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Координаты справочные -->
|
||||
<div class="coord-group">
|
||||
<div class="coord-group-header">Координаты справочные</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="id_reference_latitude" class="form-label">Широта:</label>
|
||||
{{ form.reference_latitude }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="id_reference_longitude" class="form-label">Долгота:</label>
|
||||
{{ form.reference_longitude }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Привязанные объекты -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-header">
|
||||
<h4>Привязанные объекты ({{ objitems.count }})</h4>
|
||||
</div>
|
||||
|
||||
{% if objitems %}
|
||||
<div class="table-responsive">
|
||||
<table class="objitems-table table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Название</th>
|
||||
<th>Спутник</th>
|
||||
<th>Частота, МГц</th>
|
||||
<th>Полоса, МГц</th>
|
||||
<th>Поляризация</th>
|
||||
<th>Модуляция</th>
|
||||
<th>Координаты</th>
|
||||
<th>Создан</th>
|
||||
<th>Обновлен</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in objitems %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'mainapp:objitem_update' item.id %}" class="objitem-link">
|
||||
{{ item.id }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ item.name|default:"-" }}</td>
|
||||
<td>
|
||||
{% if item.parameter_obj and item.parameter_obj.id_satellite %}
|
||||
{{ item.parameter_obj.id_satellite.name }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if item.parameter_obj %}
|
||||
{{ item.parameter_obj.frequency|default:"-" }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if item.parameter_obj %}
|
||||
{{ item.parameter_obj.freq_range|default:"-" }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if item.parameter_obj and item.parameter_obj.polarization %}
|
||||
{{ item.parameter_obj.polarization.name }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if item.parameter_obj and item.parameter_obj.modulation %}
|
||||
{{ item.parameter_obj.modulation.name }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if item.geo_obj and item.geo_obj.coords %}
|
||||
{{ item.geo_obj.coords.y|floatformat:6 }}, {{ item.geo_obj.coords.x|floatformat:6 }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ item.created_at|date:"d.m.Y H:i" }}</td>
|
||||
<td>{{ item.updated_at|date:"d.m.Y H:i" }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted">Нет привязанных объектов</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{{ block.super }}
|
||||
<!-- Подключаем Leaflet -->
|
||||
{% leaflet_js %}
|
||||
{% leaflet_css %}
|
||||
<script src="{% static 'leaflet-markers/js/leaflet-color-markers.js' %}"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Инициализация карты
|
||||
const map = L.map('map').setView([55.75, 37.62], 5);
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(map);
|
||||
|
||||
// Функция для создания иконок маркеров разных цветов
|
||||
function createMarkerIcon(color) {
|
||||
return L.icon({
|
||||
iconUrl: `{% static "leaflet-markers/img/marker-icon-" %}${color}.png`,
|
||||
shadowUrl: `{% static 'leaflet-markers/img/marker-shadow.png' %}`,
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
}
|
||||
|
||||
const editableLayerGroup = new L.FeatureGroup();
|
||||
map.addLayer(editableLayerGroup);
|
||||
|
||||
// Создаем 4 маркера для разных типов координат
|
||||
const markers = {
|
||||
average: {
|
||||
marker: L.marker([55.75, 37.62], {
|
||||
draggable: false,
|
||||
icon: createMarkerIcon('blue'),
|
||||
title: 'Координаты ГЛ'
|
||||
}).addTo(editableLayerGroup),
|
||||
latField: 'id_average_latitude',
|
||||
lngField: 'id_average_longitude',
|
||||
label: 'Координаты ГЛ'
|
||||
},
|
||||
kupsat: {
|
||||
marker: L.marker([55.75, 37.62], {
|
||||
draggable: false,
|
||||
icon: createMarkerIcon('green'),
|
||||
title: 'Координаты Кубсата'
|
||||
}).addTo(editableLayerGroup),
|
||||
latField: 'id_kupsat_latitude',
|
||||
lngField: 'id_kupsat_longitude',
|
||||
label: 'Координаты Кубсата'
|
||||
},
|
||||
valid: {
|
||||
marker: L.marker([55.75, 37.62], {
|
||||
draggable: false,
|
||||
icon: createMarkerIcon('red'),
|
||||
title: 'Координаты оперативников'
|
||||
}).addTo(editableLayerGroup),
|
||||
latField: 'id_valid_latitude',
|
||||
lngField: 'id_valid_longitude',
|
||||
label: 'Координаты оперативников'
|
||||
},
|
||||
reference: {
|
||||
marker: L.marker([55.75, 37.62], {
|
||||
draggable: false,
|
||||
icon: createMarkerIcon('yellow'),
|
||||
title: 'Координаты справочные'
|
||||
}).addTo(editableLayerGroup),
|
||||
latField: 'id_reference_latitude',
|
||||
lngField: 'id_reference_longitude',
|
||||
label: 'Координаты справочные'
|
||||
}
|
||||
};
|
||||
|
||||
// Привязываем попапы к маркерам
|
||||
Object.values(markers).forEach(m => {
|
||||
m.marker.bindPopup(m.label);
|
||||
});
|
||||
|
||||
// Синхронизация при перетаскивании
|
||||
Object.entries(markers).forEach(([key, m]) => {
|
||||
m.marker.on('dragend', function (event) {
|
||||
const latLng = event.target.getLatLng();
|
||||
document.getElementById(m.latField).value = latLng.lat.toFixed(6);
|
||||
document.getElementById(m.lngField).value = latLng.lng.toFixed(6);
|
||||
});
|
||||
|
||||
// Методы для управления
|
||||
m.marker.enableEditing = function () {
|
||||
this.dragging.enable();
|
||||
this.openPopup();
|
||||
};
|
||||
|
||||
m.marker.disableEditing = function () {
|
||||
this.dragging.disable();
|
||||
this.closePopup();
|
||||
};
|
||||
});
|
||||
|
||||
// Устанавливаем начальные координаты из полей формы
|
||||
function initMarkersFromForm() {
|
||||
let hasValidCoords = false;
|
||||
let centerLat = 55.75;
|
||||
let centerLng = 37.62;
|
||||
|
||||
Object.entries(markers).forEach(([key, m]) => {
|
||||
const lat = parseFloat(document.getElementById(m.latField).value);
|
||||
const lng = parseFloat(document.getElementById(m.lngField).value);
|
||||
|
||||
if (!isNaN(lat) && !isNaN(lng)) {
|
||||
m.marker.setLatLng([lat, lng]);
|
||||
if (!hasValidCoords) {
|
||||
centerLat = lat;
|
||||
centerLng = lng;
|
||||
hasValidCoords = true;
|
||||
}
|
||||
} else {
|
||||
// Скрываем маркер если нет координат
|
||||
m.marker.setOpacity(0);
|
||||
}
|
||||
});
|
||||
|
||||
// Центрируем карту
|
||||
map.setView([centerLat, centerLng], hasValidCoords ? 10 : 5);
|
||||
}
|
||||
|
||||
// Настройка формы для синхронизации с маркерами
|
||||
function setupFormChange(latFieldId, lngFieldId, marker) {
|
||||
const latField = document.getElementById(latFieldId);
|
||||
const lngField = document.getElementById(lngFieldId);
|
||||
|
||||
[latField, lngField].forEach(field => {
|
||||
field.addEventListener('change', function () {
|
||||
const lat = parseFloat(latField.value);
|
||||
const lng = parseFloat(lngField.value);
|
||||
|
||||
if (!isNaN(lat) && !isNaN(lng)) {
|
||||
marker.setLatLng([lat, lng]);
|
||||
marker.setOpacity(1);
|
||||
map.setView(marker.getLatLng(), 10);
|
||||
} else {
|
||||
marker.setOpacity(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Инициализация
|
||||
initMarkersFromForm();
|
||||
Object.values(markers).forEach(m => {
|
||||
setupFormChange(m.latField, m.lngField, m.marker);
|
||||
});
|
||||
|
||||
// --- УПРАВЛЕНИЕ РЕДАКТИРОВАНИЕМ ---
|
||||
const editControlsDiv = L.DomUtil.create('div', 'map-controls');
|
||||
editControlsDiv.style.position = 'absolute';
|
||||
editControlsDiv.style.top = '10px';
|
||||
editControlsDiv.style.right = '10px';
|
||||
editControlsDiv.style.zIndex = '1000';
|
||||
editControlsDiv.style.background = 'white';
|
||||
editControlsDiv.style.padding = '10px';
|
||||
editControlsDiv.style.borderRadius = '4px';
|
||||
editControlsDiv.style.boxShadow = '0 0 10px rgba(0,0,0,0.2)';
|
||||
editControlsDiv.innerHTML = `
|
||||
<div class="map-controls">
|
||||
<button type="button" id="edit-btn" class="map-control-btn edit">Редактировать</button>
|
||||
<button type="button" id="save-btn" class="map-control-btn save" disabled>Сохранить</button>
|
||||
<button type="button" id="cancel-btn" class="map-control-btn cancel" disabled>Отмена</button>
|
||||
</div>
|
||||
`;
|
||||
map.getContainer().appendChild(editControlsDiv);
|
||||
|
||||
let isEditing = false;
|
||||
const initialPositions = {};
|
||||
|
||||
// Сохраняем начальные координаты для отмены
|
||||
Object.entries(markers).forEach(([key, m]) => {
|
||||
initialPositions[key] = m.marker.getLatLng();
|
||||
});
|
||||
|
||||
// Включение редактирования
|
||||
document.getElementById('edit-btn').addEventListener('click', function () {
|
||||
if (isEditing) return;
|
||||
|
||||
isEditing = true;
|
||||
document.getElementById('edit-btn').classList.add('active');
|
||||
document.getElementById('save-btn').disabled = false;
|
||||
document.getElementById('cancel-btn').disabled = false;
|
||||
|
||||
// Включаем drag для всех маркеров
|
||||
Object.values(markers).forEach(m => {
|
||||
if (m.marker.options.opacity !== 0) {
|
||||
m.marker.enableEditing();
|
||||
}
|
||||
});
|
||||
|
||||
// Показываем подсказку
|
||||
L.popup()
|
||||
.setLatLng(map.getCenter())
|
||||
.setContent('Перетаскивайте маркеры. Нажмите "Сохранить" или "Отмена".')
|
||||
.openOn(map);
|
||||
});
|
||||
|
||||
// Сохранение изменений
|
||||
document.getElementById('save-btn').addEventListener('click', function () {
|
||||
if (!isEditing) return;
|
||||
|
||||
isEditing = false;
|
||||
document.getElementById('edit-btn').classList.remove('active');
|
||||
document.getElementById('save-btn').disabled = true;
|
||||
document.getElementById('cancel-btn').disabled = true;
|
||||
|
||||
// Отключаем редактирование
|
||||
Object.values(markers).forEach(m => {
|
||||
m.marker.disableEditing();
|
||||
});
|
||||
|
||||
// Обновляем начальные позиции
|
||||
Object.entries(markers).forEach(([key, m]) => {
|
||||
initialPositions[key] = m.marker.getLatLng();
|
||||
});
|
||||
|
||||
map.closePopup();
|
||||
});
|
||||
|
||||
// Отмена изменений
|
||||
document.getElementById('cancel-btn').addEventListener('click', function () {
|
||||
if (!isEditing) return;
|
||||
|
||||
isEditing = false;
|
||||
document.getElementById('edit-btn').classList.remove('active');
|
||||
document.getElementById('save-btn').disabled = true;
|
||||
document.getElementById('cancel-btn').disabled = true;
|
||||
|
||||
// Возвращаем маркеры на исходные позиции
|
||||
Object.entries(markers).forEach(([key, m]) => {
|
||||
m.marker.setLatLng(initialPositions[key]);
|
||||
m.marker.disableEditing();
|
||||
|
||||
// Синхронизируем форму с исходными значениями
|
||||
document.getElementById(m.latField).value = initialPositions[key].lat.toFixed(6);
|
||||
document.getElementById(m.lngField).value = initialPositions[key].lng.toFixed(6);
|
||||
});
|
||||
|
||||
map.closePopup();
|
||||
});
|
||||
|
||||
// Легенда
|
||||
const legend = L.control({ position: 'bottomright' });
|
||||
|
||||
legend.onAdd = function () {
|
||||
const div = L.DomUtil.create('div', 'info legend');
|
||||
div.style.fontSize = '14px';
|
||||
div.style.backgroundColor = 'white';
|
||||
div.style.padding = '10px';
|
||||
div.style.borderRadius = '4px';
|
||||
div.style.boxShadow = '0 0 10px rgba(0,0,0,0.2)';
|
||||
div.innerHTML = `
|
||||
<h5>Легенда</h5>
|
||||
<div><span style="color: blue; font-weight: bold;">•</span> Координаты ГЛ</div>
|
||||
<div><span style="color: green; font-weight: bold;">•</span> Координаты Кубсата</div>
|
||||
<div><span style="color: red; font-weight: bold;">•</span> Координаты оперативников</div>
|
||||
<div><span style="color: gold; font-weight: bold;">•</span> Координаты справочные</div>
|
||||
`;
|
||||
return div;
|
||||
};
|
||||
|
||||
legend.addTo(map);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -12,6 +12,16 @@
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
.btn-group .badge {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
font-size: 0.65rem;
|
||||
padding: 0.2em 0.4em;
|
||||
}
|
||||
.btn-group .btn {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -55,11 +65,20 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" title="Показать на карте"
|
||||
onclick="showSelectedOnMap()">
|
||||
<i class="bi bi-map"></i> Карта
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Filter Toggle Button -->
|
||||
<div>
|
||||
<button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="offcanvas"
|
||||
data-bs-target="#offcanvasFilters" aria-controls="offcanvasFilters">
|
||||
<i class="bi bi-funnel"></i> Фильтры
|
||||
<span id="filterCounter" class="badge bg-danger" style="display: none;">0</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -185,6 +204,9 @@
|
||||
<table class="table table-striped table-hover table-sm" style="font-size: 0.85rem;">
|
||||
<thead class="table-dark sticky-top">
|
||||
<tr>
|
||||
<th scope="col" class="text-center" style="width: 3%;">
|
||||
<input type="checkbox" id="select-all" class="form-check-input">
|
||||
</th>
|
||||
<th scope="col" class="text-center" style="min-width: 60px;">
|
||||
<a href="javascript:void(0)" onclick="updateSort('id')" class="text-white text-decoration-none">
|
||||
ID
|
||||
@@ -229,12 +251,16 @@
|
||||
{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col" class="text-center" style="min-width: 100px;">Детали</th>
|
||||
<th scope="col" class="text-center" style="min-width: 150px;">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for source in processed_sources %}
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" class="form-check-input item-checkbox"
|
||||
value="{{ source.id }}">
|
||||
</td>
|
||||
<td class="text-center">{{ source.id }}</td>
|
||||
<td>{{ source.coords_average }}</td>
|
||||
<td>{{ source.coords_kupsat }}</td>
|
||||
@@ -244,15 +270,44 @@
|
||||
<td>{{ source.created_at|date:"d.m.Y H:i" }}</td>
|
||||
<td>{{ source.updated_at|date:"d.m.Y H:i" }}</td>
|
||||
<td class="text-center">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||
onclick="showSourceDetails({{ source.id }})">
|
||||
<i class="bi bi-eye"></i> Показать
|
||||
<div class="btn-group" role="group">
|
||||
{% if source.objitem_count > 0 %}
|
||||
<a href="{% url 'mainapp:show_source_with_points_map' source.id %}"
|
||||
target="_blank"
|
||||
class="btn btn-sm btn-outline-success"
|
||||
title="Показать источник с точками на карте">
|
||||
<i class="bi bi-geo-alt"></i>
|
||||
<span class="badge bg-success">{{ source.objitem_count }}</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" disabled title="Нет точек для отображения">
|
||||
<i class="bi bi-geo-alt"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||
onclick="showSourceDetails({{ source.id }})"
|
||||
title="Показать детали">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
|
||||
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
|
||||
<a href="{% url 'mainapp:source_update' source.id %}"
|
||||
class="btn btn-sm btn-outline-warning"
|
||||
title="Редактировать источник">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" disabled title="Недостаточно прав">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="9" class="text-center text-muted">Нет данных для отображения</td>
|
||||
<td colspan="10" class="text-center text-muted">Нет данных для отображения</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -285,6 +340,10 @@
|
||||
<table class="table table-striped table-hover table-sm">
|
||||
<thead class="table-light sticky-top">
|
||||
<tr>
|
||||
<th class="text-center" style="width: 3%;">
|
||||
<input type="checkbox" id="modal-select-all" class="form-check-input">
|
||||
</th>
|
||||
<th class="text-center" style="min-width: 60px;">ID</th>
|
||||
<th>Имя</th>
|
||||
<th>Спутник</th>
|
||||
<th>Частота, МГц</th>
|
||||
@@ -319,6 +378,55 @@
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
let lastCheckedIndex = null;
|
||||
|
||||
function updateRowHighlight(checkbox) {
|
||||
const row = checkbox.closest('tr');
|
||||
if (checkbox.checked) {
|
||||
row.classList.add('selected');
|
||||
} else {
|
||||
row.classList.remove('selected');
|
||||
}
|
||||
}
|
||||
|
||||
function handleCheckboxClick(e) {
|
||||
if (e.shiftKey && lastCheckedIndex !== null) {
|
||||
const checkboxes = document.querySelectorAll('.item-checkbox');
|
||||
const currentIndex = Array.from(checkboxes).indexOf(e.target);
|
||||
const startIndex = Math.min(lastCheckedIndex, currentIndex);
|
||||
const endIndex = Math.max(lastCheckedIndex, currentIndex);
|
||||
|
||||
for (let i = startIndex; i <= endIndex; i++) {
|
||||
checkboxes[i].checked = e.target.checked;
|
||||
updateRowHighlight(checkboxes[i]);
|
||||
}
|
||||
} else {
|
||||
updateRowHighlight(e.target);
|
||||
}
|
||||
lastCheckedIndex = Array.from(document.querySelectorAll('.item-checkbox')).indexOf(e.target);
|
||||
}
|
||||
|
||||
// Function to show selected sources on map
|
||||
function showSelectedOnMap() {
|
||||
// Get all checked checkboxes
|
||||
const checkedCheckboxes = document.querySelectorAll('.item-checkbox:checked');
|
||||
|
||||
if (checkedCheckboxes.length === 0) {
|
||||
alert('Пожалуйста, выберите хотя бы один источник для отображения на карте');
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract IDs from checked checkboxes
|
||||
const selectedIds = [];
|
||||
checkedCheckboxes.forEach(checkbox => {
|
||||
selectedIds.push(checkbox.value);
|
||||
});
|
||||
|
||||
// Redirect to the map view with selected IDs as query parameter
|
||||
const url = '{% url "mainapp:show_sources_map" %}' + '?ids=' + selectedIds.join(',');
|
||||
window.open(url, '_blank'); // Open in a new tab
|
||||
}
|
||||
|
||||
// Search functionality
|
||||
function performSearch() {
|
||||
const searchValue = document.getElementById('toolbar-search').value.trim();
|
||||
@@ -377,6 +485,103 @@ function updateSort(field) {
|
||||
window.location.search = urlParams.toString();
|
||||
}
|
||||
|
||||
// Setup radio-like behavior for filter checkboxes
|
||||
function setupRadioLikeCheckboxes(name) {
|
||||
const checkboxes = document.querySelectorAll(`input[name="${name}"]`);
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.addEventListener('change', function () {
|
||||
if (this.checked) {
|
||||
checkboxes.forEach(other => {
|
||||
if (other !== this) {
|
||||
other.checked = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Filter counter functionality
|
||||
function updateFilterCounter() {
|
||||
const form = document.getElementById('filter-form');
|
||||
const formData = new FormData(form);
|
||||
let filterCount = 0;
|
||||
|
||||
// Count non-empty form fields
|
||||
for (const [key, value] of formData.entries()) {
|
||||
if (value && value.trim() !== '') {
|
||||
filterCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Display the filter counter
|
||||
const counterElement = document.getElementById('filterCounter');
|
||||
if (counterElement) {
|
||||
if (filterCount > 0) {
|
||||
counterElement.textContent = filterCount;
|
||||
counterElement.style.display = 'inline';
|
||||
} else {
|
||||
counterElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Setup select-all checkbox
|
||||
const selectAllCheckbox = document.getElementById('select-all');
|
||||
const itemCheckboxes = document.querySelectorAll('.item-checkbox');
|
||||
|
||||
if (selectAllCheckbox && itemCheckboxes.length > 0) {
|
||||
selectAllCheckbox.addEventListener('change', function () {
|
||||
itemCheckboxes.forEach(checkbox => {
|
||||
checkbox.checked = selectAllCheckbox.checked;
|
||||
updateRowHighlight(checkbox);
|
||||
});
|
||||
});
|
||||
|
||||
itemCheckboxes.forEach(checkbox => {
|
||||
checkbox.addEventListener('change', function () {
|
||||
const allChecked = Array.from(itemCheckboxes).every(cb => cb.checked);
|
||||
selectAllCheckbox.checked = allChecked;
|
||||
});
|
||||
|
||||
// Add shift-click handler
|
||||
checkbox.addEventListener('click', handleCheckboxClick);
|
||||
});
|
||||
}
|
||||
|
||||
// Setup radio-like checkboxes for filters
|
||||
setupRadioLikeCheckboxes('has_coords_average');
|
||||
setupRadioLikeCheckboxes('has_coords_kupsat');
|
||||
setupRadioLikeCheckboxes('has_coords_valid');
|
||||
setupRadioLikeCheckboxes('has_coords_reference');
|
||||
|
||||
// Update filter counter on page load
|
||||
updateFilterCounter();
|
||||
|
||||
// Add event listeners to form elements to update counter when filters change
|
||||
const form = document.getElementById('filter-form');
|
||||
if (form) {
|
||||
const inputFields = form.querySelectorAll('input[type="text"], input[type="number"], input[type="date"]');
|
||||
inputFields.forEach(input => {
|
||||
input.addEventListener('input', updateFilterCounter);
|
||||
input.addEventListener('change', updateFilterCounter);
|
||||
});
|
||||
|
||||
const checkboxFields = form.querySelectorAll('input[type="checkbox"]');
|
||||
checkboxFields.forEach(checkbox => {
|
||||
checkbox.addEventListener('change', updateFilterCounter);
|
||||
});
|
||||
}
|
||||
|
||||
// Update counter when offcanvas is shown
|
||||
const offcanvasElement = document.getElementById('offcanvasFilters');
|
||||
if (offcanvasElement) {
|
||||
offcanvasElement.addEventListener('show.bs.offcanvas', updateFilterCounter);
|
||||
}
|
||||
});
|
||||
|
||||
// Show source details in modal
|
||||
function showSourceDetails(sourceId) {
|
||||
// Update modal title
|
||||
@@ -420,6 +625,10 @@ function showSourceDetails(sourceId) {
|
||||
data.objitems.forEach(objitem => {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td class="text-center">
|
||||
<input type="checkbox" class="form-check-input modal-item-checkbox" value="${objitem.id}">
|
||||
</td>
|
||||
<td class="text-center">${objitem.id}</td>
|
||||
<td>${objitem.name}</td>
|
||||
<td>${objitem.satellite_name}</td>
|
||||
<td>${objitem.frequency}</td>
|
||||
@@ -434,6 +643,9 @@ function showSourceDetails(sourceId) {
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
|
||||
// Setup modal select-all checkbox
|
||||
setupModalSelectAll();
|
||||
} else {
|
||||
// Show no data message
|
||||
document.getElementById('modalNoData').style.display = 'block';
|
||||
@@ -449,5 +661,32 @@ function showSourceDetails(sourceId) {
|
||||
errorDiv.style.display = 'block';
|
||||
});
|
||||
}
|
||||
|
||||
// Setup select-all functionality for modal
|
||||
function setupModalSelectAll() {
|
||||
const modalSelectAll = document.getElementById('modal-select-all');
|
||||
const modalItemCheckboxes = document.querySelectorAll('.modal-item-checkbox');
|
||||
|
||||
if (modalSelectAll && modalItemCheckboxes.length > 0) {
|
||||
// Remove old event listeners by cloning
|
||||
const newModalSelectAll = modalSelectAll.cloneNode(true);
|
||||
modalSelectAll.parentNode.replaceChild(newModalSelectAll, modalSelectAll);
|
||||
|
||||
newModalSelectAll.addEventListener('change', function() {
|
||||
const checkboxes = document.querySelectorAll('.modal-item-checkbox');
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.checked = newModalSelectAll.checked;
|
||||
});
|
||||
});
|
||||
|
||||
modalItemCheckboxes.forEach(checkbox => {
|
||||
checkbox.addEventListener('change', function() {
|
||||
const allCheckboxes = document.querySelectorAll('.modal-item-checkbox');
|
||||
const allChecked = Array.from(allCheckboxes).every(cb => cb.checked);
|
||||
document.getElementById('modal-select-all').checked = allChecked;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
189
dbapp/mainapp/templates/mainapp/source_map.html
Normal file
189
dbapp/mainapp/templates/mainapp/source_map.html
Normal file
@@ -0,0 +1,189 @@
|
||||
{% extends "mainapp/base.html" %}
|
||||
{% load static %}
|
||||
{% block title %}Карта источников{% endblock title %}
|
||||
|
||||
{% block extra_css %}
|
||||
<!-- Leaflet CSS -->
|
||||
<link href="{% static 'leaflet/leaflet.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'leaflet-measure/leaflet-measure.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'leaflet-tree/L.Control.Layers.Tree.css' %}" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
#map {
|
||||
position: fixed;
|
||||
top: 56px; /* Высота navbar */
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.legend {
|
||||
background: white;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.2);
|
||||
font-size: 11px;
|
||||
}
|
||||
.legend h6 {
|
||||
font-size: 12px;
|
||||
margin: 0 0 6px 0;
|
||||
}
|
||||
.legend-item {
|
||||
margin: 3px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.legend-marker {
|
||||
width: 18px;
|
||||
height: 30px;
|
||||
margin-right: 6px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="map"></div>
|
||||
{% endblock content %}
|
||||
|
||||
{% block extra_js %}
|
||||
<!-- Leaflet JavaScript -->
|
||||
<script src="{% static 'leaflet/leaflet.js' %}"></script>
|
||||
<script src="{% static 'leaflet-measure/leaflet-measure.ru.js' %}"></script>
|
||||
<script src="{% static 'leaflet-tree/L.Control.Layers.Tree.js' %}"></script>
|
||||
|
||||
<script>
|
||||
// Инициализация карты
|
||||
let map = L.map('map').setView([55.75, 37.62], 5);
|
||||
L.control.scale({
|
||||
imperial: false,
|
||||
metric: true
|
||||
}).addTo(map);
|
||||
map.attributionControl.setPrefix(false);
|
||||
|
||||
const street = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
});
|
||||
street.addTo(map);
|
||||
|
||||
const satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
||||
attribution: 'Tiles © Esri'
|
||||
});
|
||||
|
||||
const street_local = L.tileLayer('http://127.0.0.1:8080/styles/basic-preview/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: 'Local Tiles'
|
||||
});
|
||||
|
||||
const baseLayers = {
|
||||
"Улицы": street,
|
||||
"Спутник": satellite,
|
||||
"Локально": street_local
|
||||
};
|
||||
|
||||
L.control.layers(baseLayers).addTo(map);
|
||||
map.setMaxZoom(18);
|
||||
map.setMinZoom(0);
|
||||
L.control.measure({ primaryLengthUnit: 'kilometers' }).addTo(map);
|
||||
|
||||
// Цвета для маркеров
|
||||
var markerColors = {
|
||||
'blue': 'blue',
|
||||
'orange': 'orange',
|
||||
'green': 'green',
|
||||
'violet': 'violet'
|
||||
};
|
||||
|
||||
var getColorIcon = function(color) {
|
||||
return L.icon({
|
||||
iconUrl: '{% static "leaflet-markers/img/marker-icon-" %}' + color + '.png',
|
||||
shadowUrl: '{% static "leaflet-markers/img/marker-shadow.png" %}',
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
};
|
||||
|
||||
var overlays = [];
|
||||
|
||||
// Создаём слои для каждого типа координат
|
||||
{% for group in groups %}
|
||||
var groupName = '{{ group.name|escapejs }}';
|
||||
var colorName = '{{ group.color }}';
|
||||
var groupIcon = getColorIcon(colorName);
|
||||
var groupLayer = L.layerGroup();
|
||||
|
||||
var subgroup = [];
|
||||
{% for point_data in group.points %}
|
||||
var pointName = "{{ point_data.source_id|escapejs }}";
|
||||
var marker = L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}], {
|
||||
icon: groupIcon
|
||||
}).bindPopup(pointName);
|
||||
groupLayer.addLayer(marker);
|
||||
|
||||
subgroup.push({
|
||||
label: "{{ forloop.counter }} - {{ point_data.source_id|escapejs }}",
|
||||
layer: marker
|
||||
});
|
||||
{% endfor %}
|
||||
|
||||
overlays.push({
|
||||
label: groupName,
|
||||
selectAllCheckbox: true,
|
||||
children: subgroup,
|
||||
layer: groupLayer
|
||||
});
|
||||
{% endfor %}
|
||||
|
||||
// Корневая группа
|
||||
const rootGroup = {
|
||||
label: "Все точки",
|
||||
selectAllCheckbox: true,
|
||||
children: overlays,
|
||||
layer: L.layerGroup()
|
||||
};
|
||||
|
||||
// Создаём tree control
|
||||
const layerControl = L.control.layers.tree(baseLayers, [rootGroup], {
|
||||
collapsed: false,
|
||||
autoZIndex: true
|
||||
});
|
||||
layerControl.addTo(map);
|
||||
|
||||
// Подгоняем карту под все маркеры
|
||||
{% if groups %}
|
||||
var groupBounds = L.featureGroup([]);
|
||||
{% for group in groups %}
|
||||
{% for point_data in group.points %}
|
||||
groupBounds.addLayer(L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}]));
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
map.fitBounds(groupBounds.getBounds().pad(0.1));
|
||||
{% endif %}
|
||||
|
||||
// Добавляем легенду в левый нижний угол
|
||||
var legend = L.control({ position: 'bottomleft' });
|
||||
legend.onAdd = function(map) {
|
||||
var div = L.DomUtil.create('div', 'legend');
|
||||
div.innerHTML = '<h6><strong>Легенда</strong></h6>';
|
||||
|
||||
{% for group in groups %}
|
||||
div.innerHTML += `
|
||||
<div class="legend-item">
|
||||
<div class="legend-marker" style="background-image: url('{% static "leaflet-markers/img/marker-icon-" %}{{ group.color }}.png');"></div>
|
||||
<span>{{ group.name|escapejs }}</span>
|
||||
</div>
|
||||
`;
|
||||
{% endfor %}
|
||||
|
||||
return div;
|
||||
};
|
||||
legend.addTo(map);
|
||||
</script>
|
||||
{% endblock extra_js %}
|
||||
244
dbapp/mainapp/templates/mainapp/source_with_points_map.html
Normal file
244
dbapp/mainapp/templates/mainapp/source_with_points_map.html
Normal file
@@ -0,0 +1,244 @@
|
||||
{% extends "mainapp/base.html" %}
|
||||
{% load static %}
|
||||
{% block title %}Карта источника #{{ source_id }} с точками{% endblock title %}
|
||||
|
||||
{% block extra_css %}
|
||||
<!-- Leaflet CSS -->
|
||||
<link href="{% static 'leaflet/leaflet.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'leaflet-measure/leaflet-measure.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'leaflet-tree/L.Control.Layers.Tree.css' %}" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
#map {
|
||||
position: fixed;
|
||||
top: 56px; /* Высота navbar */
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
.legend {
|
||||
background: white;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.2);
|
||||
font-size: 11px;
|
||||
}
|
||||
.legend h6 {
|
||||
font-size: 12px;
|
||||
margin: 0 0 6px 0;
|
||||
}
|
||||
.legend-item {
|
||||
margin: 3px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.legend-marker {
|
||||
width: 18px;
|
||||
height: 30px;
|
||||
margin-right: 6px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.legend-section {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
.legend-section:first-child {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
border-top: none;
|
||||
}
|
||||
.legend-section-title {
|
||||
font-weight: bold;
|
||||
font-size: 11px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="map"></div>
|
||||
{% endblock content %}
|
||||
|
||||
{% block extra_js %}
|
||||
<!-- Leaflet JavaScript -->
|
||||
<script src="{% static 'leaflet/leaflet.js' %}"></script>
|
||||
<script src="{% static 'leaflet-measure/leaflet-measure.ru.js' %}"></script>
|
||||
<script src="{% static 'leaflet-tree/L.Control.Layers.Tree.js' %}"></script>
|
||||
|
||||
<script>
|
||||
// Инициализация карты
|
||||
let map = L.map('map').setView([55.75, 37.62], 5);
|
||||
L.control.scale({
|
||||
imperial: false,
|
||||
metric: true
|
||||
}).addTo(map);
|
||||
map.attributionControl.setPrefix(false);
|
||||
|
||||
const street = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
});
|
||||
street.addTo(map);
|
||||
|
||||
const satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
||||
attribution: 'Tiles © Esri'
|
||||
});
|
||||
|
||||
const street_local = L.tileLayer('http://127.0.0.1:8080/styles/basic-preview/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: 'Local Tiles'
|
||||
});
|
||||
|
||||
const baseLayers = {
|
||||
"Улицы": street,
|
||||
"Спутник": satellite,
|
||||
"Локально": street_local
|
||||
};
|
||||
|
||||
L.control.layers(baseLayers).addTo(map);
|
||||
map.setMaxZoom(18);
|
||||
map.setMinZoom(0);
|
||||
L.control.measure({ primaryLengthUnit: 'kilometers' }).addTo(map);
|
||||
|
||||
// Функция для создания иконки
|
||||
var getColorIcon = function(color) {
|
||||
return L.icon({
|
||||
iconUrl: '{% static "leaflet-markers/img/marker-icon-" %}' + color + '.png',
|
||||
shadowUrl: '{% static "leaflet-markers/img/marker-shadow.png" %}',
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
};
|
||||
|
||||
var sourceOverlays = [];
|
||||
var glPointLayers = [];
|
||||
|
||||
// Создаём слои для координат источника и точек ГЛ
|
||||
{% for group in groups %}
|
||||
var groupName = '{{ group.name|escapejs }}';
|
||||
var colorName = '{{ group.color }}';
|
||||
var groupIcon = getColorIcon(colorName);
|
||||
var groupLayer = L.layerGroup();
|
||||
|
||||
{% for point_data in group.points %}
|
||||
{% if point_data.source_id %}
|
||||
// Это координата источника
|
||||
var pointName = "{{ point_data.source_id|escapejs }}";
|
||||
var marker = L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}], {
|
||||
icon: groupIcon
|
||||
}).bindPopup(pointName);
|
||||
groupLayer.addLayer(marker);
|
||||
{% else %}
|
||||
// Это точка ГЛ
|
||||
var pointName = "{{ point_data.name|escapejs }}";
|
||||
var marker = L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}], {
|
||||
icon: groupIcon
|
||||
}).bindPopup(pointName + '<br>' + "{{ point_data.frequency|escapejs }}");
|
||||
groupLayer.addLayer(marker);
|
||||
|
||||
// Добавляем каждую точку ГЛ отдельно в список
|
||||
glPointLayers.push({
|
||||
label: "{{ forloop.counter }} - {{ point_data.name|escapejs }} ({{ point_data.frequency|escapejs }})",
|
||||
layer: marker
|
||||
});
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
// Для координат источника добавляем как отдельный слой без вложенности
|
||||
{% if group.color in 'blue,orange,green,violet' %}
|
||||
sourceOverlays.push({
|
||||
label: groupName,
|
||||
layer: groupLayer
|
||||
});
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
// Создаём иерархию
|
||||
var treeOverlays = [];
|
||||
|
||||
if (sourceOverlays.length > 0) {
|
||||
treeOverlays.push({
|
||||
label: "Координаты источника #{{ source_id }}",
|
||||
selectAllCheckbox: true,
|
||||
children: sourceOverlays,
|
||||
layer: L.layerGroup()
|
||||
});
|
||||
}
|
||||
|
||||
if (glPointLayers.length > 0) {
|
||||
treeOverlays.push({
|
||||
label: "Точки ГЛ",
|
||||
selectAllCheckbox: true,
|
||||
children: glPointLayers,
|
||||
layer: L.layerGroup()
|
||||
});
|
||||
}
|
||||
|
||||
// Создаём tree control
|
||||
const layerControl = L.control.layers.tree(baseLayers, treeOverlays, {
|
||||
collapsed: false,
|
||||
autoZIndex: true
|
||||
});
|
||||
layerControl.addTo(map);
|
||||
|
||||
// Подгоняем карту под все маркеры
|
||||
{% if groups %}
|
||||
var groupBounds = L.featureGroup([]);
|
||||
{% for group in groups %}
|
||||
{% for point_data in group.points %}
|
||||
groupBounds.addLayer(L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}]));
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
map.fitBounds(groupBounds.getBounds().pad(0.1));
|
||||
{% endif %}
|
||||
|
||||
// Добавляем легенду в левый нижний угол
|
||||
var legend = L.control({ position: 'bottomleft' });
|
||||
legend.onAdd = function(map) {
|
||||
var div = L.DomUtil.create('div', 'legend');
|
||||
div.innerHTML = '<h6><strong>Легенда</strong></h6>';
|
||||
|
||||
// Координаты источника
|
||||
var hasSourceCoords = false;
|
||||
{% for group in groups %}
|
||||
{% if group.color in 'blue,orange,green,violet' %}
|
||||
{% if not hasSourceCoords %}
|
||||
if (!hasSourceCoords) {
|
||||
div.innerHTML += '<div class="legend-section-title">Координаты источника:</div>';
|
||||
hasSourceCoords = true;
|
||||
}
|
||||
{% endif %}
|
||||
div.innerHTML += `
|
||||
<div class="legend-item">
|
||||
<div class="legend-marker" style="background-image: url('{% static "leaflet-markers/img/marker-icon-" %}{{ group.color }}.png');"></div>
|
||||
<span>{{ group.name|escapejs }}</span>
|
||||
</div>
|
||||
`;
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
// Точки ГЛ (все одним цветом)
|
||||
{% for group in groups %}
|
||||
{% if group.color not in 'blue,orange,green,violet' %}
|
||||
div.innerHTML += '<div class="legend-section"><div class="legend-section-title">Точки ГЛ:</div></div>';
|
||||
div.innerHTML += `
|
||||
<div class="legend-item">
|
||||
<div class="legend-marker" style="background-image: url('{% static "leaflet-markers/img/marker-icon-" %}{{ group.color }}.png');"></div>
|
||||
<span>{{ group.name|escapejs }}</span>
|
||||
</div>
|
||||
`;
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
return div;
|
||||
};
|
||||
legend.addTo(map);
|
||||
</script>
|
||||
{% endblock extra_js %}
|
||||
@@ -25,9 +25,14 @@ from .views import (
|
||||
ProcessKubsatView,
|
||||
ShowMapView,
|
||||
ShowSelectedObjectsMapView,
|
||||
ShowSourcesMapView,
|
||||
ShowSourceWithPointsMapView,
|
||||
SourceListView,
|
||||
SourceUpdateView,
|
||||
SourceDeleteView,
|
||||
SourceObjItemsAPIView,
|
||||
SigmaParameterDataAPIView,
|
||||
TransponderDataAPIView,
|
||||
UploadVchLoadView,
|
||||
custom_logout,
|
||||
)
|
||||
@@ -36,6 +41,8 @@ app_name = 'mainapp'
|
||||
|
||||
urlpatterns = [
|
||||
path('', SourceListView.as_view(), name='home'),
|
||||
path('source/<int:pk>/edit/', SourceUpdateView.as_view(), name='source_update'),
|
||||
path('source/<int:pk>/delete/', SourceDeleteView.as_view(), name='source_delete'),
|
||||
path('objitems/', ObjItemListView.as_view(), name='objitem_list'),
|
||||
path('actions/', ActionsPageView.as_view(), name='actions'),
|
||||
path('excel-data', LoadExcelDataView.as_view(), name='load_excel_data'),
|
||||
@@ -45,6 +52,8 @@ urlpatterns = [
|
||||
path('csv-data', LoadCsvDataView.as_view(), name='load_csv_data'),
|
||||
path('map-points/', ShowMapView.as_view(), name='admin_show_map'),
|
||||
path('show-selected-objects-map/', ShowSelectedObjectsMapView.as_view(), name='show_selected_objects_map'),
|
||||
path('show-sources-map/', ShowSourcesMapView.as_view(), name='show_sources_map'),
|
||||
path('show-source-with-points-map/<int:source_id>/', ShowSourceWithPointsMapView.as_view(), name='show_source_with_points_map'),
|
||||
path('delete-selected-objects/', DeleteSelectedObjectsView.as_view(), name='delete_selected_objects'),
|
||||
path('cluster/', ClusterTestView.as_view(), name='cluster'),
|
||||
path('vch-upload/', UploadVchLoadView.as_view(), name='vch_load'),
|
||||
@@ -53,6 +62,7 @@ urlpatterns = [
|
||||
path('api/lyngsat/<int:lyngsat_id>/', LyngsatDataAPIView.as_view(), name='lyngsat_data_api'),
|
||||
path('api/sigma-parameter/<int:parameter_id>/', SigmaParameterDataAPIView.as_view(), name='sigma_parameter_data_api'),
|
||||
path('api/source/<int:source_id>/objitems/', SourceObjItemsAPIView.as_view(), name='source_objitems_api'),
|
||||
path('api/transponder/<int:transponder_id>/', TransponderDataAPIView.as_view(), name='transponder_data_api'),
|
||||
path('kubsat-excel/', ProcessKubsatView.as_view(), name='kubsat_excel'),
|
||||
path('object/create/', ObjItemCreateView.as_view(), name='objitem_create'),
|
||||
path('object/<int:pk>/edit/', ObjItemUpdateView.as_view(), name='objitem_update'),
|
||||
|
||||
@@ -23,6 +23,7 @@ from .api import (
|
||||
SigmaParameterDataAPIView,
|
||||
SourceObjItemsAPIView,
|
||||
LyngsatTaskStatusAPIView,
|
||||
TransponderDataAPIView,
|
||||
)
|
||||
from .lyngsat import (
|
||||
LinkLyngsatSourcesView,
|
||||
@@ -30,8 +31,14 @@ from .lyngsat import (
|
||||
LyngsatTaskStatusView,
|
||||
ClearLyngsatCacheView,
|
||||
)
|
||||
from .source import SourceListView
|
||||
from .map import ShowMapView, ShowSelectedObjectsMapView, ClusterTestView
|
||||
from .source import SourceListView, SourceUpdateView, SourceDeleteView
|
||||
from .map import (
|
||||
ShowMapView,
|
||||
ShowSelectedObjectsMapView,
|
||||
ShowSourcesMapView,
|
||||
ShowSourceWithPointsMapView,
|
||||
ClusterTestView,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Base
|
||||
@@ -58,6 +65,7 @@ __all__ = [
|
||||
'SigmaParameterDataAPIView',
|
||||
'SourceObjItemsAPIView',
|
||||
'LyngsatTaskStatusAPIView',
|
||||
'TransponderDataAPIView',
|
||||
# LyngSat
|
||||
'LinkLyngsatSourcesView',
|
||||
'FillLyngsatDataView',
|
||||
@@ -65,8 +73,12 @@ __all__ = [
|
||||
'ClearLyngsatCacheView',
|
||||
# Source
|
||||
'SourceListView',
|
||||
'SourceUpdateView',
|
||||
'SourceDeleteView',
|
||||
# Map
|
||||
'ShowMapView',
|
||||
'ShowSelectedObjectsMapView',
|
||||
'ShowSourcesMapView',
|
||||
'ShowSourceWithPointsMapView',
|
||||
'ClusterTestView',
|
||||
]
|
||||
|
||||
@@ -299,3 +299,34 @@ class LyngsatTaskStatusAPIView(LoginRequiredMixin, View):
|
||||
response_data['status'] = task.state
|
||||
|
||||
return JsonResponse(response_data)
|
||||
|
||||
|
||||
class TransponderDataAPIView(LoginRequiredMixin, View):
|
||||
"""API endpoint for getting Transponder data."""
|
||||
|
||||
def get(self, request, transponder_id):
|
||||
from mapsapp.models import Transponders
|
||||
|
||||
try:
|
||||
transponder = Transponders.objects.select_related(
|
||||
'sat_id',
|
||||
'polarization'
|
||||
).get(id=transponder_id)
|
||||
|
||||
data = {
|
||||
'id': transponder.id,
|
||||
'name': transponder.name or '-',
|
||||
'satellite': transponder.sat_id.name if transponder.sat_id else '-',
|
||||
'downlink': f"{transponder.downlink:.3f}" if transponder.downlink else '-',
|
||||
'uplink': f"{transponder.uplink:.3f}" if transponder.uplink else None,
|
||||
'frequency_range': f"{transponder.frequency_range:.3f}" if transponder.frequency_range else '-',
|
||||
'polarization': transponder.polarization.name if transponder.polarization else '-',
|
||||
'zone_name': transponder.zone_name or '-',
|
||||
'transfer': f"{transponder.transfer:.3f}" if transponder.transfer else None,
|
||||
}
|
||||
|
||||
return JsonResponse(data)
|
||||
except Transponders.DoesNotExist:
|
||||
return JsonResponse({'error': 'Транспондер не найден'}, status=404)
|
||||
except Exception as e:
|
||||
return JsonResponse({'error': str(e)}, status=500)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Map related views for displaying objects on maps.
|
||||
"""
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from django.contrib.admin.views.decorators import staff_member_required
|
||||
@@ -41,7 +42,7 @@ class ShowMapView(RoleRequiredMixin, View):
|
||||
or not obj.geo_obj.coords
|
||||
):
|
||||
continue
|
||||
param = getattr(obj, 'parameter_obj', None)
|
||||
param = getattr(obj, "parameter_obj", None)
|
||||
if not param:
|
||||
continue
|
||||
points.append(
|
||||
@@ -92,7 +93,7 @@ class ShowSelectedObjectsMapView(LoginRequiredMixin, View):
|
||||
or not obj.geo_obj.coords
|
||||
):
|
||||
continue
|
||||
param = getattr(obj, 'parameter_obj', None)
|
||||
param = getattr(obj, "parameter_obj", None)
|
||||
if not param:
|
||||
continue
|
||||
points.append(
|
||||
@@ -121,6 +122,139 @@ class ShowSelectedObjectsMapView(LoginRequiredMixin, View):
|
||||
return render(request, "mainapp/objitem_map.html", context)
|
||||
|
||||
|
||||
class ShowSourcesMapView(LoginRequiredMixin, View):
|
||||
"""View for displaying selected sources on map."""
|
||||
|
||||
def get(self, request):
|
||||
from ..models import Source
|
||||
|
||||
ids = request.GET.get("ids", "")
|
||||
groups = []
|
||||
|
||||
if ids:
|
||||
id_list = [int(x) for x in ids.split(",") if x.isdigit()]
|
||||
sources = Source.objects.filter(id__in=id_list)
|
||||
|
||||
# Define coordinate types with their labels and colors
|
||||
coord_types = [
|
||||
("coords_average", "Усредненные координаты", "blue"),
|
||||
("coords_kupsat", "Координаты Кубсата", "orange"),
|
||||
("coords_valid", "Координаты оперативников", "green"),
|
||||
("coords_reference", "Координаты справочные", "violet"),
|
||||
]
|
||||
|
||||
# Group points by coordinate type
|
||||
for coord_field, label, color in coord_types:
|
||||
points = []
|
||||
for source in sources:
|
||||
coords = getattr(source, coord_field)
|
||||
if coords:
|
||||
# coords is a Point object with x (longitude) and y (latitude)
|
||||
points.append(
|
||||
{
|
||||
"point": (coords.x, coords.y), # (lon, lat)
|
||||
"source_id": f"Источник #{source.id}",
|
||||
}
|
||||
)
|
||||
|
||||
if points:
|
||||
groups.append(
|
||||
{
|
||||
"name": label,
|
||||
"points": points,
|
||||
"color": color,
|
||||
}
|
||||
)
|
||||
else:
|
||||
return redirect("mainapp:home")
|
||||
|
||||
context = {
|
||||
"groups": groups,
|
||||
}
|
||||
return render(request, "mainapp/source_map.html", context)
|
||||
|
||||
|
||||
class ShowSourceWithPointsMapView(LoginRequiredMixin, View):
|
||||
"""View for displaying a single source with all its related ObjItem points."""
|
||||
|
||||
def get(self, request, source_id):
|
||||
from ..models import Source
|
||||
|
||||
try:
|
||||
source = Source.objects.prefetch_related(
|
||||
"source_objitems",
|
||||
"source_objitems__parameter_obj",
|
||||
"source_objitems__geo_obj",
|
||||
).get(id=source_id)
|
||||
except Source.DoesNotExist:
|
||||
return redirect("mainapp:home")
|
||||
|
||||
groups = []
|
||||
|
||||
# Цвета для разных типов координат источника
|
||||
source_coord_types = [
|
||||
("coords_average", "Усредненные координаты", "blue"),
|
||||
("coords_kupsat", "Координаты Кубсата", "orange"),
|
||||
("coords_valid", "Координаты оперативников", "green"),
|
||||
("coords_reference", "Координаты справочные", "violet"),
|
||||
]
|
||||
|
||||
# Добавляем координаты источника
|
||||
for coord_field, label, color in source_coord_types:
|
||||
coords = getattr(source, coord_field)
|
||||
if coords:
|
||||
groups.append(
|
||||
{
|
||||
"name": label,
|
||||
"points": [
|
||||
{
|
||||
"point": (coords.x, coords.y),
|
||||
"source_id": f"Источник #{source.id}",
|
||||
}
|
||||
],
|
||||
"color": color,
|
||||
}
|
||||
)
|
||||
|
||||
# Добавляем все точки ГЛ одной группой
|
||||
gl_points = source.source_objitems.select_related(
|
||||
"parameter_obj", "geo_obj"
|
||||
).all()
|
||||
|
||||
# Собираем все точки ГЛ в одну группу
|
||||
all_gl_points = []
|
||||
for obj in gl_points:
|
||||
if (
|
||||
not hasattr(obj, "geo_obj")
|
||||
or not obj.geo_obj
|
||||
or not obj.geo_obj.coords
|
||||
):
|
||||
continue
|
||||
param = getattr(obj, "parameter_obj", None)
|
||||
if not param:
|
||||
continue
|
||||
|
||||
all_gl_points.append(
|
||||
{
|
||||
"point": (obj.geo_obj.coords.x, obj.geo_obj.coords.y),
|
||||
"name": obj.name,
|
||||
"frequency": f"{param.frequency} [{param.freq_range}] МГц",
|
||||
}
|
||||
)
|
||||
|
||||
# Добавляем все точки ГЛ одним цветом (красный)
|
||||
if all_gl_points:
|
||||
groups.append(
|
||||
{"name": "Точки ГЛ", "points": all_gl_points, "color": "red"}
|
||||
)
|
||||
|
||||
context = {
|
||||
"groups": groups,
|
||||
"source_id": source_id,
|
||||
}
|
||||
return render(request, "mainapp/source_with_points_map.html", context)
|
||||
|
||||
|
||||
class ClusterTestView(LoginRequiredMixin, View):
|
||||
"""Test view for clustering functionality."""
|
||||
|
||||
|
||||
@@ -3,12 +3,15 @@ Source related views.
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Count
|
||||
from django.shortcuts import render
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.views import View
|
||||
|
||||
from ..forms import SourceForm
|
||||
from ..models import Source
|
||||
from ..utils import parse_pagination_params
|
||||
|
||||
@@ -22,8 +25,8 @@ class SourceListView(LoginRequiredMixin, View):
|
||||
# Get pagination parameters
|
||||
page_number, items_per_page = parse_pagination_params(request)
|
||||
|
||||
# Get sorting parameters
|
||||
sort_param = request.GET.get("sort", "-created_at")
|
||||
# Get sorting parameters (default to ID ascending)
|
||||
sort_param = request.GET.get("sort", "id")
|
||||
|
||||
# Get filter parameters
|
||||
search_query = request.GET.get("search", "").strip()
|
||||
@@ -185,3 +188,117 @@ class SourceListView(LoginRequiredMixin, View):
|
||||
}
|
||||
|
||||
return render(request, "mainapp/source_list.html", context)
|
||||
|
||||
|
||||
|
||||
class AdminModeratorMixin(UserPassesTestMixin):
|
||||
"""Mixin to restrict access to admin and moderator roles only."""
|
||||
|
||||
def test_func(self):
|
||||
return (
|
||||
self.request.user.is_authenticated and
|
||||
hasattr(self.request.user, 'customuser') and
|
||||
self.request.user.customuser.role in ['admin', 'moderator']
|
||||
)
|
||||
|
||||
def handle_no_permission(self):
|
||||
messages.error(self.request, 'У вас нет прав для выполнения этого действия.')
|
||||
return redirect('mainapp:home')
|
||||
|
||||
|
||||
class SourceUpdateView(LoginRequiredMixin, AdminModeratorMixin, View):
|
||||
"""View for editing Source with 4 coordinate fields and related ObjItems."""
|
||||
|
||||
def get(self, request, pk):
|
||||
source = get_object_or_404(Source, pk=pk)
|
||||
form = SourceForm(instance=source)
|
||||
|
||||
# Get related ObjItems ordered by creation date
|
||||
objitems = source.source_objitems.select_related(
|
||||
'parameter_obj',
|
||||
'parameter_obj__id_satellite',
|
||||
'parameter_obj__polarization',
|
||||
'parameter_obj__modulation',
|
||||
'parameter_obj__standard',
|
||||
'geo_obj',
|
||||
'created_by__user',
|
||||
'updated_by__user'
|
||||
).order_by('created_at')
|
||||
|
||||
context = {
|
||||
'object': source,
|
||||
'form': form,
|
||||
'objitems': objitems,
|
||||
'full_width_page': True,
|
||||
}
|
||||
|
||||
return render(request, 'mainapp/source_form.html', context)
|
||||
|
||||
def post(self, request, pk):
|
||||
source = get_object_or_404(Source, pk=pk)
|
||||
form = SourceForm(request.POST, instance=source)
|
||||
|
||||
if form.is_valid():
|
||||
source = form.save(commit=False)
|
||||
# Set updated_by to current user
|
||||
if hasattr(request.user, 'customuser'):
|
||||
source.updated_by = request.user.customuser
|
||||
source.save()
|
||||
|
||||
messages.success(request, f'Источник #{source.id} успешно обновлен.')
|
||||
|
||||
# Redirect back with query params if present
|
||||
if request.GET.urlencode():
|
||||
return redirect(f"{reverse('mainapp:source_update', args=[source.id])}?{request.GET.urlencode()}")
|
||||
return redirect('mainapp:source_update', pk=source.id)
|
||||
|
||||
# If form is invalid, re-render with errors
|
||||
objitems = source.source_objitems.select_related(
|
||||
'parameter_obj',
|
||||
'parameter_obj__id_satellite',
|
||||
'parameter_obj__polarization',
|
||||
'parameter_obj__modulation',
|
||||
'parameter_obj__standard',
|
||||
'geo_obj',
|
||||
'created_by__user',
|
||||
'updated_by__user'
|
||||
).order_by('created_at')
|
||||
|
||||
context = {
|
||||
'object': source,
|
||||
'form': form,
|
||||
'objitems': objitems,
|
||||
'full_width_page': True,
|
||||
}
|
||||
|
||||
return render(request, 'mainapp/source_form.html', context)
|
||||
|
||||
|
||||
class SourceDeleteView(LoginRequiredMixin, AdminModeratorMixin, View):
|
||||
"""View for deleting Source."""
|
||||
|
||||
def get(self, request, pk):
|
||||
source = get_object_or_404(Source, pk=pk)
|
||||
|
||||
context = {
|
||||
'object': source,
|
||||
'objitems_count': source.source_objitems.count(),
|
||||
}
|
||||
|
||||
return render(request, 'mainapp/source_confirm_delete.html', context)
|
||||
|
||||
def post(self, request, pk):
|
||||
source = get_object_or_404(Source, pk=pk)
|
||||
source_id = source.id
|
||||
|
||||
try:
|
||||
source.delete()
|
||||
messages.success(request, f'Источник #{source_id} успешно удален.')
|
||||
except Exception as e:
|
||||
messages.error(request, f'Ошибка при удалении источника: {str(e)}')
|
||||
return redirect('mainapp:source_update', pk=pk)
|
||||
|
||||
# Redirect to source list
|
||||
if request.GET.urlencode():
|
||||
return redirect(f"{reverse('mainapp:home')}?{request.GET.urlencode()}")
|
||||
return redirect('mainapp:home')
|
||||
|
||||
@@ -23,7 +23,7 @@ class Transponders(models.Model):
|
||||
blank=True,
|
||||
verbose_name="Название транспондера",
|
||||
db_index=True,
|
||||
help_text="Название транспондера"
|
||||
help_text="Название транспондера",
|
||||
)
|
||||
downlink = models.FloatField(
|
||||
blank=True,
|
||||
@@ -52,14 +52,14 @@ class Transponders(models.Model):
|
||||
null=True,
|
||||
verbose_name="Название зоны",
|
||||
db_index=True,
|
||||
help_text="Название зоны покрытия транспондера"
|
||||
help_text="Название зоны покрытия транспондера",
|
||||
)
|
||||
snr = models.FloatField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Полоса",
|
||||
# validators=[MinValueValidator(0), MaxValueValidator(1000)],
|
||||
help_text="Полоса частот в МГц (0-1000)"
|
||||
help_text="Полоса частот в МГц (0-1000)",
|
||||
)
|
||||
created_at = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
@@ -99,7 +99,7 @@ class Transponders(models.Model):
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Поляризация",
|
||||
help_text="Поляризация сигнала"
|
||||
help_text="Поляризация сигнала",
|
||||
)
|
||||
sat_id = models.ForeignKey(
|
||||
Satellite,
|
||||
@@ -107,20 +107,19 @@ class Transponders(models.Model):
|
||||
related_name="tran_satellite",
|
||||
verbose_name="Спутник",
|
||||
db_index=True,
|
||||
help_text="Спутник, которому принадлежит транспондер"
|
||||
help_text="Спутник, которому принадлежит транспондер",
|
||||
)
|
||||
|
||||
# Вычисляемые поля
|
||||
transfer = models.GeneratedField(
|
||||
expression=ExpressionWrapper(
|
||||
Abs(F('downlink') - F('uplink')),
|
||||
output_field=models.FloatField()
|
||||
Abs(F("downlink") - F("uplink")), output_field=models.FloatField()
|
||||
),
|
||||
output_field=models.FloatField(),
|
||||
db_persist=True,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Перенос"
|
||||
verbose_name="Перенос",
|
||||
)
|
||||
|
||||
# def clean(self):
|
||||
@@ -143,10 +142,8 @@ class Transponders(models.Model):
|
||||
class Meta:
|
||||
verbose_name = "Транспондер"
|
||||
verbose_name_plural = "Транспондеры"
|
||||
ordering = ['sat_id', 'downlink']
|
||||
ordering = ["sat_id", "downlink"]
|
||||
indexes = [
|
||||
models.Index(fields=['sat_id', 'downlink']),
|
||||
models.Index(fields=['sat_id', 'zone_name']),
|
||||
models.Index(fields=["sat_id", "downlink"]),
|
||||
models.Index(fields=["sat_id", "zone_name"]),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
{% load static %}
|
||||
{% block content %}
|
||||
|
||||
<div class="db-objects-panel" style="position: absolute; top: 100px; z-index: 1000; background: white; padding: 10px; border: 1px solid #ccc; border-radius: 5px;">
|
||||
<div class="db-objects-panel"
|
||||
style="position: absolute; top: 100px; z-index: 1000; background: white; padding: 10px; border: 1px solid #ccc; border-radius: 5px;">
|
||||
<div class="panel-title">Объекты из базы</div>
|
||||
<select id="objectSelector" class="object-select">
|
||||
<option value="">— Выберите объект —</option>
|
||||
@@ -10,14 +11,18 @@
|
||||
<option value="{{ sat.id }}">{{ sat.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button id="loadObjectBtn" class="load-btn" style="display: block; width: 100%; margin-top: 10px;">Все точки</button>
|
||||
<button id="loadObjectTransBtn" class="load-btn" style="display: block; width: 100%; margin-top: 10px;">Точки транспондеров</button>
|
||||
<button id="clearMarkersBtn" type="button" onclick="clearAllMarkers()" style="display: block; width: 100%; margin-top: 10px;">Очистить маркеры</button>
|
||||
<button id="loadObjectBtn" class="load-btn" style="display: block; width: 100%; margin-top: 10px;">Все
|
||||
точки</button>
|
||||
<button id="loadObjectTransBtn" class="load-btn" style="display: block; width: 100%; margin-top: 10px;">Точки
|
||||
транспондеров</button>
|
||||
<button id="clearMarkersBtn" type="button" onclick="clearAllMarkers()"
|
||||
style="display: block; width: 100%; margin-top: 10px;">Очистить маркеры</button>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="footprint-control" style="position: absolute; top: 270px; z-index: 1000; background: white; padding: 10px; border: 1px solid #ccc; border-radius: 5px;">
|
||||
<div class="footprint-control"
|
||||
style="position: absolute; top: 270px; z-index: 1000; background: white; padding: 10px; border: 1px solid #ccc; border-radius: 5px;">
|
||||
<div class="panel-title">Области покрытия</div>
|
||||
<div class="footprint-actions">
|
||||
<button id="showAllFootprints">Показать все</button>
|
||||
|
||||
Reference in New Issue
Block a user