init commit

This commit is contained in:
2025-10-24 13:08:08 +03:00
commit 5e40201460
531 changed files with 919042 additions and 0 deletions

17
.gitignore vendored Normal file
View 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
View File

@@ -0,0 +1 @@
3.13

1
README.md Normal file
View File

@@ -0,0 +1 @@
Сервис для работы с базой данных. Django + PostreSQL с подулем Postgis

0
dbapp/dbapp/__init__.py Normal file
View File

16
dbapp/dbapp/asgi.py Normal file
View 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
View 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': '&copy; <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
View 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
View 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()

View File

506
dbapp/mainapp/admin.py Normal file
View 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
View 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
View 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
View 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
View 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': 'Введите второе число'
})
)

View 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'),
),
]

View 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='Расстояние между купсатом и гео'),
),
]

View File

@@ -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='Расстояние между купсатом и гео, км'),
),
]

View 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',
},
),
]

View 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,
),
]

View File

@@ -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='Спутник'),
),
]

View File

@@ -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='Спутник'),
),
]

View File

@@ -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='Частота, МГц'),
),
]

View File

@@ -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'),
),
]

View File

@@ -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='Отметка'),
),
]

View File

@@ -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='Отметка'),
),
]

View 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='Отметка'),
),
]

View File

@@ -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'),
),
]

View 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='Модуляция'),
),
]

View File

@@ -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'),
),
]

View File

@@ -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'),
),
]

View File

@@ -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='ВЧ'),
),
]

View File

256
dbapp/mainapp/models.py Normal file
View 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'
)
]

View 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()

View 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 %}

View 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 %}

View 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 %}

View 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>

View 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 %}

View 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 %}

View 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
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

20
dbapp/mainapp/urls.py Normal file
View 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
View 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
View 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
View 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()

View File

24
dbapp/mapsapp/admin.py Normal file
View 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
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class MapsappConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'mapsapp'

View 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': 'Транспондеры',
},
),
]

View File

21
dbapp/mapsapp/models.py Normal file
View 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 = "Транспондеры"

View 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, "&amp;")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
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 %}

View 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: '&copy; <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 &copy; 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>

View 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
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

17
dbapp/mapsapp/urls.py Normal file
View 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
View 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
View 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)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Some files were not shown because too many files have changed in this diff Show More