init commit
17
.gitignore
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Python-generated files
|
||||||
|
__pycache__/
|
||||||
|
*.py[oc]
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
wheels/
|
||||||
|
*.egg-info
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv
|
||||||
|
.hintrc
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
django-leaflet
|
||||||
|
admin-interface
|
||||||
|
|
||||||
|
docker-*
|
||||||
1
.python-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.13
|
||||||
1
README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Сервис для работы с базой данных. Django + PostreSQL с подулем Postgis
|
||||||
0
dbapp/dbapp/__init__.py
Normal file
16
dbapp/dbapp/asgi.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
ASGI config for dbapp project.
|
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dbapp.settings')
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
||||||
201
dbapp/dbapp/settings.py
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
"""
|
||||||
|
Django settings for dbapp project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 5.2.7.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.2/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/5.2/ref/settings/
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
|
||||||
|
if os.name == 'nt':
|
||||||
|
OSGEO4W = r"C:\Program Files\OSGeo4W"
|
||||||
|
assert os.path.isdir(OSGEO4W), "Directory does not exist: " + OSGEO4W
|
||||||
|
os.environ['OSGEO4W_ROOT'] = OSGEO4W
|
||||||
|
os.environ['PROJ_LIB'] = os.path.join(OSGEO4W, r"share\proj")
|
||||||
|
os.environ['PATH'] = OSGEO4W + r"\bin;" + os.environ['PATH']
|
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
# GDAL_LIBRARY_PATH = r'C:/Program Files/OSGeo4W/bin/gdall311.dll'
|
||||||
|
# Quick-start development settings - unsuitable for production
|
||||||
|
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = 'django-insecure-7etj5f7buo2a57xv=w3^&llusq8rii7b_gd)9$t_1xcnao!^tq'
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = ['*']
|
||||||
|
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
# 'django_daisy',
|
||||||
|
'dal',
|
||||||
|
'dal_select2',
|
||||||
|
"admin_interface",
|
||||||
|
"colorfield",
|
||||||
|
'django.contrib.gis',
|
||||||
|
'leaflet',
|
||||||
|
'dynamic_raw_id',
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.humanize',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
'mainapp',
|
||||||
|
'mapsapp',
|
||||||
|
'rangefilter',
|
||||||
|
'django_admin_multiple_choice_list_filter',
|
||||||
|
'more_admin_filters',
|
||||||
|
'import_export',
|
||||||
|
'debug_toolbar'
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
"debug_toolbar.middleware.DebugToolbarMiddleware", #Добавил
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware', #Добавил
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'dbapp.urls'
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [],
|
||||||
|
'APP_DIRS': True,
|
||||||
|
'OPTIONS': {
|
||||||
|
'context_processors': [
|
||||||
|
'django.template.context_processors.request',
|
||||||
|
'django.contrib.auth.context_processors.auth',
|
||||||
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = 'dbapp.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.contrib.gis.db.backends.postgis',
|
||||||
|
'NAME': 'geodb',
|
||||||
|
'USER': 'geralt',
|
||||||
|
'PASSWORD': '27082025STC',
|
||||||
|
'HOST': 'localhost',
|
||||||
|
'PORT': '5432',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
# {
|
||||||
|
# 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
|
# },
|
||||||
|
# {
|
||||||
|
# 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
|
# },
|
||||||
|
# {
|
||||||
|
# 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
|
# },
|
||||||
|
# {
|
||||||
|
# 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
|
# },
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = 'ru'
|
||||||
|
|
||||||
|
TIME_ZONE = 'Europe/Moscow'
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
||||||
|
|
||||||
|
STATIC_URL = '/static/'
|
||||||
|
STATICFILES_DIRS = [
|
||||||
|
os.path.join(BASE_DIR, 'static'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Default primary key field type
|
||||||
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
|
# DAISY_SETTINGS = {
|
||||||
|
# 'SITE_TITLE': 'Geo admin', # The title of the site
|
||||||
|
# 'SITE_HEADER': 'GEO', # Header text displayed in the admin panel
|
||||||
|
# 'INDEX_TITLE': 'Заголовок', # The title for the index page of dashboard
|
||||||
|
# 'SITE_LOGO': '/static/admin/img/icon-clock.svg', # Path to the logo image displayed in the sidebar
|
||||||
|
# 'EXTRA_STYLES': [], # List of extra stylesheets to be loaded in base.html (optional)
|
||||||
|
# 'EXTRA_SCRIPTS': [], # List of extra script URLs to be loaded in base.html (optional)
|
||||||
|
# 'LOAD_FULL_STYLES': False, # If True, loads full DaisyUI components in the admin (useful if you have custom template overrides)
|
||||||
|
# 'SHOW_CHANGELIST_FILTER': False, # If True, the filter sidebar will open by default on changelist views
|
||||||
|
# 'DONT_SUPPORT_ME': True, # Hide github link in sidebar footer
|
||||||
|
# 'SIDEBAR_FOOTNOTE': 'Что-то о как', # add footnote to sidebar
|
||||||
|
# 'DEFAULT_THEME': None, # Set a default theme (e.g., 'corporate', 'dark', 'light')
|
||||||
|
# 'DEFAULT_THEME_DARK': None, # Set a default dark theme when system prefers dark mode
|
||||||
|
# 'SHOW_THEME_SELECTOR': True, # If False, hides the theme selector dropdown entirely
|
||||||
|
# }
|
||||||
|
|
||||||
|
# AUTH_USER_MODEL = 'mainapp.CustomUser'
|
||||||
|
X_FRAME_OPTIONS = "SAMEORIGIN"
|
||||||
|
SILENCED_SYSTEM_CHECKS = ["security.W019"]
|
||||||
|
|
||||||
|
LEAFLET_CONFIG = {
|
||||||
|
'ATTRIBUTION_PREFIX': '',
|
||||||
|
'TILES': [('Satellite', 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {'attribution': '© Esri', 'maxZoom': 16}),
|
||||||
|
('Streets', 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {'attribution': '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'})
|
||||||
|
],
|
||||||
|
# 'RESET_VIEW': False,
|
||||||
|
# 'NO_GLOBALS': False,
|
||||||
|
# 'PLUGINS': {
|
||||||
|
# 'leaflet-measure': {
|
||||||
|
# 'css': ['https://cdn.jsdelivr.net/npm/leaflet-measure@3.1.0/dist/leaflet-measure.min.css'],
|
||||||
|
# 'js': 'https://cdn.jsdelivr.net/npm/leaflet-measure@3.1.0/dist/leaflet-measure.min.js',
|
||||||
|
# 'auto-include': True,
|
||||||
|
# },
|
||||||
|
# 'leaflet-featuregroup': {
|
||||||
|
# # 'css': ['https://cdn.jsdelivr.net/npm/leaflet-measure@3.1.0/dist/leaflet-measure.min.css'],
|
||||||
|
# 'js': 'https://cdn.jsdelivr.net/npm/leaflet.featuregroup.subgroup@1.0.2/dist/leaflet.featuregroup.subgroup.min.js',
|
||||||
|
# 'auto-include': True,
|
||||||
|
# },
|
||||||
|
# }
|
||||||
|
}
|
||||||
|
|
||||||
|
INTERNAL_IPS = [
|
||||||
|
'127.0.0.1',
|
||||||
|
]
|
||||||
28
dbapp/dbapp/urls.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"""
|
||||||
|
URL configuration for dbapp project.
|
||||||
|
|
||||||
|
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||||
|
https://docs.djangoproject.com/en/5.2/topics/http/urls/
|
||||||
|
Examples:
|
||||||
|
Function views
|
||||||
|
1. Add an import: from my_app import views
|
||||||
|
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||||
|
Class-based views
|
||||||
|
1. Add an import: from other_app.views import Home
|
||||||
|
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||||
|
Including another URLconf
|
||||||
|
1. Import the include() function: from django.urls import include, path
|
||||||
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
|
"""
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import path, include
|
||||||
|
from mainapp import views
|
||||||
|
from debug_toolbar.toolbar import debug_toolbar_urls
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
# path('admin/dynamic_raw_id/', include('dynamic_raw_id.urls')),
|
||||||
|
path('admin/', admin.site.urls, name='admin'),
|
||||||
|
# path('admin/map/', views.show_map_view, name='admin_show_map'),
|
||||||
|
path('', include('mainapp.urls')),
|
||||||
|
path('', include('mapsapp.urls'))
|
||||||
|
] + debug_toolbar_urls()
|
||||||
16
dbapp/dbapp/wsgi.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
WSGI config for dbapp project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dbapp.settings')
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
||||||
0
dbapp/mainapp/__init__.py
Normal file
506
dbapp/mainapp/admin.py
Normal file
@@ -0,0 +1,506 @@
|
|||||||
|
# admin.py
|
||||||
|
from django.contrib import admin
|
||||||
|
from .models import (
|
||||||
|
Polarization,
|
||||||
|
Modulation,
|
||||||
|
Standard,
|
||||||
|
SigmaParMark,
|
||||||
|
SigmaParameter,
|
||||||
|
Parameter,
|
||||||
|
Satellite,
|
||||||
|
Mirror,
|
||||||
|
Geo,
|
||||||
|
ObjItem,
|
||||||
|
CustomUser
|
||||||
|
)
|
||||||
|
from leaflet.admin import LeafletGeoAdmin
|
||||||
|
from django import forms
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
|
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.contrib.gis.db import models as gis
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from rangefilter.filters import (
|
||||||
|
DateRangeFilterBuilder,
|
||||||
|
DateTimeRangeFilterBuilder,
|
||||||
|
NumericRangeFilterBuilder,
|
||||||
|
DateRangeQuickSelectListFilterBuilder,
|
||||||
|
)
|
||||||
|
from dynamic_raw_id.admin import DynamicRawIDMixin
|
||||||
|
from more_admin_filters import MultiSelectDropdownFilter, MultiSelectFilter, MultiSelectRelatedDropdownFilter
|
||||||
|
from import_export.admin import ImportExportActionModelAdmin
|
||||||
|
from .filters import GeoKupDistanceFilter, GeoValidDistanceFilter, UniqueToggleFilter, HasSigmaParameterFilter
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.site_title = "Геолокация"
|
||||||
|
admin.site.site_header = "Geolocation"
|
||||||
|
admin.site.index_title = "Geo"
|
||||||
|
admin.site.unregister(User)
|
||||||
|
admin.site.unregister(Group)
|
||||||
|
|
||||||
|
|
||||||
|
class LocationForm(forms.ModelForm):
|
||||||
|
latitude_geo = forms.FloatField(required=False, label="Широта")
|
||||||
|
longitude_geo = forms.FloatField(required=False, label="Долгота")
|
||||||
|
latitude_kupsat = forms.FloatField(required=False, label="Широта")
|
||||||
|
longitude_kupsat = forms.FloatField(required=False, label="Долгота")
|
||||||
|
latitude_valid = forms.FloatField(required=False, label="Широта")
|
||||||
|
longitude_valid = forms.FloatField(required=False, label="Долгота")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Geo
|
||||||
|
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]
|
||||||
|
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]
|
||||||
|
if self.instance and self.instance.coords_valid:
|
||||||
|
self.fields['latitude_valid'].initial = self.instance.coords_valid[1]
|
||||||
|
self.fields['longitude_valid'].initial = self.instance.coords_valid[0]
|
||||||
|
|
||||||
|
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')
|
||||||
|
if lat is not None and lon is not None:
|
||||||
|
instance.coords = Point(lon, lat, srid=4326)
|
||||||
|
|
||||||
|
lat = self.cleaned_data.get('latitude_kupsat')
|
||||||
|
lon = self.cleaned_data.get('longitude_kupsat')
|
||||||
|
if lat is not None and lon is not None:
|
||||||
|
instance.coords_kupsat = Point(lon, lat, srid=4326)
|
||||||
|
|
||||||
|
lat = self.cleaned_data.get('latitude_valid')
|
||||||
|
lon = self.cleaned_data.get('longitude_valid')
|
||||||
|
if lat is not None and lon is not None:
|
||||||
|
instance.coords_valid = Point(lon, lat, srid=4326)
|
||||||
|
|
||||||
|
if commit:
|
||||||
|
instance.save()
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class CustomUserInline(admin.StackedInline):
|
||||||
|
model = CustomUser
|
||||||
|
can_delete = False
|
||||||
|
verbose_name_plural = 'Дополнительная информация пользователя'
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(CustomUser)
|
||||||
|
class CustomUserAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('user', 'role')
|
||||||
|
list_filter = ('role',)
|
||||||
|
|
||||||
|
|
||||||
|
class UserAdmin(BaseUserAdmin):
|
||||||
|
inlines = [CustomUserInline]
|
||||||
|
|
||||||
|
admin.site.register(User, UserAdmin)
|
||||||
|
|
||||||
|
@admin.register(SigmaParMark)
|
||||||
|
class SigmaParMarkAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("mark", "timestamp")
|
||||||
|
search_fields = ("mark", )
|
||||||
|
ordering = ("timestamp",)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Polarization)
|
||||||
|
class PolarizationAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("name",)
|
||||||
|
search_fields = ("name",)
|
||||||
|
ordering = ("name",)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Modulation)
|
||||||
|
class ModulationAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("name",)
|
||||||
|
search_fields = ("name",)
|
||||||
|
ordering = ("name",)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Standard)
|
||||||
|
class StandardAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("name",)
|
||||||
|
search_fields = ("name",)
|
||||||
|
ordering = ("name",)
|
||||||
|
|
||||||
|
|
||||||
|
class SigmaParameterInline(admin.StackedInline):
|
||||||
|
model = SigmaParameter
|
||||||
|
extra = 0
|
||||||
|
autocomplete_fields = ['mark']
|
||||||
|
readonly_fields = (
|
||||||
|
"datetime_begin",
|
||||||
|
"datetime_end",
|
||||||
|
)
|
||||||
|
def has_add_permission(self, request, obj=None):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Parameter)
|
||||||
|
class ParameterAdmin(ImportExportActionModelAdmin, admin.ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
"id_satellite",
|
||||||
|
"frequency",
|
||||||
|
"freq_range",
|
||||||
|
"polarization",
|
||||||
|
"modulation",
|
||||||
|
"bod_velocity",
|
||||||
|
"snr",
|
||||||
|
"standard",
|
||||||
|
"sigma_parameter"
|
||||||
|
)
|
||||||
|
# fields = ( "id_satellite",
|
||||||
|
# "frequency",
|
||||||
|
# "freq_range",
|
||||||
|
# "polarization",
|
||||||
|
# "modulation",
|
||||||
|
# "bod_velocity",
|
||||||
|
# "snr",
|
||||||
|
# "standard",
|
||||||
|
# "id_sigma_parameter")
|
||||||
|
list_display_links = ("frequency", "id_satellite", )
|
||||||
|
list_filter = (
|
||||||
|
HasSigmaParameterFilter,
|
||||||
|
("id_satellite", MultiSelectRelatedDropdownFilter),
|
||||||
|
("polarization__name", MultiSelectDropdownFilter),
|
||||||
|
("modulation", MultiSelectRelatedDropdownFilter),
|
||||||
|
("standard", MultiSelectRelatedDropdownFilter),
|
||||||
|
("frequency", NumericRangeFilterBuilder()),
|
||||||
|
("freq_range", NumericRangeFilterBuilder()),
|
||||||
|
("snr", NumericRangeFilterBuilder()),
|
||||||
|
)
|
||||||
|
search_fields = (
|
||||||
|
"id_satellite",
|
||||||
|
"frequency",
|
||||||
|
"freq_range",
|
||||||
|
"bod_velocity",
|
||||||
|
"snr",
|
||||||
|
"modulation__name",
|
||||||
|
"polarization__name",
|
||||||
|
"standard__name",
|
||||||
|
)
|
||||||
|
ordering = ("frequency",)
|
||||||
|
list_select_related = ("polarization", "modulation", "standard", "id_satellite",)
|
||||||
|
# raw_id_fields = ("id_sigma_parameter", )
|
||||||
|
inlines = [SigmaParameterInline]
|
||||||
|
# autocomplete_fields = ("id_sigma_parameter", )
|
||||||
|
|
||||||
|
def sigma_parameter(self, obj):
|
||||||
|
sigma_obj = obj.sigma_parameter.all()
|
||||||
|
if sigma_obj:
|
||||||
|
return f"{sigma_obj[0].frequency}: {sigma_obj[0].freq_range}"
|
||||||
|
return '-'
|
||||||
|
sigma_parameter.short_description = "ВЧ sigma"
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(SigmaParameter)
|
||||||
|
class SigmaParameterAdmin(ImportExportActionModelAdmin, admin.ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
"id_satellite",
|
||||||
|
"status",
|
||||||
|
"frequency",
|
||||||
|
"freq_range",
|
||||||
|
"power",
|
||||||
|
"modulation",
|
||||||
|
"bod_velocity",
|
||||||
|
"snr",
|
||||||
|
"standard",
|
||||||
|
"parameter",
|
||||||
|
"packets",
|
||||||
|
"datetime_begin",
|
||||||
|
"datetime_end",
|
||||||
|
)
|
||||||
|
readonly_fields = (
|
||||||
|
"datetime_begin",
|
||||||
|
"datetime_end",
|
||||||
|
|
||||||
|
)
|
||||||
|
list_display_links = ("id_satellite",)
|
||||||
|
list_filter = (
|
||||||
|
("id_satellite__name", MultiSelectDropdownFilter),
|
||||||
|
("modulation__name", MultiSelectDropdownFilter),
|
||||||
|
("standard__name", MultiSelectDropdownFilter),
|
||||||
|
("frequency", NumericRangeFilterBuilder()),
|
||||||
|
("freq_range", NumericRangeFilterBuilder()),
|
||||||
|
("snr", NumericRangeFilterBuilder()),
|
||||||
|
)
|
||||||
|
search_fields = (
|
||||||
|
"id_satellite__name",
|
||||||
|
"frequency",
|
||||||
|
"freq_range",
|
||||||
|
"bod_velocity",
|
||||||
|
"snr",
|
||||||
|
"modulation__name",
|
||||||
|
"standard__name",
|
||||||
|
)
|
||||||
|
autocomplete_fields = ('mark',)
|
||||||
|
ordering = ("frequency",)
|
||||||
|
list_select_related = ("modulation", "standard", "id_satellite", "parameter")
|
||||||
|
prefetch_related = ("mark",)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Satellite)
|
||||||
|
class SatelliteAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("name",)
|
||||||
|
search_fields = ("name",)
|
||||||
|
ordering = ("name",)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Mirror)
|
||||||
|
class MirrorAdmin(ImportExportActionModelAdmin, admin.ModelAdmin):
|
||||||
|
list_display = ("name",)
|
||||||
|
search_fields = ("name",)
|
||||||
|
ordering = ("name",)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Geo)
|
||||||
|
class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin):
|
||||||
|
form = LocationForm
|
||||||
|
readonly_fields = ("distance_coords_kup", "distance_coords_valid", "distance_kup_valid")
|
||||||
|
fieldsets = (
|
||||||
|
("Основная информация", {
|
||||||
|
"fields": ("mirrors", "location", "distance_coords_kup",
|
||||||
|
"distance_coords_valid", "distance_kup_valid", "timestamp", "comment", "id_user_add")
|
||||||
|
}),
|
||||||
|
("Координаты: геолокация", {
|
||||||
|
"fields": ("longitude_geo", "latitude_geo", "coords"),
|
||||||
|
}),
|
||||||
|
("Координаты: Кубсат", {
|
||||||
|
"fields": ("longitude_kupsat", "latitude_kupsat", "coords_kupsat"),
|
||||||
|
}),
|
||||||
|
("Координаты: Оперативный отдел", {
|
||||||
|
"fields": ("longitude_valid", "latitude_valid", "coords_valid"),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
list_display = (
|
||||||
|
"formatted_timestamp",
|
||||||
|
"location",
|
||||||
|
"mirrors_names",
|
||||||
|
"geo_coords",
|
||||||
|
"kupsat_coords",
|
||||||
|
"valid_coords",
|
||||||
|
"is_average",
|
||||||
|
)
|
||||||
|
autocomplete_fields = ('mirrors',)
|
||||||
|
list_display_links = ("formatted_timestamp",)
|
||||||
|
list_filter = (
|
||||||
|
("mirrors", MultiSelectRelatedDropdownFilter),
|
||||||
|
"is_average",
|
||||||
|
("location", MultiSelectDropdownFilter),
|
||||||
|
("timestamp", DateRangeQuickSelectListFilterBuilder()),
|
||||||
|
("id_user_add", MultiSelectRelatedDropdownFilter),
|
||||||
|
)
|
||||||
|
search_fields = (
|
||||||
|
"mirrors__name",
|
||||||
|
"location",
|
||||||
|
"coords",
|
||||||
|
"coords_kupsat",
|
||||||
|
"coords_valid"
|
||||||
|
)
|
||||||
|
list_select_related = ("id_user_add", )
|
||||||
|
prefetch_related = ("mirrors", )
|
||||||
|
|
||||||
|
|
||||||
|
settings_overrides = {
|
||||||
|
'DEFAULT_CENTER': (55.7558, 37.6173),
|
||||||
|
'DEFAULT_ZOOM': 12,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def mirrors_names(self, obj):
|
||||||
|
return ", ".join(m.name for m in obj.mirrors.all())
|
||||||
|
mirrors_names.short_description = "Зеркала"
|
||||||
|
|
||||||
|
def formatted_timestamp(self, obj):
|
||||||
|
if not obj.timestamp:
|
||||||
|
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"
|
||||||
|
|
||||||
|
def geo_coords(self, obj):
|
||||||
|
longitude = obj.coords.coords[0]
|
||||||
|
latitude = obj.coords.coords[1]
|
||||||
|
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||||
|
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||||
|
return f"{lat} {lon}"
|
||||||
|
geo_coords.short_description = "Координаты геолокации"
|
||||||
|
|
||||||
|
def kupsat_coords(self, obj):
|
||||||
|
if obj.coords_kupsat is None:
|
||||||
|
return "-"
|
||||||
|
longitude = obj.coords_kupsat.coords[0]
|
||||||
|
latitude = obj.coords_kupsat.coords[1]
|
||||||
|
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||||
|
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||||
|
return f"{lat} {lon}"
|
||||||
|
kupsat_coords.short_description = "Координаты Кубсата"
|
||||||
|
|
||||||
|
def valid_coords(self, obj):
|
||||||
|
if obj.coords_valid is None:
|
||||||
|
return "-"
|
||||||
|
longitude = obj.coords_valid.coords[0]
|
||||||
|
latitude = obj.coords_valid.coords[1]
|
||||||
|
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||||
|
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||||
|
return f"{lat} {lon}"
|
||||||
|
valid_coords.short_description = "Координаты оперативного отдела"
|
||||||
|
|
||||||
|
def show_on_map(modeladmin, request, queryset):
|
||||||
|
# Получаем список ID выбранных объектов
|
||||||
|
selected_ids = queryset.values_list('id', flat=True)
|
||||||
|
# Формируем строку вида "1,2,3"
|
||||||
|
ids_str = ','.join(str(pk) for pk in selected_ids)
|
||||||
|
# Перенаправляем на ваш кастомный view с картой
|
||||||
|
return redirect(reverse('admin_show_map') + f'?ids={ids_str}')
|
||||||
|
|
||||||
|
show_on_map.short_description = "Показать выбранные на карте"
|
||||||
|
|
||||||
|
@admin.register(ObjItem)
|
||||||
|
class ObjectAdmin(admin.ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
"name",
|
||||||
|
"sat_name",
|
||||||
|
"freq",
|
||||||
|
"freq_range",
|
||||||
|
"pol",
|
||||||
|
"bod_velocity",
|
||||||
|
"modulation",
|
||||||
|
"snr",
|
||||||
|
"geo_coords",
|
||||||
|
"kupsat_coords",
|
||||||
|
"valid_coords",
|
||||||
|
"distance_geo_kup",
|
||||||
|
"distance_geo_valid",
|
||||||
|
"distance_kup_valid",
|
||||||
|
)
|
||||||
|
list_display_links = ("name",)
|
||||||
|
list_filter = (
|
||||||
|
UniqueToggleFilter,
|
||||||
|
("id_vch_load__id_satellite", MultiSelectRelatedDropdownFilter),
|
||||||
|
("id_vch_load__frequency", NumericRangeFilterBuilder()),
|
||||||
|
("id_vch_load__freq_range", NumericRangeFilterBuilder()),
|
||||||
|
("id_vch_load__snr", NumericRangeFilterBuilder()),
|
||||||
|
("id_vch_load__modulation", MultiSelectRelatedDropdownFilter),
|
||||||
|
("id_vch_load__polarization", MultiSelectRelatedDropdownFilter),
|
||||||
|
GeoKupDistanceFilter,
|
||||||
|
GeoValidDistanceFilter
|
||||||
|
)
|
||||||
|
search_fields = (
|
||||||
|
"name",
|
||||||
|
# "id_geo",
|
||||||
|
# "id_satellite__name",
|
||||||
|
# "id_vch_load__frequency",
|
||||||
|
)
|
||||||
|
ordering = ("name",)
|
||||||
|
list_select_related = (
|
||||||
|
# "id_satellite",
|
||||||
|
"id_vch_load",
|
||||||
|
"id_vch_load__polarization",
|
||||||
|
"id_vch_load__modulation",
|
||||||
|
"id_vch_load__id_satellite",
|
||||||
|
"id_geo",
|
||||||
|
)
|
||||||
|
autocomplete_fields = ("id_geo",)
|
||||||
|
raw_id_fields = ("id_vch_load",)
|
||||||
|
# dynamic_raw_id_fields = ("id_vch_load",)
|
||||||
|
actions = [show_on_map]
|
||||||
|
|
||||||
|
def sat_name(self, obj):
|
||||||
|
return obj.id_vch_load.id_satellite
|
||||||
|
sat_name.short_description = "Спутник"
|
||||||
|
|
||||||
|
def freq(self, obj):
|
||||||
|
par = obj.id_vch_load
|
||||||
|
return par.frequency
|
||||||
|
freq.short_description = "Частота, МГц"
|
||||||
|
|
||||||
|
def distance_geo_kup(self, obj):
|
||||||
|
par = obj.id_geo.distance_coords_kup
|
||||||
|
if par is None:
|
||||||
|
return "-"
|
||||||
|
return round(par, 3)
|
||||||
|
distance_geo_kup.short_description = "Гео-куб, км"
|
||||||
|
|
||||||
|
def distance_geo_valid(self, obj):
|
||||||
|
par = obj.id_geo.distance_coords_valid
|
||||||
|
if par is None:
|
||||||
|
return "-"
|
||||||
|
return round(par, 3)
|
||||||
|
distance_geo_valid.short_description = "Гео-опер, км"
|
||||||
|
|
||||||
|
def distance_kup_valid(self, obj):
|
||||||
|
par = obj.id_geo.distance_kup_valid
|
||||||
|
if par is None:
|
||||||
|
return "-"
|
||||||
|
return round(par, 3)
|
||||||
|
distance_kup_valid.short_description = "Куб-опер, км"
|
||||||
|
|
||||||
|
def pol(self, obj):
|
||||||
|
par = obj.id_vch_load.polarization
|
||||||
|
return par.name
|
||||||
|
pol.short_description = "Поляризация"
|
||||||
|
|
||||||
|
def freq_range(self, obj):
|
||||||
|
par = obj.id_vch_load
|
||||||
|
return par.freq_range
|
||||||
|
freq_range.short_description = "Полоса, МГц"
|
||||||
|
|
||||||
|
def bod_velocity(self, obj):
|
||||||
|
par = obj.id_vch_load
|
||||||
|
return par.bod_velocity
|
||||||
|
bod_velocity.short_description = "Сим. v, БОД"
|
||||||
|
|
||||||
|
def modulation(self, obj):
|
||||||
|
par = obj.id_vch_load.modulation
|
||||||
|
return par.name
|
||||||
|
modulation.short_description = "Модуляция"
|
||||||
|
|
||||||
|
def snr(self, obj):
|
||||||
|
par = obj.id_vch_load
|
||||||
|
return par.snr
|
||||||
|
snr.short_description = "ОСШ"
|
||||||
|
|
||||||
|
def geo_coords(self, obj):
|
||||||
|
geo = obj.id_geo
|
||||||
|
longitude = geo.coords.coords[0]
|
||||||
|
latitude = geo.coords.coords[1]
|
||||||
|
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||||
|
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||||
|
return f"{lat} {lon}"
|
||||||
|
geo_coords.short_description = "Координаты геолокации"
|
||||||
|
|
||||||
|
def kupsat_coords(self, obj):
|
||||||
|
obj = obj.id_geo
|
||||||
|
if obj.coords_kupsat is None:
|
||||||
|
return "-"
|
||||||
|
longitude = obj.coords_kupsat.coords[0]
|
||||||
|
latitude = obj.coords_kupsat.coords[1]
|
||||||
|
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||||
|
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||||
|
return f"{lat} {lon}"
|
||||||
|
kupsat_coords.short_description = "Координаты Кубсата"
|
||||||
|
|
||||||
|
def valid_coords(self, obj):
|
||||||
|
obj = obj.id_geo
|
||||||
|
if obj.coords_valid is None:
|
||||||
|
return "-"
|
||||||
|
longitude = obj.coords_valid.coords[0]
|
||||||
|
latitude = obj.coords_valid.coords[1]
|
||||||
|
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||||
|
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||||
|
return f"{lat} {lon}"
|
||||||
|
valid_coords.short_description = "Координаты оперативного отдела"
|
||||||
6
dbapp/mainapp/apps.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class MainappConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'mainapp'
|
||||||
31
dbapp/mainapp/clusters.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
from .models import ObjItem
|
||||||
|
from sklearn.cluster import DBSCAN, HDBSCAN, KMeans
|
||||||
|
import numpy as np
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
def get_clusters(coords: list[tuple[float, float]]):
|
||||||
|
coords = np.radians(coords)
|
||||||
|
lat, lon = coords[:, 0], coords[:, 1]
|
||||||
|
db = DBSCAN(eps=0.06, min_samples=5, algorithm='ball_tree', metric='haversine')
|
||||||
|
# db = HDBSCAN()
|
||||||
|
cluster_labels = db.fit_predict(coords)
|
||||||
|
plt.figure(figsize=(10, 8))
|
||||||
|
unique_labels = set(cluster_labels)
|
||||||
|
colors = plt.cm.tab10(np.linspace(0, 1, len(unique_labels)))
|
||||||
|
|
||||||
|
for label, color in zip(unique_labels, colors):
|
||||||
|
if label == -1:
|
||||||
|
color = 'k'
|
||||||
|
label_name = 'Шум'
|
||||||
|
else:
|
||||||
|
label_name = f'Кластер {label}'
|
||||||
|
|
||||||
|
mask = cluster_labels == label
|
||||||
|
plt.scatter(lon[mask], lat[mask], c=[color], label=label_name, s=30)
|
||||||
|
|
||||||
|
plt.xlabel('Долгота')
|
||||||
|
plt.ylabel('Широта')
|
||||||
|
plt.title('Кластеризация геоданных с DBSCAN (метрика Хаверсина)')
|
||||||
|
plt.legend()
|
||||||
|
plt.grid(True)
|
||||||
|
plt.show()
|
||||||
73
dbapp/mainapp/filters.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
from django.contrib.admin import SimpleListFilter
|
||||||
|
from .models import ObjItem
|
||||||
|
|
||||||
|
class GeoKupDistanceFilter(SimpleListFilter):
|
||||||
|
title = 'Расстояние между гео и кубсатом'
|
||||||
|
parameter_name = 'distance_geo_kup'
|
||||||
|
|
||||||
|
def lookups(self, request, model_admin):
|
||||||
|
return (
|
||||||
|
('small', 'Меньше 100 км'),
|
||||||
|
('medium', '100-500 км'),
|
||||||
|
('large', 'Больше 500 км'),
|
||||||
|
)
|
||||||
|
|
||||||
|
def queryset(self, request, queryset):
|
||||||
|
if self.value() == 'small':
|
||||||
|
return queryset.filter(distance_coords_kup__lt=100)
|
||||||
|
if self.value() == 'medium':
|
||||||
|
return queryset.filter(distance_coords_kup__gte=100, distance_coords_kup__lte=500)
|
||||||
|
if self.value() == 'large':
|
||||||
|
return queryset.filter(distance_coords_kup__gt=500)
|
||||||
|
|
||||||
|
|
||||||
|
class GeoValidDistanceFilter(SimpleListFilter):
|
||||||
|
title = 'Расстояние между гео и оперативным отделом'
|
||||||
|
parameter_name = 'distance_geo_valid'
|
||||||
|
|
||||||
|
def lookups(self, request, model_admin):
|
||||||
|
return (
|
||||||
|
('small', 'Меньше 100 км'),
|
||||||
|
('medium', '100-500 км'),
|
||||||
|
('large', 'Больше 500 км'),
|
||||||
|
)
|
||||||
|
|
||||||
|
def queryset(self, request, queryset):
|
||||||
|
if self.value() == 'small':
|
||||||
|
return queryset.filter(distance_coords_valid__lt=100)
|
||||||
|
if self.value() == 'medium':
|
||||||
|
return queryset.filter(distance_coords_valid__gte=100, distance_coords_valid__lte=500)
|
||||||
|
if self.value() == 'large':
|
||||||
|
return queryset.filter(distance_coords_valid__gt=500)
|
||||||
|
|
||||||
|
class UniqueToggleFilter(SimpleListFilter):
|
||||||
|
title = 'Уникальность по имени'
|
||||||
|
parameter_name = 'name'
|
||||||
|
|
||||||
|
def lookups(self, request, model_admin):
|
||||||
|
return (
|
||||||
|
('unique', 'Только уникальные'),
|
||||||
|
('all', 'Все'),
|
||||||
|
)
|
||||||
|
|
||||||
|
def queryset(self, request, queryset):
|
||||||
|
if self.value() == 'unique':
|
||||||
|
return queryset.order_by('name').distinct('name')
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
class HasSigmaParameterFilter(SimpleListFilter):
|
||||||
|
title = 'ВЧ sigma'
|
||||||
|
parameter_name = 'has_sigma'
|
||||||
|
|
||||||
|
def lookups(self, request, model_admin):
|
||||||
|
return (
|
||||||
|
('yes', 'Заполнено'),
|
||||||
|
('no', 'Пусто'),
|
||||||
|
)
|
||||||
|
|
||||||
|
def queryset(self, request, queryset):
|
||||||
|
if self.value() == 'yes':
|
||||||
|
return queryset.filter(sigma_parameter__isnull=False)
|
||||||
|
if self.value() == 'no':
|
||||||
|
return queryset.filter(sigma_parameter__isnull=True)
|
||||||
|
return queryset
|
||||||
78
dbapp/mainapp/forms.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
from django import forms
|
||||||
|
from .models import Satellite
|
||||||
|
|
||||||
|
class LoadExcelData(forms.Form):
|
||||||
|
file = forms.FileField(
|
||||||
|
label="Выберите Excel файл",
|
||||||
|
widget=forms.FileInput(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'accept': '.xlsx,.xls'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
sat_choice = forms.ModelChoiceField(
|
||||||
|
queryset=Satellite.objects.all(),
|
||||||
|
label="Выберите спутник",
|
||||||
|
widget=forms.Select(attrs={
|
||||||
|
'class': 'form-select'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
number_input = forms.IntegerField(
|
||||||
|
label="Введите число объектов",
|
||||||
|
min_value=0,
|
||||||
|
widget=forms.NumberInput(attrs={
|
||||||
|
'class': 'form-control'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
class LoadCsvData(forms.Form):
|
||||||
|
file = forms.FileField(
|
||||||
|
label="Выберите CSV файл",
|
||||||
|
widget=forms.FileInput(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'accept': '.csv'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
class UploadFileForm(forms.Form):
|
||||||
|
sat_choice = forms.ModelChoiceField(
|
||||||
|
queryset=Satellite.objects.all(),
|
||||||
|
label="Выберите спутник",
|
||||||
|
widget=forms.Select(attrs={
|
||||||
|
'class': 'form-select'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
file = forms.FileField(
|
||||||
|
label="Выберите текстовый файл",
|
||||||
|
widget=forms.FileInput(attrs={
|
||||||
|
'class': 'form-file-input'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
class VchLinkForm(forms.Form):
|
||||||
|
sat_choice = forms.ModelChoiceField(
|
||||||
|
queryset=Satellite.objects.all(),
|
||||||
|
label="Выберите спутник",
|
||||||
|
widget=forms.Select(attrs={
|
||||||
|
'class': 'form-select'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
ku_range = forms.ChoiceField(
|
||||||
|
choices=[(9750.0, '9750'), (10750.0, '10750')],
|
||||||
|
# coerce=lambda x: x == 'True',
|
||||||
|
widget=forms.Select(attrs={'class': 'form-select'}),
|
||||||
|
label='Выбор диапазона'
|
||||||
|
)
|
||||||
|
value1 = forms.FloatField(
|
||||||
|
label="Первое число",
|
||||||
|
widget=forms.NumberInput(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'Введите первое число'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
value2 = forms.FloatField(
|
||||||
|
label="Второе число",
|
||||||
|
widget=forms.NumberInput(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'Введите второе число'
|
||||||
|
})
|
||||||
|
)
|
||||||
147
dbapp/mainapp/migrations/0001_initial.py
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-10-13 12:47
|
||||||
|
|
||||||
|
import django.contrib.gis.db.models.fields
|
||||||
|
import django.db.models.deletion
|
||||||
|
import mainapp.models
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Mirror',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=30, unique=True, verbose_name='Имя зеркала')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Зеркало',
|
||||||
|
'verbose_name_plural': 'Зеркала',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Modulation',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=20, unique=True, verbose_name='Модуляция')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Модуляция',
|
||||||
|
'verbose_name_plural': 'Модуляции',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Polarization',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=20, unique=True, verbose_name='Поляризация')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Поляризация',
|
||||||
|
'verbose_name_plural': 'Поляризация',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Satellite',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=30, unique=True, verbose_name='Имя спутника')),
|
||||||
|
('norad', models.IntegerField(blank=True, null=True, verbose_name='NORAD ID')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Спутник',
|
||||||
|
'verbose_name_plural': 'Спутники',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Standard',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=20, unique=True, verbose_name='Стандарт')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Стандарт',
|
||||||
|
'verbose_name_plural': 'Стандарты',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CustomUser',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('role', models.CharField(choices=[('admin', 'Администратор'), ('moderator', 'Модератор'), ('user', 'Пользователь')], default='user', max_length=20, verbose_name='Роль пользователя')),
|
||||||
|
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Пользователь',
|
||||||
|
'verbose_name_plural': 'Пользователи',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Geo',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('timestamp', models.DateTimeField(blank=True, null=True, verbose_name='Время')),
|
||||||
|
('coords', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326, verbose_name='Координата геолокации')),
|
||||||
|
('location', models.CharField(blank=True, max_length=255, null=True, verbose_name='Метоположение')),
|
||||||
|
('comment', models.CharField(blank=True, max_length=255, verbose_name='Комментарий')),
|
||||||
|
('coords_kupsat', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326, verbose_name='Координаты Кубсата')),
|
||||||
|
('coords_valid', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326, verbose_name='Координаты оперативников')),
|
||||||
|
('is_average', models.BooleanField(blank=True, null=True, verbose_name='Усреднённое')),
|
||||||
|
('id_user_add', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='geos_added', to='mainapp.customuser', verbose_name='Пользователь')),
|
||||||
|
('mirrors', models.ManyToManyField(related_name='geo_mirrors', to='mainapp.mirror', verbose_name='Зеркала')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Гео',
|
||||||
|
'verbose_name_plural': 'Гео',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Parameter',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('frequency', models.FloatField(blank=True, default=0, null=True, verbose_name='Частота, МГц')),
|
||||||
|
('freq_range', models.FloatField(blank=True, default=0, null=True, verbose_name='Полоса частот, МГц')),
|
||||||
|
('bod_velocity', models.FloatField(blank=True, default=0, null=True, verbose_name='Символьная скорость, БОД')),
|
||||||
|
('snr', models.FloatField(blank=True, default=0, null=True, verbose_name='ОСШ')),
|
||||||
|
('id_user_add', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parameter_added', to='mainapp.customuser', verbose_name='Пользователь')),
|
||||||
|
('modulation', models.ForeignKey(blank=True, default=mainapp.models.get_default_modulation, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='modulations', to='mainapp.modulation', verbose_name='Модуляция')),
|
||||||
|
('polarization', models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='polarizations', to='mainapp.polarization', verbose_name='Поляризация')),
|
||||||
|
('standard', models.ForeignKey(blank=True, default=mainapp.models.get_default_standard, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='standards', to='mainapp.standard', verbose_name='Стандарт')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'ВЧ загрузка',
|
||||||
|
'verbose_name_plural': 'ВЧ загрузки',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ObjItem',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(blank=True, max_length=100, null=True, verbose_name='Имя объекта')),
|
||||||
|
('id_geo', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='objitems', to='mainapp.geo', verbose_name='Геоданные')),
|
||||||
|
('id_user_add', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems', to='mainapp.customuser', verbose_name='Пользователь')),
|
||||||
|
('id_vch_load', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='objitems', to='mainapp.parameter', verbose_name='ВЧ загрузка')),
|
||||||
|
('id_satellite', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='objitems', to='mainapp.satellite', verbose_name='Спутник')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Объект',
|
||||||
|
'verbose_name_plural': 'Объекты',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='geo',
|
||||||
|
constraint=models.UniqueConstraint(fields=('timestamp', 'coords'), name='unique_geo_combination'),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='objitem',
|
||||||
|
constraint=models.UniqueConstraint(fields=('id_vch_load', 'id_geo'), name='unique_objitem_combination'),
|
||||||
|
),
|
||||||
|
]
|
||||||
20
dbapp/mainapp/migrations/0002_geo_distance_coords_kup.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-10-15 09:23
|
||||||
|
|
||||||
|
import django.contrib.gis.db.models.functions
|
||||||
|
import django.db.models.expressions
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('mainapp', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='geo',
|
||||||
|
name='distance_coords_kup',
|
||||||
|
field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords', 'coords_kupsat'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между купсатом и гео'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-10-15 09:43
|
||||||
|
|
||||||
|
import django.contrib.gis.db.models.functions
|
||||||
|
import django.db.models.expressions
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('mainapp', '0002_geo_distance_coords_kup'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='geo',
|
||||||
|
name='distance_coords_valid',
|
||||||
|
field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords', 'coords_valid'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между гео и оперативным отделом, км'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='geo',
|
||||||
|
name='distance_kup_valid',
|
||||||
|
field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords_valid', 'coords_kupsat'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между купсатом и оперативным отделом, км'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='geo',
|
||||||
|
name='distance_coords_kup',
|
||||||
|
field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.contrib.gis.db.models.functions.Distance('coords', 'coords_kupsat'), '/', models.Value(1000)), null=True, output_field=models.FloatField(), verbose_name='Расстояние между купсатом и гео, км'),
|
||||||
|
),
|
||||||
|
]
|
||||||
36
dbapp/mainapp/migrations/0004_sigmaparameter.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-10-16 12:50
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import mainapp.models
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('mainapp', '0003_geo_distance_coords_valid_geo_distance_kup_valid_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SigmaParameter',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('status', models.CharField(blank=True, max_length=20, null=True, verbose_name='Статус')),
|
||||||
|
('frequency', models.FloatField(blank=True, default=0, null=True, verbose_name='Частота, МГц')),
|
||||||
|
('freq_range', models.FloatField(blank=True, default=0, null=True, verbose_name='Полоса частот, МГц')),
|
||||||
|
('power', models.FloatField(blank=True, default=0, null=True, verbose_name='Мощность, дБм')),
|
||||||
|
('bod_velocity', models.FloatField(blank=True, default=0, null=True, verbose_name='Символьная скорость, БОД')),
|
||||||
|
('snr', models.FloatField(blank=True, default=0, null=True, verbose_name='ОСШ, Дб')),
|
||||||
|
('packets', models.BooleanField(blank=True, null=True, verbose_name='Пакетность')),
|
||||||
|
('datetime_begin', models.DateTimeField(blank=True, null=True, verbose_name='Время начала измерения')),
|
||||||
|
('datetime_end', models.DateTimeField(blank=True, null=True, verbose_name='Время окончания измерения')),
|
||||||
|
('modulation', models.ForeignKey(blank=True, default=mainapp.models.get_default_modulation, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='modulations_sigma', to='mainapp.modulation', verbose_name='Модуляция')),
|
||||||
|
('standard', models.ForeignKey(blank=True, default=mainapp.models.get_default_standard, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='standards_sigma', to='mainapp.standard', verbose_name='Стандарт')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'ВЧ sigma',
|
||||||
|
'verbose_name_plural': 'ВЧ sigma',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
20
dbapp/mainapp/migrations/0005_sigmaparameter_id_satellite.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-10-20 07:57
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('mainapp', '0004_sigmaparameter'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sigmaparameter',
|
||||||
|
name='id_satellite',
|
||||||
|
field=models.ForeignKey(default=2, on_delete=django.db.models.deletion.PROTECT, related_name='sigmapar_sat', to='mainapp.satellite', verbose_name='Спутник'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-10-20 11:57
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('mainapp', '0005_sigmaparameter_id_satellite'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='objitem',
|
||||||
|
name='id_satellite',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='parameter',
|
||||||
|
name='id_satellite',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='parameters', to='mainapp.satellite', verbose_name='Спутник'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-10-20 11:58
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('mainapp', '0006_remove_objitem_id_satellite_parameter_id_satellite'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='parameter',
|
||||||
|
name='id_satellite',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='parameters', to='mainapp.satellite', verbose_name='Спутник'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-10-22 11:21
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('mainapp', '0007_alter_parameter_id_satellite'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='geo',
|
||||||
|
name='timestamp',
|
||||||
|
field=models.DateTimeField(blank=True, db_index=True, null=True, verbose_name='Время'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='objitem',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(blank=True, db_index=True, max_length=100, null=True, verbose_name='Имя объекта'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='parameter',
|
||||||
|
name='frequency',
|
||||||
|
field=models.FloatField(blank=True, db_index=True, default=0, null=True, verbose_name='Частота, МГц'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='satellite',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_index=True, max_length=30, unique=True, verbose_name='Имя спутника'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sigmaparameter',
|
||||||
|
name='frequency',
|
||||||
|
field=models.FloatField(blank=True, db_index=True, default=0, null=True, verbose_name='Частота, МГц'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-10-22 12:08
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('mainapp', '0008_alter_geo_timestamp_alter_objitem_name_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='parameter',
|
||||||
|
name='id_sigma_parameter',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sigma_parameter', to='mainapp.sigmaparameter', verbose_name='ВЧ с sigma'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-10-22 12:38
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('mainapp', '0009_parameter_id_sigma_parameter'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SigmaParMark',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('mark', models.BooleanField(blank=True, null=True, verbose_name='Наличие сигнала')),
|
||||||
|
('timestamp', models.DateTimeField(blank=True, null=True, verbose_name='Время')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Отметка',
|
||||||
|
'verbose_name_plural': 'Отметки',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sigmaparameter',
|
||||||
|
name='mark',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='mainapp.sigmaparmark', verbose_name='Отметка'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-10-22 13:15
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('mainapp', '0010_sigmaparmark_sigmaparameter_mark'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='sigmaparameter',
|
||||||
|
name='mark',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sigmaparameter',
|
||||||
|
name='mark',
|
||||||
|
field=models.ManyToManyField(blank=True, null=True, to='mainapp.sigmaparmark', verbose_name='Отметка'),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
dbapp/mainapp/migrations/0012_alter_sigmaparameter_mark.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-10-22 13:16
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('mainapp', '0011_remove_sigmaparameter_mark_sigmaparameter_mark'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sigmaparameter',
|
||||||
|
name='mark',
|
||||||
|
field=models.ManyToManyField(blank=True, to='mainapp.sigmaparmark', verbose_name='Отметка'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-10-22 13:48
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('mainapp', '0012_alter_sigmaparameter_mark'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='parameter',
|
||||||
|
index=models.Index(fields=['id_satellite', 'frequency'], name='mainapp_par_id_sate_cbfab2_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='parameter',
|
||||||
|
index=models.Index(fields=['frequency', 'polarization'], name='mainapp_par_frequen_75a049_idx'),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
dbapp/mainapp/migrations/0014_alter_modulation_name.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-10-23 08:12
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('mainapp', '0013_parameter_mainapp_par_id_sate_cbfab2_idx_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='modulation',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_index=True, max_length=20, unique=True, verbose_name='Модуляция'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-10-23 09:40
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('mainapp', '0014_alter_modulation_name'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='parameter',
|
||||||
|
name='id_sigma_parameter',
|
||||||
|
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sigma_parameter', to='mainapp.sigmaparameter', verbose_name='ВЧ с sigma'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-10-23 09:58
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('mainapp', '0015_alter_parameter_id_sigma_parameter'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='parameter',
|
||||||
|
name='id_sigma_parameter',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sigmaparameter',
|
||||||
|
name='parameter',
|
||||||
|
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sigma_parameter', to='mainapp.parameter', verbose_name='ВЧ с sigma'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-10-23 12:52
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('mainapp', '0016_remove_parameter_id_sigma_parameter_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sigmaparameter',
|
||||||
|
name='parameter',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sigma_parameter', to='mainapp.parameter', verbose_name='ВЧ'),
|
||||||
|
),
|
||||||
|
]
|
||||||
0
dbapp/mainapp/migrations/__init__.py
Normal file
256
dbapp/mainapp/models.py
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.contrib.gis.db import models as gis
|
||||||
|
from django.contrib.gis.db.models import functions
|
||||||
|
|
||||||
|
def get_default_polarization():
|
||||||
|
obj, created = Polarization.objects.get_or_create(
|
||||||
|
name="-"
|
||||||
|
)
|
||||||
|
return obj.id
|
||||||
|
|
||||||
|
def get_default_modulation():
|
||||||
|
obj, created = Modulation.objects.get_or_create(
|
||||||
|
name="-"
|
||||||
|
)
|
||||||
|
return obj.id
|
||||||
|
|
||||||
|
def get_default_standard():
|
||||||
|
obj, created = Standard.objects.get_or_create(
|
||||||
|
name="-"
|
||||||
|
)
|
||||||
|
return obj.id
|
||||||
|
|
||||||
|
class CustomUser(models.Model):
|
||||||
|
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||||
|
ROLE_CHOICES = [
|
||||||
|
('admin', 'Администратор'),
|
||||||
|
('moderator', 'Модератор'),
|
||||||
|
('user', 'Пользователь'),
|
||||||
|
]
|
||||||
|
|
||||||
|
role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='user', verbose_name='Роль пользователя')
|
||||||
|
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.user.first_name} {self.user.last_name}" if self.user.first_name and self.user.last_name else self.user.username
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Пользователь"
|
||||||
|
verbose_name_plural = "Пользователи"
|
||||||
|
|
||||||
|
class SigmaParMark(models.Model):
|
||||||
|
mark = models.BooleanField(null=True, blank=True, verbose_name="Наличие сигнала")
|
||||||
|
timestamp = models.DateTimeField(null=True, blank=True, verbose_name="Время")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
timestamp = self.timestamp.strftime("%d.%m.%Y %H:%M")
|
||||||
|
return f'+ {timestamp}' if self.mark else f'- {timestamp}'
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Отметка"
|
||||||
|
verbose_name_plural = "Отметки"
|
||||||
|
|
||||||
|
|
||||||
|
class Mirror(models.Model):
|
||||||
|
name = models.CharField(max_length=30, unique=True, verbose_name="Имя зеркала")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Зеркало"
|
||||||
|
verbose_name_plural = "Зеркала"
|
||||||
|
|
||||||
|
class Polarization(models.Model):
|
||||||
|
name = models.CharField(max_length=20, unique=True, verbose_name="Поляризация")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Поляризация"
|
||||||
|
verbose_name_plural = "Поляризация"
|
||||||
|
|
||||||
|
|
||||||
|
class Modulation(models.Model):
|
||||||
|
name = models.CharField(max_length=20, unique=True, verbose_name="Модуляция", db_index=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Модуляция"
|
||||||
|
verbose_name_plural = "Модуляции"
|
||||||
|
|
||||||
|
|
||||||
|
class Standard(models.Model):
|
||||||
|
name = models.CharField(max_length=20, unique=True, verbose_name="Стандарт")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Стандарт"
|
||||||
|
verbose_name_plural = "Стандарты"
|
||||||
|
|
||||||
|
|
||||||
|
class Satellite(models.Model):
|
||||||
|
name = models.CharField(max_length=30, unique=True, verbose_name="Имя спутника", db_index=True)
|
||||||
|
norad = models.IntegerField(blank=True, null=True, verbose_name="NORAD ID")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Спутник"
|
||||||
|
verbose_name_plural = "Спутники"
|
||||||
|
|
||||||
|
|
||||||
|
class Parameter(models.Model):
|
||||||
|
id_satellite = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="parameters", verbose_name="Спутник", null=True)
|
||||||
|
polarization = models.ForeignKey(
|
||||||
|
Polarization, default=get_default_polarization, on_delete=models.SET_DEFAULT, related_name="polarizations", null=True, blank=True, verbose_name="Поляризация"
|
||||||
|
)
|
||||||
|
frequency = models.FloatField(default=0, null=True, blank=True, verbose_name="Частота, МГц", db_index=True)
|
||||||
|
freq_range = models.FloatField(default=0, null=True, blank=True, verbose_name="Полоса частот, МГц")
|
||||||
|
bod_velocity = models.FloatField(default=0, null=True, blank=True, verbose_name="Символьная скорость, БОД")
|
||||||
|
modulation = models.ForeignKey(
|
||||||
|
Modulation, default=get_default_modulation, on_delete=models.SET_DEFAULT, related_name="modulations", null=True, blank=True, verbose_name="Модуляция"
|
||||||
|
)
|
||||||
|
snr = models.FloatField(default=0, null=True, blank=True, verbose_name="ОСШ")
|
||||||
|
standard = models.ForeignKey(
|
||||||
|
Standard, default=get_default_standard, on_delete=models.SET_DEFAULT, related_name="standards", null=True, blank=True, verbose_name="Стандарт"
|
||||||
|
)
|
||||||
|
id_user_add = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, related_name="parameter_added", verbose_name="Пользователь", null=True, blank=True)
|
||||||
|
# id_sigma_parameter = models.ManyToManyField(SigmaParameter, on_delete=models.SET_NULL, related_name="sigma_parameter", verbose_name="ВЧ с sigma", null=True, blank=True)
|
||||||
|
# id_sigma_parameter = models.ManyToManyField(SigmaParameter, verbose_name="ВЧ с sigma", null=True, blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
polarization_name = self.polarization.name if self.polarization else "-"
|
||||||
|
modulation_name = self.modulation.name if self.modulation else "-"
|
||||||
|
return f"Источник-{self.frequency}:{self.freq_range} МГц:{polarization_name}:{modulation_name}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "ВЧ загрузка"
|
||||||
|
verbose_name_plural = "ВЧ загрузки"
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=['id_satellite', 'frequency']),
|
||||||
|
models.Index(fields=['frequency', 'polarization']),
|
||||||
|
]
|
||||||
|
# constraints = [
|
||||||
|
# models.UniqueConstraint(
|
||||||
|
# fields=[
|
||||||
|
# 'polarization', 'frequency', 'freq_range',
|
||||||
|
# 'bod_velocity', 'modulation', 'snr', 'standard'
|
||||||
|
# ],
|
||||||
|
# name='unique_parameter_combination'
|
||||||
|
# )
|
||||||
|
# ]
|
||||||
|
|
||||||
|
|
||||||
|
class SigmaParameter(models.Model):
|
||||||
|
id_satellite = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="sigmapar_sat", verbose_name="Спутник")
|
||||||
|
status = models.CharField(max_length=20, blank=True, null=True, verbose_name="Статус")
|
||||||
|
frequency = models.FloatField(default=0, null=True, blank=True, verbose_name="Частота, МГц", db_index=True)
|
||||||
|
freq_range = models.FloatField(default=0, null=True, blank=True, verbose_name="Полоса частот, МГц")
|
||||||
|
power = models.FloatField(default=0, null=True, blank=True, verbose_name="Мощность, дБм")
|
||||||
|
bod_velocity = models.FloatField(default=0, null=True, blank=True, verbose_name="Символьная скорость, БОД")
|
||||||
|
modulation = models.ForeignKey(
|
||||||
|
Modulation, default=get_default_modulation, on_delete=models.SET_DEFAULT, related_name="modulations_sigma", null=True, blank=True, verbose_name="Модуляция"
|
||||||
|
)
|
||||||
|
snr = models.FloatField(default=0, null=True, blank=True, verbose_name="ОСШ, Дб")
|
||||||
|
standard = models.ForeignKey(
|
||||||
|
Standard, default=get_default_standard, on_delete=models.SET_DEFAULT, related_name="standards_sigma", null=True, blank=True, verbose_name="Стандарт"
|
||||||
|
)
|
||||||
|
packets = models.BooleanField(null=True, blank=True, verbose_name="Пакетность")
|
||||||
|
datetime_begin = models.DateTimeField(null=True, blank=True, verbose_name="Время начала измерения")
|
||||||
|
datetime_end = models.DateTimeField(null=True, blank=True, verbose_name="Время окончания измерения")
|
||||||
|
mark = models.ManyToManyField(SigmaParMark, verbose_name="Отметка", blank=True)
|
||||||
|
parameter = models.ForeignKey(
|
||||||
|
Parameter,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='sigma_parameter',
|
||||||
|
verbose_name="ВЧ",
|
||||||
|
null=True,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
modulation_name = self.modulation.name if self.modulation else "-"
|
||||||
|
return f"Sigma-{self.frequency}:{self.freq_range} МГц:{modulation_name}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "ВЧ sigma"
|
||||||
|
verbose_name_plural = "ВЧ sigma"
|
||||||
|
|
||||||
|
class Geo(models.Model):
|
||||||
|
mirrors = models.ManyToManyField(Mirror, related_name="geo_mirrors", verbose_name="Зеркала",)
|
||||||
|
timestamp = models.DateTimeField(null=True, blank=True, verbose_name="Время", db_index=True)
|
||||||
|
coords = gis.PointField(srid=4326, null=True, blank=True, verbose_name="Координата геолокации")
|
||||||
|
location = models.CharField(max_length=255, null=True, blank=True, verbose_name="Метоположение")
|
||||||
|
comment = models.CharField(max_length=255, blank=True, verbose_name="Комментарий")
|
||||||
|
coords_kupsat = gis.PointField(srid=4326, null=True, blank=True, verbose_name="Координаты Кубсата")
|
||||||
|
coords_valid = gis.PointField(srid=4326, null=True, blank=True, verbose_name="Координаты оперативников")
|
||||||
|
is_average = models.BooleanField(null=True, blank=True, verbose_name="Усреднённое")
|
||||||
|
id_user_add = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, related_name="geos_added", verbose_name="Пользователь", null=True, blank=True)
|
||||||
|
distance_coords_kup = models.GeneratedField(
|
||||||
|
expression=functions.Distance("coords", "coords_kupsat")/1000,
|
||||||
|
output_field=models.FloatField(),
|
||||||
|
db_persist=True,
|
||||||
|
null=True, blank=True, verbose_name="Расстояние между купсатом и гео, км"
|
||||||
|
)
|
||||||
|
distance_coords_valid = models.GeneratedField(
|
||||||
|
expression=functions.Distance("coords", "coords_valid")/1000,
|
||||||
|
output_field=models.FloatField(),
|
||||||
|
db_persist=True,
|
||||||
|
null=True, blank=True, verbose_name="Расстояние между гео и оперативным отделом, км"
|
||||||
|
)
|
||||||
|
distance_kup_valid = models.GeneratedField(
|
||||||
|
expression=functions.Distance("coords_valid", "coords_kupsat")/1000,
|
||||||
|
output_field=models.FloatField(),
|
||||||
|
db_persist=True,
|
||||||
|
null=True, blank=True, verbose_name="Расстояние между купсатом и оперативным отделом, км"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
longitude = self.coords.coords[0]
|
||||||
|
latitude = self.coords.coords[1]
|
||||||
|
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||||
|
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||||
|
return f"{lat} {lon}, {self.location}"
|
||||||
|
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Гео"
|
||||||
|
verbose_name_plural = "Гео"
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=[
|
||||||
|
'timestamp', 'coords'
|
||||||
|
],
|
||||||
|
name='unique_geo_combination'
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ObjItem(models.Model):
|
||||||
|
name = models.CharField(null=True, blank=True, max_length=100, verbose_name="Имя объекта", db_index=True)
|
||||||
|
# id_satellite = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="objitems", verbose_name="Спутник")
|
||||||
|
id_vch_load = models.ForeignKey(Parameter, on_delete=models.CASCADE, related_name="objitems", verbose_name="ВЧ загрузка")
|
||||||
|
id_geo = models.ForeignKey(Geo, on_delete=models.CASCADE, related_name="objitems", verbose_name="Геоданные")
|
||||||
|
id_user_add = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, related_name="objitems", verbose_name="Пользователь", null=True, blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Объект {self.name}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Объект"
|
||||||
|
verbose_name_plural = "Объекты"
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=['id_vch_load', 'id_geo'],
|
||||||
|
name='unique_objitem_combination'
|
||||||
|
)
|
||||||
|
]
|
||||||
75
dbapp/mainapp/popup_filters.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
from django.contrib.admin.filters import ChoicesFieldListFilter
|
||||||
|
from django.forms import Media
|
||||||
|
|
||||||
|
|
||||||
|
class PopupCompatibleMultiSelectRelatedDropdownFilter(ChoicesFieldListFilter):
|
||||||
|
"""
|
||||||
|
A custom filter that maintains popup context when used in raw_id_fields modals.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, field, request, params, model, model_admin, field_path):
|
||||||
|
super().__init__(field, request, params, model, model_admin, field_path)
|
||||||
|
|
||||||
|
# Check if we're in a popup context
|
||||||
|
self.is_popup = '_popup' in request.GET or 'pop' in request.GET or 'admin' not in request.path
|
||||||
|
|
||||||
|
# Get all choices (related objects)
|
||||||
|
self.lookup_choices = field.get_choices(include_blank=False)
|
||||||
|
|
||||||
|
def has_output(self):
|
||||||
|
return len(self.lookup_choices) > 1
|
||||||
|
|
||||||
|
def value(self):
|
||||||
|
return self.lookup_val
|
||||||
|
|
||||||
|
def expected_parameters(self):
|
||||||
|
return [self.lookup_kwarg, self.lookup_kwarg_isnull]
|
||||||
|
|
||||||
|
def choices(self, changelist):
|
||||||
|
# If in popup, preserve the popup parameters in the filter URL
|
||||||
|
popup_params = {}
|
||||||
|
if self.is_popup:
|
||||||
|
# Preserve popup parameters
|
||||||
|
if '_popup' in changelist.params:
|
||||||
|
popup_params['_popup'] = 1
|
||||||
|
if 'pop' in changelist.params:
|
||||||
|
popup_params['pop'] = changelist.params['pop']
|
||||||
|
if '_to_field' in changelist.params:
|
||||||
|
popup_params['_to_field'] = changelist.params['_to_field']
|
||||||
|
|
||||||
|
# Create the base URL with popup parameters
|
||||||
|
all_params = changelist.get_filters_params()
|
||||||
|
all_params.update(popup_params)
|
||||||
|
|
||||||
|
# Generate the URL for the filter
|
||||||
|
url = changelist.get_query_string(all_params, [self.lookup_kwarg])
|
||||||
|
|
||||||
|
yield {
|
||||||
|
'selected': self.lookup_val is None,
|
||||||
|
'query_string': url,
|
||||||
|
'display': 'All',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add choices
|
||||||
|
for lookup, title in self.lookup_choices:
|
||||||
|
params = dict(all_params)
|
||||||
|
params[self.lookup_kwarg] = lookup
|
||||||
|
|
||||||
|
# Remove the parameter if it's being set to the same value (for unselecting)
|
||||||
|
if self.lookup_val == str(lookup):
|
||||||
|
params.pop(self.lookup_kwarg, None)
|
||||||
|
|
||||||
|
# Add popup parameters to each choice URL
|
||||||
|
choice_params = params.copy()
|
||||||
|
choice_params.update(popup_params)
|
||||||
|
|
||||||
|
yield {
|
||||||
|
'selected': str(lookup) == self.lookup_val,
|
||||||
|
'query_string': changelist.get_query_string(choice_params, [self.lookup_kwarg_isnull]),
|
||||||
|
'display': title,
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media(self):
|
||||||
|
# Include necessary CSS/JS for dropdown functionality if needed
|
||||||
|
return Media()
|
||||||
60
dbapp/mainapp/templates/admin/map_custom.html
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
{% extends "mapsapp/map2d_base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
{% block title %}Вынос точек{% endblock title %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
// Цвета для стандартных маркеров (из leaflet-color-markers)
|
||||||
|
var markerColors = ['red', 'green', 'orange', 'violet', 'grey', 'black', 'blue'];
|
||||||
|
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 groupIndex = {{ forloop.counter0 }};
|
||||||
|
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);
|
||||||
|
|
||||||
|
groupLayer.addLayer(marker);
|
||||||
|
|
||||||
|
subgroup.push({
|
||||||
|
label: "{{ forloop.counter }} - {{ point_data.frequency }}",
|
||||||
|
layer: marker
|
||||||
|
});
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
overlays.push({
|
||||||
|
label: '{{ group.name|escapejs }}',
|
||||||
|
selectAllCheckbox: true,
|
||||||
|
children: subgroup,
|
||||||
|
layer: groupLayer
|
||||||
|
});
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
|
||||||
|
// Используем именно tree-контрол
|
||||||
|
L.control.layers.tree(baseLayers, overlays, {
|
||||||
|
collapsed: false,
|
||||||
|
autoZIndex: true
|
||||||
|
}).addTo(map);
|
||||||
|
</script>
|
||||||
|
{% endblock extra_js %}
|
||||||
48
dbapp/mainapp/templates/mainapp/add_data_from_csv.html
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
{% extends 'mainapp/base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Загрузка данных из CSV{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-success text-white">
|
||||||
|
<h2 class="mb-0">Загрузка данных из CSV</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if messages %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert {{ message.tags }} alert-dismissible fade show" role="alert">
|
||||||
|
{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p class="card-text">Загрузите CSV-файл для загрузки данных в базу.</p>
|
||||||
|
|
||||||
|
<form method="post" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<!-- Form fields with Bootstrap styling -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.file.id_for_label }}" class="form-label">Выберите CSV файл:</label>
|
||||||
|
{{ form.file }}
|
||||||
|
{% if form.file.errors %}
|
||||||
|
<div class="text-danger mt-1">{{ form.file.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="form-text">Загрузите CSV-файл с данными для обработки</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||||
|
<a href="{% url 'home' %}" class="btn btn-secondary me-md-2">Назад</a>
|
||||||
|
<button type="submit" class="btn btn-success">Добавить в базу</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
65
dbapp/mainapp/templates/mainapp/add_data_from_excel.html
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
{% extends 'mainapp/base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Загрузка данных из Excel{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h2 class="mb-0">Загрузка данных из Excel</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if messages %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert {{ message.tags }} alert-dismissible fade show" role="alert">
|
||||||
|
{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p class="card-text">Загрузите Excel-файл и выберите спутник для загрузки данных в базу.</p>
|
||||||
|
|
||||||
|
<form method="post" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<!-- Form fields with Bootstrap styling -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.file.id_for_label }}" class="form-label">Выберите Excel файл:</label>
|
||||||
|
{{ form.file }}
|
||||||
|
{% if form.file.errors %}
|
||||||
|
<div class="text-danger mt-1">{{ form.file.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="form-text">Загрузите Excel-файл (.xlsx или .xls) с данными для обработки</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.sat_choice.id_for_label }}" class="form-label">Выберите спутник:</label>
|
||||||
|
{{ form.sat_choice }}
|
||||||
|
{% if form.sat_choice.errors %}
|
||||||
|
<div class="text-danger mt-1">{{ form.sat_choice.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.number_input.id_for_label }}" class="form-label">Количество строк для обработки:</label>
|
||||||
|
{{ form.number_input }}
|
||||||
|
{% if form.number_input.errors %}
|
||||||
|
<div class="text-danger mt-1">{{ form.number_input.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="form-text">Оставьте пустым или введите 0 для обработки всех строк</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||||
|
<a href="{% url 'home' %}" class="btn btn-secondary me-md-2">Назад</a>
|
||||||
|
<button type="submit" class="btn btn-primary">Добавить в базу</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
53
dbapp/mainapp/templates/mainapp/base.html
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
{% load static %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
|
||||||
|
<title>{% block title %}Геолокация{% endblock %}</title>
|
||||||
|
|
||||||
|
<link href="{% static 'bootstrap/bootstrap.min.css' %}" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Дополнительные стили (если нужно) -->
|
||||||
|
{% block extra_css %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- Навигационная панель -->
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand" href="{% url 'home' %}">Геолокация</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav me-auto">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'home' %}">Главная</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url '3dmap' %}">3D карта</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url '2dmap' %}">2D карта</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'admin:index' %}">Админ панель</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Основной контент -->
|
||||||
|
<main class="container mt-4">
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="{% static 'bootstrap/bootstrap.bundle.min.js' %}"></script>
|
||||||
|
|
||||||
|
{% block extra_js %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
176
dbapp/mainapp/templates/mainapp/home.html
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
{% extends 'mainapp/base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Главная{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="text-center mb-5">
|
||||||
|
<h1 class="display-4 fw-bold">Геолокация</h1>
|
||||||
|
<p class="lead">Управление данными спутников</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Alert messages -->
|
||||||
|
{% if messages %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert {{ message.tags }} alert-dismissible fade show" role="alert">
|
||||||
|
{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Main feature cards -->
|
||||||
|
<div class="row g-4">
|
||||||
|
<!-- Excel Data Upload Card -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card h-100 shadow-sm border-0">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-center mb-3">
|
||||||
|
<div class="bg-primary bg-opacity-10 rounded-circle p-2 me-3">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-file-earmark-excel text-primary" viewBox="0 0 16 16">
|
||||||
|
<path d="M5.884 6.68a.5.5 0 1 0-.768.64L7.349 10l-2.233 2.68a.5.5 0 0 0 .768.64L8 10.781l2.116 2.54a.5.5 0 0 0 .768-.641L8.651 10l2.233-2.68a.5.5 0 0 0-.768-.64L8 9.219z"/>
|
||||||
|
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2M9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="card-title mb-0">Загрузка данных из Excel</h3>
|
||||||
|
</div>
|
||||||
|
<p class="card-text">Загрузите данные из Excel-файла в базу данных. Поддерживается выбор спутника и ограничение количества записей.</p>
|
||||||
|
<a href="{% url 'load_excel_data' %}" class="btn btn-primary">
|
||||||
|
Перейти к загрузке данных
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CSV Data Upload Card -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card h-100 shadow-sm border-0">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-center mb-3">
|
||||||
|
<div class="bg-success bg-opacity-10 rounded-circle p-2 me-3">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-file-earmark-text text-success" viewBox="0 0 16 16">
|
||||||
|
<path d="M5.5 7a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1zm0 2a.5.5 0 0 0 0 1h2a.5.5 0 0 0 0-1z"/>
|
||||||
|
<path d="M9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.5L9.5 0m0 1v2A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="card-title mb-0">Загрузка данных из CSV</h3>
|
||||||
|
</div>
|
||||||
|
<p class="card-text">Загрузите данные из CSV-файла в базу данных. Простая загрузка с возможностью указания пути к файлу.</p>
|
||||||
|
<a href="{% url 'load_csv_data' %}" class="btn btn-success">
|
||||||
|
Перейти к загрузке данных
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Satellite List Card -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card h-100 shadow-sm border-0">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-center mb-3">
|
||||||
|
<div class="bg-info bg-opacity-10 rounded-circle p-2 me-3">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-satellite text-info" viewBox="0 0 16 16">
|
||||||
|
<path d="M13.37 1.37c-2.75 0-5.4 1.13-7.29 3.02C4.13 6.33 3 8.98 3 11.73c0 2.75 1.13 5.4 3.02 7.29 1.94 1.94 4.54 3.02 7.29 3.02 2.75 0 5.4-1.13 7.29-3.02 1.94-1.94 3.02-4.54 3.02-7.29 0-2.75-1.13-5.4-3.02-7.29C18.77 2.5-2.75 1.37-5.5 1.37m-5.5 8.26c0-1.52.62-3.02 1.73-4.13 1.11-1.11 2.61-1.73 4.13-1.73 1.52 0 3.02.62 4.13 1.73 1.11 1.11 1.73 2.61 1.73 4.13 0 1.52-.62 3.02-1.73 4.13-1.11 1.11-2.61 1.73-4.13 1.73-1.52 0-3.02-.62-4.13-1.73-1.11-1.11-1.73-2.61-1.73-4.13"/>
|
||||||
|
<path d="M6.63 6.63c.62-.62 1.45-.98 2.27-.98.82 0 1.65.36 2.27.98.62.62.98 1.45.98 2.27 0 .82-.36 1.65-.98 2.27-.62.62-1.45.98-2.27.98-.82 0-1.65-.36-2.27-.98-.62-.62-.98-1.45-.98-2.27 0-.82.36-1.65.98-2.27m2.27 1.02c-.26 0-.52.1-.71.29-.2.2-.29.46-.29.71 0 .26.1.52.29.71.2.2.46.29.71.29.26 0 .52-.1.71-.29.2-.2.29-.46.29-.71 0-.26-.1-.52-.29-.71-.19-.19-.45-.29-.71-.29"/>
|
||||||
|
<path d="M5.13 5.13c.46-.46 1.08-.73 1.73-.73.65 0 1.27.27 1.73.73.46.46.73 1.08.73 1.73 0 .65-.27 1.27-.73 1.73-.46.46-1.08.73-1.73.73-.65 0-1.27-.27-1.73-.73-.46-.46-.73-1.08-.73-1.73 0-.65.27-1.27.73-1.73m1.73.58c-.15 0-.3.06-.42.18-.12.12-.18.27-.18.42 0 .15.06.3.18.42.12.12.27.18.42.18.15 0 .3-.06.42-.18.12-.12.18-.27.18-.42 0-.15-.06-.3-.18-.42-.12-.12-.27-.18-.42-.18"/>
|
||||||
|
<path d="M8 3.5c.28 0 .5.22.5.5v1c0 .28-.22.5-.5.5s-.5-.22-.5-.5v-1c0-.28.22-.5.5-.5"/>
|
||||||
|
<path d="M10.5 8c0-.28.22-.5.5-.5h1c.28 0 .5.22.5.5s-.22.5-.5.5h-1c-.28 0-.5-.22-.5-.5"/>
|
||||||
|
<path d="M8 12.5c-.28 0-.5.22-.5.5v1c0 .28.22.5.5.5s.5-.22.5-.5v-1c0-.28-.22-.5-.5-.5"/>
|
||||||
|
<path d="M3.5 8c0 .28-.22.5-.5.5h-1c-.28 0-.5-.22-.5-.5s.22-.5.5-.5h1c.28 0 .5.22.5.5"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="card-title mb-0">Добавление списка спутников</h3>
|
||||||
|
</div>
|
||||||
|
<p class="card-text">Добавьте новый список спутников в базу данных для последующего использования в загрузке данных.</p>
|
||||||
|
<a href="{% url 'add_sats' %}" class="btn btn-info">
|
||||||
|
Добавить список спутников
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Transponders Card -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card h-100 shadow-sm border-0">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-center mb-3">
|
||||||
|
<div class="bg-warning bg-opacity-10 rounded-circle p-2 me-3">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-wifi text-warning" viewBox="0 0 16 16">
|
||||||
|
<path d="M6.002 3.5a5.5 5.5 0 1 1 3.996 9.5H10A5.5 5.5 0 0 1 6.002 3.5M6.002 5.5a3.5 3.5 0 1 0 3.996 5.5H10A3.5 3.5 0 0 0 6.002 5.5"/>
|
||||||
|
<path d="M10.5 12.5a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5.5.5 0 0 0-1 0 .5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5.5.5 0 0 0-1 0 .5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5 3.5 3.5 0 0 1 7 0"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="card-title mb-0">Добавление транспондеров</h3>
|
||||||
|
</div>
|
||||||
|
<p class="card-text">Добавьте список транспондеров из JSON-файла в базу данных. Требуется наличие файла transponders.json.</p>
|
||||||
|
<a href="{% url 'add_trans' %}" class="btn btn-warning">
|
||||||
|
Добавить транспондеры
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- VCH Load Data Card -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card h-100 shadow-sm border-0">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-center mb-3">
|
||||||
|
<div class="bg-danger bg-opacity-10 rounded-circle p-2 me-3">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-upload text-danger" viewBox="0 0 16 16">
|
||||||
|
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5"/>
|
||||||
|
<path d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="card-title mb-0">Добавление данных ВЧ загрузки</h3>
|
||||||
|
</div>
|
||||||
|
<p class="card-text">Загрузите данные ВЧ загрузки из HTML-файла с таблицами. Поддерживается выбор спутника для привязки данных.</p>
|
||||||
|
<a href="{% url 'vch_load' %}" class="btn btn-danger">
|
||||||
|
Добавить данные ВЧ загрузки
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Map Views Card -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card h-100 shadow-sm border-0">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-center mb-3">
|
||||||
|
<div class="bg-secondary bg-opacity-10 rounded-circle p-2 me-3">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-map text-secondary" viewBox="0 0 16 16">
|
||||||
|
<path d="M15.817.113A.5.5 0 0 1 16 .5v14a.5.5 0 0 1-.402.49l-5 1a.502.502 0 0 1-.196 0L5.5 15.01l-4.902.98A.5.5 0 0 1 0 15.5v-14a.5.5 0 0 1 .402-.49l5-1a.5.5 0 0 1 .196 0L10.5.99l4.902-.98a.5.5 0 0 1 .415.103M10 1.91l-4-.8v12.98l4 .8zM1.61 2.22l4.39.88v10.88l-4.39-.88zm9.18 10.88 4-.8V2.34l-4 .8z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="card-title mb-0">Карты</h3>
|
||||||
|
</div>
|
||||||
|
<p class="card-text">Просматривайте данные на 2D и 3D картах для визуализации геолокации спутников.</p>
|
||||||
|
<div class="mt-2">
|
||||||
|
<a href="{% url '2dmap' %}" class="btn btn-secondary me-2">2D Карта</a>
|
||||||
|
<a href="{% url '3dmap' %}" class="btn btn-outline-secondary">3D Карта</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Calculation Card -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card h-100 shadow-sm border-0">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-center mb-3">
|
||||||
|
<div class="bg-info bg-opacity-10 rounded-circle p-2 me-3">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-calculator text-info" viewBox="0 0 16 16">
|
||||||
|
<path d="M2 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2zm2-1a1 1 0 0 0-1 1v4h2V2a1 1 0 0 0-1-1M5 6v1h1V6zm2 0v1h1V6zm2 0v1h1V6zm2 0v1h1V6zm1 2v1h1V8zm0 2v1h1v-1zm0 2v1h1v-1zm-8-6v8H3V8zm2 0v8h1V8zm2 0v8h1V8zm2 0v8h1V8z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="card-title mb-0">Привязка ВЧ загрузки</h3>
|
||||||
|
</div>
|
||||||
|
<p class="card-text">Привязка ВЧ загрузки с sigma</p>
|
||||||
|
<a href="{% url 'link_vch_sigma' %}" class="btn btn-info">
|
||||||
|
Открыть форму
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
68
dbapp/mainapp/templates/mainapp/link_vch.html
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
{% extends 'mainapp/base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Привязка ВЧ{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-info text-white">
|
||||||
|
<h2 class="mb-0">Привязка ВЧ загрузки</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if messages %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert {{ message.tags }} alert-dismissible fade show" role="alert">
|
||||||
|
{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p class="card-text">Введите допустимый разброс для частоты и полосы(в кГц)</p>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.sat_choice.id_for_label }}" class="form-label">Выберите спутник:</label>
|
||||||
|
{{ form.sat_choice }}
|
||||||
|
{% if form.sat_choice.errors %}
|
||||||
|
<div class="text-danger mt-1">{{ form.sat_choice.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.ku_range.id_for_label }}" class="form-label">Выберите перенос по частоте(МГц):</label>
|
||||||
|
{{ form.ku_range }}
|
||||||
|
{% if form.ku_range.errors %}
|
||||||
|
<div class="text-danger mt-1">{{ form.ku_range.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.value1.id_for_label }}" class="form-label">Разброс по частоте(в %)</label>
|
||||||
|
{{ form.value1 }}
|
||||||
|
{% if form.value1.errors %}
|
||||||
|
<div class="text-danger mt-1">{{ form.value1.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.value2.id_for_label }}" class="form-label">Разброс по полосе(в %)</label>
|
||||||
|
{{ form.value2 }}
|
||||||
|
{% if form.value2.errors %}
|
||||||
|
<div class="text-danger mt-1">{{ form.value2.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||||
|
<a href="{% url 'home' %}" class="btn btn-secondary me-md-2">Назад</a>
|
||||||
|
{% comment %} <a href="{% url 'home' %}" class="btn btn-danger me-md-2">Сбросить привязку</a> {% endcomment %}
|
||||||
|
<button type="submit" class="btn btn-info">Выполнить привязку</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
56
dbapp/mainapp/templates/mainapp/upload_html.html
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
{% extends 'mainapp/base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Загрузка данных ВЧ загрузки{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-danger text-white">
|
||||||
|
<h2 class="mb-0">Загрузка данных ВЧ загрузки</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if messages %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert {{ message.tags }} alert-dismissible fade show" role="alert">
|
||||||
|
{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p class="card-text">Загрузите HTML-файл с таблицами данных ВЧ загрузки и выберите спутник для привязки данных.</p>
|
||||||
|
|
||||||
|
<form method="post" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<!-- Form fields with Bootstrap styling -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.file.id_for_label }}" class="form-label">Выберите HTML файл:</label>
|
||||||
|
{{ form.file }}
|
||||||
|
{% if form.file.errors %}
|
||||||
|
<div class="text-danger mt-1">{{ form.file.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="form-text">Загрузите HTML-файл, содержащий таблицы с данными ВЧ загрузки</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.sat_choice.id_for_label }}" class="form-label">Выберите спутник:</label>
|
||||||
|
{{ form.sat_choice }}
|
||||||
|
{% if form.sat_choice.errors %}
|
||||||
|
<div class="text-danger mt-1">{{ form.sat_choice.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||||
|
<a href="{% url 'home' %}" class="btn btn-secondary me-md-2">Назад</a>
|
||||||
|
<button type="submit" class="btn btn-danger">Обработать файл</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
3
dbapp/mainapp/tests.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
20
dbapp/mainapp/urls.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
from django.conf.urls.static import static
|
||||||
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', views.home_page, name='home'),
|
||||||
|
path('excel-data', views.load_excel_data, name='load_excel_data'),
|
||||||
|
path('satellites', views.add_satellites, name='add_sats'),
|
||||||
|
path('api/locations/<int:sat_id>/geojson/', views.get_locations, name='locations_by_id'),
|
||||||
|
path('transponders', views.add_transponders, name='add_trans'),
|
||||||
|
path('csv-data', views.load_raw_csv_data, name='load_csv_data'),
|
||||||
|
path('map-points/', views.show_map_view, name='admin_show_map'),
|
||||||
|
path('cluster/', views.cluster_test, name='cluster'),
|
||||||
|
path('vch-upload/', views.upload_vch_load_from_html, name='vch_load'),
|
||||||
|
path('vch-link/', views.link_vch_sigma, name='link_vch_sigma'),
|
||||||
|
# path('upload/', views.upload_file, name='upload_file'),
|
||||||
|
|
||||||
|
]
|
||||||
415
dbapp/mainapp/utils.py
Normal file
@@ -0,0 +1,415 @@
|
|||||||
|
from .models import (
|
||||||
|
Satellite,
|
||||||
|
Standard,
|
||||||
|
Polarization,
|
||||||
|
Mirror,
|
||||||
|
Modulation,
|
||||||
|
Geo,
|
||||||
|
Parameter,
|
||||||
|
SigmaParameter,
|
||||||
|
ObjItem,
|
||||||
|
CustomUser
|
||||||
|
)
|
||||||
|
from datetime import datetime, time
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
from django.contrib.gis.geos import Point
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
|
def get_all_constants():
|
||||||
|
sats = [sat.name for sat in Satellite.objects.all()]
|
||||||
|
standards = [sat.name for sat in Standard.objects.all()]
|
||||||
|
pols = [sat.name for sat in Polarization.objects.all()]
|
||||||
|
mirrors = [sat.name for sat in Mirror.objects.all()]
|
||||||
|
modulations = [sat.name for sat in Modulation.objects.all()]
|
||||||
|
return sats, standards, pols, mirrors, modulations
|
||||||
|
|
||||||
|
def coords_transform(coords: str):
|
||||||
|
lat_part, lon_part = coords.strip().split()
|
||||||
|
sign_map = {'N': 1, 'E': 1, 'S': -1, 'W': -1}
|
||||||
|
|
||||||
|
lat_sign_char = lat_part[-1]
|
||||||
|
lat_value = float(lat_part[:-1].replace(",", "."))
|
||||||
|
latitude = lat_value * sign_map.get(lat_sign_char, 1)
|
||||||
|
|
||||||
|
lon_sign_char = lon_part[-1]
|
||||||
|
lon_value = float(lon_part[:-1].replace(",", "."))
|
||||||
|
longitude = lon_value * sign_map.get(lon_sign_char, 1)
|
||||||
|
|
||||||
|
return (longitude, latitude)
|
||||||
|
|
||||||
|
def remove_str(s: str):
|
||||||
|
if isinstance(s, str):
|
||||||
|
if s.strip() == "-" or s.strip() == "" or s.strip() == " " or "неизв" in s.strip():
|
||||||
|
return -1
|
||||||
|
return float(s.strip().replace(",", "."))
|
||||||
|
return s
|
||||||
|
|
||||||
|
def fill_data_from_df(df: pd.DataFrame, sat: Satellite):
|
||||||
|
try:
|
||||||
|
df.rename(columns={'Модуляция ': 'Модуляция'}, inplace=True)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
consts = get_all_constants()
|
||||||
|
df.fillna(-1, inplace=True)
|
||||||
|
for stroka in df.iterrows():
|
||||||
|
geo_point = Point(coords_transform(stroka[1]['Координаты']), srid=4326)
|
||||||
|
valid_point = None
|
||||||
|
kupsat_point = None
|
||||||
|
try:
|
||||||
|
if stroka[1]['Координаты объекта'] != -1 and stroka[1]['Координаты Кубсата'] != '+':
|
||||||
|
if 'ИРИ' not in stroka[1]['Координаты объекта'] and 'БЛА' not in stroka[1]['Координаты объекта']:
|
||||||
|
valid_point = list(map(float, stroka[1]['Координаты объекта'].replace(',', '.').split('. ')))
|
||||||
|
valid_point = Point(valid_point[1], valid_point[0], srid=4326)
|
||||||
|
if stroka[1]['Координаты Кубсата'] != -1 and stroka[1]['Координаты Кубсата'] != '+':
|
||||||
|
kupsat_point = list(map(float, stroka[1]['Координаты Кубсата'].replace(',', '.').split('. ')))
|
||||||
|
kupsat_point = Point(kupsat_point[1], kupsat_point[0], srid=4326)
|
||||||
|
except KeyError:
|
||||||
|
print("В таблице нет столбцов с координатами кубсата")
|
||||||
|
try:
|
||||||
|
polarization_obj, _ = Polarization.objects.get_or_create(name=stroka[1]['Поляризация'].strip())
|
||||||
|
except KeyError:
|
||||||
|
polarization_obj, _ = Polarization.objects.get_or_create(name="-")
|
||||||
|
freq = remove_str(stroka[1]['Частота, МГц'])
|
||||||
|
freq_line = remove_str(stroka[1]['Полоса, МГц'])
|
||||||
|
v = remove_str(stroka[1]['Символьная скорость, БОД'])
|
||||||
|
mod_obj, _ = Modulation.objects.get_or_create(name=stroka[1]['Модуляция'].strip())
|
||||||
|
snr = remove_str(stroka[1]['ОСШ'])
|
||||||
|
date = stroka[1]['Дата'].date()
|
||||||
|
time_ = stroka[1]['Время']
|
||||||
|
if isinstance(time_, str):
|
||||||
|
time_ = time(0,0,0)
|
||||||
|
timestamp = datetime.combine(date, time_)
|
||||||
|
current_mirrors = []
|
||||||
|
mirror_1 = stroka[1]['Зеркало 1'].strip().split("\n")
|
||||||
|
mirror_2 = stroka[1]['Зеркало 2'].strip().split("\n")
|
||||||
|
if len(mirror_1) > 1:
|
||||||
|
for mir in mirror_1:
|
||||||
|
mir_obj, _ = Mirror.objects.get_or_create(name=mir.strip())
|
||||||
|
current_mirrors.append(mir.strip())
|
||||||
|
elif mirror_1[0] not in consts[3]:
|
||||||
|
mir_obj, _ = Mirror.objects.get_or_create(name=mirror_1[0].strip())
|
||||||
|
current_mirrors.append(mirror_1[0].strip())
|
||||||
|
if len(mirror_2) > 1:
|
||||||
|
for mir in mirror_2:
|
||||||
|
mir_obj, _ = Mirror.objects.get_or_create(name=mir.strip())
|
||||||
|
current_mirrors.append(mir.strip())
|
||||||
|
elif mirror_2[0] not in consts[3]:
|
||||||
|
mir_obj, _ = Mirror.objects.get_or_create(name=mirror_2[0].strip())
|
||||||
|
current_mirrors.append(mirror_2[0].strip())
|
||||||
|
location = stroka[1]['Местоопределение'].strip()
|
||||||
|
comment = stroka[1]['Комментарий']
|
||||||
|
source = stroka[1]['Объект наблюдения']
|
||||||
|
|
||||||
|
vch_load_obj, vch_created = Parameter.objects.get_or_create(
|
||||||
|
id_satellite=sat,
|
||||||
|
polarization=polarization_obj,
|
||||||
|
frequency=freq,
|
||||||
|
freq_range=freq_line,
|
||||||
|
bod_velocity=v,
|
||||||
|
modulation=mod_obj,
|
||||||
|
snr=snr,
|
||||||
|
defaults={'id_user_add': CustomUser.objects.get(id=1)}
|
||||||
|
)
|
||||||
|
|
||||||
|
geo, _ = Geo.objects.get_or_create(
|
||||||
|
timestamp=timestamp,
|
||||||
|
coords=geo_point,
|
||||||
|
defaults={
|
||||||
|
'coords_kupsat': kupsat_point,
|
||||||
|
'coords_valid': valid_point,
|
||||||
|
'location': location,
|
||||||
|
'comment': comment,
|
||||||
|
'is_average': (comment != -1.0),
|
||||||
|
'id_user_add': CustomUser.objects.get(id=1)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
geo.save()
|
||||||
|
geo.mirrors.set(Mirror.objects.filter(name__in=current_mirrors))
|
||||||
|
|
||||||
|
obj_item, _ = ObjItem.objects.get_or_create(
|
||||||
|
id_geo=geo,
|
||||||
|
id_vch_load=vch_load_obj,
|
||||||
|
defaults={
|
||||||
|
'name': source,
|
||||||
|
'id_user_add': CustomUser.objects.get(id=1),
|
||||||
|
# 'id_satellite': sat
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
obj_item.save()
|
||||||
|
|
||||||
|
|
||||||
|
def add_satellite_list():
|
||||||
|
sats = ['AZERSPACE 2', 'Amos 4', 'Astra 4A', 'ComsatBW-1', 'Eutelsat 16A',
|
||||||
|
'Eutelsat 21B', 'Eutelsat 7B', 'ExpressAM6', 'Hellas Sat 3',
|
||||||
|
'Intelsat 39', 'Intelsat 17',
|
||||||
|
'NSS 12', 'Sicral 2', 'SkyNet 5B', 'SkyNet 5D', 'Syracuse 4A',
|
||||||
|
'Turksat 3A', 'Turksat 4A', 'WGS 10', 'Yamal 402']
|
||||||
|
|
||||||
|
for sat in sats:
|
||||||
|
sat_obj, _ = Satellite.objects.get_or_create(
|
||||||
|
name=sat
|
||||||
|
)
|
||||||
|
sat_obj.save()
|
||||||
|
|
||||||
|
def parse_string(s: str):
|
||||||
|
pattern = r'^(.+?) (-?\d+\,\d+) \[(-?\d+\,\d+)\] ([^\s]+) ([A-Za-z]) - (\d{1,2}\.\d{1,2}\.\d{1,4} \d{1,2}:\d{1,2}:\d{1,2})$'
|
||||||
|
match = re.match(pattern, s)
|
||||||
|
if match:
|
||||||
|
return list(match.groups())
|
||||||
|
else:
|
||||||
|
raise ValueError("Некорректный формат строки")
|
||||||
|
|
||||||
|
|
||||||
|
def get_point_from_json(filepath: str):
|
||||||
|
with open(filepath, encoding='utf-8-sig') as jf:
|
||||||
|
data = json.load(jf)
|
||||||
|
|
||||||
|
for obj in data:
|
||||||
|
if not obj.get('bearingBehavior', {}):
|
||||||
|
if obj['tacticObjectType'] == "source":
|
||||||
|
# if not obj['bearingBehavior']:
|
||||||
|
source_id = obj['id']
|
||||||
|
name = obj['name']
|
||||||
|
elements = parse_string(name)
|
||||||
|
sat_name = elements[0]
|
||||||
|
freq = elements[1]
|
||||||
|
freq_range = elements[2]
|
||||||
|
pol = elements[4]
|
||||||
|
timestamp = datetime.strptime(elements[-1], '%d.%m.%y %H:%M:%S')
|
||||||
|
lat = None
|
||||||
|
lon = None
|
||||||
|
for pos in data:
|
||||||
|
if pos["id"] == source_id and pos["tacticObjectType"] == "position":
|
||||||
|
lat = pos["latitude"]
|
||||||
|
lon = pos["longitude"]
|
||||||
|
break
|
||||||
|
print(f"Name - {sat_name}, f - {freq}, f range - {freq_range}, pol - {pol} "
|
||||||
|
f"time - {timestamp}, pos - ({lat}, {lon})")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def get_points_from_csv(file_content):
|
||||||
|
import io
|
||||||
|
if hasattr(file_content, 'read'):
|
||||||
|
content = file_content.read()
|
||||||
|
if isinstance(content, bytes):
|
||||||
|
content = content.decode('utf-8')
|
||||||
|
else:
|
||||||
|
if isinstance(file_content, bytes):
|
||||||
|
content = content.decode('utf-8')
|
||||||
|
else:
|
||||||
|
content = file_content
|
||||||
|
df = pd.read_csv(io.StringIO(content), sep=";",
|
||||||
|
names=['id', 'obj', 'lat', 'lon', 'h', 'time', 'sat', 'norad_id', 'freq', 'f_range', 'et', 'qaul', 'mir_1', 'mir_2', 'mir_3'])
|
||||||
|
df[['lat', 'lon', 'freq', 'f_range']] = df[['lat', 'lon', 'freq', 'f_range']].replace(',', '.', regex=True).astype(float)
|
||||||
|
df['time'] = pd.to_datetime(df['time'], format='%d.%m.%Y %H:%M:%S')
|
||||||
|
for row in df.iterrows():
|
||||||
|
row = row[1]
|
||||||
|
match row['obj'].split(' ')[-1]:
|
||||||
|
case 'V':
|
||||||
|
pol = 'Вертикальная'
|
||||||
|
case 'H':
|
||||||
|
pol = 'Горизонтальная'
|
||||||
|
case 'R':
|
||||||
|
pol = 'Правая'
|
||||||
|
case 'L':
|
||||||
|
pol = 'Левая'
|
||||||
|
case _:
|
||||||
|
pol = '-'
|
||||||
|
pol_obj, _ = Polarization.objects.get_or_create(
|
||||||
|
name=pol
|
||||||
|
)
|
||||||
|
sat_obj, _ = Satellite.objects.get_or_create(
|
||||||
|
name=row['sat'],
|
||||||
|
defaults={'norad': row['norad_id']}
|
||||||
|
)
|
||||||
|
mir_1_obj, _ = Mirror.objects.get_or_create(
|
||||||
|
name=row['mir_1']
|
||||||
|
)
|
||||||
|
mir_2_obj, _ = Mirror.objects.get_or_create(
|
||||||
|
name=row['mir_2']
|
||||||
|
)
|
||||||
|
mir_lst = [row['mir_1'], row['mir_2']]
|
||||||
|
if not pd.isna(row['mir_3']):
|
||||||
|
mir_3_obj, _ = Mirror.objects.get_or_create(
|
||||||
|
|
||||||
|
name=row['mir_3']
|
||||||
|
)
|
||||||
|
vch_load_obj, _ = Parameter.objects.get_or_create(
|
||||||
|
id_satellite=sat_obj,
|
||||||
|
polarization=pol_obj,
|
||||||
|
frequency=row['freq'],
|
||||||
|
freq_range=row['f_range'],
|
||||||
|
defaults={'id_user_add': CustomUser.objects.get(id=1)}
|
||||||
|
)
|
||||||
|
|
||||||
|
geo_obj, _ = Geo.objects.get_or_create(
|
||||||
|
timestamp=row['time'],
|
||||||
|
coords=Point(row['lon'], row['lat'], srid=4326),
|
||||||
|
defaults={
|
||||||
|
'is_average': False,
|
||||||
|
'id_user_add': CustomUser.objects.get(id=1),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
geo_obj.mirrors.set(Mirror.objects.filter(name__in=mir_lst))
|
||||||
|
|
||||||
|
obj_item_obj, _ = ObjItem.objects.get_or_create(
|
||||||
|
name=row['obj'],
|
||||||
|
# id_satellite=sat_obj,
|
||||||
|
id_vch_load=vch_load_obj,
|
||||||
|
id_geo=geo_obj,
|
||||||
|
defaults={
|
||||||
|
'id_user_add': CustomUser.objects.get(id=1)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
obj_item_obj.save()
|
||||||
|
# df = pd.read_csv(filepath, sep=";",
|
||||||
|
# names=['id', 'obj', 'lat', 'lon', 'h', 'time', 'sat', 'norad_id', 'freq', 'f_range', 'et', 'qaul', 'mir_1', 'mir_2', 'mir_3'])
|
||||||
|
# df[['lat', 'lon', 'freq', 'f_range']] = df[['lat', 'lon', 'freq', 'f_range']].replace(',', '.', regex=True).astype(float)
|
||||||
|
# df['time'] = pd.to_datetime(df['time'], format='%d.%m.%Y %H:%M:%S')
|
||||||
|
# for row in df.iterrows():
|
||||||
|
# row = row[1]
|
||||||
|
# match row['obj'].split(' ')[-1]:
|
||||||
|
# case 'V':
|
||||||
|
# pol = 'Вертикальная'
|
||||||
|
# case 'H':
|
||||||
|
# pol = 'Горизонтальная'
|
||||||
|
# case 'R':
|
||||||
|
# pol = 'Правая'
|
||||||
|
# case 'L':
|
||||||
|
# pol = 'Левая'
|
||||||
|
# case _:
|
||||||
|
# pol = '-'
|
||||||
|
# pol_obj, _ = Polarization.objects.get_or_create(
|
||||||
|
# name=pol
|
||||||
|
# )
|
||||||
|
# sat_obj, _ = Satellite.objects.get_or_create(
|
||||||
|
# name=row['sat'],
|
||||||
|
# defaults={'norad': row['norad_id']}
|
||||||
|
# )
|
||||||
|
# mir_1_obj, _ = Mirror.objects.get_or_create(
|
||||||
|
# name=row['mir_1']
|
||||||
|
# )
|
||||||
|
# mir_2_obj, _ = Mirror.objects.get_or_create(
|
||||||
|
# name=row['mir_2']
|
||||||
|
# )
|
||||||
|
# mir_lst = [row['mir_1'], row['mir_2']]
|
||||||
|
# if not pd.isna(row['mir_3']):
|
||||||
|
# mir_3_obj, _ = Mirror.objects.get_or_create(
|
||||||
|
|
||||||
|
# name=row['mir_3']
|
||||||
|
# )
|
||||||
|
# vch_load_obj, _ = Parameter.objects.get_or_create(
|
||||||
|
# id_satellite=sat_obj,
|
||||||
|
# polarization=pol_obj,
|
||||||
|
# frequency=row['freq'],
|
||||||
|
# freq_range=row['f_range'],
|
||||||
|
# defaults={'id_user_add': CustomUser.objects.get(id=1)}
|
||||||
|
# )
|
||||||
|
|
||||||
|
# geo_obj, _ = Geo.objects.get_or_create(
|
||||||
|
# timestamp=row['time'],
|
||||||
|
# coords=Point(row['lon'], row['lat'], srid=4326),
|
||||||
|
# defaults={
|
||||||
|
# 'is_average': False,
|
||||||
|
# 'id_user_add': CustomUser.objects.get(id=1),
|
||||||
|
# }
|
||||||
|
# )
|
||||||
|
# geo_obj.mirrors.set(Mirror.objects.filter(name__in=mir_lst))
|
||||||
|
|
||||||
|
# obj_item_obj, _ = ObjItem.objects.get_or_create(
|
||||||
|
# name=row['obj'],
|
||||||
|
# # id_satellite=sat_obj,
|
||||||
|
# id_vch_load=vch_load_obj,
|
||||||
|
# id_geo=geo_obj,
|
||||||
|
# defaults={
|
||||||
|
# 'id_user_add': CustomUser.objects.get(id=1)
|
||||||
|
# }
|
||||||
|
# )
|
||||||
|
# obj_item_obj.save()
|
||||||
|
|
||||||
|
|
||||||
|
def get_vch_load_from_html(file, sat: Satellite) -> None:
|
||||||
|
tables = pd.read_html(file, encoding='windows-1251')
|
||||||
|
df = tables[0]
|
||||||
|
df = df.drop(0).reset_index(drop=True)
|
||||||
|
df.columns = df.iloc[0]
|
||||||
|
df = df.drop(0).reset_index(drop=True)
|
||||||
|
df.replace('Неизвестно', '-', inplace=True)
|
||||||
|
df[['Частота, МГц', 'Полоса, МГц', 'Мощность, дБм']] = df[['Частота, МГц', 'Полоса, МГц', 'Мощность, дБм']].apply(pd.to_numeric)
|
||||||
|
df['Время начала измерения'] = df['Время начала измерения'].apply(lambda x: datetime.strptime(x, '%d.%m.%Y %H:%M:%S'))
|
||||||
|
df['Время окончания измерения'] = df['Время окончания измерения'].apply(lambda x: datetime.strptime(x, '%d.%m.%Y %H:%M:%S'))
|
||||||
|
|
||||||
|
for stroka in df.iterrows():
|
||||||
|
value = stroka[1]
|
||||||
|
if value['Полоса, МГц'] < 0.08:
|
||||||
|
continue
|
||||||
|
if '-' in value['Символьная скорость']:
|
||||||
|
bod_velocity = -1.0
|
||||||
|
else:
|
||||||
|
bod_velocity = value['Символьная скорость']
|
||||||
|
if '-' in value['Сигнал/шум, дБ']:
|
||||||
|
snr = - 1.0
|
||||||
|
else:
|
||||||
|
snr = value['Сигнал/шум, дБ']
|
||||||
|
if value['Пакетность'] == 'да':
|
||||||
|
pack = True
|
||||||
|
elif value['Пакетность'] == 'нет':
|
||||||
|
pack = False
|
||||||
|
else:
|
||||||
|
pack = None
|
||||||
|
|
||||||
|
mod, _ = Modulation.objects.get_or_create(
|
||||||
|
name=value['Модуляция']
|
||||||
|
)
|
||||||
|
standard, _ = Standard.objects.get_or_create(
|
||||||
|
name=value['Стандарт']
|
||||||
|
)
|
||||||
|
sigma_load, _ = SigmaParameter.objects.get_or_create(
|
||||||
|
id_satellite=sat,
|
||||||
|
frequency=value['Частота, МГц'],
|
||||||
|
freq_range=value['Полоса, МГц'],
|
||||||
|
defaults={
|
||||||
|
"status": value['Статус'],
|
||||||
|
"power": value['Мощность, дБм'],
|
||||||
|
"bod_velocity": bod_velocity,
|
||||||
|
"modulation": mod,
|
||||||
|
"snr": snr,
|
||||||
|
"packets": pack,
|
||||||
|
"datetime_begin": value['Время начала измерения'],
|
||||||
|
"datetime_end": value['Время окончания измерения'],
|
||||||
|
}
|
||||||
|
|
||||||
|
)
|
||||||
|
sigma_load.save()
|
||||||
|
|
||||||
|
def define_ku_transfer(min_freq: float, max_freq: float) -> int | None:
|
||||||
|
fss = (10700, 11700)
|
||||||
|
dss = (11700, 12750)
|
||||||
|
if min_freq + 9750 >= fss[0] and max_freq + 9750 <= fss[1]:
|
||||||
|
return 9750
|
||||||
|
elif min_freq + 10750 >= dss[0] and max_freq + 10750 <= dss[1]:
|
||||||
|
return 10750
|
||||||
|
return None
|
||||||
|
|
||||||
|
def compare_and_link_vch_load(sat_id: Satellite, eps_freq: float, eps_frange: float, ku_range: float):
|
||||||
|
item_obj = ObjItem.objects.filter(id_vch_load__id_satellite=sat_id)
|
||||||
|
vch_sigma = SigmaParameter.objects.filter(id_satellite=sat_id)
|
||||||
|
link_count = 0
|
||||||
|
obj_count = len(item_obj)
|
||||||
|
for idx, obj in enumerate(item_obj):
|
||||||
|
vch_load = obj.id_vch_load
|
||||||
|
if vch_load.frequency == -1.0:
|
||||||
|
continue
|
||||||
|
# if unique_points = Point.objects.order_by('frequency').distinct('frequency')
|
||||||
|
for sigma in vch_sigma:
|
||||||
|
if abs(sigma.frequency + ku_range - vch_load.frequency) <= vch_load.frequency*eps_freq/100 and abs(sigma.freq_range - vch_load.freq_range) <= vch_load.freq_range*eps_frange/100:
|
||||||
|
sigma.parameter = vch_load
|
||||||
|
sigma.save()
|
||||||
|
link_count += 1
|
||||||
|
return obj_count, link_count
|
||||||
|
|
||||||
|
|
||||||
213
dbapp/mainapp/views.py
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
from django.shortcuts import render, redirect
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.views.decorators.http import require_GET
|
||||||
|
from django.contrib.admin.views.decorators import staff_member_required
|
||||||
|
import pandas as pd
|
||||||
|
from .utils import (
|
||||||
|
fill_data_from_df,
|
||||||
|
add_satellite_list,
|
||||||
|
get_points_from_csv,
|
||||||
|
get_vch_load_from_html,
|
||||||
|
compare_and_link_vch_load
|
||||||
|
)
|
||||||
|
from mapsapp.utils import parse_transponders_from_json
|
||||||
|
from .forms import LoadExcelData, LoadCsvData, UploadFileForm, VchLinkForm
|
||||||
|
from .models import ObjItem
|
||||||
|
from .clusters import get_clusters
|
||||||
|
from dbapp.settings import BASE_DIR
|
||||||
|
|
||||||
|
|
||||||
|
def add_satellites(request):
|
||||||
|
add_satellite_list()
|
||||||
|
return redirect('home')
|
||||||
|
|
||||||
|
def add_transponders(request):
|
||||||
|
try:
|
||||||
|
parse_transponders_from_json(BASE_DIR / "transponders.json")
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("Файл не найден")
|
||||||
|
return redirect('home')
|
||||||
|
|
||||||
|
|
||||||
|
def home_page(request):
|
||||||
|
return render(request, 'mainapp/home.html')
|
||||||
|
|
||||||
|
|
||||||
|
def load_excel_data(request):
|
||||||
|
if request.method == "POST":
|
||||||
|
form = LoadExcelData(request.POST, request.FILES)
|
||||||
|
if form.is_valid():
|
||||||
|
uploaded_file = request.FILES['file']
|
||||||
|
selected_sat = form.cleaned_data['sat_choice']
|
||||||
|
number = form.cleaned_data['number_input']
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create a temporary file-like object from the uploaded file
|
||||||
|
import io
|
||||||
|
df = pd.read_excel(io.BytesIO(uploaded_file.read()))
|
||||||
|
if number > 0:
|
||||||
|
df = df.head(number)
|
||||||
|
result = fill_data_from_df(df, selected_sat)
|
||||||
|
|
||||||
|
messages.success(request, f"Данные успешно загружены! Обработано строк: {result}")
|
||||||
|
return redirect('load_excel_data')
|
||||||
|
except Exception as e:
|
||||||
|
messages.error(request, f"Ошибка при обработке файла: {str(e)}")
|
||||||
|
return redirect('load_excel_data')
|
||||||
|
else:
|
||||||
|
form = LoadExcelData()
|
||||||
|
|
||||||
|
return render(request, 'mainapp/add_data_from_excel.html', {'form': form})
|
||||||
|
|
||||||
|
|
||||||
|
def get_locations(request, sat_id):
|
||||||
|
locations = ObjItem.objects.filter(id_vch_load__id_satellite=sat_id)
|
||||||
|
if not locations:
|
||||||
|
return JsonResponse({'error': 'Объектов не найдено'}, status=400)
|
||||||
|
|
||||||
|
features = []
|
||||||
|
for loc in locations:
|
||||||
|
features.append({
|
||||||
|
"type": "Feature",
|
||||||
|
"geometry": {
|
||||||
|
"type": "Point",
|
||||||
|
"coordinates": [loc.id_geo.coords[0], loc.id_geo.coords[1]]
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"pol": loc.id_vch_load.polarization.name,
|
||||||
|
"freq": loc.id_vch_load.frequency*1000000,
|
||||||
|
"name": f"{loc.name}",
|
||||||
|
"id": loc.id_geo.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
"type": "FeatureCollection",
|
||||||
|
"features": features
|
||||||
|
})
|
||||||
|
|
||||||
|
def load_raw_csv_data(request):
|
||||||
|
if request.method == "POST":
|
||||||
|
form = LoadCsvData(request.POST, request.FILES)
|
||||||
|
if form.is_valid():
|
||||||
|
uploaded_file = request.FILES['file']
|
||||||
|
try:
|
||||||
|
# Read the file content and pass it directly to the function
|
||||||
|
content = uploaded_file.read()
|
||||||
|
if isinstance(content, bytes):
|
||||||
|
content = content.decode('utf-8')
|
||||||
|
|
||||||
|
get_points_from_csv(content)
|
||||||
|
messages.success(request, f"Данные успешно загружены!")
|
||||||
|
return redirect('load_csv_data')
|
||||||
|
except Exception as e:
|
||||||
|
messages.error(request, f"Ошибка при обработке файла: {str(e)}")
|
||||||
|
return redirect('load_csv_data')
|
||||||
|
|
||||||
|
else:
|
||||||
|
form = LoadCsvData()
|
||||||
|
|
||||||
|
return render(request, 'mainapp/add_data_from_csv.html', {'form': form})
|
||||||
|
|
||||||
|
# def upload_file(request):
|
||||||
|
# if request.method == 'POST' and request.FILES:
|
||||||
|
# form = UploadFileForm(request.POST, request.FILES)
|
||||||
|
# if form.is_valid():
|
||||||
|
# uploaded_file = request.FILES['file']
|
||||||
|
# # Обработка текстового файла, например:
|
||||||
|
# df = pd.read_csv(uploaded_file)
|
||||||
|
# df = pd.read_csv(filepath, sep=";",
|
||||||
|
# names=['id', 'obj', 'lat', 'lon', 'h', 'time', 'sat', 'norad_id', 'freq', 'f_range', 'et', 'qaul', 'mir_1', 'mir_2', 'mir_3'])
|
||||||
|
# df[['lat', 'lon', 'freq', 'f_range']] = df[['lat', 'lon', 'freq', 'f_range']].replace(',', '.', regex=True).astype(float)
|
||||||
|
# df['time'] = pd.to_datetime(df['time'], format='%d.%m.%Y %H:%M:%S')
|
||||||
|
# get_points_from_csv(df)
|
||||||
|
# return JsonResponse({'status': 'success'})
|
||||||
|
# else:
|
||||||
|
# return JsonResponse({'status': 'error', 'errors': form.errors}, status=400)
|
||||||
|
# return render(request, 'mainapp/add_data_from_csv.html')
|
||||||
|
from collections import defaultdict
|
||||||
|
@staff_member_required
|
||||||
|
def show_map_view(request):
|
||||||
|
ids = request.GET.get('ids', '')
|
||||||
|
points = []
|
||||||
|
if ids:
|
||||||
|
id_list = [int(x) for x in ids.split(',') if x.isdigit()]
|
||||||
|
locations = ObjItem.objects.filter(id__in=id_list)
|
||||||
|
for obj in locations:
|
||||||
|
points.append({
|
||||||
|
'name': f"{obj.name}",
|
||||||
|
'freq': f"{obj.id_vch_load.frequency} [{obj.id_vch_load.freq_range}] МГц",
|
||||||
|
'point': (obj.id_geo.coords.x, obj.id_geo.coords.y)
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return redirect('admin')
|
||||||
|
grouped = defaultdict(list)
|
||||||
|
for p in points:
|
||||||
|
grouped[p["name"]].append({
|
||||||
|
'point': p["point"],
|
||||||
|
'frequency': p["freq"]
|
||||||
|
})
|
||||||
|
|
||||||
|
# Преобразуем в список словарей для удобства в шаблоне
|
||||||
|
groups = [
|
||||||
|
{
|
||||||
|
"name": name,
|
||||||
|
"points": coords_list
|
||||||
|
}
|
||||||
|
for name, coords_list in grouped.items()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'groups': groups,
|
||||||
|
}
|
||||||
|
return render(request, 'admin/map_custom.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
def cluster_test(request):
|
||||||
|
objs = ObjItem.objects.filter(name__icontains="! Astra 4A 12654,040 [1,962] МГц H")
|
||||||
|
coords = []
|
||||||
|
for obj in objs:
|
||||||
|
coords.append((obj.id_geo.coords[1], obj.id_geo.coords[0]))
|
||||||
|
get_clusters(coords)
|
||||||
|
|
||||||
|
return JsonResponse({"success": "ок"})
|
||||||
|
|
||||||
|
def upload_vch_load_from_html(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = UploadFileForm(request.POST, request.FILES)
|
||||||
|
if form.is_valid():
|
||||||
|
selected_sat = form.cleaned_data['sat_choice']
|
||||||
|
uploaded_file = request.FILES['file']
|
||||||
|
try:
|
||||||
|
get_vch_load_from_html(uploaded_file, selected_sat)
|
||||||
|
messages.success(request, "Файл успешно обработан")
|
||||||
|
except ValueError as e:
|
||||||
|
messages.error(request, f"Ошибка при чтении таблиц: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
messages.error(request, f"Неизвестная ошибка: {e}")
|
||||||
|
else:
|
||||||
|
messages.error(request, "Форма заполнена некорректно.")
|
||||||
|
else:
|
||||||
|
form = UploadFileForm()
|
||||||
|
|
||||||
|
return render(request, 'mainapp/upload_html.html', {'form': form})
|
||||||
|
|
||||||
|
|
||||||
|
def link_vch_sigma(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = VchLinkForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
freq = form.cleaned_data['value1']
|
||||||
|
freq_range = form.cleaned_data['value2']
|
||||||
|
ku_range = float(form.cleaned_data['ku_range'])
|
||||||
|
sat_id = form.cleaned_data['sat_choice']
|
||||||
|
# print(freq, freq_range, ku_range, sat_id.pk)
|
||||||
|
count_all, link_count = compare_and_link_vch_load(sat_id, freq, freq_range, ku_range)
|
||||||
|
messages.success(request, f"Привязано {link_count} из {count_all} объектов")
|
||||||
|
return redirect('link_vch_sigma')
|
||||||
|
else:
|
||||||
|
form = VchLinkForm()
|
||||||
|
|
||||||
|
return render(request, 'mainapp/link_vch.html', {'form': form})
|
||||||
22
dbapp/manage.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run administrative tasks."""
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dbapp.settings')
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
0
dbapp/mapsapp/__init__.py
Normal file
24
dbapp/mapsapp/admin.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from .models import Transponders
|
||||||
|
from rangefilter.filters import NumericRangeFilterBuilder
|
||||||
|
from more_admin_filters import MultiSelectDropdownFilter, MultiSelectFilter, MultiSelectRelatedDropdownFilter
|
||||||
|
from import_export.admin import ImportExportActionModelAdmin
|
||||||
|
|
||||||
|
@admin.register(Transponders)
|
||||||
|
class PolarizationAdmin(ImportExportActionModelAdmin, admin.ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
"sat_id",
|
||||||
|
"name",
|
||||||
|
"zone_name",
|
||||||
|
"frequency",
|
||||||
|
"frequency_range",
|
||||||
|
"polarization",
|
||||||
|
)
|
||||||
|
list_filter = (
|
||||||
|
("polarization", MultiSelectRelatedDropdownFilter),
|
||||||
|
("sat_id", MultiSelectRelatedDropdownFilter),
|
||||||
|
("frequency", NumericRangeFilterBuilder()),
|
||||||
|
"zone_name"
|
||||||
|
)
|
||||||
|
search_fields = ("name",)
|
||||||
|
ordering = ("name",)
|
||||||
6
dbapp/mapsapp/apps.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class MapsappConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'mapsapp'
|
||||||
33
dbapp/mapsapp/migrations/0001_initial.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-10-13 12:47
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import mainapp.models
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('mainapp', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Transponders',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(blank=True, max_length=30, null=True, verbose_name='Название транспондера')),
|
||||||
|
('frequency', models.FloatField(blank=True, null=True, verbose_name='Центральная частота')),
|
||||||
|
('frequency_range', models.FloatField(blank=True, null=True, verbose_name='Полоса частот')),
|
||||||
|
('zone_name', models.CharField(blank=True, max_length=60, null=True, verbose_name='Название зоны')),
|
||||||
|
('polarization', models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='tran_polarizations', to='mainapp.polarization', verbose_name='Поляризация')),
|
||||||
|
('sat_id', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='tran_satellite', to='mainapp.satellite', verbose_name='Спутник')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Транспондер',
|
||||||
|
'verbose_name_plural': 'Транспондеры',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
dbapp/mapsapp/migrations/__init__.py
Normal file
21
dbapp/mapsapp/models.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from django.db import models
|
||||||
|
from mainapp.models import Satellite, Polarization, get_default_polarization
|
||||||
|
|
||||||
|
class Transponders(models.Model):
|
||||||
|
name = models.CharField(max_length=30, null=True, blank=True, verbose_name="Название транспондера")
|
||||||
|
frequency = models.FloatField(blank=True, null=True, verbose_name="Центральная частота")
|
||||||
|
frequency_range = models.FloatField(blank=True, null=True, verbose_name="Полоса частот")
|
||||||
|
zone_name = models.CharField(max_length=60, blank=True, null=True, verbose_name="Название зоны")
|
||||||
|
polarization = models.ForeignKey(
|
||||||
|
Polarization, default=get_default_polarization, on_delete=models.SET_DEFAULT, related_name="tran_polarizations", null=True, blank=True, verbose_name="Поляризация"
|
||||||
|
)
|
||||||
|
sat_id = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="tran_satellite", verbose_name="Спутник")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Транспондер"
|
||||||
|
verbose_name_plural = "Транспондеры"
|
||||||
|
|
||||||
|
|
||||||
561
dbapp/mapsapp/templates/mapsapp/map2d.html
Normal file
@@ -0,0 +1,561 @@
|
|||||||
|
{% extends "mapsapp/map2d_base.html" %}
|
||||||
|
{% 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="panel-title">Объекты из базы</div>
|
||||||
|
<select id="objectSelector" class="object-select">
|
||||||
|
<option value="">— Выберите объект —</option>
|
||||||
|
{% for sat in sats %}
|
||||||
|
<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>
|
||||||
|
|
||||||
|
|
||||||
|
</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="panel-title">Области покрытия</div>
|
||||||
|
<div class="footprint-actions">
|
||||||
|
<button id="showAllFootprints">Показать все</button>
|
||||||
|
<button id="hideAllFootprints">Скрыть все</button>
|
||||||
|
</div>
|
||||||
|
<div id="footprintToggles"></div>
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
function clearAllMarkers() {
|
||||||
|
if (window.mainTreeControl && window.mainTreeControl._map) {
|
||||||
|
map.removeControl(window.mainTreeControl);
|
||||||
|
delete window.mainTreeControl;
|
||||||
|
if (window.geoJsonOverlaysControl) {
|
||||||
|
delete window.geoJsonOverlaysControl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
map.eachLayer(function(layer) {
|
||||||
|
if (!(layer instanceof L.TileLayer)) {
|
||||||
|
map.removeLayer(layer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let markerColors = ['red', 'green', 'orange', 'violet', 'grey', 'black', 'blue'];
|
||||||
|
|
||||||
|
function getColorIcon(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]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Новая функция загрузки и отображения GeoJSON ---
|
||||||
|
function loadGeoJsonForSatellite(satId) {
|
||||||
|
if (!satId) {
|
||||||
|
alert('Пожалуйста, выберите объект.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.mainTreeControl && window.mainTreeControl._map) {
|
||||||
|
map.removeControl(window.mainTreeControl);
|
||||||
|
delete window.mainTreeControl;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `/api/locations/${encodeURIComponent(satId)}/geojson`;
|
||||||
|
|
||||||
|
fetch(url)
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Сервер вернул ошибку: ' + response.status);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (!data.features || data.features.length === 0) {
|
||||||
|
alert('Объекты с таким именем не найдены.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Группировка данных по частоте ---
|
||||||
|
const groupedByFreq = {};
|
||||||
|
data.features.forEach(feature => {
|
||||||
|
const freq = feature.properties.freq/1000000;
|
||||||
|
if (!groupedByFreq[freq]) {
|
||||||
|
groupedByFreq[freq] = [];
|
||||||
|
}
|
||||||
|
groupedByFreq[freq].push(feature);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Создание overlay слоев для L.control.layers.tree ---
|
||||||
|
const overlays = [];
|
||||||
|
let freqIndex = 0;
|
||||||
|
|
||||||
|
for (const [freq, features] of Object.entries(groupedByFreq)) {
|
||||||
|
const colorName = markerColors[freqIndex % markerColors.length];
|
||||||
|
const freqIcon = getColorIcon(colorName);
|
||||||
|
const freqGroupLayer = L.layerGroup();
|
||||||
|
const subgroup = [];
|
||||||
|
|
||||||
|
features.forEach((feature, idx) => {
|
||||||
|
const [lon, lat] = feature.geometry.coordinates;
|
||||||
|
const pointName = feature.properties.name || `Точка ${idx}`;
|
||||||
|
const marker = L.marker([lat, lon], { icon: freqIcon })
|
||||||
|
.bindPopup(`${pointName}<br>Частота: ${freq}`);
|
||||||
|
freqGroupLayer.addLayer(marker);
|
||||||
|
|
||||||
|
subgroup.push({
|
||||||
|
label: freq == -1 ? `${idx + 1} - Неизвестно` : `${idx + 1} - ${freq} МГц`,
|
||||||
|
layer: marker
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Группа для частоты
|
||||||
|
overlays.push({
|
||||||
|
label: `${features[0].properties.name} (${freq} МГц)`,
|
||||||
|
selectAllCheckbox: true,
|
||||||
|
children: subgroup,
|
||||||
|
layer: freqGroupLayer
|
||||||
|
});
|
||||||
|
|
||||||
|
freqIndex++;
|
||||||
|
}
|
||||||
|
const rootGroup = {
|
||||||
|
label: "Все точки",
|
||||||
|
selectAllCheckbox: true,
|
||||||
|
children: overlays,
|
||||||
|
layer: L.layerGroup()
|
||||||
|
};
|
||||||
|
|
||||||
|
const geoJsonControl = L.control.layers.tree(baseLayers, [rootGroup], {
|
||||||
|
collapsed: false,
|
||||||
|
autoZIndex: true
|
||||||
|
});
|
||||||
|
|
||||||
|
window.geoJsonOverlaysControl = geoJsonControl;
|
||||||
|
if (window.mainTreeControl && window.mainTreeControl._map) {
|
||||||
|
map.removeControl(window.mainTreeControl);
|
||||||
|
delete window.mainTreeControl;
|
||||||
|
}
|
||||||
|
window.mainTreeControl = geoJsonControl.addTo(map);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Ошибка загрузки GeoJSON:', err);
|
||||||
|
alert('Не удалось загрузить объекты: ' + err.message);
|
||||||
|
if (window.mainTreeControl && window.mainTreeControl._map) {
|
||||||
|
map.removeControl(window.mainTreeControl);
|
||||||
|
delete window.mainTreeControl;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadTranspondersPointsForSatellite(satId) {
|
||||||
|
if (!satId) {
|
||||||
|
alert('Пожалуйста, выберите объект.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Явная очистка перед началом ---
|
||||||
|
if (window.mainTreeControl && window.mainTreeControl._map) {
|
||||||
|
console.log('Удаляем старый контрол точек');
|
||||||
|
map.removeControl(window.mainTreeControl);
|
||||||
|
delete window.mainTreeControl;
|
||||||
|
// window.geoJsonOverlaysControl также можно удалить, если он больше не нужен
|
||||||
|
if (window.geoJsonOverlaysControl) {
|
||||||
|
delete window.geoJsonOverlaysControl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// --- Конец очистки ---
|
||||||
|
|
||||||
|
const url_points = `/api/locations/${encodeURIComponent(satId)}/geojson`;
|
||||||
|
const url_trans = `/api/transponders/${encodeURIComponent(satId)}`;
|
||||||
|
|
||||||
|
fetch(url_trans)
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Сервер вернул ошибку при загрузке транспондеров: ' + response.status);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data_trans => {
|
||||||
|
console.log('Загруженные транспондеры:', data_trans);
|
||||||
|
|
||||||
|
return fetch(url_points)
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Сервер вернул ошибку при загрузке точек: ' + response.status);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data_points => {
|
||||||
|
console.log('Загруженные точки:', data_points);
|
||||||
|
processAndDisplayTransponderPointsByZone(data_points, data_trans);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Ошибка загрузки транспондеров или точек:', err);
|
||||||
|
alert('Не удалось загрузить данные: ' + err.message);
|
||||||
|
// Повторная проверка на случай ошибки
|
||||||
|
if (window.mainTreeControl && window.mainTreeControl._map) {
|
||||||
|
map.removeControl(window.mainTreeControl);
|
||||||
|
delete window.mainTreeControl;
|
||||||
|
if (window.geoJsonOverlaysControl) {
|
||||||
|
delete window.geoJsonOverlaysControl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function processAndDisplayTransponderPointsByZone(data_points, transpondersData) {
|
||||||
|
if (!data_points.features || data_points.features.length === 0) {
|
||||||
|
alert('Точки с таким идентификатором спутника не найдены.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!transpondersData || !Array.isArray(transpondersData)) {
|
||||||
|
console.error('Данные транспондеров недоступны или некорректны.');
|
||||||
|
alert('Ошибка: данные транспондеров отсутствуют.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Функция для определения транспондера по частоте и поляризации ---
|
||||||
|
function findTransponderForPoint(pointFreqHz, pointPolarization) {
|
||||||
|
const pointFreqMhz = pointFreqHz / 1000000; // Переводим в МГц
|
||||||
|
for (const trans of transpondersData) {
|
||||||
|
if (typeof trans.frequency !== 'number' || typeof trans.frequency_range !== 'number' || typeof trans.polarization !== 'string') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const centerFreq = trans.frequency;
|
||||||
|
const bandwidth = trans.frequency_range;
|
||||||
|
const halfBandwidth = bandwidth / 2;
|
||||||
|
const lowerBound = centerFreq - halfBandwidth;
|
||||||
|
const upperBound = centerFreq + halfBandwidth;
|
||||||
|
|
||||||
|
if (
|
||||||
|
pointFreqMhz >= lowerBound &&
|
||||||
|
pointFreqMhz <= upperBound &&
|
||||||
|
pointPolarization === trans.polarization
|
||||||
|
) {
|
||||||
|
return trans;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Группировка точек по транспондерам ---
|
||||||
|
const groupedByTransponder = {};
|
||||||
|
data_points.features.forEach(feature => {
|
||||||
|
const pointFreqHz = feature.properties.freq;
|
||||||
|
let pointPolarization = feature.properties.polarization;
|
||||||
|
if (pointPolarization === undefined || pointPolarization === null) {
|
||||||
|
pointPolarization = feature.properties.pol;
|
||||||
|
}
|
||||||
|
if (pointPolarization === undefined || pointPolarization === null) {
|
||||||
|
pointPolarization = feature.properties.polar;
|
||||||
|
}
|
||||||
|
if (pointPolarization === undefined || pointPolarization === null) {
|
||||||
|
pointPolarization = feature.properties.polarisation;
|
||||||
|
}
|
||||||
|
if (pointPolarization === undefined || pointPolarization === null) {
|
||||||
|
console.warn('Точка без поляризации, игнорируется:', feature);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const transponder = findTransponderForPoint(pointFreqHz, pointPolarization);
|
||||||
|
|
||||||
|
if (transponder) {
|
||||||
|
// --- ИСПРАВЛЕНО: используем name как уникальный идентификатор ---
|
||||||
|
const transId = transponder.name;
|
||||||
|
if (!groupedByTransponder[transId]) {
|
||||||
|
groupedByTransponder[transId] = {
|
||||||
|
transponder: transponder,
|
||||||
|
features: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
groupedByTransponder[transId].features.push(feature);
|
||||||
|
} else {
|
||||||
|
console.log(`Точка ${pointFreqHz / 1000000} МГц, ${pointPolarization} -> Не найден транспондер`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Сгруппированные данные:', groupedByTransponder);
|
||||||
|
|
||||||
|
// --- Создание иерархии: зоны -> транспондеры -> точки (внутри слоя) ---
|
||||||
|
const zonesMap = {};
|
||||||
|
|
||||||
|
for (const [transId, groupData] of Object.entries(groupedByTransponder)) {
|
||||||
|
const trans = groupData.transponder;
|
||||||
|
const zoneName = trans.zone_name || 'Без зоны';
|
||||||
|
|
||||||
|
if (!zonesMap[zoneName]) {
|
||||||
|
zonesMap[zoneName] = [];
|
||||||
|
}
|
||||||
|
zonesMap[zoneName].push({
|
||||||
|
transponder: trans,
|
||||||
|
features: groupData.features
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Сгруппировано по зонам:', zonesMap);
|
||||||
|
|
||||||
|
// --- Создание overlay слоев для L.control.layers.tree ---
|
||||||
|
const overlays = [];
|
||||||
|
let zoneIndex = 0;
|
||||||
|
|
||||||
|
for (const [zoneName, transponderGroups] of Object.entries(zonesMap)) {
|
||||||
|
const zoneGroupLayer = L.layerGroup();
|
||||||
|
const zoneChildren = [];
|
||||||
|
|
||||||
|
let transIndex = 0;
|
||||||
|
for (const transGroup of transponderGroups) {
|
||||||
|
const trans = transGroup.transponder;
|
||||||
|
const features = transGroup.features;
|
||||||
|
|
||||||
|
// Слой для одного транспондера
|
||||||
|
const transGroupLayer = L.layerGroup();
|
||||||
|
|
||||||
|
// Проходим по точкам транспондера
|
||||||
|
features.forEach(feature => {
|
||||||
|
const [lon, lat] = feature.geometry.coordinates;
|
||||||
|
const pointName = feature.properties.name || `Точка`;
|
||||||
|
const pointFreqHz = feature.properties.freq;
|
||||||
|
const pointFreqMhz = (pointFreqHz / 1000000).toFixed(2);
|
||||||
|
const pointPolarization = feature.properties.polarization || feature.properties.pol || feature.properties.polar || feature.properties.polarisation || '?';
|
||||||
|
|
||||||
|
// --- НОВОЕ: определяем цвет по частоте точки ---
|
||||||
|
// Округляем частоту до ближайшего целого Гц или, например, до 1000 Гц (1 кГц) для группировки
|
||||||
|
// const freqKey = Math.round(pointFreqHz / 1000); // Группировка по 1 кГц
|
||||||
|
const freqKey = Math.round(pointFreqHz); // Группировка по 1 Гц (можно изменить)
|
||||||
|
const colorIndex = freqKey % markerColors.length; // Индекс цвета зависит от частоты
|
||||||
|
const colorName = markerColors[colorIndex];
|
||||||
|
const pointIcon = getColorIcon(colorName); // Создаём иконку с цветом для этой точки
|
||||||
|
|
||||||
|
const marker = L.marker([lat, lon], { icon: pointIcon }) // Используем иконку точки
|
||||||
|
.bindPopup(`${pointName}<br>Частота: ${pointFreqMhz} МГц<br>Поляр.: ${pointPolarization}<br>Транспондер: ${trans.name}<br>Зона: ${trans.zone_name}`);
|
||||||
|
transGroupLayer.addLayer(marker);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Добавляем транспондер в дочерние элементы зоны
|
||||||
|
// Транспондер не будет иметь фиксированного цвета, только его точки
|
||||||
|
const lowerBound = (trans.frequency - trans.frequency_range/2).toFixed(2);
|
||||||
|
const upperBound = (trans.frequency + trans.frequency_range/2).toFixed(2);
|
||||||
|
zoneChildren.push({
|
||||||
|
label: `${trans.name} (${lowerBound} - ${upperBound})`,
|
||||||
|
selectAllCheckbox: true,
|
||||||
|
layer: transGroupLayer // Этот слой содержит точки с разными цветами
|
||||||
|
});
|
||||||
|
|
||||||
|
zoneGroupLayer.addLayer(transGroupLayer);
|
||||||
|
transIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
overlays.push({
|
||||||
|
label: zoneName,
|
||||||
|
selectAllCheckbox: true,
|
||||||
|
children: zoneChildren,
|
||||||
|
layer: zoneGroupLayer
|
||||||
|
});
|
||||||
|
|
||||||
|
zoneIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Корневая группа ---
|
||||||
|
const rootGroup = {
|
||||||
|
label: "Все точки",
|
||||||
|
selectAllCheckbox: true,
|
||||||
|
children: overlays,
|
||||||
|
layer: L.layerGroup()
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Создаем контрол и добавляем на карту ---
|
||||||
|
const geoJsonControl = L.control.layers.tree(baseLayers, [rootGroup], {
|
||||||
|
collapsed: false,
|
||||||
|
autoZIndex: true
|
||||||
|
});
|
||||||
|
|
||||||
|
window.geoJsonOverlaysControl = geoJsonControl;
|
||||||
|
if (window.mainTreeControl && window.mainTreeControl._map) {
|
||||||
|
map.removeControl(window.mainTreeControl);
|
||||||
|
delete window.mainTreeControl;
|
||||||
|
}
|
||||||
|
window.mainTreeControl = geoJsonControl.addTo(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Обработчики событий ---
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const select = document.getElementById('objectSelector');
|
||||||
|
select.selectedIndex = 0;
|
||||||
|
const loadBtn = document.getElementById('loadObjectBtn');
|
||||||
|
const transBtn = document.getElementById('loadObjectTransBtn')
|
||||||
|
|
||||||
|
// Загружаем footprint'ы при смене выбора
|
||||||
|
select.addEventListener('change', function () {
|
||||||
|
const satId = this.value;
|
||||||
|
console.log(satId);
|
||||||
|
loadFootprintsForSatellite(satId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Загружаем GeoJSON при нажатии кнопки
|
||||||
|
loadBtn.addEventListener('click', function () {
|
||||||
|
const satId = select.value;
|
||||||
|
loadGeoJsonForSatellite(satId);
|
||||||
|
});
|
||||||
|
|
||||||
|
transBtn.addEventListener('click', function () {
|
||||||
|
const satId = select.value;
|
||||||
|
loadTranspondersPointsForSatellite(satId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
let currentFootprintLayers = {};
|
||||||
|
let currentSatelliteId = null;
|
||||||
|
|
||||||
|
const togglesContainer = document.getElementById('footprintToggles');
|
||||||
|
const showAllBtn = document.getElementById('showAllFootprints');
|
||||||
|
const hideAllBtn = document.getElementById('hideAllFootprints');
|
||||||
|
|
||||||
|
// --- Функции ---
|
||||||
|
|
||||||
|
function escapeHtml(unsafe) {
|
||||||
|
// Простая функция для экранирования HTML-символов в именах
|
||||||
|
return unsafe
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFootprintUIAndLayers() {
|
||||||
|
// Удаляем все текущие слои footprint'ов с карты и очищаем объект
|
||||||
|
Object.entries(currentFootprintLayers).forEach(([name, layer]) => {
|
||||||
|
if (map.hasLayer(layer)) {
|
||||||
|
map.removeLayer(layer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
currentFootprintLayers = {};
|
||||||
|
|
||||||
|
// Очищаем контейнер с чекбоксами
|
||||||
|
togglesContainer.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadFootprintsForSatellite(satId) {
|
||||||
|
// Проверка, если satId пустой - очищаем
|
||||||
|
if (!satId) {
|
||||||
|
clearFootprintUIAndLayers();
|
||||||
|
currentSatelliteId = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем текущий ID спутника
|
||||||
|
currentSatelliteId = satId;
|
||||||
|
|
||||||
|
const url = `/api/footprint-names/${encodeURIComponent(satId)}`;
|
||||||
|
|
||||||
|
fetch(url)
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Ошибка загрузки footprint\'ов: ' + response.statusText);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(footprints => {
|
||||||
|
if (!Array.isArray(footprints)) {
|
||||||
|
throw new Error('Ожидался массив footprint\'ов');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Очищаем старое состояние
|
||||||
|
clearFootprintUIAndLayers();
|
||||||
|
|
||||||
|
// Создаём новые слои и чекбоксы
|
||||||
|
footprints.forEach(fp => {
|
||||||
|
// 1. Создаём тайловый слой Leaflet
|
||||||
|
// Убедитесь, что URL соответствует вашей структуре тайлов
|
||||||
|
const layer = L.tileLayer(`/tiles/${fp.name}/{z}/{x}/{y}.png`, {
|
||||||
|
minZoom: 0,
|
||||||
|
maxZoom: 21, // Установите соответствующий maxZoom
|
||||||
|
opacity: 0.7, // Установите нужную прозрачность
|
||||||
|
// attribution: 'SatBeams Rendered' // Можно добавить атрибуцию
|
||||||
|
});
|
||||||
|
|
||||||
|
// Слои изначально ДОБАВЛЕНЫ на карту (и видимы), если хотите изначально скрытыми - закомментируйте следующую строку
|
||||||
|
layer.addTo(map);
|
||||||
|
|
||||||
|
// Сохраняем слой в объекте
|
||||||
|
currentFootprintLayers[fp.name] = layer;
|
||||||
|
|
||||||
|
const safeNameAttr = encodeURIComponent(fp.name); // для data-атрибута
|
||||||
|
const safeFullName = escapeHtml(fp.fullname); // для отображения
|
||||||
|
|
||||||
|
// 2. Создаём чекбокс и метку
|
||||||
|
const label = document.createElement('label');
|
||||||
|
label.style.display = 'block';
|
||||||
|
label.style.margin = '4px 0';
|
||||||
|
// Чекбокс изначально отмечен, если слой добавлен на карту
|
||||||
|
label.innerHTML = `
|
||||||
|
<input type="checkbox"
|
||||||
|
data-footprint="${safeNameAttr}"
|
||||||
|
checked> <!-- Отмечен, так как слой добавлен -->
|
||||||
|
${safeFullName}
|
||||||
|
`;
|
||||||
|
togglesContainer.appendChild(label);
|
||||||
|
|
||||||
|
// 3. Связываем чекбокс со слоем
|
||||||
|
const checkbox = label.querySelector('input');
|
||||||
|
checkbox.addEventListener('change', function () {
|
||||||
|
const footprintName = decodeURIComponent(this.dataset.footprint);
|
||||||
|
const layer = currentFootprintLayers[footprintName];
|
||||||
|
if (layer) {
|
||||||
|
if (this.checked && !map.hasLayer(layer)) {
|
||||||
|
// Если чекбокс отмечен и слой не на карте - добавляем
|
||||||
|
map.addLayer(layer);
|
||||||
|
} else if (!this.checked && map.hasLayer(layer)) {
|
||||||
|
// Если чекбокс снят и слой на карте - удаляем
|
||||||
|
map.removeLayer(layer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Ошибка загрузки footprint\'ов:', err);
|
||||||
|
alert('Не удалось загрузить области покрытия: ' + err.message);
|
||||||
|
clearFootprintUIAndLayers(); // При ошибке очищаем UI
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function showAllFootprints() {
|
||||||
|
Object.entries(currentFootprintLayers).forEach(([name, layer]) => {
|
||||||
|
if (!map.hasLayer(layer)) {
|
||||||
|
map.addLayer(layer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Синхронизируем чекбоксы
|
||||||
|
document.querySelectorAll('#footprintToggles input[type="checkbox"]').forEach(cb => {
|
||||||
|
cb.checked = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideAllFootprints() {
|
||||||
|
Object.entries(currentFootprintLayers).forEach(([name, layer]) => {
|
||||||
|
if (map.hasLayer(layer)) {
|
||||||
|
map.removeLayer(layer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.querySelectorAll('#footprintToggles input[type="checkbox"]').forEach(cb => {
|
||||||
|
cb.checked = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Обработчики событий для кнопок ---
|
||||||
|
showAllBtn.addEventListener('click', showAllFootprints);
|
||||||
|
hideAllBtn.addEventListener('click', hideAllFootprints);
|
||||||
|
</script>
|
||||||
|
{% endblock extra_js %}
|
||||||
74
dbapp/mapsapp/templates/mapsapp/map2d_base.html
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
{% load static %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>{% block title %}Карта{% endblock %}</title>
|
||||||
|
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
|
||||||
|
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
|
||||||
|
<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">
|
||||||
|
{% block extra_css %}{% endblock %}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
#map {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="map"></div>
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
<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>
|
||||||
|
{% comment %} <script src="{% static 'leaflet-tree/LayersTree.js' %}"></script> {% endcomment %}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let map = L.map('map').setView([0, 0], 2);
|
||||||
|
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 — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'
|
||||||
|
});
|
||||||
|
const baseLayers = {
|
||||||
|
"Улицы": street,
|
||||||
|
"Спутник": satellite
|
||||||
|
};
|
||||||
|
L.control.layers(baseLayers).addTo(map);
|
||||||
|
map.setMaxZoom(18);
|
||||||
|
map.setMinZoom(0);
|
||||||
|
L.control.measure({ primaryLengthUnit: 'kilometers' }).addTo(map);
|
||||||
|
|
||||||
|
{% comment %} let imageUrl = '{% static "mapsapp/assets/world_map.jpg" %}';
|
||||||
|
let imageBounds = [[-82, -180], [82, 180]];
|
||||||
|
|
||||||
|
L.imageOverlay(imageUrl, imageBounds).addTo(map); {% endcomment %}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% block extra_js %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
115
dbapp/mapsapp/templates/mapsapp/map3d.html
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
{% load static %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
|
||||||
|
<title>Cesium Map Editor</title>
|
||||||
|
<script src="{% static 'cesium/Cesium.js' %}"></script>
|
||||||
|
<link href="{% static 'cesium/Widgets/widgets.css' %}" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="{% static 'mapsapp/style.css' %}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="cesiumContainer"></div>
|
||||||
|
<input type="file" id="fileInput" accept=".geojson,.json,.kml" style="display: none;" />
|
||||||
|
<!-- Панель инструментов -->
|
||||||
|
<div class="toolbar">
|
||||||
|
<!-- Группа 1: Режимы рисования -->
|
||||||
|
<div class="toolbar-section">
|
||||||
|
<div class="section-title">Рисование</div>
|
||||||
|
<div class="toolbar-group">
|
||||||
|
<button id="selectMode" class="tool-btn active" title="Режим выделения (S)">
|
||||||
|
<span>🔍</span> Выделение
|
||||||
|
</button>
|
||||||
|
<button id="markerMode" class="tool-btn" title="Добавить маркер (M)">
|
||||||
|
<span>📌</span> Маркер
|
||||||
|
</button>
|
||||||
|
<button id="polygonMode" class="tool-btn" title="Рисовать полигон (P)">
|
||||||
|
<span>⬢</span> Полигон
|
||||||
|
</button>
|
||||||
|
<button id="polylineMode" class="tool-btn" title="Рисовать линию (L)">
|
||||||
|
<span>〰️</span> Линия
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Группа 2: Импорт/Экспорт -->
|
||||||
|
<div class="toolbar-section">
|
||||||
|
<div class="section-title">Импорт/экспорт всех объектов</div>
|
||||||
|
<div class="toolbar-group">
|
||||||
|
<button id="importBtn" class="tool-btn" title="Импортировать GeoJSON или KML">
|
||||||
|
<span>📥</span> Импорт
|
||||||
|
</button>
|
||||||
|
<button id="exportBtn" class="tool-btn" title="Экспортировать в GeoJSON или KML">
|
||||||
|
<span>📤</span> Экспорт
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Группа 3: Действия -->
|
||||||
|
<div class="toolbar-section">
|
||||||
|
<div class="section-title">Действия</div>
|
||||||
|
<div class="toolbar-group">
|
||||||
|
<button id="deleteSelected" class="tool-btn danger" title="Удалить выделенное (Del)">
|
||||||
|
<span>🗑️</span> Удалить
|
||||||
|
</button>
|
||||||
|
<button id="clearAll" class="tool-btn danger" title="Очистить всё">
|
||||||
|
<span>🧹</span> Очистить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Строка состояния -->
|
||||||
|
<div class="status-bar">
|
||||||
|
<span id="modeStatus">Режим: Выделение</span>
|
||||||
|
<span id="coordinates" style="color: #eeeeeeff; font-size: 11px;"></span>
|
||||||
|
<span id="hint">Нажмите ESC для отмены</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Блок выбора объектов из БД -->
|
||||||
|
<div class="db-objects-panel">
|
||||||
|
<div class="panel-title">Объекты из базы</div>
|
||||||
|
<select id="objectSelector" class="object-select">
|
||||||
|
<option value="">— Выберите объект —</option>
|
||||||
|
{% for sat in sats %}
|
||||||
|
<option value="{{sat.id}}">{{sat.name}}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<button id="loadObjectBtn" class="load-btn">Загрузить на карту</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footprint-control">
|
||||||
|
<div class="panel-title">Области покрытия</div>
|
||||||
|
<div class="footprint-actions">
|
||||||
|
<button id="showAllFootprints">Показать все</button>
|
||||||
|
<button id="hideAllFootprints">Скрыть все</button>
|
||||||
|
</div>
|
||||||
|
<div id="footprintToggles"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Модальное окно для описания -->
|
||||||
|
<div id="descriptionModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h3>Добавить описание</h3>
|
||||||
|
<textarea id="descriptionInput" placeholder="Введите описание объекта..."></textarea>
|
||||||
|
<div class="modal-buttons">
|
||||||
|
<button id="confirmDescription">Сохранить</button>
|
||||||
|
<button id="cancelDescription">Отмена</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="exportModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h3>Экспорт данных</h3>
|
||||||
|
<p>Выберите формат для экспорта всех объектов:</p>
|
||||||
|
<div class="modal-buttons" style="justify-content: center; gap: 15px; margin-top: 20px;">
|
||||||
|
<button id="exportGeoJson">GeoJSON</button>
|
||||||
|
<button id="exportKml">KML</button>
|
||||||
|
<button id="cancelExport">Отмена</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="{% static 'mapsapp/main.js' %}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3
dbapp/mapsapp/tests.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
17
dbapp/mapsapp/urls.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
from django.conf.urls.static import static
|
||||||
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('3dmap', views.cesium_map, name='3dmap'),
|
||||||
|
path('2dmap', views.leaflet_map, name='2dmap'),
|
||||||
|
path('api/footprint-names/<int:sat_id>', views.get_footprints, name="footprint_names"),
|
||||||
|
path('api/transponders/<int:sat_id>', views.get_transponder_on_satid, name='transponders_data'),
|
||||||
|
path('tiles/<str:footprint_name>/<int:z>/<int:x>/<int:y>.png', views.tile_proxy, name='tile_proxy'),
|
||||||
|
# path('', views.home_page, name='home'),
|
||||||
|
# path('excel-data', views.load_excel_data, name='load_excel_data'),
|
||||||
|
# path('satellites', views.add_satellites, name='add_sats'),
|
||||||
|
|
||||||
|
]
|
||||||
92
dbapp/mapsapp/utils.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import requests
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
from .models import Transponders
|
||||||
|
from mainapp.models import Polarization, Satellite
|
||||||
|
|
||||||
|
def search_satellite_on_page(data: dict, satellite_name: str):
|
||||||
|
for pos, value in data.get('page', {}).get('positions').items():
|
||||||
|
for name in value['satellites']:
|
||||||
|
if name['other_names'] is None:
|
||||||
|
name['other_names'] = ''
|
||||||
|
if satellite_name.lower() in name['name'].lower() or satellite_name.lower() in name['other_names'].lower():
|
||||||
|
return pos, name['id']
|
||||||
|
return '', ''
|
||||||
|
|
||||||
|
def get_footprint_data(position: str = 62) -> dict:
|
||||||
|
"""Возвращает словарь с данным по footprint для спутников на выбранной долготе"""
|
||||||
|
response = requests.get(f"https://www.satbeams.com/footprints?position={position}")
|
||||||
|
response.raise_for_status()
|
||||||
|
match = re.search(r'var data = ({.*?});', response.text, re.DOTALL)
|
||||||
|
if match:
|
||||||
|
json_str = match.group(1)
|
||||||
|
try:
|
||||||
|
data = json.loads(json_str)
|
||||||
|
return data.get("page", {}).get("footprint_data", {}).get("beams",[])
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print("Ошибка парсинга JSON:", e)
|
||||||
|
else:
|
||||||
|
print("Нужных данных не найдено")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_page_data(url:str = 'https://www.satbeams.com/footprints') -> dict:
|
||||||
|
"""Возвращает словарь с данными по всем спутникам на странице"""
|
||||||
|
response = requests.get(url)
|
||||||
|
response.raise_for_status()
|
||||||
|
match = re.search(r'var data = ({.*?});', response.text, re.DOTALL)
|
||||||
|
if match:
|
||||||
|
json_str = match.group(1)
|
||||||
|
try:
|
||||||
|
data = json.loads(json_str)
|
||||||
|
# Файл json на диске для достоверности
|
||||||
|
with open('data.json', 'w') as jf:
|
||||||
|
json.dump(data, jf, indent=2)
|
||||||
|
return data
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print("Ошибка парсинга JSON:", e)
|
||||||
|
else:
|
||||||
|
print("Нужных данных не найдено")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def get_names_footprints_for_satellite(footprint_data: dict, sat_id: str) -> list[str]:
|
||||||
|
names = []
|
||||||
|
for beam in footprint_data:
|
||||||
|
if 'ku' in beam['band'].lower() and sat_id in beam['satellite_id']:
|
||||||
|
names.append(
|
||||||
|
{
|
||||||
|
"name": beam['name'],
|
||||||
|
"fullname": beam['fullname'][8:]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return names
|
||||||
|
|
||||||
|
|
||||||
|
def get_band_names(satellite_name: str) -> list[str]:
|
||||||
|
data = get_all_page_data()
|
||||||
|
pos, sat_id = search_satellite_on_page(data, satellite_name)
|
||||||
|
footprints = get_footprint_data(pos)
|
||||||
|
names = get_names_footprints_for_satellite(footprints, sat_id)
|
||||||
|
return names
|
||||||
|
|
||||||
|
def parse_transponders_from_json(filepath: str):
|
||||||
|
with open(filepath, encoding="utf-8") as jf:
|
||||||
|
data = json.load(jf)
|
||||||
|
for sat_name, trans_zone in data["satellites"].items():
|
||||||
|
for zone, trans in trans_zone.items():
|
||||||
|
for tran in trans:
|
||||||
|
f_b, f_e = tran["freq"][0].split("-")
|
||||||
|
f = round((float(f_b) + float(f_e))/2, 3)
|
||||||
|
f_range = round(abs(float(f_e) - float(f_b)), 3)
|
||||||
|
tran_obj = Transponders.objects.create(
|
||||||
|
name=tran["name"],
|
||||||
|
frequency=f,
|
||||||
|
frequency_range=f_range,
|
||||||
|
zone_name=zone,
|
||||||
|
polarization=Polarization.objects.get(name=tran["pol"]),
|
||||||
|
sat_id=Satellite.objects.get(name__iexact=sat_name)
|
||||||
|
)
|
||||||
|
tran_obj.save()
|
||||||
|
|
||||||
68
dbapp/mapsapp/views.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
from django.http import JsonResponse
|
||||||
|
import requests
|
||||||
|
from django.core import serializers
|
||||||
|
from django.http import HttpResponse, HttpResponseNotFound
|
||||||
|
from django.views.decorators.cache import cache_page
|
||||||
|
from django.views.decorators.http import require_GET
|
||||||
|
from mainapp.models import Satellite
|
||||||
|
from .models import Transponders
|
||||||
|
from .utils import get_band_names
|
||||||
|
|
||||||
|
def cesium_map(request):
|
||||||
|
sats = Satellite.objects.all()
|
||||||
|
|
||||||
|
return render(request, 'mapsapp/map3d.html', {'sats': sats})
|
||||||
|
|
||||||
|
def get_footprints(request, sat_id):
|
||||||
|
try:
|
||||||
|
sat_name = Satellite.objects.get(id=sat_id).name
|
||||||
|
footprint_names = get_band_names(sat_name)
|
||||||
|
|
||||||
|
return JsonResponse(footprint_names, safe=False)
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({"error": str(e)}, status=400)
|
||||||
|
|
||||||
|
|
||||||
|
@require_GET
|
||||||
|
@cache_page(60 * 60 * 24)
|
||||||
|
def tile_proxy(request, footprint_name, z, x, y):
|
||||||
|
if not footprint_name.replace('-', '').replace('_', '').isalnum():
|
||||||
|
return HttpResponse("Invalid footprint name", status=400)
|
||||||
|
|
||||||
|
url = f"https://static.satbeams.com/tiles/{footprint_name}/{z}/{x}/{y}.png"
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = requests.get(url, timeout=10)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
response = HttpResponse(resp.content, content_type='image/png')
|
||||||
|
response["Access-Control-Allow-Origin"] = "*"
|
||||||
|
return response
|
||||||
|
else:
|
||||||
|
return HttpResponseNotFound("Tile not found")
|
||||||
|
except Exception as e:
|
||||||
|
return HttpResponse(f"Proxy error: {e}", status=500)
|
||||||
|
|
||||||
|
def leaflet_map(request):
|
||||||
|
sats = Satellite.objects.all()
|
||||||
|
trans = Transponders.objects.all()
|
||||||
|
return render(request, 'mapsapp/map2d.html', {'sats': sats, 'trans': trans})
|
||||||
|
|
||||||
|
|
||||||
|
def get_transponder_on_satid(request, sat_id):
|
||||||
|
trans = Transponders.objects.filter(sat_id=sat_id)
|
||||||
|
output = []
|
||||||
|
for tran in trans:
|
||||||
|
output.append(
|
||||||
|
{
|
||||||
|
"name": tran.name,
|
||||||
|
"frequency": tran.frequency,
|
||||||
|
"frequency_range": tran.frequency_range,
|
||||||
|
"zone_name": tran.zone_name,
|
||||||
|
"polarization": tran.polarization.name
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if not trans:
|
||||||
|
return JsonResponse({'error': 'Объектов не найдено'}, status=400)
|
||||||
|
|
||||||
|
return JsonResponse(output, safe=False)
|
||||||
7
dbapp/static/bootstrap/bootstrap.bundle.min.js
vendored
Normal file
6
dbapp/static/bootstrap/bootstrap.min.css
vendored
Normal file
BIN
dbapp/static/cesium/Assets/Images/bing_maps_credit.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
dbapp/static/cesium/Assets/Images/cesium_credit.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
dbapp/static/cesium/Assets/Images/google_earth_credit.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
dbapp/static/cesium/Assets/Images/ion-credit.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
dbapp/static/cesium/Assets/Textures/LensFlare/DirtMask.jpg
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
dbapp/static/cesium/Assets/Textures/LensFlare/StarBurst.jpg
Normal file
|
After Width: | Height: | Size: 191 KiB |
BIN
dbapp/static/cesium/Assets/Textures/NaturalEarthII/0/0/0.jpg
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
dbapp/static/cesium/Assets/Textures/NaturalEarthII/0/1/0.jpg
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
dbapp/static/cesium/Assets/Textures/NaturalEarthII/1/0/0.jpg
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
dbapp/static/cesium/Assets/Textures/NaturalEarthII/1/0/1.jpg
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
dbapp/static/cesium/Assets/Textures/NaturalEarthII/1/1/0.jpg
Normal file
|
After Width: | Height: | Size: 10 KiB |