Compare commits

...

8 Commits

2148 changed files with 18240 additions and 919 deletions

1
.gitignore vendored
View File

@@ -17,3 +17,4 @@ django-leaflet
admin-interface admin-interface
docker-* docker-*
maplibre-gl-js-5.10.0.zip

View File

@@ -18,34 +18,26 @@ RUN apt-get update && apt-get install -y \
postgresql-client \ postgresql-client \
build-essential \ build-essential \
libpq-dev \ libpq-dev \
gcc \
g++ \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Install Python dependencies for GDAL
RUN pip install --upgrade pip && \
pip install --no-cache-dir GDAL==$(gdal-config --version)
# Set work directory # Set work directory
WORKDIR /app WORKDIR /app
# Copy project requirements # Copy project files
COPY pyproject.toml uv.lock ./ COPY pyproject.toml uv.lock ./
# Install uv package manager # Install uv and dependencies
RUN pip install --upgrade pip && pip install uv RUN pip install --no-cache-dir uv && \
uv sync --frozen --no-dev
# Install dependencies using uv # Copy project code (после установки зависимостей для лучшего кэширования)
RUN uv pip install --system --no-cache-dir -r uv.lock
# Copy project
COPY . . COPY . .
# Collect static files # Collect static files
RUN python manage.py collectstatic --noinput RUN uv run manage.py collectstatic --noinput
# Expose port # Expose port
EXPOSE 8000 EXPOSE 8000
# Run gunicorn server # Run gunicorn server
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "dbapp.wsgi:application"] CMD [".venv/bin/gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "dbapp.wsgi:application"]

View File

@@ -72,6 +72,7 @@ INSTALLED_APPS = [
MIDDLEWARE = [ MIDDLEWARE = [
"debug_toolbar.middleware.DebugToolbarMiddleware", #Добавил "debug_toolbar.middleware.DebugToolbarMiddleware", #Добавил
'django.middleware.locale.LocaleMiddleware', #Добавил
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', #Добавил 'django.contrib.messages.middleware.MessageMiddleware', #Добавил
@@ -149,6 +150,11 @@ USE_I18N = True
USE_TZ = True USE_TZ = True
# Authentication settings
LOGIN_URL = 'login'
LOGIN_REDIRECT_URL = 'home'
LOGOUT_REDIRECT_URL = 'home'
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.2/howto/static-files/ # https://docs.djangoproject.com/en/5.2/howto/static-files/
@@ -163,21 +169,6 @@ STATICFILES_DIRS = [
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 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' # AUTH_USER_MODEL = 'mainapp.CustomUser'
X_FRAME_OPTIONS = "SAMEORIGIN" X_FRAME_OPTIONS = "SAMEORIGIN"

View File

@@ -17,6 +17,7 @@ Including another URLconf
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from mainapp import views from mainapp import views
from django.contrib.auth import views as auth_views
from debug_toolbar.toolbar import debug_toolbar_urls from debug_toolbar.toolbar import debug_toolbar_urls
urlpatterns = [ urlpatterns = [
@@ -24,5 +25,8 @@ urlpatterns = [
path('admin/', admin.site.urls, name='admin'), path('admin/', admin.site.urls, name='admin'),
# path('admin/map/', views.show_map_view, name='admin_show_map'), # path('admin/map/', views.show_map_view, name='admin_show_map'),
path('', include('mainapp.urls')), path('', include('mainapp.urls')),
path('', include('mapsapp.urls')) path('', include('mapsapp.urls')),
# Authentication URLs
path('login/', auth_views.LoginView.as_view(), name='login'),
path('logout/', views.custom_logout, name='logout'),
] + debug_toolbar_urls() ] + debug_toolbar_urls()

View File

View File

@@ -0,0 +1,8 @@
from django.contrib import admin
from .models import LyngSat
@admin.register(LyngSat)
class LyngSatAdmin(admin.ModelAdmin):
list_display = ("mark", "timestamp")
search_fields = ("mark", )
ordering = ("timestamp",)

6
dbapp/lyngsatapp/apps.py Normal file
View File

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

View File

View File

@@ -0,0 +1,36 @@
from django.db import models
from mainapp.models import (
Satellite,
Polarization,
Modulation,
Standard,
get_default_polarization,
get_default_modulation,
get_default_standard
)
class LyngSat(models.Model):
id_satellite = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="lyngsat", verbose_name="Спутник", null=True)
polarization = models.ForeignKey(
Polarization, default=get_default_polarization, on_delete=models.SET_DEFAULT, related_name="lyngsat", null=True, blank=True, verbose_name="Поляризация"
)
modulation = models.ForeignKey(
Modulation, default=get_default_modulation, on_delete=models.SET_DEFAULT, related_name="lyngsat", null=True, blank=True, verbose_name="Модуляция"
)
standard = models.ForeignKey(
Standard, default=get_default_standard, on_delete=models.SET_DEFAULT, related_name="lyngsat", null=True, blank=True, verbose_name="Стандарт"
)
frequency = models.FloatField(default=0, null=True, blank=True, verbose_name="Частота, МГц")
sym_velocity = models.FloatField(default=0, null=True, blank=True, verbose_name="Символьная скорость, БОД")
last_update = models.DateTimeField(null=True, blank=True, verbose_name="Время")
channel_info = models.CharField(max_length=20, blank=True, null=True, verbose_name="Описание источника")
# url = models.URLField(max_length = 200, blank=True, null=True, verbose_name="Ссылка на страницу")
def __str__(self):
return f"Ист {self.frequency}, {self.polarization}"
class Meta:
verbose_name = "Источник LyngSat"
verbose_name_plural = "Источники LyngSat"

371
dbapp/lyngsatapp/parser.py Normal file
View File

@@ -0,0 +1,371 @@
import requests
from bs4 import BeautifulSoup
from datetime import datetime
import re
import time
class LyngSatParser:
"""Парсер данных для LyngSat(Для работы нужен flaresolver)"""
def __init__(
self,
flaresolver_url: str = "http://localhost:8191/v1",
regions: list[str] | None = None,
target_sats: list[str] | None = None,
):
self.flaresolver_url = flaresolver_url
self.regions = regions
self.target_sats = list(map(lambda sat: sat.strip().lower(), target_sats)) if regions else None
self.regions = regions if regions else ["europe", "asia", "america", "atlantic"]
self.BASE_URL = "https://www.lyngsat.com"
def parse_metadata(self, metadata: str) -> dict:
if not metadata or not metadata.strip():
return {
'standard': None,
'modulation': None,
'symbol_rate': None,
'fec': None
}
normalized = re.sub(r'\s+', '', metadata.strip())
fec_match = re.search(r'([1-9]/[1-9])$', normalized)
fec = fec_match.group(1) if fec_match else None
if fec_match:
core = normalized[:fec_match.start()]
else:
core = normalized
std_match = re.match(r'(DVB-S2?|ABS-S|DVB-T2?|ATSC|ISDB)', core)
standard = std_match.group(1) if std_match else None
rest = core[len(standard):] if standard else core
modulation = None
mod_match = re.match(r'(8PSK|QPSK|16APSK|32APSK|64QAM|256QAM|BPSK)', rest)
if mod_match:
modulation = mod_match.group(1)
rest = rest[len(modulation):]
symbol_rate = None
sr_match = re.search(r'(\d+)$', rest)
if sr_match:
try:
symbol_rate = int(sr_match.group(1))
except ValueError:
pass
return {
'standard': standard,
'modulation': modulation,
'symbol_rate': symbol_rate,
'fec': fec
}
def extract_date(self, s: str) -> datetime | None:
s = s.strip()
match = re.search(r'(\d{6})$', s)
if not match:
return None
yymmdd = match.group(1)
try:
return datetime.strptime(yymmdd, '%y%m%d').date()
except ValueError:
return None
def convert_polarization(self, polarization: str) -> str:
"""Преобразовать код поляризации в понятное название на русском"""
polarization_map = {
'V': 'Вертикальная',
'H': 'Горизонтальная',
'R': 'Правая',
'L': 'Левая'
}
return polarization_map.get(polarization.upper(), polarization)
def get_region_pages(self) -> list[str]:
html_regions = []
for region in self.regions:
url = f"{self.BASE_URL}/{region}.html"
payload = {
"cmd": "request.get",
"url": url,
"maxTimeout": 60000
}
response = requests.post(self.flaresolver_url, json=payload)
if response.status_code != 200:
continue
html_content = response.json().get("solution", {}).get("response", "")
html_regions.append(html_content)
print(f"Обработал страницу по {region}")
return html_regions
def get_satellites_data(self) -> dict[dict]:
sat_data = {}
for region_page in self.get_region_pages():
soup = BeautifulSoup(region_page, "html.parser")
col_table = soup.find_all("div", class_="desktab")[0]
tables = col_table.find_next_sibling('table').find_all('table')
trs = []
for table in tables:
trs.extend(table.find_all('tr'))
for tr in trs:
sat_name = tr.find('span').text
if self.target_sats is not None:
if sat_name.strip().lower() not in self.target_sats:
continue
try:
sat_url = tr.find_all('a')[2]['href']
except IndexError:
sat_url = tr.find_all('a')[0]['href']
update_date = tr.find_all('td')[-1].text
sat_response = requests.post(self.flaresolver_url, json={
"cmd": "request.get",
"url": f"{self.BASE_URL}/{sat_url}",
"maxTimeout": 60000
})
html_content = sat_response.json().get("solution", {}).get("response", "")
sat_page_data = self.get_satellite_content(html_content)
sat_data[sat_name] = {
"url": f"{self.BASE_URL}/{sat_url}",
"update_date": datetime.strptime(update_date, "%y%m%d").date(),
"sources": sat_page_data
}
return sat_data
def get_satellite_content(self, html_content: str) -> dict:
sat_soup = BeautifulSoup(html_content, "html.parser")
big_table = sat_soup.find('table', class_='bigtable')
all_tables = big_table.find_all("div", class_="desktab")[:-1]
data = []
for table in all_tables:
trs = table.find_next_sibling('table').find_all('tr')
for idx, tr in enumerate(trs):
tds = tr.find_all('td')
if len(tds) < 9 or idx < 2:
continue
freq, polarization = tds[0].find('b').text.strip().split('\xa0')
polarization = self.convert_polarization(polarization)
meta = self.parse_metadata(tds[1].text)
provider_name = tds[3].text
last_update = self.extract_date(tds[-1].text)
data.append({
"freq": freq,
"pol": polarization,
"metadata": meta,
"provider_name": provider_name,
"last_update": last_update
})
return data
class KingOfSatParser:
def __init__(self, base_url="https://ru.kingofsat.net", max_satellites=0):
"""
Инициализация парсера
:param base_url: Базовый URL сайта
:param max_satellites: Максимальное количество спутников для парсинга (0 - все)
"""
self.base_url = base_url
self.max_satellites = max_satellites
self.session = requests.Session()
self.session.headers.update({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
})
def convert_polarization(self, polarization):
"""Преобразовать код поляризации в понятное название на русском"""
polarization_map = {
'V': 'Вертикальная',
'H': 'Горизонтальная',
'R': 'Правая',
'L': 'Левая'
}
return polarization_map.get(polarization.upper(), polarization)
def fetch_page(self, url):
"""Получить HTML страницу"""
try:
response = self.session.get(url, timeout=30)
response.raise_for_status()
return response.text
except Exception as e:
print(f"Ошибка при получении страницы {url}: {e}")
return None
def parse_satellite_table(self, html_content):
"""Распарсить таблицу со спутниками"""
soup = BeautifulSoup(html_content, 'html.parser')
satellites = []
table = soup.find('table')
if not table:
print("Таблица не найдена")
return satellites
rows = table.find_all('tr')[1:]
for row in rows:
cols = row.find_all('td')
if len(cols) < 13:
continue
try:
position_cell = cols[0].text.strip()
position_match = re.search(r'([\d\.]+)°([EW])', position_cell)
if position_match:
position_value = position_match.group(1)
position_direction = position_match.group(2)
position = f"{position_value}{position_direction}"
else:
position = None
# Название спутника (2-я колонка)
satellite_cell = cols[1]
satellite_name = satellite_cell.get_text(strip=True)
# Удаляем возможные лишние символы или пробелы
satellite_name = re.sub(r'\s+', ' ', satellite_name).strip()
# NORAD (3-я колонка)
norad = cols[2].text.strip()
if not norad or norad == "-":
norad = None
ini_link = None
ini_cell = cols[3]
ini_img = ini_cell.find('img', src=lambda x: x and 'disquette.gif' in x)
if ini_img and position:
ini_link = f"https://ru.kingofsat.net/dl.php?pos={position}&fkhz=0"
update_date = cols[12].text.strip() if len(cols) > 12 else None
if satellite_name and ini_link and position:
satellites.append({
'position': position,
'name': satellite_name,
'norad': norad,
'ini_url': ini_link,
'update_date': update_date
})
except Exception as e:
print(f"Ошибка при обработке строки таблицы: {e}")
continue
return satellites
def parse_ini_file(self, ini_content):
"""Распарсить содержимое .ini файла"""
data = {
'metadata': {},
'sattype': {},
'dvb': {}
}
# # Извлекаем метаданные из комментариев
# metadata_match = re.search(r'\[ downloaded from www\.kingofsat\.net \(c\) (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) \]', ini_content)
# if metadata_match:
# data['metadata']['downloaded'] = metadata_match.group(1)
# Парсим секцию [SATTYPE]
sattype_match = re.search(r'\[SATTYPE\](.*?)\n\[', ini_content, re.DOTALL)
if sattype_match:
sattype_content = sattype_match.group(1).strip()
for line in sattype_content.split('\n'):
line = line.strip()
if '=' in line:
key, value = line.split('=', 1)
data['sattype'][key.strip()] = value.strip()
# Парсим секцию [DVB]
dvb_match = re.search(r'\[DVB\](.*?)(?:\n\[|$)', ini_content, re.DOTALL)
if dvb_match:
dvb_content = dvb_match.group(1).strip()
for line in dvb_content.split('\n'):
line = line.strip()
if '=' in line:
key, value = line.split('=', 1)
params = [p.strip() for p in value.split(',')]
polarization = params[1] if len(params) > 1 else ''
if polarization:
polarization = self.convert_polarization(polarization)
data['dvb'][key.strip()] = {
'frequency': params[0] if len(params) > 0 else '',
'polarization': polarization,
'symbol_rate': params[2] if len(params) > 2 else '',
'fec': params[3] if len(params) > 3 else '',
'standard': params[4] if len(params) > 4 else '',
'modulation': params[5] if len(params) > 5 else ''
}
return data
def download_ini_file(self, url):
"""Скачать содержимое .ini файла"""
try:
response = self.session.get(url, timeout=30)
response.raise_for_status()
return response.text
except Exception as e:
print(f"Ошибка при скачивании .ini файла {url}: {e}")
return None
def get_all_satellites_data(self):
"""Получить данные всех спутников с учетом ограничения max_satellites"""
html_content = self.fetch_page(self.base_url + '/satellites')
if not html_content:
return []
satellites = self.parse_satellite_table(html_content)
if self.max_satellites > 0 and len(satellites) > self.max_satellites:
satellites = satellites[:self.max_satellites]
results = []
processed_count = 0
for satellite in satellites:
print(f"Обработка спутника: {satellite['name']} ({satellite['position']})")
ini_content = self.download_ini_file(satellite['ini_url'])
if not ini_content:
print(f"Не удалось скачать .ini файл для {satellite['name']}")
continue
parsed_ini = self.parse_ini_file(ini_content)
result = {
'satellite_name': satellite['name'],
'position': satellite['position'],
'norad': satellite['norad'],
'update_date': satellite['update_date'],
'ini_url': satellite['ini_url'],
'ini_data': parsed_ini
}
results.append(result)
processed_count += 1
if self.max_satellites > 0 and processed_count >= self.max_satellites:
break
time.sleep(1)
return results
def create_satellite_dict(self, satellites_data):
"""Создать словарь с данными спутников"""
satellite_dict = {}
for data in satellites_data:
key = f"{data['position']}_{data['satellite_name'].replace(' ', '_').replace('/', '_')}"
satellite_dict[key] = {
'name': data['satellite_name'],
'position': data['position'],
'norad': data['norad'],
'update_date': data['update_date'],
'ini_url': data['ini_url'],
'transponders_count': len(data['ini_data']['dvb']),
'transponders': data['ini_data']['dvb'],
'sattype_info': data['ini_data']['sattype'],
'metadata': data['ini_data']['metadata']
}
return satellite_dict

View File

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

View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@@ -6,6 +6,7 @@ from .models import (
Standard, Standard,
SigmaParMark, SigmaParMark,
SigmaParameter, SigmaParameter,
SourceType,
Parameter, Parameter,
Satellite, Satellite,
Mirror, Mirror,
@@ -22,6 +23,7 @@ from django.contrib.gis.db import models as gis
from django.shortcuts import redirect from django.shortcuts import redirect
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from leaflet.forms.widgets import LeafletWidget
from rangefilter.filters import ( from rangefilter.filters import (
DateRangeFilterBuilder, DateRangeFilterBuilder,
@@ -38,10 +40,17 @@ from .filters import GeoKupDistanceFilter, GeoValidDistanceFilter, UniqueToggleF
admin.site.site_title = "Геолокация" admin.site.site_title = "Геолокация"
admin.site.site_header = "Geolocation" admin.site.site_header = "Geolocation"
admin.site.index_title = "Geo" admin.site.index_title = "Geo"
# Unregister default User and Group since we're customizing them
admin.site.unregister(User) admin.site.unregister(User)
admin.site.unregister(Group) admin.site.unregister(Group)
class CustomUserInline(admin.StackedInline):
model = CustomUser
can_delete = False
verbose_name_plural = 'Дополнительная информация пользователя'
class LocationForm(forms.ModelForm): class LocationForm(forms.ModelForm):
latitude_geo = forms.FloatField(required=False, label="Широта") latitude_geo = forms.FloatField(required=False, label="Широта")
longitude_geo = forms.FloatField(required=False, label="Долгота") longitude_geo = forms.FloatField(required=False, label="Долгота")
@@ -89,18 +98,30 @@ class LocationForm(forms.ModelForm):
return instance return instance
class GeoInline(admin.StackedInline):
model = Geo
class CustomUserInline(admin.StackedInline): extra = 0
model = CustomUser verbose_name = "Гео"
can_delete = False verbose_name_plural = "Гео"
verbose_name_plural = 'Дополнительная информация пользователя' form = LocationForm
readonly_fields = ("distance_coords_kup", "distance_coords_valid", "distance_kup_valid")
prefetch_related = ("mirrors",)
@admin.register(CustomUser) autocomplete_fields = ('mirrors',)
class CustomUserAdmin(admin.ModelAdmin): fieldsets = (
list_display = ('user', 'role') ("Основная информация", {
list_filter = ('role',) "fields": ("mirrors", "location", "distance_coords_kup",
"distance_coords_valid", "distance_kup_valid", "timestamp", "comment",)
}),
("Координаты: геолокация", {
"fields": ("longitude_geo", "latitude_geo", "coords"),
}),
("Координаты: Кубсат", {
"fields": ("longitude_kupsat", "latitude_kupsat", "coords_kupsat"),
}),
("Координаты: Оперативный отдел", {
"fields": ("longitude_valid", "latitude_valid", "coords_valid"),
}),
)
class UserAdmin(BaseUserAdmin): class UserAdmin(BaseUserAdmin):
@@ -108,6 +129,13 @@ class UserAdmin(BaseUserAdmin):
admin.site.register(User, UserAdmin) admin.site.register(User, UserAdmin)
# @admin.register(CustomUser)
# class CustomUserAdmin(admin.ModelAdmin):
# list_display = ('user', 'role')
# list_filter = ('role',)
# raw_id_fields = ('user',) # For better performance with large number of users
@admin.register(SigmaParMark) @admin.register(SigmaParMark)
class SigmaParMarkAdmin(admin.ModelAdmin): class SigmaParMarkAdmin(admin.ModelAdmin):
list_display = ("mark", "timestamp") list_display = ("mark", "timestamp")
@@ -128,6 +156,12 @@ class ModulationAdmin(admin.ModelAdmin):
search_fields = ("name",) search_fields = ("name",)
ordering = ("name",) ordering = ("name",)
@admin.register(SourceType)
class SourceTypeAdmin(admin.ModelAdmin):
list_display = ("name",)
search_fields = ("name",)
ordering = ("name",)
@admin.register(Standard) @admin.register(Standard)
class StandardAdmin(admin.ModelAdmin): class StandardAdmin(admin.ModelAdmin):
@@ -161,15 +195,6 @@ class ParameterAdmin(ImportExportActionModelAdmin, admin.ModelAdmin):
"standard", "standard",
"sigma_parameter" "sigma_parameter"
) )
# fields = ( "id_satellite",
# "frequency",
# "freq_range",
# "polarization",
# "modulation",
# "bod_velocity",
# "snr",
# "standard",
# "id_sigma_parameter")
list_display_links = ("frequency", "id_satellite", ) list_display_links = ("frequency", "id_satellite", )
list_filter = ( list_filter = (
HasSigmaParameterFilter, HasSigmaParameterFilter,
@@ -193,6 +218,7 @@ class ParameterAdmin(ImportExportActionModelAdmin, admin.ModelAdmin):
) )
ordering = ("frequency",) ordering = ("frequency",)
list_select_related = ("polarization", "modulation", "standard", "id_satellite",) list_select_related = ("polarization", "modulation", "standard", "id_satellite",)
autocomplete_fields = ('objitems',)
# raw_id_fields = ("id_sigma_parameter", ) # raw_id_fields = ("id_sigma_parameter", )
inlines = [SigmaParameterInline] inlines = [SigmaParameterInline]
# autocomplete_fields = ("id_sigma_parameter", ) # autocomplete_fields = ("id_sigma_parameter", )
@@ -209,23 +235,25 @@ class ParameterAdmin(ImportExportActionModelAdmin, admin.ModelAdmin):
class SigmaParameterAdmin(ImportExportActionModelAdmin, admin.ModelAdmin): class SigmaParameterAdmin(ImportExportActionModelAdmin, admin.ModelAdmin):
list_display = ( list_display = (
"id_satellite", "id_satellite",
"status", # "status",
"frequency", "frequency",
"transfer_frequency",
"freq_range", "freq_range",
"power", # "power",
"polarization",
"modulation", "modulation",
"bod_velocity", "bod_velocity",
"snr", "snr",
"standard", # "standard",
"parameter", "parameter",
"packets", # "packets",
"datetime_begin", "datetime_begin",
"datetime_end", "datetime_end",
) )
readonly_fields = ( readonly_fields = (
"datetime_begin", "datetime_begin",
"datetime_end", "datetime_end",
"transfer_frequency"
) )
list_display_links = ("id_satellite",) list_display_links = ("id_satellite",)
list_filter = ( list_filter = (
@@ -272,7 +300,7 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin):
fieldsets = ( fieldsets = (
("Основная информация", { ("Основная информация", {
"fields": ("mirrors", "location", "distance_coords_kup", "fields": ("mirrors", "location", "distance_coords_kup",
"distance_coords_valid", "distance_kup_valid", "timestamp", "comment", "id_user_add") "distance_coords_valid", "distance_kup_valid", "timestamp", "comment",)
}), }),
("Координаты: геолокация", { ("Координаты: геолокация", {
"fields": ("longitude_geo", "latitude_geo", "coords"), "fields": ("longitude_geo", "latitude_geo", "coords"),
@@ -300,7 +328,6 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin):
"is_average", "is_average",
("location", MultiSelectDropdownFilter), ("location", MultiSelectDropdownFilter),
("timestamp", DateRangeQuickSelectListFilterBuilder()), ("timestamp", DateRangeQuickSelectListFilterBuilder()),
("id_user_add", MultiSelectRelatedDropdownFilter),
) )
search_fields = ( search_fields = (
"mirrors__name", "mirrors__name",
@@ -309,7 +336,6 @@ class GeoAdmin(ImportExportActionModelAdmin, LeafletGeoAdmin):
"coords_kupsat", "coords_kupsat",
"coords_valid" "coords_valid"
) )
list_select_related = ("id_user_add", )
prefetch_related = ("mirrors", ) prefetch_related = ("mirrors", )
@@ -369,6 +395,26 @@ def show_on_map(modeladmin, request, queryset):
show_on_map.short_description = "Показать выбранные на карте" show_on_map.short_description = "Показать выбранные на карте"
def show_selected_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('show_selected_objects_map') + f'?ids={ids_str}')
show_selected_on_map.short_description = "Показать выбранные объекты на карте"
show_selected_on_map.icon = 'map'
class ParameterObjItemInline(admin.StackedInline):
model = ObjItem.parameters_obj.through
extra = 0
max_num = 1
verbose_name = "ВЧ загрузка"
verbose_name_plural = "ВЧ загрузки"
@admin.register(ObjItem) @admin.register(ObjItem)
class ObjectAdmin(admin.ModelAdmin): class ObjectAdmin(admin.ModelAdmin):
list_display = ( list_display = (
@@ -386,120 +432,159 @@ class ObjectAdmin(admin.ModelAdmin):
"distance_geo_kup", "distance_geo_kup",
"distance_geo_valid", "distance_geo_valid",
"distance_kup_valid", "distance_kup_valid",
"created_at",
"updated_at",
) )
list_display_links = ("name",) list_display_links = ("name",)
list_filter = ( list_filter = (
UniqueToggleFilter, UniqueToggleFilter,
("id_vch_load__id_satellite", MultiSelectRelatedDropdownFilter), ("parameters_obj__id_satellite", MultiSelectRelatedDropdownFilter),
("id_vch_load__frequency", NumericRangeFilterBuilder()), ("parameters_obj__frequency", NumericRangeFilterBuilder()),
("id_vch_load__freq_range", NumericRangeFilterBuilder()), ("parameters_obj__freq_range", NumericRangeFilterBuilder()),
("id_vch_load__snr", NumericRangeFilterBuilder()), ("parameters_obj__snr", NumericRangeFilterBuilder()),
("id_vch_load__modulation", MultiSelectRelatedDropdownFilter), ("parameters_obj__modulation", MultiSelectRelatedDropdownFilter),
("id_vch_load__polarization", MultiSelectRelatedDropdownFilter), ("parameters_obj__polarization", MultiSelectRelatedDropdownFilter),
GeoKupDistanceFilter, GeoKupDistanceFilter,
GeoValidDistanceFilter GeoValidDistanceFilter
) )
search_fields = ( search_fields = (
"name", "name",
# "id_geo", "geo_obj__coords",
# "id_satellite__name", "parameters_obj__frequency",
# "id_vch_load__frequency",
) )
ordering = ("name",) ordering = ("name",)
list_select_related = ( inlines = [ParameterObjItemInline, GeoInline]
# "id_satellite", actions = [show_on_map, show_selected_on_map]
"id_vch_load", readonly_fields = ('created_at', 'created_by', 'updated_at', 'updated_by')
"id_vch_load__polarization",
"id_vch_load__modulation", def get_queryset(self, request):
"id_vch_load__id_satellite", qs = super().get_queryset(request)
"id_geo", return qs.select_related('geo_obj', 'created_by', 'updated_by').prefetch_related(
'parameters_obj__id_satellite',
'parameters_obj__polarization',
'parameters_obj__modulation',
'parameters_obj__standard'
) )
autocomplete_fields = ("id_geo",)
raw_id_fields = ("id_vch_load",) def get_readonly_fields(self, request, obj=None):
# dynamic_raw_id_fields = ("id_vch_load",) return self.readonly_fields
actions = [show_on_map]
def save_model(self, request, obj, form, change):
if not change:
if not obj.created_by_id:
obj.created_by = request.user.customuser if hasattr(request.user, 'customuser') else None
obj.updated_by = request.user.customuser if hasattr(request.user, 'customuser') else None
super().save_model(request, obj, form, change)
def sat_name(self, obj): def sat_name(self, obj):
return obj.id_vch_load.id_satellite param = next(iter(obj.parameters_obj.all()), None)
if param and param.id_satellite:
return param.id_satellite.name
return "-"
sat_name.short_description = "Спутник" sat_name.short_description = "Спутник"
sat_name.admin_order_field = "parameters_obj__id_satellite__name"
def freq(self, obj): def freq(self, obj):
par = obj.id_vch_load # param = obj.parameters_obj.first()
return par.frequency param = next(iter(obj.parameters_obj.all()), None)
if param:
return param.frequency
return "-"
freq.short_description = "Частота, МГц" freq.short_description = "Частота, МГц"
freq.admin_order_field = "parameters_obj__frequency"
def distance_geo_kup(self, obj): def distance_geo_kup(self, obj):
par = obj.id_geo.distance_coords_kup geo = obj.geo_obj
if par is None: if not geo or geo.distance_coords_kup is None:
return "-" return "-"
return round(par, 3) return round(geo.distance_coords_kup, 3)
distance_geo_kup.short_description = "Гео-куб, км" distance_geo_kup.short_description = "Гео-куб, км"
def distance_geo_valid(self, obj): def distance_geo_valid(self, obj):
par = obj.id_geo.distance_coords_valid geo = obj.geo_obj
if par is None: if not geo or geo.distance_coords_valid is None:
return "-" return "-"
return round(par, 3) return round(geo.distance_coords_valid, 3)
distance_geo_valid.short_description = "Гео-опер, км" distance_geo_valid.short_description = "Гео-опер, км"
def distance_kup_valid(self, obj): def distance_kup_valid(self, obj):
par = obj.id_geo.distance_kup_valid geo = obj.geo_obj
if par is None: if not geo or geo.distance_kup_valid is None:
return "-" return "-"
return round(par, 3) return round(geo.distance_kup_valid, 3)
distance_kup_valid.short_description = "Куб-опер, км" distance_kup_valid.short_description = "Куб-опер, км"
def pol(self, obj): def pol(self, obj):
par = obj.id_vch_load.polarization # Get the first parameter associated with this objitem to display polarization
return par.name param = next(iter(obj.parameters_obj.all()), None)
if param and param.polarization:
return param.polarization.name
return "-"
pol.short_description = "Поляризация" pol.short_description = "Поляризация"
def freq_range(self, obj): def freq_range(self, obj):
par = obj.id_vch_load # Get the first parameter associated with this objitem to display freq_range
return par.freq_range param = next(iter(obj.parameters_obj.all()), None)
if param:
return param.freq_range
return "-"
freq_range.short_description = "Полоса, МГц" freq_range.short_description = "Полоса, МГц"
freq_range.admin_order_field = "parameters_obj__freq_range"
def bod_velocity(self, obj): def bod_velocity(self, obj):
par = obj.id_vch_load # Get the first parameter associated with this objitem to display bod_velocity
return par.bod_velocity param = next(iter(obj.parameters_obj.all()), None)
if param:
return param.bod_velocity
return "-"
bod_velocity.short_description = "Сим. v, БОД" bod_velocity.short_description = "Сим. v, БОД"
def modulation(self, obj): def modulation(self, obj):
par = obj.id_vch_load.modulation # Get the first parameter associated with this objitem to display modulation
return par.name param = next(iter(obj.parameters_obj.all()), None)
if param and param.modulation:
return param.modulation.name
return "-"
modulation.short_description = "Модуляция" modulation.short_description = "Модуляция"
def snr(self, obj): def snr(self, obj):
par = obj.id_vch_load # Get the first parameter associated with this objitem to display snr
return par.snr param = next(iter(obj.parameters_obj.all()), None)
if param:
return param.snr
return "-"
snr.short_description = "ОСШ" snr.short_description = "ОСШ"
def geo_coords(self, obj): def geo_coords(self, obj):
geo = obj.id_geo geo = obj.geo_obj
if not geo or not geo.coords:
return "-"
longitude = geo.coords.coords[0] longitude = geo.coords.coords[0]
latitude = geo.coords.coords[1] latitude = geo.coords.coords[1]
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W" lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S" lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
return f"{lat} {lon}" return f"{lat} {lon}"
geo_coords.short_description = "Координаты геолокации" geo_coords.short_description = "Координаты геолокации"
geo_coords.admin_order_field = "geo_obj__coords"
def kupsat_coords(self, obj): def kupsat_coords(self, obj):
obj = obj.id_geo geo = obj.geo_obj
if obj.coords_kupsat is None: if not geo or not geo.coords_kupsat:
return "-" return "-"
longitude = obj.coords_kupsat.coords[0] longitude = geo.coords_kupsat.coords[0]
latitude = obj.coords_kupsat.coords[1] latitude = geo.coords_kupsat.coords[1]
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W" lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S" lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
return f"{lat} {lon}" return f"{lat} {lon}"
kupsat_coords.short_description = "Координаты Кубсата" kupsat_coords.short_description = "Координаты Кубсата"
def valid_coords(self, obj): def valid_coords(self, obj):
obj = obj.id_geo geo = obj.geo_obj
if obj.coords_valid is None: if not geo or not geo.coords_valid:
return "-" return "-"
longitude = obj.coords_valid.coords[0] longitude = geo.coords_valid.coords[0]
latitude = obj.coords_valid.coords[1] latitude = geo.coords_valid.coords[1]
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W" lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S" lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
return f"{lat} {lon}" return f"{lat} {lon}"

View File

@@ -4,3 +4,6 @@ from django.apps import AppConfig
class MainappConfig(AppConfig): class MainappConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'
name = 'mainapp' name = 'mainapp'
def ready(self):
import mainapp.signals # noqa

View File

@@ -1,5 +1,14 @@
from django import forms from django import forms
from .models import Satellite from .models import Satellite, Polarization, ObjItem, Parameter, Geo, Modulation, Standard
class UploadFileForm(forms.Form):
file = forms.FileField(
label="Выберите файл",
widget=forms.FileInput(attrs={
'class': 'form-file-input'
})
)
class LoadExcelData(forms.Form): class LoadExcelData(forms.Form):
file = forms.FileField( file = forms.FileField(
@@ -33,7 +42,7 @@ class LoadCsvData(forms.Form):
}) })
) )
class UploadFileForm(forms.Form): class UploadVchLoad(UploadFileForm):
sat_choice = forms.ModelChoiceField( sat_choice = forms.ModelChoiceField(
queryset=Satellite.objects.all(), queryset=Satellite.objects.all(),
label="Выберите спутник", label="Выберите спутник",
@@ -41,12 +50,7 @@ class UploadFileForm(forms.Form):
'class': 'form-select' 'class': 'form-select'
}) })
) )
file = forms.FileField(
label="Выберите текстовый файл",
widget=forms.FileInput(attrs={
'class': 'form-file-input'
})
)
class VchLinkForm(forms.Form): class VchLinkForm(forms.Form):
sat_choice = forms.ModelChoiceField( sat_choice = forms.ModelChoiceField(
@@ -56,12 +60,12 @@ class VchLinkForm(forms.Form):
'class': 'form-select' 'class': 'form-select'
}) })
) )
ku_range = forms.ChoiceField( # ku_range = forms.ChoiceField(
choices=[(9750.0, '9750'), (10750.0, '10750')], # choices=[(9750.0, '9750'), (10750.0, '10750')],
# coerce=lambda x: x == 'True', # # coerce=lambda x: x == 'True',
widget=forms.Select(attrs={'class': 'form-select'}), # widget=forms.Select(attrs={'class': 'form-select'}),
label='Выбор диапазона' # label='Выбор диапазона'
) # )
value1 = forms.FloatField( value1 = forms.FloatField(
label="Первое число", label="Первое число",
widget=forms.NumberInput(attrs={ widget=forms.NumberInput(attrs={
@@ -76,3 +80,69 @@ class VchLinkForm(forms.Form):
'placeholder': 'Введите второе число' 'placeholder': 'Введите второе число'
}) })
) )
class NewEventForm(forms.Form):
# sat_choice = forms.ModelChoiceField(
# queryset=Satellite.objects.all(),
# label="Выберите спутник",
# widget=forms.Select(attrs={
# 'class': 'form-select'
# })
# )
# pol_choice = forms.ModelChoiceField(
# queryset=Polarization.objects.all(),
# label="Выберите поляризацию",
# widget=forms.Select(attrs={
# 'class': 'form-select'
# })
# )
file = forms.FileField(
label="Выберите файл",
widget=forms.FileInput(attrs={
'class': 'form-control',
'accept': '.xlsx,.xls'
})
)
class ParameterForm(forms.ModelForm):
class Meta:
model = Parameter
fields = [
'id_satellite', 'frequency', 'freq_range', 'polarization',
'bod_velocity', 'modulation', 'snr', 'standard'
]
widgets = {
'id_satellite': forms.Select(attrs={'class': 'form-select'}, choices=[]),
'frequency': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}),
'freq_range': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}),
'bod_velocity': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}),
'snr': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}),
'polarization': forms.Select(attrs={'class': 'form-select'}, choices=[]),
'modulation': forms.Select(attrs={'class': 'form-select'}, choices=[]),
'standard': forms.Select(attrs={'class': 'form-select'}, choices=[]),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['id_satellite'].choices = [(s.id, s.name) for s in Satellite.objects.all()]
self.fields['polarization'].choices = [(p.id, p.name) for p in Polarization.objects.all()]
self.fields['modulation'].choices = [(m.id, m.name) for m in Modulation.objects.all()]
self.fields['standard'].choices = [(s.id, s.name) for s in Standard.objects.all()]
class GeoForm(forms.ModelForm):
class Meta:
model = Geo
fields = ['location', 'comment', 'is_average']
widgets = {
'location': forms.TextInput(attrs={'class': 'form-control'}),
'comment': forms.TextInput(attrs={'class': 'form-control'}),
'is_average': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
}
class ObjItemForm(forms.ModelForm):
class Meta:
model = ObjItem
fields = ['name']
widgets = {
'name': forms.TextInput(attrs={'class': 'form-control'}),
}

View File

@@ -0,0 +1,20 @@
from django.core.management.base import BaseCommand
from django.contrib.auth.models import User
from mainapp.models import CustomUser
class Command(BaseCommand):
help = 'Create CustomUser profiles for existing users who do not have them'
def handle(self, *args, **options):
# Find all users who don't have a CustomUser profile
for user in User.objects.all():
if not hasattr(user, 'customuser'):
custom_user = CustomUser.objects.create(user=user)
self.stdout.write(
self.style.SUCCESS(f'Created CustomUser for {user.username}')
)
self.stdout.write(
self.style.SUCCESS('Successfully ensured all users have CustomUser profiles')
)

View File

@@ -1,7 +1,9 @@
# Generated by Django 5.2.7 on 2025-10-13 12:47 # Generated by Django 5.2.7 on 2025-10-31 13:36
import django.contrib.gis.db.models.fields import django.contrib.gis.db.models.fields
import django.contrib.gis.db.models.functions
import django.db.models.deletion import django.db.models.deletion
import django.db.models.expressions
import mainapp.models import mainapp.models
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
@@ -31,7 +33,7 @@ class Migration(migrations.Migration):
name='Modulation', name='Modulation',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=20, unique=True, verbose_name='Модуляция')), ('name', models.CharField(db_index=True, max_length=20, unique=True, verbose_name='Модуляция')),
], ],
options={ options={
'verbose_name': 'Модуляция', 'verbose_name': 'Модуляция',
@@ -53,7 +55,7 @@ class Migration(migrations.Migration):
name='Satellite', name='Satellite',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=30, unique=True, verbose_name='Имя спутника')), ('name', models.CharField(db_index=True, max_length=100, unique=True, verbose_name='Имя спутника')),
('norad', models.IntegerField(blank=True, null=True, verbose_name='NORAD ID')), ('norad', models.IntegerField(blank=True, null=True, verbose_name='NORAD ID')),
], ],
options={ options={
@@ -61,6 +63,18 @@ class Migration(migrations.Migration):
'verbose_name_plural': 'Спутники', 'verbose_name_plural': 'Спутники',
}, },
), ),
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.CreateModel( migrations.CreateModel(
name='Standard', name='Standard',
fields=[ fields=[
@@ -85,35 +99,30 @@ class Migration(migrations.Migration):
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='Geo', name='ObjItem',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('timestamp', models.DateTimeField(blank=True, null=True, verbose_name='Время')), ('name', models.CharField(blank=True, db_index=True, max_length=100, null=True, verbose_name='Имя объекта')),
('coords', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326, 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='Пользователь')),
('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={ options={
'verbose_name': 'Гео', 'verbose_name': 'Объект',
'verbose_name_plural': 'Гео', 'verbose_name_plural': 'Объекты',
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='Parameter', name='Parameter',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('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='Частота, МГц')), ('frequency', models.FloatField(blank=True, db_index=True, default=0, null=True, verbose_name='Частота, МГц')),
('freq_range', 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='Символьная скорость, БОД')), ('bod_velocity', models.FloatField(blank=True, default=0, null=True, verbose_name='Символьная скорость, БОД')),
('snr', 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='Пользователь')), ('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='Модуляция')), ('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='Модуляция')),
('objitems', models.ManyToManyField(blank=True, related_name='parameters_obj', to='mainapp.objitem', 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='Поляризация')), ('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='Поляризация')),
('id_satellite', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='parameters', to='mainapp.satellite', 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='Стандарт')), ('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={ options={
@@ -122,26 +131,74 @@ class Migration(migrations.Migration):
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='ObjItem', name='SourceType',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('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='Имя объекта')), ('name', models.CharField(max_length=50, unique=True, verbose_name='Тип источника')),
('id_geo', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='objitems', to='mainapp.geo', verbose_name='Геоданные')), ('objitem', models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='source_type_obj', to='mainapp.objitem', 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={ options={
'verbose_name': 'Объект', 'verbose_name': 'Тип источника',
'verbose_name_plural': 'Объекты', 'verbose_name_plural': 'Типы источников',
}, },
), ),
migrations.AddConstraint( migrations.CreateModel(
model_name='geo', name='SigmaParameter',
constraint=models.UniqueConstraint(fields=('timestamp', 'coords'), name='unique_geo_combination'), fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('transfer', models.FloatField(choices=[(-1.0, '-'), (9750.0, '9750 МГц'), (10750.0, '10750 МГц')], default=-1.0, verbose_name='Перенос по частоте')),
('status', models.CharField(blank=True, max_length=20, null=True, verbose_name='Статус')),
('frequency', models.FloatField(blank=True, db_index=True, default=0, null=True, verbose_name='Частота, МГц')),
('transfer_frequency', models.GeneratedField(db_persist=True, expression=models.ExpressionWrapper(django.db.models.expressions.CombinedExpression(models.F('frequency'), '+', models.F('transfer')), output_field=models.FloatField()), null=True, output_field=models.FloatField(), verbose_name='Частота в Ku, МГц')),
('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='Время окончания измерения')),
('id_satellite', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='sigmapar_sat', to='mainapp.satellite', 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='Модуляция')),
('parameter', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sigma_parameter', to='mainapp.parameter', 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_sigma', to='mainapp.polarization', verbose_name='Поляризация')),
('mark', models.ManyToManyField(blank=True, to='mainapp.sigmaparmark', 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',
},
), ),
migrations.AddConstraint( migrations.CreateModel(
model_name='objitem', name='Geo',
constraint=models.UniqueConstraint(fields=('id_vch_load', 'id_geo'), name='unique_objitem_combination'), fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('timestamp', models.DateTimeField(blank=True, db_index=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='Усреднённое')),
('distance_coords_kup', 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='Расстояние между купсатом и гео, км')),
('distance_coords_valid', 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='Расстояние между гео и оперативным отделом, км')),
('distance_kup_valid', 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='Расстояние между купсатом и оперативным отделом, км')),
('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='Зеркала')),
('objitem', models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='geo_obj', to='mainapp.objitem', verbose_name='Гео')),
],
options={
'verbose_name': 'Гео',
'verbose_name_plural': 'Гео',
'constraints': [models.UniqueConstraint(fields=('timestamp', 'coords'), name='unique_geo_combination')],
},
),
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

@@ -1,20 +0,0 @@
# 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,35 @@
# Generated by Django 5.2.7 on 2025-10-31 13:56
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='objitem',
name='created_at',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Дата создания'),
),
migrations.AddField(
model_name='objitem',
name='created_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_created', to='mainapp.customuser', verbose_name='Создан пользователем'),
),
migrations.AddField(
model_name='objitem',
name='updated_at',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Дата последнего изменения'),
),
migrations.AddField(
model_name='objitem',
name='updated_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems_updated', to='mainapp.customuser', verbose_name='Изменен пользователем'),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.2.7 on 2025-10-31 14:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0002_objitem_created_at_objitem_created_by_and_more'),
]
operations = [
migrations.AlterField(
model_name='objitem',
name='created_at',
field=models.DateTimeField(auto_now_add=True, verbose_name='Дата создания'),
),
migrations.AlterField(
model_name='objitem',
name='updated_at',
field=models.DateTimeField(auto_now=True, verbose_name='Дата последнего изменения'),
),
]

View File

@@ -1,30 +0,0 @@
# 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,25 @@
# Generated by Django 5.2.7 on 2025-11-01 07:38
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0003_alter_objitem_created_at_alter_objitem_updated_at'),
]
operations = [
migrations.RemoveField(
model_name='geo',
name='id_user_add',
),
migrations.RemoveField(
model_name='objitem',
name='id_user_add',
),
migrations.RemoveField(
model_name='parameter',
name='id_user_add',
),
]

View File

@@ -1,36 +0,0 @@
# 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

@@ -1,20 +0,0 @@
# 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

@@ -1,23 +0,0 @@
# 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

@@ -1,19 +0,0 @@
# 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

@@ -1,38 +0,0 @@
# 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

@@ -1,19 +0,0 @@
# 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

@@ -1,31 +0,0 @@
# 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

@@ -1,22 +0,0 @@
# 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

@@ -1,18 +0,0 @@
# 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

@@ -1,21 +0,0 @@
# 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

@@ -1,18 +0,0 @@
# 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

@@ -1,19 +0,0 @@
# 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

@@ -1,23 +0,0 @@
# 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

@@ -1,19 +0,0 @@
# 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

@@ -2,6 +2,8 @@ from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.gis.db import models as gis from django.contrib.gis.db import models as gis
from django.contrib.gis.db.models import functions from django.contrib.gis.db.models import functions
from django.db.models import F, ExpressionWrapper
from django.utils import timezone
def get_default_polarization(): def get_default_polarization():
obj, created = Polarization.objects.get_or_create( obj, created = Polarization.objects.get_or_create(
@@ -96,7 +98,7 @@ class Standard(models.Model):
class Satellite(models.Model): class Satellite(models.Model):
name = models.CharField(max_length=30, unique=True, verbose_name="Имя спутника", db_index=True) name = models.CharField(max_length=100, unique=True, verbose_name="Имя спутника", db_index=True)
norad = models.IntegerField(blank=True, null=True, verbose_name="NORAD ID") norad = models.IntegerField(blank=True, null=True, verbose_name="NORAD ID")
def __str__(self): def __str__(self):
@@ -107,6 +109,47 @@ class Satellite(models.Model):
verbose_name_plural = "Спутники" verbose_name_plural = "Спутники"
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)
# id_source_type = models.ForeignKey(SourceType, on_delete=models.SET_NULL, related_name="objitems", verbose_name='Тип источника', null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
created_by = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, related_name="objitems_created",
null=True, blank=True, verbose_name="Создан пользователем")
updated_at = models.DateTimeField(auto_now=True, verbose_name="Дата последнего изменения")
updated_by = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, related_name="objitems_updated",
null=True, blank=True, verbose_name="Изменен пользователем")
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'
# )
# ]
class SourceType(models.Model):
name = models.CharField(max_length=50, unique=True, verbose_name="Тип источника")
objitem = models.OneToOneField(ObjItem, on_delete=models.SET_NULL, verbose_name="Гео", related_name="source_type_obj", null=True)
def __str__(self):
return self.name
class Meta:
verbose_name = "Тип источника"
verbose_name_plural = 'Типы источников'
class Parameter(models.Model): class Parameter(models.Model):
id_satellite = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="parameters", verbose_name="Спутник", null=True) id_satellite = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="parameters", verbose_name="Спутник", null=True)
polarization = models.ForeignKey( polarization = models.ForeignKey(
@@ -122,7 +165,8 @@ class Parameter(models.Model):
standard = models.ForeignKey( standard = models.ForeignKey(
Standard, default=get_default_standard, on_delete=models.SET_DEFAULT, related_name="standards", null=True, blank=True, verbose_name="Стандарт" 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_user_add = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, related_name="parameter_added", verbose_name="Пользователь", null=True, blank=True)
objitems = models.ManyToManyField(ObjItem, related_name="parameters_obj", verbose_name="Источники", 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, 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) # id_sigma_parameter = models.ManyToManyField(SigmaParameter, verbose_name="ВЧ с sigma", null=True, blank=True)
@@ -151,12 +195,35 @@ class Parameter(models.Model):
class SigmaParameter(models.Model): class SigmaParameter(models.Model):
TRANSFERS = [
(-1.0, "-"),
(9750.0, "9750 МГц"),
(10750.0, "10750 МГц")
]
id_satellite = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="sigmapar_sat", verbose_name="Спутник") id_satellite = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="sigmapar_sat", verbose_name="Спутник")
transfer = models.FloatField(
choices=TRANSFERS,
default=-1.0,
verbose_name="Перенос по частоте"
)
status = models.CharField(max_length=20, blank=True, null=True, 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) frequency = models.FloatField(default=0, null=True, blank=True, verbose_name="Частота, МГц", db_index=True)
transfer_frequency = models.GeneratedField(
expression=ExpressionWrapper(
F('frequency') + F('transfer'),
output_field=models.FloatField()
),
output_field=models.FloatField(),
db_persist=True,
null=True, blank=True, verbose_name="Частота в Ku, МГц"
)
freq_range = models.FloatField(default=0, null=True, blank=True, verbose_name="Полоса частот, МГц") freq_range = models.FloatField(default=0, null=True, blank=True, verbose_name="Полоса частот, МГц")
power = 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="Символьная скорость, БОД") bod_velocity = models.FloatField(default=0, null=True, blank=True, verbose_name="Символьная скорость, БОД")
polarization = models.ForeignKey(
Polarization, default=get_default_polarization, on_delete=models.SET_DEFAULT, related_name="polarizations_sigma", null=True, blank=True, verbose_name="Поляризация"
)
modulation = models.ForeignKey( modulation = models.ForeignKey(
Modulation, default=get_default_modulation, on_delete=models.SET_DEFAULT, related_name="modulations_sigma", null=True, blank=True, verbose_name="Модуляция" Modulation, default=get_default_modulation, on_delete=models.SET_DEFAULT, related_name="modulations_sigma", null=True, blank=True, verbose_name="Модуляция"
) )
@@ -194,7 +261,7 @@ class Geo(models.Model):
coords_kupsat = gis.PointField(srid=4326, null=True, 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="Координаты оперативников") coords_valid = gis.PointField(srid=4326, null=True, blank=True, verbose_name="Координаты оперативников")
is_average = models.BooleanField(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) # 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( distance_coords_kup = models.GeneratedField(
expression=functions.Distance("coords", "coords_kupsat")/1000, expression=functions.Distance("coords", "coords_kupsat")/1000,
output_field=models.FloatField(), output_field=models.FloatField(),
@@ -213,6 +280,7 @@ class Geo(models.Model):
db_persist=True, db_persist=True,
null=True, blank=True, verbose_name="Расстояние между купсатом и оперативным отделом, км" null=True, blank=True, verbose_name="Расстояние между купсатом и оперативным отделом, км"
) )
objitem = models.OneToOneField(ObjItem, on_delete=models.CASCADE, verbose_name="Гео", related_name="geo_obj", null=True)
def __str__(self): def __str__(self):
longitude = self.coords.coords[0] longitude = self.coords.coords[0]
@@ -234,23 +302,3 @@ class Geo(models.Model):
) )
] ]
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'
)
]

11
dbapp/mainapp/signals.py Normal file
View File

@@ -0,0 +1,11 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import User
from .models import CustomUser
@receiver(post_save, sender=User)
def create_or_update_user_profile(sender, instance, created, **kwargs):
if created:
CustomUser.objects.create(user=instance)
instance.customuser.save()

View File

@@ -0,0 +1,196 @@
{% 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>
<!-- New Event 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-plus-circle text-success" viewBox="0 0 16 16">
<path d="M8 0a8 8 0 1 0 0 16A8 8 0 0 0 8 0M4.5 7.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5M7.5 4.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 0 1h-1a.5.5 0 0 1-.5-.5m1 3a.5.5 0 0 1 .5.5v3a.5.5 0 0 1-1 0v-3a.5.5 0 0 1 .5-.5"/>
</svg>
</div>
<h3 class="card-title mb-0">Формирование таблицы для Кубсатов</h3>
</div>
<p class="card-text">Добавьте новое событие с помощью выбора спутника и загрузки файла данных.</p>
<a href="{% url 'kubsat_excel' %}" class="btn btn-success">
Добавить событие
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon"> <link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
<title>{% block title %}Геолокация{% endblock %}</title> <title>{% block title %}Геолокация{% endblock %}</title>
<link href="{% static 'bootstrap-icons/bootstrap-icons.css' %}" rel="stylesheet">
<link href="{% static 'bootstrap/bootstrap.min.css' %}" rel="stylesheet"> <link href="{% static 'bootstrap/bootstrap.min.css' %}" rel="stylesheet">
<!-- Дополнительные стили (если нужно) --> <!-- Дополнительные стили (если нужно) -->
@@ -22,9 +22,13 @@
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navbarNav"> <div class="collapse navbar-collapse" id="navbarNav">
{% if user.is_authenticated %}
<ul class="navbar-nav me-auto"> <ul class="navbar-nav me-auto">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'home' %}">Главная</a> <a class="nav-link" href="{% url 'home' %}">Объекты</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'actions' %}">Действия</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url '3dmap' %}">3D карта</a> <a class="nav-link" href="{% url '3dmap' %}">3D карта</a>
@@ -36,14 +40,36 @@
<a class="nav-link" href="{% url 'admin:index' %}">Админ панель</a> <a class="nav-link" href="{% url 'admin:index' %}">Админ панель</a>
</li> </li>
</ul> </ul>
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown">
{% if user.first_name and user.last_name %}
{{ user.first_name }} {{ user.last_name }}
{% elif user.get_full_name %}
{{ user.get_full_name }}
{% else %}
{{ user.username }}
{% endif %}
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="{% url 'logout' %}">Выйти</a></li>
</ul>
</li>
</ul>
{% else %}
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" href="{% url 'login' %}">Войти</a>
</li>
</ul>
{% endif %}
</div> </div>
</div> </div>
</nav> </nav>
<!-- Основной контент --> <!-- Основной контент -->
<main class="container mt-4"> <main class="{% if full_width_page %}container-fluid p-0{% else %}container mt-4{% endif %}">
{% block content %} {% block content %}{% endblock %}
{% endblock %}
</main> </main>
<script src="{% static 'bootstrap/bootstrap.bundle.min.js' %}"></script> <script src="{% static 'bootstrap/bootstrap.bundle.min.js' %}"></script>

View File

@@ -1,176 +1,422 @@
{% extends 'mainapp/base.html' %} {% extends 'mainapp/base.html' %}
{% block title %}Главная{% endblock %} {% block title %}Список объектов{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container-fluid px-3">
<div class="text-center mb-5"> <div class="row mb-3">
<h1 class="display-4 fw-bold">Геолокация</h1> <div class="col-12">
<p class="lead">Управление данными спутников</p> <h2>Список объектов</h2>
</div>
</div> </div>
<!-- Alert messages --> <!-- Toolbar -->
{% if messages %} <div class="row mb-3">
{% for message in messages %} <div class="col-12">
<div class="alert {{ message.tags }} alert-dismissible fade show" role="alert"> <div class="card">
{{ message }} <div class="card-body">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> <div class="d-flex flex-wrap align-items-center gap-3">
<div style="min-width: 300px; flex-grow: 1;">
<label for="toolbar-search" class="form-label mb-0">Поиск:</label>
<input type="text" id="toolbar-search" class="form-control" placeholder="Поиск по имени, местоположению..." value="{{ search_query|default:'' }}">
</div> </div>
<div class="ms-auto">
<button type="button" class="btn btn-outline-primary" onclick="performSearch()">Найти</button>
<button type="button" class="btn btn-outline-secondary" onclick="clearSearch()">Очистить</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row g-3">
<!-- Filters Sidebar - Made narrower -->
<div class="col-md-2">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">Фильтры</h5>
<form method="get" id="filter-form">
<!-- Satellite Selection - Multi-select -->
<div class="mb-2">
<label class="form-label">Спутник:</label>
<div class="d-flex justify-content-between mb-1">
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('satellite_id', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('satellite_id', false)">Снять</button>
</div>
<select name="satellite_id" class="form-select form-select-sm mb-2" multiple size="4">
{% for satellite in satellites %}
<option value="{{ satellite.id }}"
{% if satellite.id in selected_satellites %}selected{% endif %}>
{{ satellite.name }}
</option>
{% endfor %} {% endfor %}
</select>
</div>
<!-- Frequency Filter -->
<div class="mb-2">
<label class="form-label">Частота, МГц:</label>
<input type="number" step="0.001" name="freq_min" class="form-control form-control-sm mb-1" placeholder="От" value="{{ freq_min|default:'' }}">
<input type="number" step="0.001" name="freq_max" class="form-control form-control-sm" placeholder="До" value="{{ freq_max|default:'' }}">
</div>
<!-- Range Filter -->
<div class="mb-2">
<label class="form-label">Полоса, МГц:</label>
<input type="number" step="0.001" name="range_min" class="form-control form-control-sm mb-1" placeholder="От" value="{{ range_min|default:'' }}">
<input type="number" step="0.001" name="range_max" class="form-control form-control-sm" placeholder="До" value="{{ range_max|default:'' }}">
</div>
<!-- SNR Filter -->
<div class="mb-2">
<label class="form-label">ОСШ:</label>
<input type="number" step="0.001" name="snr_min" class="form-control form-control-sm mb-1" placeholder="От" value="{{ snr_min|default:'' }}">
<input type="number" step="0.001" name="snr_max" class="form-control form-control-sm" placeholder="До" value="{{ snr_max|default:'' }}">
</div>
<!-- Symbol Rate Filter -->
<div class="mb-2">
<label class="form-label">Сим. v, БОД:</label>
<input type="number" step="0.001" name="bod_min" class="form-control form-control-sm mb-1" placeholder="От" value="{{ bod_min|default:'' }}">
<input type="number" step="0.001" name="bod_max" class="form-control form-control-sm" placeholder="До" value="{{ bod_max|default:'' }}">
</div>
<!-- Removed old search input as it's now in the toolbar -->
<!-- Modulation Filter -->
<div class="mb-2">
<label class="form-label">Модуляция:</label>
<div class="d-flex justify-content-between mb-1">
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('modulation', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('modulation', false)">Снять</button>
</div>
<select name="modulation" class="form-select form-select-sm mb-2" multiple size="4">
{% for mod in modulations %}
<option value="{{ mod.id }}"
{% if mod.id in selected_modulations %}selected{% endif %}>
{{ mod.name }}
</option>
{% endfor %}
</select>
</div>
<!-- Polarization Filter -->
<div class="mb-2">
<label class="form-label">Поляризация:</label>
<div class="d-flex justify-content-between mb-1">
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('polarization', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('polarization', false)">Снять</button>
</div>
<select name="polarization" class="form-select form-select-sm mb-2" multiple size="4">
{% for pol in polarizations %}
<option value="{{ pol.id }}"
{% if pol.id in selected_polarizations %}selected{% endif %}>
{{ pol.name }}
</option>
{% endfor %}
</select>
</div>
<!-- Kubsat Coordinates Filter -->
<div class="mb-2">
<label class="form-label">Координаты Кубсата:</label>
<div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_kupsat" id="has_kupsat_1" value="1"
{% if has_kupsat == '1' %}checked{% endif %}>
<label class="form-check-label" for="has_kupsat_1">Есть</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_kupsat" id="has_kupsat_0" value="0"
{% if has_kupsat == '0' %}checked{% endif %}>
<label class="form-check-label" for="has_kupsat_0">Нет</label>
</div>
</div>
</div>
<!-- Valid Coordinates Filter -->
<div class="mb-2">
<label class="form-label">Координаты опер. отдела:</label>
<div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_valid" id="has_valid_1" value="1"
{% if has_valid == '1' %}checked{% endif %}>
<label class="form-check-label" for="has_valid_1">Есть</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_valid" id="has_valid_0" value="0"
{% if has_valid == '0' %}checked{% endif %}>
<label class="form-check-label" for="has_valid_0">Нет</label>
</div>
</div>
</div>
<!-- Items Per Page -->
<div class="mb-2">
<label for="items-per-page" class="form-label">Элементов:</label>
<select name="items_per_page" id="items-per-page" class="form-select form-select-sm" onchange="document.getElementById('filter-form').submit();">
{% for option in available_items_per_page %}
<option value="{{ option }}" {% if option == items_per_page %}selected{% endif %}>
{{ option }}
</option>
{% endfor %}
</select>
</div>
<!-- Apply Filters and Reset Buttons -->
<div class="d-grid gap-2 mt-2">
<button type="submit" class="btn btn-primary btn-sm">Применить</button>
<a href="?" class="btn btn-secondary btn-sm">Сбросить</a>
</div>
</form>
</div>
</div>
</div>
<!-- Main Table -->
<div class="col-md-10">
<div class="card h-100">
<div class="card-body p-0">
<div class="table-responsive" style="max-height: 75vh; overflow-y: auto;">
<table class="table table-striped table-hover table-sm" style="font-size: 0.85rem;">
<thead class="table-dark sticky-top">
<tr>
<th scope="col" class="text-center" style="width: 3%;">
<input type="checkbox" id="select-all" class="form-check-input">
</th>
<th scope="col">Имя</th>
<th scope="col">Спутник</th>
<th scope="col">Част, МГц</th>
<th scope="col">Полоса, МГц</th>
<th scope="col">Поляр</th>
<th scope="col">Сим. v</th>
<th scope="col">Модул</th>
<th scope="col">ОСШ</th>
<th scope="col">Геолокация</th>
<th scope="col">Кубсат</th>
<th scope="col">Опер. отд</th>
<th scope="col">Гео-куб, км</th>
<th scope="col">Гео-опер, км</th>
<th scope="col">Куб-опер, км</th>
</tr>
</thead>
<tbody>
{% for item in processed_objects %}
<tr>
<td class="text-center">
<input type="checkbox" class="form-check-input item-checkbox" value="{{ item.id }}">
</td>
<td>{{ item.name }}</td>
<td>{{ item.satellite_name }}</td>
<td>{{ item.frequency }}</td>
<td>{{ item.freq_range }}</td>
<td>{{ item.polarization }}</td>
<td>{{ item.bod_velocity }}</td>
<td>{{ item.modulation }}</td>
<td>{{ item.snr }}</td>
<td>{{ item.geo_coords }}</td>
<td>{{ item.kupsat_coords }}</td>
<td>{{ item.valid_coords }}</td>
<td>{{ item.distance_geo_kup }}</td>
<td>{{ item.distance_geo_valid }}</td>
<td>{{ item.distance_kup_valid }}</td>
</tr>
{% empty %}
<tr>
<td colspan="15" class="text-center py-4">
{% if selected_satellite_id %}
Нет данных для выбранных фильтров
{% else %}
Пожалуйста, выберите спутник для отображения данных
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% if page_obj.paginator.num_pages > 1 %}
<nav aria-label="Page navigation" class="px-3 pb-3">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page=1">Первая</a>
</li>
<li class="page-item">
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.previous_page_number }}">Предыдущая</a>
</li>
{% endif %} {% endif %}
<!-- Main feature cards --> {% for num in page_obj.paginator.page_range %}
<div class="row g-4"> {% if page_obj.number == num %}
<!-- Excel Data Upload Card --> <li class="page-item active">
<div class="col-lg-6"> <span class="page-link">{{ num }}</span>
<div class="card h-100 shadow-sm border-0"> </li>
<div class="card-body"> {% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<div class="d-flex align-items-center mb-3"> <li class="page-item">
<div class="bg-primary bg-opacity-10 rounded-circle p-2 me-3"> <a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ num }}">{{ num }}</a>
<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"> </li>
<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"/> {% endif %}
<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"/> {% endfor %}
</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 --> {% if page_obj.has_next %}
<div class="col-lg-6"> <li class="page-item">
<div class="card h-100 shadow-sm border-0"> <a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.next_page_number }}">Следующая</a>
<div class="card-body"> </li>
<div class="d-flex align-items-center mb-3"> <li class="page-item">
<div class="bg-success bg-opacity-10 rounded-circle p-2 me-3"> <a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.paginator.num_pages }}">Последняя</a>
<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"> </li>
<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"/> {% endif %}
<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"/> </ul>
</svg> </nav>
</div> {% endif %}
<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 --> <!-- Pagination Info -->
<div class="col-lg-6"> {% if page_obj %}
<div class="card h-100 shadow-sm border-0"> <div class="px-3 pb-3 d-flex justify-content-between align-items-center">
<div class="card-body"> <div>Показано {{ page_obj.start_index }}-{{ page_obj.end_index }} из {{ page_obj.paginator.count }} записей</div>
<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> </div>
<h3 class="card-title mb-0">Добавление списка спутников</h3> {% endif %}
</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> </div>
</div> </div>
</div> </div>
<!-- JavaScript for checkbox functionality and filters -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Select/Deselect all checkboxes
const selectAllCheckbox = document.getElementById('select-all');
const itemCheckboxes = document.querySelectorAll('.item-checkbox');
if (selectAllCheckbox && itemCheckboxes.length > 0) {
selectAllCheckbox.addEventListener('change', function() {
itemCheckboxes.forEach(checkbox => {
checkbox.checked = selectAllCheckbox.checked;
});
});
// Update select all checkbox state based on individual selections
itemCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', function() {
const allChecked = Array.from(itemCheckboxes).every(cb => cb.checked);
selectAllCheckbox.checked = allChecked;
});
});
}
// Handle multiple selection for modulations and polarizations
const modulationSelect = document.querySelector('select[name="modulation"]');
const polarizationSelect = document.querySelector('select[name="polarization"]');
// Prevent deselecting all options when Ctrl+click is used
if (modulationSelect) {
modulationSelect.addEventListener('change', function(e) {
document.getElementById('filter-form').submit();
});
}
if (polarizationSelect) {
polarizationSelect.addEventListener('change', function(e) {
document.getElementById('filter-form').submit();
});
}
// Handle kubsat and valid coords checkboxes (mutually exclusive)
// Add a function to handle radio-like behavior for these checkboxes
function setupRadioLikeCheckboxes(name) {
const checkboxes = document.querySelectorAll(`input[name="${name}"]`);
checkboxes.forEach(checkbox => {
checkbox.addEventListener('change', function() {
// If this checkbox is checked, uncheck the other
if (this.checked) {
checkboxes.forEach(other => {
if (other !== this) {
other.checked = false;
}
});
} else {
// If both are unchecked, no action needed
}
document.getElementById('filter-form').submit();
});
});
}
setupRadioLikeCheckboxes('has_kupsat');
setupRadioLikeCheckboxes('has_valid');
// Function to select/deselect all options in a select element
window.selectAllOptions = function(selectName, selectAll) {
const selectElement = document.querySelector(`select[name="${selectName}"]`);
if (selectElement) {
for (let i = 0; i < selectElement.options.length; i++) {
selectElement.options[i].selected = selectAll;
}
document.getElementById('filter-form').submit();
}
};
// Function to update the page when satellite selection changes
function updateSatelliteSelection() {
document.getElementById('filter-form').submit();
}
// Get all current filter values and return as URL parameters
function getAllFilterParams() {
const form = document.getElementById('filter-form');
const searchValue = document.getElementById('toolbar-search').value;
// Create URLSearchParams object from the form
const params = new URLSearchParams(new FormData(form));
// Add search value from toolbar if present
if (searchValue.trim() !== '') {
params.set('search', searchValue);
} else {
// Remove search parameter if empty
params.delete('search');
}
return params.toString();
}
// Function to perform search
window.performSearch = function() {
const filterParams = getAllFilterParams();
window.location.search = filterParams;
};
// Function to clear search
window.clearSearch = function() {
// Clear only the search input in the toolbar
document.getElementById('toolbar-search').value = '';
// Submit the form to update the results
const filterParams = getAllFilterParams();
window.location.search = filterParams;
};
// Handle Enter key in toolbar search
const toolbarSearch = document.getElementById('toolbar-search');
if (toolbarSearch) {
toolbarSearch.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
performSearch();
}
});
}
// Add event listener to satellite select for immediate update
const satelliteSelect = document.querySelector('select[name="satellite_id"]');
if (satelliteSelect) {
satelliteSelect.addEventListener('change', function() {
updateSatelliteSelection();
});
}
});
</script>
{% endblock %} {% endblock %}

View File

@@ -20,7 +20,7 @@
{% endfor %} {% endfor %}
{% endif %} {% endif %}
<p class="card-text">Введите допустимый разброс для частоты и полосы(в кГц)</p> <p class="card-text">Введите допустимый разброс для частоты и полосы</p>
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
@@ -31,15 +31,15 @@
<div class="text-danger mt-1">{{ form.sat_choice.errors }}</div> <div class="text-danger mt-1">{{ form.sat_choice.errors }}</div>
{% endif %} {% endif %}
</div> </div>
<div class="mb-3"> {% comment %} <div class="mb-3">
<label for="{{ form.ku_range.id_for_label }}" class="form-label">Выберите перенос по частоте(МГц):</label> <label for="{{ form.ku_range.id_for_label }}" class="form-label">Выберите перенос по частоте(МГц):</label>
{{ form.ku_range }} {{ form.ku_range }}
{% if form.ku_range.errors %} {% if form.ku_range.errors %}
<div class="text-danger mt-1">{{ form.ku_range.errors }}</div> <div class="text-danger mt-1">{{ form.ku_range.errors }}</div>
{% endif %} {% endif %}
</div> </div> {% endcomment %}
<div class="mb-3"> <div class="mb-3">
<label for="{{ form.value1.id_for_label }}" class="form-label">Разброс по частоте(в %)</label> <label for="{{ form.value1.id_for_label }}" class="form-label">Разброс по частоте(в МГц)</label>
{{ form.value1 }} {{ form.value1 }}
{% if form.value1.errors %} {% if form.value1.errors %}
<div class="text-danger mt-1">{{ form.value1.errors }}</div> <div class="text-danger mt-1">{{ form.value1.errors }}</div>

View File

@@ -0,0 +1,19 @@
{% extends 'mainapp/base.html' %}
{% block title %}Войдите в систему{% endblock %}
{% block content %}
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-body text-center">
<h2 class="card-title">Требуется авторизация</h2>
<p class="card-text">Для просмотра содержимого сайта необходимо войти в систему.</p>
<a href="{% url 'login' %}" class="btn btn-primary">Войти</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,25 @@
{% extends 'mainapp/base.html' %}
{% block title %}Удалить объект{% endblock %}
{% block content %}
<div class="container-fluid px-3">
<div class="row mb-3">
<div class="col-12">
<h2>Удалить объект "{{ object }}"?</h2>
</div>
</div>
<div class="card">
<div class="card-body">
<form method="post">
{% csrf_token %}
<p>Вы уверены, что хотите удалить этот объект? Это действие нельзя будет отменить.</p>
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-danger">Удалить</button>
<a href="{% url 'home' %}" class="btn btn-secondary ms-2">Отмена</a>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,688 @@
{% extends 'mainapp/base.html' %}
{% load static %}
{% load static leaflet_tags %}
{% block title %}{% if object %}Редактировать объект: {{ object.name }}{% else %}Создать объект{% endif %}{% endblock %}
{% block extra_css %}
<style>
.form-section { margin-bottom: 2rem; border: 1px solid #dee2e6; border-radius: 0.25rem; padding: 1rem; }
.form-section-header { border-bottom: 1px solid #dee2e6; padding-bottom: 0.5rem; margin-bottom: 1rem; }
.btn-action { margin-right: 0.5rem; }
.dynamic-form { border: 1px dashed #ced4da; padding: 1rem; margin-top: 1rem; border-radius: 0.25rem; }
.dynamic-form-header { display: flex; justify-content: space-between; align-items: center; }
.readonly-field { background-color: #f8f9fa; padding: 0.375rem 0.75rem; border: 1px solid #ced4da; border-radius: 0.25rem; }
.coord-group { border: 1px solid #dee2e6; padding: 0.75rem; border-radius: 0.25rem; margin-bottom: 1rem; }
.coord-group-header { font-weight: bold; margin-bottom: 0.5rem; }
.form-check-input { margin-top: 0.25rem; }
.datetime-group { display: flex; gap: 1rem; }
.datetime-group > div { flex: 1; }
#map { height: 500px; width: 100%; margin-bottom: 1rem; }
.map-container { margin-bottom: 1rem; }
.coord-sync-group { border: 1px solid #dee2e6; padding: 0.75rem; border-radius: 0.25rem; }
.map-controls {
display: flex;
gap: 10px;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.map-control-btn {
padding: 0.375rem 0.75rem;
border: 1px solid #ced4da;
background-color: #f8f9fa;
border-radius: 0.25rem;
cursor: pointer;
}
.map-control-btn.active {
background-color: #e9ecef;
border-color: #dee2e6;
}
.map-control-btn.edit {
background-color: #fff3cd;
border-color: #ffeeba;
}
.map-control-btn.save {
background-color: #d1ecf1;
border-color: #bee5eb;
}
.map-control-btn.cancel {
background-color: #f8d7da;
border-color: #f5c6cb;
}
.leaflet-marker-icon {
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.3));
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid px-3">
<div class="row mb-3">
<div class="col-12 d-flex justify-content-between align-items-center">
<h2>{% if object %}Редактировать объект: {{ object.name }}{% else %}Создать объект{% endif %}</h2>
<div>
<a href="{% url 'home' %}" class="btn btn-secondary btn-action">Назад</a>
</div>
</div>
</div>
<form method="post">
{% csrf_token %}
<!-- Основная информация -->
<div class="form-section">
<div class="form-section-header">
<h4>Основная информация</h4>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.name.id_for_label }}" class="form-label">Имя объекта:</label>
{{ form.name }}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Дата создания:</label>
<div class="readonly-field">
{% if object.created_at %}{{ object.created_at|date:"d.m.Y H:i" }}{% else %}-{% endif %}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Создан пользователем:</label>
<div class="readonly-field">
{% if object.created_by %}{{ object.created_by }}{% else %}-{% endif %}
</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Дата последнего изменения:</label>
<div class="readonly-field">
{% if object.updated_at %}{{ object.updated_at|date:"d.m.Y H:i" }}{% else %}-{% endif %}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Изменен пользователем:</label>
<div class="readonly-field">
{% if object.updated_by %}{{ object.updated_by }}{% else %}-{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- ВЧ загрузки -->
<div class="form-section">
<div class="form-section-header d-flex justify-content-between align-items-center">
<h4>ВЧ загрузка</h4>
{% if not parameter_forms.forms.0.instance.pk %}
<button type="button" class="btn btn-sm btn-outline-primary" id="add-parameter">Добавить ВЧ загрузку</button>
{% endif %}
</div>
<div id="parameters-container">
{% for param_form in parameter_forms %}
{% comment %} <div class="dynamic-form" data-parameter-index="{{ forloop.counter0 }}"> {% endcomment %}
<div class="dynamic-form-header">
{% if parameter_forms.forms|length > 1 %}
<button type="button" class="btn btn-sm btn-outline-danger remove-parameter">Удалить</button>
{% endif %}
</div>
<div class="row">
<div class="col-md-3">
<div class="mb-3">
<label for="{{ param_form.id_satellite.id_for_label }}" class="form-label">Спутник:</label>
{{ param_form.id_satellite }}
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label for="{{ param_form.frequency.id_for_label }}" class="form-label">Частота, МГц:</label>
{{ param_form.frequency }}
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label for="{{ param_form.freq_range.id_for_label }}" class="form-label">Полоса, МГц:</label>
{{ param_form.freq_range }}
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label for="{{ param_form.polarization.id_for_label }}" class="form-label">Поляризация:</label>
{{ param_form.polarization }}
</div>
</div>
</div>
<div class="row">
<div class="col-md-3">
<div class="mb-3">
<label for="{{ param_form.bod_velocity.id_for_label }}" class="form-label">Симв. скорость, БОД:</label>
{{ param_form.bod_velocity }}
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label for="{{ param_form.modulation.id_for_label }}" class="form-label">Модуляция:</label>
{{ param_form.modulation }}
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label for="{{ param_form.snr.id_for_label }}" class="form-label">ОСШ:</label>
{{ param_form.snr }}
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label for="{{ param_form.standard.id_for_label }}" class="form-label">Стандарт:</label>
{{ param_form.standard }}
</div>
</div>
</div>
{% comment %} </div> {% endcomment %}
{% endfor %}
</div>
</div>
<!-- Блок с картой -->
<div class="form-section">
<div class="form-section-header">
<h4>Карта</h4>
</div>
<div class="map-container">
<div id="map"></div>
</div>
</div>
<!-- Геоданные -->
<div class="form-section">
<div class="form-section-header">
<h4>Геоданные</h4>
</div>
<!-- Координаты геолокации -->
<div class="coord-sync-group">
<div class="coord-group-header">Координаты геолокации</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="id_geo_latitude" class="form-label">Широта:</label>
<input type="number" step="0.000001" class="form-control"
id="id_geo_latitude" name="geo_latitude"
value="{% if object.geo_obj and object.geo_obj.coords %}{{ object.geo_obj.coords.y }}{% endif %}">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="id_geo_longitude" class="form-label">Долгота:</label>
<input type="number" step="0.000001" class="form-control"
id="id_geo_longitude" name="geo_longitude"
value="{% if object.geo_obj and object.geo_obj.coords %}{{ object.geo_obj.coords.x }}{% endif %}">
</div>
</div>
</div>
</div>
<!-- Координаты Кубсата -->
<div class="coord-group">
<div class="coord-group-header">Координаты Кубсата</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="id_kupsat_longitude" class="form-label">Долгота:</label>
<input type="number" step="0.000001" class="form-control"
id="id_kupsat_longitude" name="kupsat_longitude"
value="{% if object.geo_obj and object.geo_obj.coords_kupsat %}{{ object.geo_obj.coords_kupsat.x }}{% endif %}">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="id_kupsat_latitude" class="form-label">Широта:</label>
<input type="number" step="0.000001" class="form-control"
id="id_kupsat_latitude" name="kupsat_latitude"
value="{% if object.geo_obj and object.geo_obj.coords_kupsat %}{{ object.geo_obj.coords_kupsat.y }}{% endif %}">
</div>
</div>
</div>
</div>
<!-- Координаты оперативников -->
<div class="coord-group">
<div class="coord-group-header">Координаты оперативников</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="id_valid_longitude" class="form-label">Долгота:</label>
<input type="number" step="0.000001" class="form-control"
id="id_valid_longitude" name="valid_longitude"
value="{% if object.geo_obj and object.geo_obj.coords_valid %}{{ object.geo_obj.coords_valid.x }}{% endif %}">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="id_valid_latitude" class="form-label">Широта:</label>
<input type="number" step="0.000001" class="form-control"
id="id_valid_latitude" name="valid_latitude"
value="{% if object.geo_obj and object.geo_obj.coords_valid %}{{ object.geo_obj.coords_valid.y }}{% endif %}">
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ geo_form.location.id_for_label }}" class="form-label">Местоположение:</label>
{{ geo_form.location }}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ geo_form.comment.id_for_label }}" class="form-label">Комментарий:</label>
{{ geo_form.comment }}
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Дата и время:</label>
<div class="datetime-group">
<div>
<label for="id_timestamp_date" class="form-label">Дата:</label>
<input type="date" class="form-control"
id="id_timestamp_date" name="timestamp_date"
value="{% if object.geo_obj and object.geo_obj.timestamp %}{{ object.geo_obj.timestamp|date:'Y-m-d' }}{% endif %}">
</div>
<div>
<label for="id_timestamp_time" class="form-label">Время:</label>
<input type="time" class="form-control"
id="id_timestamp_time" name="timestamp_time"
value="{% if object.geo_obj and object.geo_obj.timestamp %}{{ object.geo_obj.timestamp|time:'H:i' }}{% endif %}">
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ geo_form.is_average.id_for_label }}" class="form-check-label">Усреднённое:</label>
{{ geo_form.is_average }}
</div>
</div>
</div>
{% if object.geo_obj %}
<div class="row mt-3">
<div class="col-md-4">
<div class="mb-3">
<label class="form-label">Расстояние гео-кубсат, км:</label>
<div class="readonly-field">
{% if object.geo_obj.distance_coords_kup is not None %}
{{ object.geo_obj.distance_coords_kup|floatformat:2 }}
{% else %}
-
{% endif %}
</div>
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label class="form-label">Расстояние гео-опер, км:</label>
<div class="readonly-field">
{% if object.geo_obj.distance_coords_valid is not None %}
{{ object.geo_obj.distance_coords_valid|floatformat:2 }}
{% else %}
-
{% endif %}
</div>
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label class="form-label">Расстояние кубсат-опер, км:</label>
<div class="readonly-field">
{% if object.geo_obj.distance_kup_valid is not None %}
{{ object.geo_obj.distance_kup_valid|floatformat:2 }}
{% else %}
-
{% endif %}
</div>
</div>
</div>
</div>
{% endif %}
</div>
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
<div class="d-flex justify-content-end mt-4">
<button type="submit" class="btn btn-primary btn-action">Сохранить</button>
{% if object %}
<a href="{% url 'objitem_delete' object.id %}" class="btn btn-danger btn-action">Удалить</a>
{% endif %}
</div>
{% endif %}
</form>
</div>
{% endblock %}
{% block extra_js %}
{{ block.super }}
<!-- Подключаем Leaflet и его плагины -->
{% leaflet_js %}
{% leaflet_css %}
<script src="{% static 'leaflet-markers/js/leaflet-color-markers.js' %}"></script>
<script>
// Динамическое добавление ВЧ загрузок
let parameterIndex = {{ parameter_forms|length }};
document.getElementById('add-parameter')?.addEventListener('click', function() {
const container = document.getElementById('parameters-container');
const template = document.querySelector('.dynamic-form');
if (template) {
const clone = template.cloneNode(true);
clone.querySelectorAll('[id]').forEach(el => {
el.id = el.id.replace(/-\d+-/g, `-${parameterIndex}-`);
});
clone.querySelectorAll('[name]').forEach(el => {
el.name = el.name.replace(/-\d+-/g, `-${parameterIndex}-`);
});
clone.querySelectorAll('[for]').forEach(el => {
el.htmlFor = el.htmlFor.replace(/-\d+-/g, `-${parameterIndex}-`);
});
clone.querySelector('.dynamic-form-header h5').textContent = `ВЧ загрузка #${parameterIndex + 1}`;
clone.dataset.parameterIndex = parameterIndex;
container.appendChild(clone);
parameterIndex++;
}
});
// Удаление ВЧ загрузок
document.addEventListener('click', function(e) {
if (e.target.classList.contains('remove-parameter')) {
if (document.querySelectorAll('.dynamic-form').length > 1) {
e.target.closest('.dynamic-form').remove();
} else {
alert('Должна быть хотя бы одна ВЧ загрузка');
}
}
});
document.addEventListener('DOMContentLoaded', function() {
// Инициализация карты
const map = L.map('map').setView([55.75, 37.62], 5);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
// Определяем цвета для маркеров
const colors = {
geo: 'blue',
kupsat: 'red',
valid: 'green'
};
// Функция для создания иконки маркера
function createMarkerIcon(color) {
return L.icon({
iconUrl: '{% static "leaflet-markers/img/marker-icon-" %}' + color + '.png',
shadowUrl: `{% static 'leaflet-markers/img/marker-shadow.png' %}`,
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
});
}
const editableLayerGroup = new L.FeatureGroup();
map.addLayer(editableLayerGroup);
// Маркеры
const markers = {};
function createMarker(latFieldId, lngFieldId, position, color, name) {
const marker = L.marker(position, {
draggable: false,
icon: createMarkerIcon(color),
title: name
}).addTo(editableLayerGroup);
marker.bindPopup(name);
// Синхронизация при изменении формы
function syncFromForm() {
const lat = parseFloat(document.getElementById(latFieldId).value);
const lng = parseFloat(document.getElementById(lngFieldId).value);
if (!isNaN(lat) && !isNaN(lng)) {
marker.setLatLng([lat, lng]);
}
}
// Синхронизация при перетаскивании (только если активировано)
marker.on('dragend', function(event) {
const latLng = event.target.getLatLng();
document.getElementById(latFieldId).value = latLng.lat.toFixed(6);
document.getElementById(lngFieldId).value = latLng.lng.toFixed(6);
});
// Добавляем методы для управления
marker.enableEditing = function() {
this.dragging.enable();
this.openPopup();
};
marker.disableEditing = function() {
this.dragging.disable();
this.closePopup();
};
marker.syncFromForm = syncFromForm;
return marker;
}
// Создаем маркеры
markers.geo = createMarker(
'id_geo_latitude',
'id_geo_longitude',
[55.75, 37.62],
colors.geo,
'Геолокация'
);
markers.kupsat = createMarker(
'id_kupsat_latitude',
'id_kupsat_longitude',
[55.75, 37.61],
colors.kupsat,
'Кубсат'
);
markers.valid = createMarker(
'id_valid_latitude',
'id_valid_longitude',
[55.75, 37.63],
colors.valid,
'Оперативник'
);
// Устанавливаем начальные координаты из полей формы
function initMarkersFromForm() {
const geoLat = parseFloat(document.getElementById('id_geo_latitude').value) || 55.75;
const geoLng = parseFloat(document.getElementById('id_geo_longitude').value) || 37.62;
markers.geo.setLatLng([geoLat, geoLng]);
const kupsatLat = parseFloat(document.getElementById('id_kupsat_latitude').value) || 55.75;
const kupsatLng = parseFloat(document.getElementById('id_kupsat_longitude').value) || 37.61;
markers.kupsat.setLatLng([kupsatLat, kupsatLng]);
const validLat = parseFloat(document.getElementById('id_valid_latitude').value) || 55.75;
const validLng = parseFloat(document.getElementById('id_valid_longitude').value) || 37.63;
markers.valid.setLatLng([validLat, validLng]);
// Центрируем карту на первом маркере
map.setView(markers.geo.getLatLng(), 10);
}
// Настройка формы для синхронизации с маркерами
function setupFormChange(latFieldId, lngFieldId, marker) {
const latField = document.getElementById(latFieldId);
const lngField = document.getElementById(lngFieldId);
[latField, lngField].forEach(field => {
field.addEventListener('change', function() {
const lat = parseFloat(latField.value);
const lng = parseFloat(lngField.value);
if (!isNaN(lat) && !isNaN(lng)) {
marker.setLatLng([lat, lng]);
map.setView(marker.getLatLng(), 10);
}
});
});
}
// Инициализация
initMarkersFromForm();
// Настройка формы для синхронизации с маркерами
setupFormChange('id_geo_latitude', 'id_geo_longitude', markers.geo);
setupFormChange('id_kupsat_latitude', 'id_kupsat_longitude', markers.kupsat);
setupFormChange('id_valid_latitude', 'id_valid_longitude', markers.valid);
// --- УПРАВЛЕНИЕ РЕДАКТИРОВАНИЕМ ---
// Кнопки редактирования
const editControlsDiv = L.DomUtil.create('div', 'map-controls');
editControlsDiv.style.position = 'absolute';
editControlsDiv.style.top = '10px';
editControlsDiv.style.right = '10px';
editControlsDiv.style.zIndex = '1000';
editControlsDiv.style.background = 'white';
editControlsDiv.style.padding = '10px';
editControlsDiv.style.borderRadius = '4px';
editControlsDiv.style.boxShadow = '0 0 10px rgba(0,0,0,0.2)';
editControlsDiv.innerHTML = `
<div class="map-controls">
<button type="button" id="edit-btn" class="map-control-btn edit">Редактировать</button>
<button type="button" id="save-btn" class="map-control-btn save" disabled>Сохранить</button>
<button type="button" id="cancel-btn" class="map-control-btn cancel" disabled>Отмена</button>
</div>
`;
map.getContainer().appendChild(editControlsDiv);
let isEditing = false;
// Сохраняем начальные координаты для отмены
const initialPositions = {
geo: markers.geo.getLatLng(),
kupsat: markers.kupsat.getLatLng(),
valid: markers.valid.getLatLng()
};
// Включение редактирования
document.getElementById('edit-btn').addEventListener('click', function() {
if (isEditing) return;
isEditing = true;
document.getElementById('edit-btn').classList.add('active');
document.getElementById('save-btn').disabled = false;
document.getElementById('cancel-btn').disabled = false;
// Включаем drag для всех маркеров
Object.values(markers).forEach(marker => {
marker.enableEditing();
});
// Показываем подсказку
L.popup()
.setLatLng(map.getCenter())
.setContent('Перетаскивайте маркеры. Нажмите "Сохранить" или "Отмена".')
.openOn(map);
});
// Сохранение изменений
document.getElementById('save-btn').addEventListener('click', function() {
if (!isEditing) return;
isEditing = false;
document.getElementById('edit-btn').classList.remove('active');
document.getElementById('save-btn').disabled = true;
document.getElementById('cancel-btn').disabled = true;
// Отключаем редактирование
Object.values(markers).forEach(marker => {
marker.disableEditing();
});
// Обновляем начальные позиции
initialPositions.geo = markers.geo.getLatLng();
initialPositions.kupsat = markers.kupsat.getLatLng();
initialPositions.valid = markers.valid.getLatLng();
// Убираем попап подсказки
map.closePopup();
});
// Отмена изменений
document.getElementById('cancel-btn').addEventListener('click', function() {
if (!isEditing) return;
isEditing = false;
document.getElementById('edit-btn').classList.remove('active');
document.getElementById('save-btn').disabled = true;
document.getElementById('cancel-btn').disabled = true;
// Возвращаем маркеры на исходные позиции
markers.geo.setLatLng(initialPositions.geo);
markers.kupsat.setLatLng(initialPositions.kupsat);
markers.valid.setLatLng(initialPositions.valid);
// Отключаем редактирование
Object.values(markers).forEach(marker => {
marker.disableEditing();
});
// Синхронизируем форму с исходными значениями
document.getElementById('id_geo_latitude').value = initialPositions.geo.lat.toFixed(6);
document.getElementById('id_geo_longitude').value = initialPositions.geo.lng.toFixed(6);
document.getElementById('id_kupsat_latitude').value = initialPositions.kupsat.lat.toFixed(6);
document.getElementById('id_kupsat_longitude').value = initialPositions.kupsat.lng.toFixed(6);
document.getElementById('id_valid_latitude').value = initialPositions.valid.lat.toFixed(6);
document.getElementById('id_valid_longitude').value = initialPositions.valid.lng.toFixed(6);
map.closePopup();
});
// Легенда
const legend = L.control({ position: 'bottomright' });
legend.onAdd = function() {
const div = L.DomUtil.create('div', 'info legend');
div.style.fontSize = '14px';
div.style.backgroundColor = 'white';
div.style.padding = '10px';
div.style.borderRadius = '4px';
div.style.boxShadow = '0 0 10px rgba(0,0,0,0.2)';
div.innerHTML = `
<h5>Легенда</h5>
<div><span style="color: blue; font-weight: bold;">•</span> Геолокация</div>
<div><span style="color: red; font-weight: bold;">•</span> Кубсат</div>
<div><span style="color: green; font-weight: bold;">•</span> Оперативники</div>
`;
return div;
};
legend.addTo(map);
});
</script>
{% endblock %}

View File

@@ -0,0 +1,856 @@
{% extends 'mainapp/base.html' %}
{% block title %}Список объектов{% endblock %}
{% block extra_css %}
<style>
.table-responsive table {
user-select: none;
}
.table-responsive tr.selected {
background-color: #d4edff;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid px-3">
<div class="row mb-3">
<div class="col-12">
<h2>Список объектов</h2>
</div>
</div>
<!-- Toolbar -->
<div class="row mb-3">
<div class="col-12">
<div class="card">
<div class="card-body">
<div class="d-flex flex-wrap align-items-center gap-3">
<!-- Search bar made more compact -->
<div style="min-width: 200px; flex-grow: 1; max-width: 400px;">
<div class="input-group">
<input type="text" id="toolbar-search" class="form-control" placeholder="Поиск..." value="{{ search_query|default:'' }}">
<button type="button" class="btn btn-outline-primary" onclick="performSearch()">Найти</button>
<button type="button" class="btn btn-outline-secondary" onclick="clearSearch()">Очистить</button>
</div>
</div>
<!-- Action buttons bar -->
<div class="d-flex gap-2">
{% comment %} <button type="button" class="btn btn-success btn-sm" title="Добавить">
<i class="bi bi-plus-circle"></i> Добавить
</button>
<button type="button" class="btn btn-info btn-sm" title="Изменить">
<i class="bi bi-pencil"></i> Изменить
</button> {% endcomment %}
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
<button type="button" class="btn btn-danger btn-sm" title="Удалить" onclick="deleteSelectedObjects()">
<i class="bi bi-trash"></i> Удалить
</button>
{% endif %}
<button type="button" class="btn btn-outline-primary btn-sm" title="Показать на карте" onclick="showSelectedOnMap()">
<i class="bi bi-map"></i> Карта
</button>
</div>
<!-- Items per page select moved here -->
<div>
<label for="items-per-page" class="form-label mb-0">Показать:</label>
<select name="items_per_page" id="items-per-page" class="form-select form-select-sm d-inline-block" style="width: auto;" onchange="updateItemsPerPage()">
{% for option in available_items_per_page %}
<option value="{{ option }}" {% if option == items_per_page %}selected{% endif %}>
{{ option }}
</option>
{% endfor %}
</select>
</div>
<!-- Column visibility toggle button -->
<div class="dropdown">
<button type="button" class="btn btn-outline-secondary btn-sm dropdown-toggle" id="columnVisibilityDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-gear"></i> Колонки
</button>
<ul class="dropdown-menu" aria-labelledby="columnVisibilityDropdown" style="z-index: 1050;">
<li>
<label class="dropdown-item">
<input type="checkbox" id="select-all-columns" checked onchange="toggleAllColumns(this)"> Выбрать всё
</label>
</li>
<li><hr class="dropdown-divider"></li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="0" checked onchange="toggleColumn(this)"> Выбрать
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="1" checked onchange="toggleColumn(this)"> Имя
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="2" checked onchange="toggleColumn(this)"> Спутник
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="3" checked onchange="toggleColumn(this)"> Част, МГц
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="4" checked onchange="toggleColumn(this)"> Полоса, МГц
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="5" checked onchange="toggleColumn(this)"> Поляризация
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="6" checked onchange="toggleColumn(this)"> Сим. V
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="7" checked onchange="toggleColumn(this)"> Модул
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="8" checked onchange="toggleColumn(this)"> ОСШ
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="9" checked onchange="toggleColumn(this)"> Время ГЛ
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="10" checked onchange="toggleColumn(this)"> Местоположение
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="11" checked onchange="toggleColumn(this)"> Геолокация
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="12" checked onchange="toggleColumn(this)"> Кубсат
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="13" checked onchange="toggleColumn(this)"> Опер. отд
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="14" checked onchange="toggleColumn(this)"> Гео-куб, км
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="15" checked onchange="toggleColumn(this)"> Гео-опер, км
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="16" checked onchange="toggleColumn(this)"> Куб-опер, км
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="17" checked onchange="toggleColumn(this)"> Обновлено
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="18" checked onchange="toggleColumn(this)"> Кем (обновление)
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="19" checked onchange="toggleColumn(this)"> Создано
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="20" checked onchange="toggleColumn(this)"> Кем (создание)
</label>
</li>
</ul>
</div>
<!-- Pagination moved here -->
<div class="ms-auto">
{% if page_obj.paginator.num_pages > 1 %}
<nav aria-label="Page navigation" class="d-flex align-items-center">
<ul class="pagination mb-0">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page=1" title="Первая"><<</a>
</li>
<li class="page-item">
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.previous_page_number }}" title="Предыдущая"><</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<span class="page-link">{{ num }}</span>
</li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item">
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ num }}">{{ num }}</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.next_page_number }}" title="Следующая">></a>
</li>
<li class="page-item">
<a class="page-link" href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value }}&{% endif %}{% endfor %}page={{ page_obj.paginator.num_pages }}" title="Последняя">>></a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
<!-- Pagination Info -->
{% if page_obj %}
<div class="ms-3 text-muted small">
{{ page_obj.start_index }}-{{ page_obj.end_index }} из {{ page_obj.paginator.count }}
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row g-3">
<!-- Filters Sidebar - Made narrower -->
<div class="col-md-auto">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">Фильтры</h5>
<form method="get" id="filter-form">
<!-- Satellite Selection - Multi-select -->
<div class="mb-2">
<label class="form-label">Спутник:</label>
<div class="d-flex justify-content-between mb-1">
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('satellite_id', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('satellite_id', false)">Снять</button>
</div>
<select name="satellite_id" class="form-select form-select-sm mb-2" multiple size="6">
{% for satellite in satellites %}
<option value="{{ satellite.id }}"
{% if satellite.id in selected_satellites %}selected{% endif %}>
{{ satellite.name }}
</option>
{% endfor %}
</select>
</div>
<!-- Frequency Filter -->
<div class="mb-2">
<label class="form-label">Частота, МГц:</label>
<input type="number" step="0.001" name="freq_min" class="form-control form-control-sm mb-1" placeholder="От" value="{{ freq_min|default:'' }}">
<input type="number" step="0.001" name="freq_max" class="form-control form-control-sm" placeholder="До" value="{{ freq_max|default:'' }}">
</div>
<!-- Range Filter -->
<div class="mb-2">
<label class="form-label">Полоса, МГц:</label>
<input type="number" step="0.001" name="range_min" class="form-control form-control-sm mb-1" placeholder="От" value="{{ range_min|default:'' }}">
<input type="number" step="0.001" name="range_max" class="form-control form-control-sm" placeholder="До" value="{{ range_max|default:'' }}">
</div>
<!-- SNR Filter -->
<div class="mb-2">
<label class="form-label">ОСШ:</label>
<input type="number" step="0.001" name="snr_min" class="form-control form-control-sm mb-1" placeholder="От" value="{{ snr_min|default:'' }}">
<input type="number" step="0.001" name="snr_max" class="form-control form-control-sm" placeholder="До" value="{{ snr_max|default:'' }}">
</div>
<!-- Symbol Rate Filter -->
<div class="mb-2">
<label class="form-label">Сим. v, БОД:</label>
<input type="number" step="0.001" name="bod_min" class="form-control form-control-sm mb-1" placeholder="От" value="{{ bod_min|default:'' }}">
<input type="number" step="0.001" name="bod_max" class="form-control form-control-sm" placeholder="До" value="{{ bod_max|default:'' }}">
</div>
<!-- Modulation Filter -->
<div class="mb-2">
<label class="form-label">Модуляция:</label>
<div class="d-flex justify-content-between mb-1">
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('modulation', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('modulation', false)">Снять</button>
</div>
<select name="modulation" class="form-select form-select-sm mb-2" multiple size="6">
{% for mod in modulations %}
<option value="{{ mod.id }}"
{% if mod.id in selected_modulations %}selected{% endif %}>
{{ mod.name }}
</option>
{% endfor %}
</select>
</div>
<!-- Polarization Filter -->
<div class="mb-2">
<label class="form-label">Поляризация:</label>
<div class="d-flex justify-content-between mb-1">
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('polarization', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('polarization', false)">Снять</button>
</div>
<select name="polarization" class="form-select form-select-sm mb-2" multiple size="4">
{% for pol in polarizations %}
<option value="{{ pol.id }}"
{% if pol.id in selected_polarizations %}selected{% endif %}>
{{ pol.name }}
</option>
{% endfor %}
</select>
</div>
<!-- Kubsat Coordinates Filter -->
<div class="mb-2">
<label class="form-label">Координаты Кубсата:</label>
<div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_kupsat" id="has_kupsat_1" value="1"
{% if has_kupsat == '1' %}checked{% endif %}>
<label class="form-check-label" for="has_kupsat_1">Есть</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_kupsat" id="has_kupsat_0" value="0"
{% if has_kupsat == '0' %}checked{% endif %}>
<label class="form-check-label" for="has_kupsat_0">Нет</label>
</div>
</div>
</div>
<!-- Valid Coordinates Filter -->
<div class="mb-2">
<label class="form-label">Координаты опер. отдела:</label>
<div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_valid" id="has_valid_1" value="1"
{% if has_valid == '1' %}checked{% endif %}>
<label class="form-check-label" for="has_valid_1">Есть</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="has_valid" id="has_valid_0" value="0"
{% if has_valid == '0' %}checked{% endif %}>
<label class="form-check-label" for="has_valid_0">Нет</label>
</div>
</div>
</div>
<!-- Apply Filters and Reset Buttons -->
<div class="d-grid gap-2 mt-2">
<button type="submit" class="btn btn-primary btn-sm">Применить</button>
<a href="?" class="btn btn-secondary btn-sm">Сбросить</a>
</div>
</form>
</div>
</div>
</div>
<!-- Main Table -->
<div class="col-md">
<div class="card h-100">
<div class="card-body p-0">
<div class="table-responsive" style="max-height: 75vh; overflow-y: auto;">
<table class="table table-striped table-hover table-sm" style="font-size: 0.85rem;">
<thead class="table-dark sticky-top">
<tr>
<th scope="col" class="text-center" style="width: 3%;">
<input type="checkbox" id="select-all" class="form-check-input">
</th>
<!-- Столбец "Имя" -->
<th scope="col">
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == 'name' %}-name{% elif sort == '-name' %}name{% else %}name{% endif %}" class="text-white text-decoration-none d-inline-flex align-items-center">
Имя
{% if sort == 'name' %} <i class="bi bi-sort-up ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% elif sort == '-name' %} <i class="bi bi-sort-down ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% else %} <i class="bi bi-arrow-down-up ms-1"></i> {% endif %}
</a>
</th>
<!-- Столбец "Спутник" -->
<th scope="col">
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == 'satellite' %}-satellite{% elif sort == '-satellite' %}satellite{% else %}satellite{% endif %}" class="text-white text-decoration-none d-inline-flex align-items-center">
Спутник
{% if sort == 'satellite' %} <i class="bi bi-sort-up ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% elif sort == '-satellite' %} <i class="bi bi-sort-down ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% else %} <i class="bi bi-arrow-down-up ms-1"></i> {% endif %}
</a>
</th>
<!-- Столбец "Част, МГц" -->
<th scope="col">
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == 'frequency' %}-frequency{% elif sort == '-frequency' %}frequency{% else %}frequency{% endif %}" class="text-white text-decoration-none d-inline-flex align-items-center">
Част, МГц
{% if sort == 'frequency' %} <i class="bi bi-sort-up ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% elif sort == '-frequency' %} <i class="bi bi-sort-down ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% else %} <i class="bi bi-arrow-down-up ms-1"></i> {% endif %}
</a>
</th>
<!-- Столбец "Полоса, МГц" -->
<th scope="col">
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == 'freq_range' %}-freq_range{% elif sort == '-freq_range' %}freq_range{% else %}freq_range{% endif %}" class="text-white text-decoration-none d-inline-flex align-items-center">
Полоса, МГц
{% if sort == 'freq_range' %} <i class="bi bi-sort-up ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% elif sort == '-freq_range' %} <i class="bi bi-sort-down ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% else %} <i class="bi bi-arrow-down-up ms-1"></i> {% endif %}
</a>
</th>
<!-- Столбец "Поляризация" -->
<th scope="col">
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == 'polarization' %}-polarization{% elif sort == '-polarization' %}polarization{% else %}polarization{% endif %}" class="text-white text-decoration-none d-inline-flex align-items-center">
Поляризация
{% if sort == 'polarization' %} <i class="bi bi-sort-up ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% elif sort == '-polarization' %} <i class="bi bi-sort-down ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% else %} <i class="bi bi-arrow-down-up ms-1"></i> {% endif %}
</a>
</th>
<!-- Столбец "Сим. V" -->
<th scope="col">
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == 'bod_velocity' %}-bod_velocity{% elif sort == '-bod_velocity' %}bod_velocity{% else %}bod_velocity{% endif %}" class="text-white text-decoration-none d-inline-flex align-items-center">
Сим. V
{% if sort == 'bod_velocity' %} <i class="bi bi-sort-up ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% elif sort == '-bod_velocity' %} <i class="bi bi-sort-down ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% else %} <i class="bi bi-arrow-down-up ms-1"></i> {% endif %}
</a>
</th>
<!-- Столбец "Модул" -->
<th scope="col">
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == 'modulation' %}-modulation{% elif sort == '-modulation' %}modulation{% else %}modulation{% endif %}" class="text-white text-decoration-none d-inline-flex align-items-center">
Модул
{% if sort == 'modulation' %} <i class="bi bi-sort-up ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% elif sort == '-modulation' %} <i class="bi bi-sort-down ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% else %} <i class="bi bi-arrow-down-up ms-1"></i> {% endif %}
</a>
</th>
<!-- Столбец "ОСШ" -->
<th scope="col">
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == 'snr' %}-snr{% elif sort == '-snr' %}snr{% else %}snr{% endif %}" class="text-white text-decoration-none d-inline-flex align-items-center">
ОСШ
{% if sort == 'snr' %} <i class="bi bi-sort-up ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% elif sort == '-snr' %} <i class="bi bi-sort-down ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% else %} <i class="bi bi-arrow-down-up ms-1"></i> {% endif %}
</a>
</th>
<!-- Столбец "Время ГЛ" -->
<th scope="col">
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == 'geo_timestamp' %}-geo_timestamp{% elif sort == '-geo_timestamp' %}geo_timestamp{% else %}geo_timestamp{% endif %}" class="text-white text-decoration-none d-inline-flex align-items-center">
Время ГЛ
{% if sort == 'geo_timestamp' %} <i class="bi bi-sort-up ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% elif sort == '-geo_timestamp' %} <i class="bi bi-sort-down ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% else %} <i class="bi bi-arrow-down-up ms-1"></i> {% endif %}
</a>
</th>
<th scope="col">Местоположение</th>
<!-- Столбец "Геолокация" - без сортировки -->
<th scope="col">Геолокация</th>
<!-- Столбец "Кубсат" - без сортировки -->
<th scope="col">Кубсат</th>
<!-- Столбец "Опер. отд" - без сортировки -->
<th scope="col">Опер. отд</th>
<!-- Столбец "Гео-куб, км" - без сортировки -->
<th scope="col">Гео-куб, км</th>
<!-- Столбец "Гео-опер, км" - без сортировки -->
<th scope="col">Гео-опер, км</th>
<!-- Столбец "Куб-опер, км" - без сортировки -->
<th scope="col">Куб-опер, км</th>
<!-- Столбец "Обновлено" -->
<th scope="col">
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == 'updated_at' %}-updated_at{% elif sort == '-updated_at' %}updated_at{% else %}updated_at{% endif %}" class="text-white text-decoration-none d-inline-flex align-items-center">
Обновлено
{% if sort == 'updated_at' %} <i class="bi bi-sort-up ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% elif sort == '-updated_at' %} <i class="bi bi-sort-down ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% else %} <i class="bi bi-arrow-down-up ms-1"></i> {% endif %}
</a>
</th>
<!-- Столбец "Кем (обновление)" - без сортировки -->
<th scope="col">Кем(обн)</th>
<!-- Столбец "Создано" -->
<th scope="col">
<a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}sort={% if sort == 'created_at' %}-created_at{% elif sort == '-created_at' %}created_at{% else %}created_at{% endif %}" class="text-white text-decoration-none d-inline-flex align-items-center">
Создано
{% if sort == 'created_at' %} <i class="bi bi-sort-up ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% elif sort == '-created_at' %} <i class="bi bi-sort-down ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% else %} <i class="bi bi-arrow-down-up ms-1"></i> {% endif %}
</a>
</th>
<!-- Столбец "Кем (создание)" - без сортировки -->
<th scope="col">Кем(созд)</th>
</tr>
</thead>
<tbody>
{% for item in processed_objects %}
<tr>
<td class="text-center">
<input type="checkbox" class="form-check-input item-checkbox" value="{{ item.id }}">
</td>
<td><a href="{% if item.obj.id %}{% url 'objitem_update' item.obj.id %}{% endif %}">{{ item.name }}</a></td>
<td>{{ item.satellite_name }}</td>
<td>{{ item.frequency }}</td>
<td>{{ item.freq_range }}</td>
<td>{{ item.polarization }}</td>
<td>{{ item.bod_velocity }}</td>
<td>{{ item.modulation }}</td>
<td>{{ item.snr }}</td>
<td>{{ item.geo_timestamp|date:"d.m.Y H:i" }}</td>
<td>{{ item.geo_location}}</td>
<td>{{ item.geo_coords }}</td>
<td>{{ item.kupsat_coords }}</td>
<td>{{ item.valid_coords }}</td>
<td>{{ item.distance_geo_kup }}</td>
<td>{{ item.distance_geo_valid }}</td>
<td>{{ item.distance_kup_valid }}</td>
<td>{{ item.obj.updated_at|date:"d.m.Y H:i" }}</td>
<td>{{ item.updated_by }}</td>
<td>{{ item.obj.created_at|date:"d.m.Y H:i" }}</td>
<td>{{ item.obj.created_by }}</td>
</tr>
{% empty %}
<tr>
<td colspan="19" class="text-center py-4">
{% if selected_satellite_id %}
Нет данных для выбранных фильтров
{% else %}
Пожалуйста, выберите спутник для отображения данных
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
let lastCheckedIndex = null;
function updateRowHighlight(checkbox) {
const row = checkbox.closest('tr');
if (checkbox.checked) {
row.classList.add('selected');
} else {
row.classList.remove('selected');
}
}
function handleCheckboxClick(e) {
if (e.shiftKey && lastCheckedIndex !== null) {
const checkboxes = document.querySelectorAll('.item-checkbox');
const currentIndex = Array.from(checkboxes).indexOf(e.target);
const startIndex = Math.min(lastCheckedIndex, currentIndex);
const endIndex = Math.max(lastCheckedIndex, currentIndex);
for (let i = startIndex; i <= endIndex; i++) {
checkboxes[i].checked = e.target.checked;
updateRowHighlight(checkboxes[i]);
}
} else {
updateRowHighlight(e.target);
}
lastCheckedIndex = Array.from(document.querySelectorAll('.item-checkbox')).indexOf(e.target);
}
// Function to show selected objects on map
function showSelectedOnMap() {
// Get all checked checkboxes
const checkedCheckboxes = document.querySelectorAll('.item-checkbox:checked');
if (checkedCheckboxes.length === 0) {
alert('Пожалуйста, выберите хотя бы один объект для отображения на карте');
return;
}
// Extract IDs from checked checkboxes
const selectedIds = [];
checkedCheckboxes.forEach(checkbox => {
selectedIds.push(checkbox.value);
});
// Redirect to the map view with selected IDs as query parameter
const url = '{% url "show_selected_objects_map" %}' + '?ids=' + selectedIds.join(',');
window.open(url, '_blank'); // Open in a new tab
}
function clearSelections() {
const itemCheckboxes = document.querySelectorAll('.item-checkbox');
itemCheckboxes.forEach(checkbox => {
checkbox.checked = false;
});
// Also uncheck the select-all checkbox if it exists
const selectAllCheckbox = document.getElementById('select-all');
if (selectAllCheckbox) {
selectAllCheckbox.checked = false;
}
// Remove selected class from rows
const selectedRows = document.querySelectorAll('tr.selected');
selectedRows.forEach(row => {
row.classList.remove('selected');
});
}
// Function to delete selected objects
function deleteSelectedObjects() {
// Get all checked checkboxes
const checkedCheckboxes = document.querySelectorAll('.item-checkbox:checked');
if (checkedCheckboxes.length === 0) {
alert('Пожалуйста, выберите хотя бы один объект для удаления');
return;
}
// Confirm deletion with user
if (!confirm(`Вы уверены, что хотите удалить ${checkedCheckboxes.length} объект(ов)?`)) {
return;
}
// Extract IDs from checked checkboxes
const selectedIds = [];
checkedCheckboxes.forEach(checkbox => {
selectedIds.push(checkbox.value);
});
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
const csrftoken = getCookie('csrftoken');
// Prepare request headers
const headers = {
'Content-Type': 'application/x-www-form-urlencoded',
};
// Add CSRF token to headers only if it exists
if (csrftoken) {
headers['X-CSRFToken'] = csrftoken;
}
// Send AJAX request to delete selected objects
fetch('{% url "delete_selected_objects" %}', {
method: 'POST',
headers: headers,
body: 'ids=' + selectedIds.join(',')
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert(data.message);
clearSelections();
location.reload();
} else {
alert('Ошибка: ' + (data.error || 'Неизвестная ошибка'));
}
})
.catch(error => {
console.error('Error:', error);
alert('Произошла ошибка при удалении объектов');
});
}
// Остальной ваш JavaScript код остается без изменений
function toggleColumn(checkbox) {
const columnIndex = parseInt(checkbox.getAttribute('data-column'));
const table = document.querySelector('.table');
const cells = table.querySelectorAll(`td:nth-child(${columnIndex + 1}), th:nth-child(${columnIndex + 1})`);
if (checkbox.checked) {
cells.forEach(cell => {
cell.style.display = '';
});
} else {
cells.forEach(cell => {
cell.style.display = 'none';
});
}
}
function toggleAllColumns(selectAllCheckbox) {
const columnCheckboxes = document.querySelectorAll('.column-toggle');
columnCheckboxes.forEach(checkbox => {
checkbox.checked = selectAllCheckbox.checked;
toggleColumn(checkbox);
});
}
document.addEventListener('DOMContentLoaded', function() {
const selectAllCheckbox = document.getElementById('select-all');
const itemCheckboxes = document.querySelectorAll('.item-checkbox');
if (selectAllCheckbox && itemCheckboxes.length > 0) {
selectAllCheckbox.addEventListener('change', function() {
itemCheckboxes.forEach(checkbox => {
checkbox.checked = selectAllCheckbox.checked;
});
});
itemCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', function() {
const allChecked = Array.from(itemCheckboxes).every(cb => cb.checked);
selectAllCheckbox.checked = allChecked;
});
});
// Добавляем обработчик для выбора диапазона
itemCheckboxes.forEach(checkbox => {
checkbox.addEventListener('click', handleCheckboxClick);
});
}
// Handle kubsat and valid coords checkboxes (mutually exclusive)
// Add a function to handle radio-like behavior for these checkboxes
function setupRadioLikeCheckboxes(name) {
const checkboxes = document.querySelectorAll(`input[name="${name}"]`);
checkboxes.forEach(checkbox => {
checkbox.addEventListener('change', function() {
// If this checkbox is checked, uncheck the other
if (this.checked) {
checkboxes.forEach(other => {
if (other !== this) {
other.checked = false;
}
});
} else {
// If both are unchecked, no action needed
}
});
});
}
setupRadioLikeCheckboxes('has_kupsat');
setupRadioLikeCheckboxes('has_valid');
// Function to select/deselect all options in a select element
window.selectAllOptions = function(selectName, selectAll) {
const selectElement = document.querySelector(`select[name="${selectName}"]`);
if (selectElement) {
for (let i = 0; i < selectElement.options.length; i++) {
selectElement.options[i].selected = selectAll;
}
}
};
// Get all current filter values and return as URL parameters
function getAllFilterParams() {
const form = document.getElementById('filter-form');
const searchValue = document.getElementById('toolbar-search').value;
// Create URLSearchParams object from the form
const params = new URLSearchParams(new FormData(form));
// Add search value from toolbar if present
if (searchValue.trim() !== '') {
params.set('search', searchValue);
} else {
params.delete('search');
}
return params.toString();
}
// Function to perform search
window.performSearch = function() {
const filterParams = getAllFilterParams();
window.location.search = filterParams;
};
// Function to clear search
window.clearSearch = function() {
document.getElementById('toolbar-search').value = '';
const filterParams = getAllFilterParams();
window.location.search = filterParams;
};
// Handle Enter key in toolbar search
const toolbarSearch = document.getElementById('toolbar-search');
if (toolbarSearch) {
toolbarSearch.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
performSearch();
}
});
}
//const satelliteSelect = document.querySelector('select[name="satellite_id"]');
//if (satelliteSelect) {
//satelliteSelect.addEventListener('change', function() {
// updateSatelliteSelection();
// });
//}
// Function to update items per page
window.updateItemsPerPage = function() {
const itemsPerPageSelect = document.getElementById('items-per-page');
const currentParams = new URLSearchParams(window.location.search);
// Add or update the items_per_page parameter
currentParams.set('items_per_page', itemsPerPageSelect.value);
// Remove page parameter to reset to first page when changing items per page
currentParams.delete('page');
// Update URL and reload
window.location.search = currentParams.toString();
};
// Initialize column visibility - hide creation columns by default
function initColumnVisibility() {
const creationDateCheckbox = document.querySelector('input[data-column="19"]');
const creationUserCheckbox = document.querySelector('input[data-column="20"]');
const creationDistanceGOpCheckbox = document.querySelector('input[data-column="15"]');
const creationDistanceKubOpCheckbox = document.querySelector('input[data-column="16"]');
if (creationDistanceGOpCheckbox) {
creationDistanceGOpCheckbox.checked = false;
toggleColumn(creationDistanceGOpCheckbox);
}
if (creationDistanceKubOpCheckbox) {
creationDistanceKubOpCheckbox.checked = false;
toggleColumn(creationDistanceKubOpCheckbox);
}
if (creationDateCheckbox) {
creationDateCheckbox.checked = false;
toggleColumn(creationDateCheckbox);
}
if (creationUserCheckbox) {
creationUserCheckbox.checked = false;
toggleColumn(creationUserCheckbox);
}
}
// Initialize column visibility after page loads
setTimeout(initColumnVisibility, 100); // Slight delay to ensure DOM is fully loaded
});
</script>
{% endblock %}

View File

@@ -0,0 +1,106 @@
{% 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 + '<br>' + "{{ point_data.frequency|escapejs }}");
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 %}
// Create the layer control with a custom container that includes a select all checkbox
var layerControl = L.control.layers.tree(baseLayers, overlays, {
collapsed: false,
autoZIndex: true
});
// Add the layer control to the map
layerControl.addTo(map);
// Calculate map bounds to fit all markers
{% if groups %}
var groupBounds = L.featureGroup([]);
{% for group in groups %}
{% for point_data in group.points %}
groupBounds.addLayer(L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}]));
{% endfor %}
{% endfor %}
map.fitBounds(groupBounds.getBounds().pad(0.1)); // Add some padding
{% else %}
map.setView([55.75, 37.62], 5); // Default view if no markers
{% endif %}
// Add a "Select All" checkbox functionality for all overlays
setTimeout(function() {
// Create a custom "select all" checkbox
var selectAllContainer = document.createElement('div');
selectAllContainer.className = 'leaflet-control-layers-select-all';
selectAllContainer.style.padding = '5px';
selectAllContainer.style.borderBottom = '1px solid #ccc';
selectAllContainer.style.marginBottom = '5px';
selectAllContainer.innerHTML = '<label><input type="checkbox" id="select-all-overlays" checked> Показать все точки</label>';
// Insert the checkbox at the top of the layer control
var layerControlContainer = document.querySelector('.leaflet-control-layers-list');
if (layerControlContainer) {
layerControlContainer.insertBefore(selectAllContainer, layerControlContainer.firstChild);
}
// Add event listener to the "select all" checkbox
document.getElementById('select-all-overlays').addEventListener('change', function() {
var isChecked = this.checked;
// Iterate through all overlays and toggle visibility
for (var i = 0; i < overlays.length; i++) {
if (isChecked) {
map.addLayer(overlays[i].layer);
} else {
map.removeLayer(overlays[i].layer);
}
}
});
}, 500); // Slight delay to ensure the tree control has been rendered
</script>
{% endblock extra_js %}

View File

@@ -0,0 +1,52 @@
{% 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 border-0">
<div class="card-header bg-success text-white">
<h2 class="mb-0">Формирование таблицы Кубсат</h2>
</div>
<div class="card-body">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% comment%}
<div class="mb-4">
<label for="{{ form.sat_choice.id_for_label }}" class="form-label">{{ form.sat_choice.label }}</label>
{{ form.sat_choice }}
{% if form.sat_choice.errors %}
<div class="text-danger mt-1">{{ form.sat_choice.errors }}</div>
{% endif %}
</div>{% endcomment %}
{% comment %} <div class="mb-4">
<label for="{{ form.pol_choice.id_for_label }}" class="form-label">{{ form.pol_choice.label }}</label>
{{ form.pol_choice }}
{% if form.pol_choice.errors %}
<div class="text-danger mt-1">{{ form.pol_choice.errors }}</div>
{% endif %}
</div> {% endcomment %}
<div class="mb-4">
<label for="{{ form.file.id_for_label }}" class="form-label">{{ form.file.label }}</label>
{{ form.file }}
{% if form.file.errors %}
<div class="text-danger mt-1">{{ form.file.errors }}</div>
{% endif %}
<div class="form-text">Выберите файл для загрузки</div>
</div>
<div class="d-flex justify-content-between">
<a href="{% url 'home' %}" class="btn btn-secondary">Назад</a>
<button type="submit" class="btn btn-success">Выполнить</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,53 @@
{% 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-warning text-white">
<h2 class="mb-0">Загрузка данных транспондеров из CellNet</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">Загрузите xml-файл и выберите спутник для загрузки данных в базу.</p>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="mb-3">
<label for="{{ form.file.id_for_label }}" class="form-label">Выберите xml файл:</label>
{{ form.file }}
{% if form.file.errors %}
<div class="text-danger mt-1">{{ form.file.errors }}</div>
{% endif %}
<div class="form-text">Загрузите xml-файл (.xml) с данными для обработки</div>
</div>
{% comment %} <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> {% endcomment %}
<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-warning">Добавить в базу</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,19 @@
{% extends 'mainapp/base.html' %}
{% block title %}Выход{% endblock %}
{% block content %}
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-body text-center">
<h2 class="card-title">Вы вышли из системы</h2>
<p class="card-text">Вы успешно вышли из системы.</p>
<a href="{% url 'login' %}" class="btn btn-primary">Войти снова</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,43 @@
{% extends 'mainapp/base.html' %}
{% block title %}Вход в систему{% endblock %}
{% block content %}
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-body">
<h2 class="card-title text-center">Вход в систему</h2>
<form method="post">
{% csrf_token %}
<div class="mb-3">
<label for="{{ form.username.id_for_label }}" class="form-label">Имя пользователя</label>
{{ form.username }}
</div>
<div class="mb-3">
<label for="{{ form.password.id_for_label }}" class="form-label">Пароль</label>
{{ form.password }}
</div>
{% if form.errors %}
<div class="alert alert-danger">
{% for field in form %}
{% for error in field.errors %}
<p>{{ error }}</p>
{% endfor %}
{% endfor %}
{% for error in form.non_field_errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
<div class="d-grid">
<button type="submit" class="btn btn-primary">Войти</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -5,16 +5,24 @@ from . import views
urlpatterns = [ urlpatterns = [
path('', views.HomePageView.as_view(), name='home'), path('', views.HomePageView.as_view(), name='home'), # Home page that redirects based on auth
path('objitems/', views.ObjItemListView.as_view(), name='objitem_list'), # Objects list page
path('actions/', views.ActionsPageView.as_view(), name='actions'), # Move actions to a separate page
path('excel-data', views.LoadExcelDataView.as_view(), name='load_excel_data'), path('excel-data', views.LoadExcelDataView.as_view(), name='load_excel_data'),
path('satellites', views.AddSatellitesView.as_view(), name='add_sats'), path('satellites', views.AddSatellitesView.as_view(), name='add_sats'),
path('api/locations/<int:sat_id>/geojson/', views.GetLocationsView.as_view(), name='locations_by_id'), path('api/locations/<int:sat_id>/geojson/', views.GetLocationsView.as_view(), name='locations_by_id'),
path('transponders', views.AddTranspondersView.as_view(), name='add_trans'), path('transponders', views.AddTranspondersView.as_view(), name='add_trans'),
path('csv-data', views.LoadCsvDataView.as_view(), name='load_csv_data'), path('csv-data', views.LoadCsvDataView.as_view(), name='load_csv_data'),
path('map-points/', views.ShowMapView.as_view(), name='admin_show_map'), path('map-points/', views.ShowMapView.as_view(), name='admin_show_map'),
path('show-selected-objects-map/', views.ShowSelectedObjectsMapView.as_view(), name='show_selected_objects_map'),
path('delete-selected-objects/', views.DeleteSelectedObjectsView.as_view(), name='delete_selected_objects'),
path('cluster/', views.ClusterTestView.as_view(), name='cluster'), path('cluster/', views.ClusterTestView.as_view(), name='cluster'),
path('vch-upload/', views.UploadVchLoadView.as_view(), name='vch_load'), path('vch-upload/', views.UploadVchLoadView.as_view(), name='vch_load'),
path('vch-link/', views.LinkVchSigmaView.as_view(), name='link_vch_sigma'), path('vch-link/', views.LinkVchSigmaView.as_view(), name='link_vch_sigma'),
path('kubsat-excel/', views.ProcessKubsatView.as_view(), name='kubsat_excel'),
path('object/create/', views.ObjItemCreateView.as_view(), name='objitem_create'),
path('object/<int:pk>/edit/', views.ObjItemUpdateView.as_view(), name='objitem_update'),
path('object/<int:pk>/delete/', views.ObjItemDeleteView.as_view(), name='objitem_delete'),
# path('upload/', views.upload_file, name='upload_file'), # path('upload/', views.upload_file, name='upload_file'),
] ]

View File

@@ -10,12 +10,18 @@ from .models import (
ObjItem, ObjItem,
CustomUser CustomUser
) )
from mapsapp.models import Transponders
from datetime import datetime, time from datetime import datetime, time
import pandas as pd import pandas as pd
import numpy as np import numpy as np
from django.contrib.gis.geos import Point from django.contrib.gis.geos import Point
import json import json
import re import re
import io
from django.db.models import F, Count, Exists, OuterRef, Min, Max
from geopy.geocoders import Nominatim
import reverse_geocoder as rg
from time import sleep
def get_all_constants(): def get_all_constants():
sats = [sat.name for sat in Satellite.objects.all()] sats = [sat.name for sat in Satellite.objects.all()]
@@ -46,7 +52,7 @@ def remove_str(s: str):
return float(s.strip().replace(",", ".")) return float(s.strip().replace(",", "."))
return s return s
def fill_data_from_df(df: pd.DataFrame, sat: Satellite): def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
try: try:
df.rename(columns={'Модуляция ': 'Модуляция'}, inplace=True) df.rename(columns={'Модуляция ': 'Модуляция'}, inplace=True)
except Exception as e: except Exception as e:
@@ -74,11 +80,15 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite):
freq = remove_str(stroka[1]['Частота, МГц']) freq = remove_str(stroka[1]['Частота, МГц'])
freq_line = remove_str(stroka[1]['Полоса, МГц']) freq_line = remove_str(stroka[1]['Полоса, МГц'])
v = remove_str(stroka[1]['Символьная скорость, БОД']) v = remove_str(stroka[1]['Символьная скорость, БОД'])
try:
mod_obj, _ = Modulation.objects.get_or_create(name=stroka[1]['Модуляция'].strip()) mod_obj, _ = Modulation.objects.get_or_create(name=stroka[1]['Модуляция'].strip())
except AttributeError:
mod_obj, _ = Modulation.objects.get_or_create(name='-')
snr = remove_str(stroka[1]['ОСШ']) snr = remove_str(stroka[1]['ОСШ'])
date = stroka[1]['Дата'].date() date = stroka[1]['Дата'].date()
time_ = stroka[1]['Время'] time_ = stroka[1]['Время']
if isinstance(time_, str): if isinstance(time_, str):
time_ = time_.strip()
time_ = time(0,0,0) time_ = time(0,0,0)
timestamp = datetime.combine(date, time_) timestamp = datetime.combine(date, time_)
current_mirrors = [] current_mirrors = []
@@ -101,8 +111,9 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite):
location = stroka[1]['Местоопределение'].strip() location = stroka[1]['Местоопределение'].strip()
comment = stroka[1]['Комментарий'] comment = stroka[1]['Комментарий']
source = stroka[1]['Объект наблюдения'] source = stroka[1]['Объект наблюдения']
user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
vch_load_obj, vch_created = Parameter.objects.get_or_create( vch_load_obj, _ = Parameter.objects.get_or_create(
id_satellite=sat, id_satellite=sat,
polarization=polarization_obj, polarization=polarization_obj,
frequency=freq, frequency=freq,
@@ -110,7 +121,6 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite):
bod_velocity=v, bod_velocity=v,
modulation=mod_obj, modulation=mod_obj,
snr=snr, snr=snr,
defaults={'id_user_add': CustomUser.objects.get(id=1)}
) )
geo, _ = Geo.objects.get_or_create( geo, _ = Geo.objects.get_or_create(
@@ -122,23 +132,24 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite):
'location': location, 'location': location,
'comment': comment, 'comment': comment,
'is_average': (comment != -1.0), 'is_average': (comment != -1.0),
'id_user_add': CustomUser.objects.get(id=1)
} }
) )
geo.save() geo.save()
geo.mirrors.set(Mirror.objects.filter(name__in=current_mirrors)) geo.mirrors.set(Mirror.objects.filter(name__in=current_mirrors))
obj_item, _ = ObjItem.objects.get_or_create( existing_obj_items = ObjItem.objects.filter(
id_geo=geo, parameters_obj=vch_load_obj,
id_vch_load=vch_load_obj, geo_obj=geo
defaults={
'name': source,
'id_user_add': CustomUser.objects.get(id=1),
# 'id_satellite': sat
}
) )
if not existing_obj_items.exists():
obj_item = ObjItem.objects.create(
name=source,
created_by=user_to_use
)
obj_item.parameters_obj.set([vch_load_obj])
geo.objitem = obj_item
geo.save()
obj_item.save()
def add_satellite_list(): def add_satellite_list():
@@ -191,18 +202,8 @@ def get_point_from_json(filepath: str):
def get_points_from_csv(file_content): def get_points_from_csv(file_content, current_user=None):
import io df = pd.read_csv(io.StringIO(file_content), sep=";",
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']) 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[['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') df['time'] = pd.to_datetime(df['time'], format='%d.%m.%Y %H:%M:%S')
@@ -238,12 +239,14 @@ def get_points_from_csv(file_content):
name=row['mir_3'] name=row['mir_3']
) )
user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
vch_load_obj, _ = Parameter.objects.get_or_create( vch_load_obj, _ = Parameter.objects.get_or_create(
id_satellite=sat_obj, id_satellite=sat_obj,
polarization=pol_obj, polarization=pol_obj,
frequency=row['freq'], frequency=row['freq'],
freq_range=row['f_range'], freq_range=row['f_range'],
defaults={'id_user_add': CustomUser.objects.get(id=1)} # defaults={'id_user_add': user_to_use}
) )
geo_obj, _ = Geo.objects.get_or_create( geo_obj, _ = Geo.objects.get_or_create(
@@ -251,88 +254,39 @@ def get_points_from_csv(file_content):
coords=Point(row['lon'], row['lat'], srid=4326), coords=Point(row['lon'], row['lat'], srid=4326),
defaults={ defaults={
'is_average': False, 'is_average': False,
'id_user_add': CustomUser.objects.get(id=1), # 'id_user_add': user_to_use,
} }
) )
geo_obj.mirrors.set(Mirror.objects.filter(name__in=mir_lst)) geo_obj.mirrors.set(Mirror.objects.filter(name__in=mir_lst))
obj_item_obj, _ = ObjItem.objects.get_or_create( existing_obj_items = ObjItem.objects.filter(
name=row['obj'], parameters_obj=vch_load_obj,
# id_satellite=sat_obj, geo_obj=geo_obj
id_vch_load=vch_load_obj,
id_geo=geo_obj,
defaults={
'id_user_add': CustomUser.objects.get(id=1)
}
) )
obj_item_obj.save() if not existing_obj_items.exists():
# df = pd.read_csv(filepath, sep=";", obj_item = ObjItem.objects.create(
# names=['id', 'obj', 'lat', 'lon', 'h', 'time', 'sat', 'norad_id', 'freq', 'f_range', 'et', 'qaul', 'mir_1', 'mir_2', 'mir_3']) name=row['obj'],
# df[['lat', 'lon', 'freq', 'f_range']] = df[['lat', 'lon', 'freq', 'f_range']].replace(',', '.', regex=True).astype(float) created_by=user_to_use
# df['time'] = pd.to_datetime(df['time'], format='%d.%m.%Y %H:%M:%S') )
# for row in df.iterrows(): obj_item.parameters_obj.set([vch_load_obj])
# row = row[1] geo_obj.objitem = obj_item
# match row['obj'].split(' ')[-1]: geo_obj.save()
# 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: def get_vch_load_from_html(file, sat: Satellite) -> None:
filename = file.name.split('_')
transfer = filename[3]
match filename[2]:
case 'H':
pol = 'Горизонтальная'
case 'V':
pol = 'Вертикальная'
case 'R':
pol = 'Правая'
case 'L':
pol = 'Левая'
case _:
pol = '-'
tables = pd.read_html(file, encoding='windows-1251') tables = pd.read_html(file, encoding='windows-1251')
df = tables[0] df = tables[0]
df = df.drop(0).reset_index(drop=True) df = df.drop(0).reset_index(drop=True)
@@ -362,6 +316,10 @@ def get_vch_load_from_html(file, sat: Satellite) -> None:
else: else:
pack = None pack = None
polarization, _ = Polarization.objects.get_or_create(
name=pol
)
mod, _ = Modulation.objects.get_or_create( mod, _ = Modulation.objects.get_or_create(
name=value['Модуляция'] name=value['Модуляция']
) )
@@ -372,7 +330,10 @@ def get_vch_load_from_html(file, sat: Satellite) -> None:
id_satellite=sat, id_satellite=sat,
frequency=value['Частота, МГц'], frequency=value['Частота, МГц'],
freq_range=value['Полоса, МГц'], freq_range=value['Полоса, МГц'],
polarization=polarization,
defaults={ defaults={
"transfer": float(transfer),
# "polarization": polarization,
"status": value['Статус'], "status": value['Статус'],
"power": value['Мощность, дБм'], "power": value['Мощность, дБм'],
"bod_velocity": bod_velocity, "bod_velocity": bod_velocity,
@@ -386,30 +347,79 @@ def get_vch_load_from_html(file, sat: Satellite) -> None:
) )
sigma_load.save() 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): 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) item_obj = ObjItem.objects.filter(parameters_obj__id_satellite=sat_id)
vch_sigma = SigmaParameter.objects.filter(id_satellite=sat_id) vch_sigma = SigmaParameter.objects.filter(id_satellite=sat_id)
link_count = 0 link_count = 0
obj_count = len(item_obj) obj_count = len(item_obj)
for idx, obj in enumerate(item_obj): for idx, obj in enumerate(item_obj):
vch_load = obj.id_vch_load vch_load = obj.parameters_obj.get()
if vch_load.frequency == -1.0: if vch_load.frequency == -1.0:
continue continue
# if unique_points = Point.objects.order_by('frequency').distinct('frequency')
for sigma in vch_sigma: 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: if (
abs(sigma.transfer_frequency - vch_load.frequency) <= eps_freq and
abs(sigma.freq_range - vch_load.freq_range) <= vch_load.freq_range*eps_frange/100 and
sigma.polarization == vch_load.polarization
):
sigma.parameter = vch_load sigma.parameter = vch_load
sigma.save() sigma.save()
link_count += 1 link_count += 1
return obj_count, link_count return obj_count, link_count
def kub_report(data_in: io.StringIO) -> pd.DataFrame:
df_in = pd.read_excel(data_in)
df = pd.DataFrame(columns=['Дата', 'Широта', 'Долгота',
'Высота', 'Населённый пункт', 'ИСЗ',
'Прямой канал, МГц', 'Обратный канал, МГц', 'Перенос, МГц', 'Полоса, МГц', 'Зеркала'])
for row in df_in.iterrows():
value = row[1]
date = datetime.date(datetime.now())
isz = value['ИСЗ']
try:
lat = float(value['Широта, град'].strip().replace(',', '.'))
lon = float(value['Долгота, град'].strip().replace(',', '.'))
downlink = float(value['Обратный канал, МГц'].strip().replace(',', '.'))
freq_range = float(value['Полоса, МГц'].strip().replace(',', '.'))
except Exception as e:
lat = value['Широта, град']
lon = value['Долгота, град']
downlink = value['Обратный канал, МГц']
freq_range = value['Полоса, МГц']
print(e)
norad = int(re.findall(r'\((\d+)\)', isz)[0])
sat_obj = Satellite.objects.get(norad=norad)
pol_obj = Polarization.objects.get(name=value['Поляризация'].strip())
transponder = Transponders.objects.filter(
sat_id=sat_obj,
polarization=pol_obj,
downlink__gte=downlink - F('frequency_range')/2,
downlink__lte=downlink + F('frequency_range')/2,
).first()
# try:
# location = geolocator.reverse(f"{lat}, {lon}", language="ru").raw['address']
# loc_name = location.get('city', '') or location.get('town', '') or location.get('province', '') or location.get('country', '')
# except AttributeError:
# loc_name = ''
# sleep(1)
loc_name = ''
if transponder: #and not (len(transponder) > 1):
transfer = transponder.transfer
uplink = transfer + downlink
new_row = pd.DataFrame([{'Дата': date,
'Широта': lat,
'Долгота': lon,
'Высота': 0.0,
'Населённый пункт': loc_name,
'ИСЗ': isz,
'Прямой канал, МГц': uplink,
'Обратный канал, МГц': downlink,
'Перенос, МГц': transfer,
'Полоса, МГц': freq_range,
'Зеркала': ''
}])
df = pd.concat([df, new_row], ignore_index=True)
else:
print("Ничего не найдено в транспондерах")
return df

View File

@@ -1,45 +1,100 @@
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
from django.contrib import messages from django.contrib import messages
from django.http import JsonResponse from django.http import JsonResponse, HttpResponse
from django.views.decorators.http import require_GET from django.views.decorators.http import require_GET
from django.contrib.admin.views.decorators import staff_member_required from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views import View from django.views import View
from django.views.generic import TemplateView, FormView from django.db.models import OuterRef, Subquery
from django.contrib.auth.mixins import UserPassesTestMixin from django.views.generic import TemplateView, FormView, UpdateView, DeleteView, CreateView
from django.contrib.auth.mixins import UserPassesTestMixin, LoginRequiredMixin
from django.contrib.auth import logout
from django.forms import inlineformset_factory, modelformset_factory
from django.db import models
from django.urls import reverse_lazy
from django.contrib.gis.geos import Point
import pandas as pd import pandas as pd
from .utils import ( from .utils import (
fill_data_from_df, fill_data_from_df,
add_satellite_list, add_satellite_list,
get_points_from_csv, get_points_from_csv,
get_vch_load_from_html, get_vch_load_from_html,
compare_and_link_vch_load compare_and_link_vch_load,
kub_report
) )
from mapsapp.utils import parse_transponders_from_json from mapsapp.utils import parse_transponders_from_json, parse_transponders_from_xml
from .forms import LoadExcelData, LoadCsvData, UploadFileForm, VchLinkForm from .forms import (
from .models import ObjItem LoadExcelData,
LoadCsvData,
UploadFileForm,
VchLinkForm,
UploadVchLoad,
NewEventForm,
ObjItemForm,
ParameterForm,
GeoForm
)
from .models import ObjItem, Modulation, Polarization
from .clusters import get_clusters from .clusters import get_clusters
from dbapp.settings import BASE_DIR from io import BytesIO
from datetime import datetime
class AddSatellitesView(View):
class AddSatellitesView(LoginRequiredMixin, View):
def get(self, request): def get(self, request):
add_satellite_list() add_satellite_list()
return redirect('home') return redirect('home')
class AddTranspondersView(View): # class AddTranspondersView(View):
def get(self, request): # def get(self, request):
# try:
# parse_transponders_from_json(BASE_DIR / "transponders.json")
# except FileNotFoundError:
# print("Файл не найден")
# return redirect('home')
class AddTranspondersView(LoginRequiredMixin, FormView):
template_name = 'mainapp/transponders_upload.html'
form_class = UploadFileForm
def form_valid(self, form):
uploaded_file = self.request.FILES['file']
try: try:
parse_transponders_from_json(BASE_DIR / "transponders.json") content = uploaded_file.read()
except FileNotFoundError: parse_transponders_from_xml(BytesIO(content))
print("Файл не найден") messages.success(self.request, "Файл успешно обработан")
return redirect('home') except ValueError as e:
messages.error(self.request, f"Ошибка при чтении таблиц: {e}")
except Exception as e:
messages.error(self.request, f"Неизвестная ошибка: {e}")
return redirect('add_trans')
class HomePageView(TemplateView): def form_invalid(self, form):
template_name = 'mainapp/home.html' messages.error(self.request, "Форма заполнена некорректно.")
return super().form_invalid(form)
from django.views.generic import View
class ActionsPageView(View):
def get(self, request):
if request.user.is_authenticated:
return render(request, 'mainapp/actions.html')
else:
return render(request, 'mainapp/login_required.html')
class LoadExcelDataView(FormView): class HomePageView(View):
def get(self, request):
if request.user.is_authenticated:
# Redirect to objitem list if authenticated
return redirect('objitem_list')
else:
return render(request, 'mainapp/login_required.html')
class LoadExcelDataView(LoginRequiredMixin, FormView):
template_name = 'mainapp/add_data_from_excel.html' template_name = 'mainapp/add_data_from_excel.html'
form_class = LoadExcelData form_class = LoadExcelData
@@ -53,7 +108,7 @@ class LoadExcelDataView(FormView):
df = pd.read_excel(io.BytesIO(uploaded_file.read())) df = pd.read_excel(io.BytesIO(uploaded_file.read()))
if number > 0: if number > 0:
df = df.head(number) df = df.head(number)
result = fill_data_from_df(df, selected_sat) result = fill_data_from_df(df, selected_sat, self.request.user.customuser)
messages.success(self.request, f"Данные успешно загружены! Обработано строк: {result}") messages.success(self.request, f"Данные успешно загружены! Обработано строк: {result}")
return redirect('load_excel_data') return redirect('load_excel_data')
@@ -67,26 +122,30 @@ class LoadExcelDataView(FormView):
from django.views.generic import View from django.views.generic import View
from django.core.paginator import Paginator
from django.db.models import Prefetch
from .models import Satellite, ObjItem, Parameter, Geo
class GetLocationsView(View): class GetLocationsView(LoginRequiredMixin, View):
def get(self, request, sat_id): def get(self, request, sat_id):
locations = ObjItem.objects.filter(id_vch_load__id_satellite=sat_id) locations = ObjItem.objects.filter(parameters_obj__id_satellite=sat_id)
if not locations: if not locations:
return JsonResponse({'error': 'Объектов не найдено'}, status=400) return JsonResponse({'error': 'Объектов не найдено'}, status=400)
features = [] features = []
for loc in locations: for loc in locations:
param = loc.parameters_obj.get()
features.append({ features.append({
"type": "Feature", "type": "Feature",
"geometry": { "geometry": {
"type": "Point", "type": "Point",
"coordinates": [loc.id_geo.coords[0], loc.id_geo.coords[1]] "coordinates": [loc.geo_obj.coords[0], loc.geo_obj.coords[1]]
}, },
"properties": { "properties": {
"pol": loc.id_vch_load.polarization.name, "pol": param.polarization.name,
"freq": loc.id_vch_load.frequency*1000000, "freq": param.frequency*1000000,
"name": f"{loc.name}", "name": f"{loc.name}",
"id": loc.id_geo.id "id": loc.geo_obj.id
} }
}) })
@@ -95,7 +154,7 @@ class GetLocationsView(View):
"features": features "features": features
}) })
class LoadCsvDataView(FormView): class LoadCsvDataView(LoginRequiredMixin, FormView):
template_name = 'mainapp/add_data_from_csv.html' template_name = 'mainapp/add_data_from_csv.html'
form_class = LoadCsvData form_class = LoadCsvData
@@ -107,7 +166,7 @@ class LoadCsvDataView(FormView):
if isinstance(content, bytes): if isinstance(content, bytes):
content = content.decode('utf-8') content = content.decode('utf-8')
get_points_from_csv(content) get_points_from_csv(content, self.request.user.customuser)
messages.success(self.request, f"Данные успешно загружены!") messages.success(self.request, f"Данные успешно загружены!")
return redirect('load_csv_data') return redirect('load_csv_data')
except Exception as e: except Exception as e:
@@ -118,22 +177,7 @@ class LoadCsvDataView(FormView):
messages.error(self.request, "Форма заполнена некорректно.") messages.error(self.request, "Форма заполнена некорректно.")
return super().form_invalid(form) return super().form_invalid(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 from collections import defaultdict
@method_decorator(staff_member_required, name='dispatch') @method_decorator(staff_member_required, name='dispatch')
@@ -146,12 +190,19 @@ class ShowMapView(UserPassesTestMixin, View):
points = [] points = []
if ids: if ids:
id_list = [int(x) for x in ids.split(',') if x.isdigit()] id_list = [int(x) for x in ids.split(',') if x.isdigit()]
locations = ObjItem.objects.filter(id__in=id_list) locations = ObjItem.objects.filter(id__in=id_list).prefetch_related(
'parameters_obj__id_satellite',
'parameters_obj__polarization',
'parameters_obj__modulation',
'parameters_obj__standard',
'geo_obj'
)
for obj in locations: for obj in locations:
param = obj.parameters_obj.get()
points.append({ points.append({
'name': f"{obj.name}", 'name': f"{obj.name}",
'freq': f"{obj.id_vch_load.frequency} [{obj.id_vch_load.freq_range}] МГц", 'freq': f"{param.frequency} [{param.freq_range}] МГц",
'point': (obj.id_geo.coords.x, obj.id_geo.coords.y) 'point': (obj.geo_obj.coords.x, obj.geo_obj.coords.y)
}) })
else: else:
return redirect('admin') return redirect('admin')
@@ -162,7 +213,6 @@ class ShowMapView(UserPassesTestMixin, View):
'frequency': p["freq"] 'frequency': p["freq"]
}) })
# Преобразуем в список словарей для удобства в шаблоне
groups = [ groups = [
{ {
"name": name, "name": name,
@@ -178,19 +228,71 @@ class ShowMapView(UserPassesTestMixin, View):
return render(request, 'admin/map_custom.html', context) return render(request, 'admin/map_custom.html', context)
class ClusterTestView(View): class ShowSelectedObjectsMapView(LoginRequiredMixin, View):
def get(self, 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).prefetch_related(
'parameters_obj__id_satellite',
'parameters_obj__polarization',
'parameters_obj__modulation',
'parameters_obj__standard',
'geo_obj'
)
for obj in locations:
param = obj.parameters_obj.get()
points.append({
'name': f"{obj.name}",
'freq': f"{param.frequency} [{param.freq_range}] МГц",
'point': (obj.geo_obj.coords.x, obj.geo_obj.coords.y)
})
else:
return redirect('objitem_list')
# Group points by object name
from collections import defaultdict
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, 'mainapp/objitem_map.html', context)
class ClusterTestView(LoginRequiredMixin, View):
def get(self, request): def get(self, request):
objs = ObjItem.objects.filter(name__icontains="! Astra 4A 12654,040 [1,962] МГц H") objs = ObjItem.objects.filter(name__icontains="! Astra 4A 12654,040 [1,962] МГц H")
coords = [] coords = []
for obj in objs: for obj in objs:
coords.append((obj.id_geo.coords[1], obj.id_geo.coords[0])) if obj.geo_obj and obj.geo_obj.coords:
coords.append((obj.geo_obj.coords.coords[1], obj.geo_obj.coords.coords[0]))
get_clusters(coords) get_clusters(coords)
return JsonResponse({"success": "ок"}) return JsonResponse({"success": "ок"})
class UploadVchLoadView(FormView):
def custom_logout(request):
logout(request)
return redirect('home')
class UploadVchLoadView(LoginRequiredMixin, FormView):
template_name = 'mainapp/upload_html.html' template_name = 'mainapp/upload_html.html'
form_class = UploadFileForm form_class = UploadVchLoad
def form_valid(self, form): def form_valid(self, form):
selected_sat = form.cleaned_data['sat_choice'] selected_sat = form.cleaned_data['sat_choice']
@@ -209,19 +311,608 @@ class UploadVchLoadView(FormView):
return super().form_invalid(form) return super().form_invalid(form)
class LinkVchSigmaView(FormView): class LinkVchSigmaView(LoginRequiredMixin, FormView):
template_name = 'mainapp/link_vch.html' template_name = 'mainapp/link_vch.html'
form_class = VchLinkForm form_class = VchLinkForm
def form_valid(self, form): def form_valid(self, form):
freq = form.cleaned_data['value1'] freq = form.cleaned_data['value1']
freq_range = form.cleaned_data['value2'] freq_range = form.cleaned_data['value2']
ku_range = float(form.cleaned_data['ku_range']) # ku_range = float(form.cleaned_data['ku_range'])
sat_id = form.cleaned_data['sat_choice'] sat_id = form.cleaned_data['sat_choice']
# print(freq, freq_range, ku_range, sat_id.pk) # 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) count_all, link_count = compare_and_link_vch_load(sat_id, freq, freq_range, 1)
messages.success(self.request, f"Привязано {link_count} из {count_all} объектов") messages.success(self.request, f"Привязано {link_count} из {count_all} объектов")
return redirect('link_vch_sigma') return redirect('link_vch_sigma')
def form_invalid(self, form): def form_invalid(self, form):
return self.render_to_response(self.get_context_data(form=form)) return self.render_to_response(self.get_context_data(form=form))
class ProcessKubsatView(LoginRequiredMixin, FormView):
template_name = 'mainapp/process_kubsat.html'
form_class = NewEventForm
def form_valid(self, form):
# selected_sat = form.cleaned_data['sat_choice']
# selected_pol = form.cleaned_data['pol_choice']
uploaded_file = self.request.FILES['file']
try:
content = uploaded_file.read()
df = kub_report(BytesIO(content))
output = BytesIO()
with pd.ExcelWriter(output, engine='openpyxl') as writer:
df.to_excel(writer, index=False, sheet_name='Результат')
output.seek(0)
response = HttpResponse(
output.getvalue(),
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
)
response['Content-Disposition'] = f'attachment; filename="kubsat_report.xlsx"'
messages.success(self.request, "Событие успешно обработано!")
return response
except Exception as e:
messages.error(self.request, f"Ошибка при обработке файла: {str(e)}")
return redirect('kubsat_excel')
# return redirect('kubsat_excel')
def form_invalid(self, form):
messages.error(self.request, "Форма заполнена некорректно.")
return super().form_invalid(form)
class DeleteSelectedObjectsView(LoginRequiredMixin, View):
def post(self, request):
if request.user.customuser.role not in ['admin', 'moderator']:
return JsonResponse({'error': 'У вас нет прав для удаления объектов'}, status=403)
ids = request.POST.get('ids', '')
if not ids:
return JsonResponse({'error': 'Нет ID для удаления'}, status=400)
try:
id_list = [int(x) for x in ids.split(',') if x.isdigit()]
deleted_count, _ = ObjItem.objects.filter(id__in=id_list).delete()
return JsonResponse({
'success': True,
'message': 'Объект успешно удалён',
# 'deleted_count': deleted_count
})
except Exception as e:
return JsonResponse({'error': f'Ошибка при удалении: {str(e)}'}, status=500)
from django.contrib.auth.mixins import LoginRequiredMixin
class ObjItemListView(LoginRequiredMixin, View):
def get(self, request):
satellites = Satellite.objects.filter(parameters__objitems__isnull=False).distinct().order_by('name')
selected_sat_id = request.GET.get('satellite_id')
page_number = request.GET.get('page', 1)
items_per_page = request.GET.get('items_per_page', '50')
sort_param = request.GET.get('sort', '')
freq_min = request.GET.get('freq_min')
freq_max = request.GET.get('freq_max')
range_min = request.GET.get('range_min')
range_max = request.GET.get('range_max')
snr_min = request.GET.get('snr_min')
snr_max = request.GET.get('snr_max')
bod_min = request.GET.get('bod_min')
bod_max = request.GET.get('bod_max')
search_query = request.GET.get('search')
selected_modulations = request.GET.getlist('modulation')
selected_polarizations = request.GET.getlist('polarization')
selected_satellites = request.GET.getlist('satellite_id')
has_kupsat = request.GET.get('has_kupsat')
has_valid = request.GET.get('has_valid')
try:
items_per_page = int(items_per_page)
except ValueError:
items_per_page = 50
objects = ObjItem.objects.none()
if selected_satellites or selected_sat_id:
if selected_sat_id and not selected_satellites:
try:
selected_sat_id_single = int(selected_sat_id)
selected_satellites = [selected_sat_id_single]
except ValueError:
selected_satellites = []
if selected_satellites:
objects = ObjItem.objects.select_related(
'geo_obj',
'updated_by__user',
'created_by__user',
).prefetch_related(
'parameters_obj__id_satellite',
'parameters_obj__polarization',
'parameters_obj__modulation',
'parameters_obj__standard'
).filter(parameters_obj__id_satellite_id__in=selected_satellites)
else:
objects = ObjItem.objects.select_related(
'geo_obj',
'updated_by__user',
'created_by__user',
).prefetch_related(
'parameters_obj__id_satellite',
'parameters_obj__polarization',
'parameters_obj__modulation',
'parameters_obj__standard'
)
if freq_min is not None and freq_min.strip() != '':
try:
freq_min_val = float(freq_min)
objects = objects.filter(parameters_obj__frequency__gte=freq_min_val)
except ValueError:
pass
if freq_max is not None and freq_max.strip() != '':
try:
freq_max_val = float(freq_max)
objects = objects.filter(parameters_obj__frequency__lte=freq_max_val)
except ValueError:
pass
if range_min is not None and range_min.strip() != '':
try:
range_min_val = float(range_min)
objects = objects.filter(parameters_obj__freq_range__gte=range_min_val)
except ValueError:
pass
if range_max is not None and range_max.strip() != '':
try:
range_max_val = float(range_max)
objects = objects.filter(parameters_obj__freq_range__lte=range_max_val)
except ValueError:
pass
if snr_min is not None and snr_min.strip() != '':
try:
snr_min_val = float(snr_min)
objects = objects.filter(parameters_obj__snr__gte=snr_min_val)
except ValueError:
pass
if snr_max is not None and snr_max.strip() != '':
try:
snr_max_val = float(snr_max)
objects = objects.filter(parameters_obj__snr__lte=snr_max_val)
except ValueError:
pass
if bod_min is not None and bod_min.strip() != '':
try:
bod_min_val = float(bod_min)
objects = objects.filter(parameters_obj__bod_velocity__gte=bod_min_val)
except ValueError:
pass
if bod_max is not None and bod_max.strip() != '':
try:
bod_max_val = float(bod_max)
objects = objects.filter(parameters_obj__bod_velocity__lte=bod_max_val)
except ValueError:
pass
if selected_modulations:
objects = objects.filter(parameters_obj__modulation__id__in=selected_modulations)
if selected_polarizations:
objects = objects.filter(parameters_obj__polarization__id__in=selected_polarizations)
if has_kupsat == '1':
objects = objects.filter(geo_obj__coords_kupsat__isnull=False)
elif has_kupsat == '0':
objects = objects.filter(geo_obj__coords_kupsat__isnull=True)
if has_valid == '1':
objects = objects.filter(geo_obj__coords_valid__isnull=False)
elif has_valid == '0':
objects = objects.filter(geo_obj__coords_valid__isnull=True)
if search_query:
search_query = search_query.strip()
if search_query:
objects = objects.filter(
models.Q(name__icontains=search_query) |
models.Q(geo_obj__location__icontains=search_query)
)
else:
selected_sat_id = None
first_param_freq_subq = self.get_first_param_subquery('frequency')
first_param_range_subq = self.get_first_param_subquery('freq_range')
first_param_snr_subq = self.get_first_param_subquery('snr')
first_param_bod_subq = self.get_first_param_subquery('bod_velocity')
first_param_sat_name_subq = self.get_first_param_subquery('id_satellite__name')
first_param_pol_name_subq = self.get_first_param_subquery('polarization__name')
first_param_mod_name_subq = self.get_first_param_subquery('modulation__name')
objects = objects.annotate(
first_param_freq=Subquery(first_param_freq_subq),
first_param_range=Subquery(first_param_range_subq),
first_param_snr=Subquery(first_param_snr_subq),
first_param_bod=Subquery(first_param_bod_subq),
first_param_sat_name=Subquery(first_param_sat_name_subq),
first_param_pol_name=Subquery(first_param_pol_name_subq),
first_param_mod_name=Subquery(first_param_mod_name_subq),
)
valid_sort_fields = {
'name': 'name',
'-name': '-name',
'updated_at': 'updated_at',
'-updated_at': '-updated_at',
'created_at': 'created_at',
'-created_at': '-created_at',
'updated_by': 'updated_by__user__username',
'-updated_by': '-updated_by__user__username',
'created_by': 'created_by__user__username',
'-created_by': '-created_by__user__username',
'geo_timestamp': 'geo_obj__timestamp',
'-geo_timestamp': '-geo_obj__timestamp',
'frequency': 'first_param_freq',
'-frequency': '-first_param_freq',
'freq_range': 'first_param_range',
'-freq_range': '-first_param_range',
'snr': 'first_param_snr',
'-snr': '-first_param_snr',
'bod_velocity': 'first_param_bod',
'-bod_velocity': '-first_param_bod',
'satellite': 'first_param_sat_name',
'-satellite': '-first_param_sat_name',
'polarization': 'first_param_pol_name',
'-polarization': '-first_param_pol_name',
'modulation': 'first_param_mod_name',
'-modulation': '-first_param_mod_name',
}
if sort_param in valid_sort_fields:
objects = objects.order_by(valid_sort_fields[sort_param])
paginator = Paginator(objects, items_per_page)
page_obj = paginator.get_page(page_number)
processed_objects = []
for obj in page_obj:
param = None
if hasattr(obj, 'parameters_obj') and obj.parameters_obj.all():
param_list = list(obj.parameters_obj.all())
if param_list:
param = param_list[0]
geo_coords = "-"
geo_timestamp = "-"
geo_location = "-"
kupsat_coords = "-"
valid_coords = "-"
distance_geo_kup = "-"
distance_geo_valid = "-"
distance_kup_valid = "-"
if obj.geo_obj:
geo_timestamp = obj.geo_obj.timestamp
geo_location = obj.geo_obj.location
if obj.geo_obj.coords:
longitude = obj.geo_obj.coords.coords[0]
latitude = obj.geo_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"
geo_coords = f"{lat} {lon}"
if obj.geo_obj.coords_kupsat:
longitude = obj.geo_obj.coords_kupsat.coords[0]
latitude = obj.geo_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"
kupsat_coords = f"{lat} {lon}"
elif obj.geo_obj.coords_kupsat is not None:
kupsat_coords = "-"
if obj.geo_obj.coords_valid:
longitude = obj.geo_obj.coords_valid.coords[0]
latitude = obj.geo_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"
valid_coords = f"{lat} {lon}"
elif obj.geo_obj.coords_valid is not None:
valid_coords = "-"
if obj.geo_obj.distance_coords_kup is not None:
distance_geo_kup = f"{obj.geo_obj.distance_coords_kup:.3f}"
if obj.geo_obj.distance_coords_valid is not None:
distance_geo_valid = f"{obj.geo_obj.distance_coords_valid:.3f}"
if obj.geo_obj.distance_kup_valid is not None:
distance_kup_valid = f"{obj.geo_obj.distance_kup_valid:.3f}"
satellite_name = "-"
frequency = "-"
freq_range = "-"
polarization_name = "-"
bod_velocity = "-"
modulation_name = "-"
snr = "-"
if param:
if hasattr(param, 'id_satellite') and param.id_satellite:
satellite_name = param.id_satellite.name if hasattr(param.id_satellite, 'name') else "-"
frequency = f"{param.frequency:.3f}" if param.frequency is not None else "-"
freq_range = f"{param.freq_range:.3f}" if param.freq_range is not None else "-"
bod_velocity = f"{param.bod_velocity:.0f}" if param.bod_velocity is not None else "-"
snr = f"{param.snr:.0f}" if param.snr is not None else "-"
if hasattr(param, 'polarization') and param.polarization:
polarization_name = param.polarization.name if hasattr(param.polarization, 'name') else "-"
if hasattr(param, 'modulation') and param.modulation:
modulation_name = param.modulation.name if hasattr(param.modulation, 'name') else "-"
processed_objects.append({
'id': obj.id,
'name': obj.name or "-",
'satellite_name': satellite_name,
'frequency': frequency,
'freq_range': freq_range,
'polarization': polarization_name,
'bod_velocity': bod_velocity,
'modulation': modulation_name,
'snr': snr,
'geo_timestamp': geo_timestamp,
'geo_location': geo_location,
'geo_coords': geo_coords,
'kupsat_coords': kupsat_coords,
'valid_coords': valid_coords,
'distance_geo_kup': distance_geo_kup,
'distance_geo_valid': distance_geo_valid,
'distance_kup_valid': distance_kup_valid,
'updated_by': obj.updated_by if obj.updated_by else '-',
'obj': obj
})
modulations = Modulation.objects.all()
polarizations = Polarization.objects.all()
context = {
'satellites': satellites,
'selected_satellite_id': selected_sat_id,
'page_obj': page_obj,
'processed_objects': processed_objects,
'items_per_page': items_per_page,
'available_items_per_page': [50, 100, 500, 1000],
'freq_min': freq_min,
'freq_max': freq_max,
'range_min': range_min,
'range_max': range_max,
'snr_min': snr_min,
'snr_max': snr_max,
'bod_min': bod_min,
'bod_max': bod_max,
'search_query': search_query,
'selected_modulations': [int(x) for x in selected_modulations if x.isdigit()],
'selected_polarizations': [int(x) for x in selected_polarizations if x.isdigit()],
'selected_satellites': [int(x) for x in selected_satellites if x.isdigit()],
'has_kupsat': has_kupsat,
'has_valid': has_valid,
'modulations': modulations,
'polarizations': polarizations,
'full_width_page': True,
'sort': sort_param,
}
return render(request, 'mainapp/objitem_list.html', context)
def get_first_param_subquery(self, field_name):
return Parameter.objects.filter(
objitems=OuterRef('pk')
).order_by('id').values(field_name)[:1]
class ObjItemUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
model = ObjItem
form_class = ObjItemForm
template_name = 'mainapp/objitem_form.html'
success_url = reverse_lazy('home')
def test_func(self):
return self.request.user.customuser.role in ['admin', 'moderator']
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['LEAFLET_CONFIG'] = {
'DEFAULT_CENTER': (55.75, 37.62),
'DEFAULT_ZOOM': 5,
}
ParameterFormSet = modelformset_factory(
Parameter,
form=ParameterForm,
extra=0,
can_delete=True
)
if self.object:
parameter_queryset = self.object.parameters_obj.all()
context['parameter_forms'] = ParameterFormSet(
queryset=parameter_queryset,
prefix='parameters'
)
if hasattr(self.object, 'geo_obj'):
context['geo_form'] = GeoForm(instance=self.object.geo_obj, prefix='geo')
else:
context['geo_form'] = GeoForm(prefix='geo')
else:
context['parameter_forms'] = ParameterFormSet(
queryset=Parameter.objects.none(),
prefix='parameters'
)
context['geo_form'] = GeoForm(prefix='geo')
return context
def form_valid(self, form):
context = self.get_context_data()
parameter_forms = context['parameter_forms']
geo_form = context['geo_form']
# Сохраняем основной объект
self.object = form.save(commit=False)
self.object.updated_by = self.request.user.customuser
self.object.save()
# Сохраняем связанные параметры
if parameter_forms.is_valid():
instances = parameter_forms.save(commit=False)
for instance in instances:
instance.save()
instance.objitems.set([self.object])
# Сохраняем геоданные
geo_instance = None
if hasattr(self.object, 'geo_obj'):
geo_instance = self.object.geo_obj
# Создаем или обновляем гео-объект
if geo_instance is None:
geo_instance = Geo(objitem=self.object)
# Обновляем поля из geo_form
if geo_form.is_valid():
geo_instance.location = geo_form.cleaned_data['location']
geo_instance.comment = geo_form.cleaned_data['comment']
geo_instance.is_average = geo_form.cleaned_data['is_average']
# Обрабатываем координаты геолокации
geo_longitude = self.request.POST.get('geo_longitude')
geo_latitude = self.request.POST.get('geo_latitude')
if geo_longitude and geo_latitude:
geo_instance.coords = Point(float(geo_longitude), float(geo_latitude), srid=4326)
# Обрабатываем координаты Кубсата
kupsat_longitude = self.request.POST.get('kupsat_longitude')
kupsat_latitude = self.request.POST.get('kupsat_latitude')
if kupsat_longitude and kupsat_latitude:
geo_instance.coords_kupsat = Point(float(kupsat_longitude), float(kupsat_latitude), srid=4326)
# Обрабатываем координаты оперативников
valid_longitude = self.request.POST.get('valid_longitude')
valid_latitude = self.request.POST.get('valid_latitude')
if valid_longitude and valid_latitude:
geo_instance.coords_valid = Point(float(valid_longitude), float(valid_latitude), srid=4326)
# Обрабатываем дату/время
timestamp_date = self.request.POST.get('timestamp_date')
timestamp_time = self.request.POST.get('timestamp_time')
if timestamp_date and timestamp_time:
naive_datetime = datetime.strptime(f"{timestamp_date} {timestamp_time}", "%Y-%m-%d %H:%M")
geo_instance.timestamp = naive_datetime
geo_instance.save()
messages.success(self.request, 'Объект успешно сохранён!')
return super().form_valid(form)
class ObjItemCreateView(LoginRequiredMixin, UserPassesTestMixin, CreateView):
model = ObjItem
form_class = ObjItemForm
template_name = 'mainapp/objitem_form.html'
success_url = reverse_lazy('home')
def test_func(self):
return self.request.user.customuser.role in ['admin', 'moderator']
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
ParameterFormSet = modelformset_factory(
Parameter,
form=ParameterForm,
extra=1,
can_delete=True
)
context['parameter_forms'] = ParameterFormSet(
queryset=Parameter.objects.none(),
prefix='parameters'
)
context['geo_form'] = GeoForm(prefix='geo')
return context
def form_valid(self, form):
context = self.get_context_data()
parameter_forms = context['parameter_forms']
geo_form = context['geo_form']
# Сохраняем основной объект
self.object = form.save(commit=False)
self.object.created_by = self.request.user.customuser
self.object.updated_by = self.request.user.customuser
self.object.save()
# Сохраняем связанные параметры
if parameter_forms.is_valid():
instances = parameter_forms.save(commit=False)
for instance in instances:
instance.save()
instance.objitems.add(self.object)
# Создаем гео-объект
geo_instance = Geo(objitem=self.object)
# Обновляем поля из geo_form
if geo_form.is_valid():
geo_instance.location = geo_form.cleaned_data['location']
geo_instance.comment = geo_form.cleaned_data['comment']
geo_instance.is_average = geo_form.cleaned_data['is_average']
# Обрабатываем координаты геолокации
geo_longitude = self.request.POST.get('geo_longitude')
geo_latitude = self.request.POST.get('geo_latitude')
if geo_longitude and geo_latitude:
geo_instance.coords = Point(float(geo_longitude), float(geo_latitude), srid=4326)
# Обрабатываем координаты Кубсата
kupsat_longitude = self.request.POST.get('kupsat_longitude')
kupsat_latitude = self.request.POST.get('kupsat_latitude')
if kupsat_longitude and kupsat_latitude:
geo_instance.coords_kupsat = Point(float(kupsat_longitude), float(kupsat_latitude), srid=4326)
# Обрабатываем координаты оперативников
valid_longitude = self.request.POST.get('valid_longitude')
valid_latitude = self.request.POST.get('valid_latitude')
if valid_longitude and valid_latitude:
geo_instance.coords_valid = Point(float(valid_longitude), float(valid_latitude), srid=4326)
# Обрабатываем дату/время
timestamp_date = self.request.POST.get('timestamp_date')
timestamp_time = self.request.POST.get('timestamp_time')
if timestamp_date and timestamp_time:
naive_datetime = datetime.strptime(f"{timestamp_date} {timestamp_time}", "%Y-%m-%d %H:%M")
geo_instance.timestamp = naive_datetime
geo_instance.save()
messages.success(self.request, 'Объект успешно создан!')
return super().form_valid(form)
class ObjItemDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
model = ObjItem
template_name = 'mainapp/objitem_confirm_delete.html'
success_url = reverse_lazy('home')
def test_func(self):
return self.request.user.customuser.role in ['admin', 'moderator']
def delete(self, request, *args, **kwargs):
messages.success(self.request, 'Объект успешно удалён!')
return super().delete(request, *args, **kwargs)

View File

@@ -5,20 +5,24 @@ from more_admin_filters import MultiSelectDropdownFilter, MultiSelectFilter, Mul
from import_export.admin import ImportExportActionModelAdmin from import_export.admin import ImportExportActionModelAdmin
@admin.register(Transponders) @admin.register(Transponders)
class PolarizationAdmin(ImportExportActionModelAdmin, admin.ModelAdmin): class TranspondersAdmin(ImportExportActionModelAdmin, admin.ModelAdmin):
list_display = ( list_display = (
"sat_id", "sat_id",
"name", "name",
"zone_name", "zone_name",
"frequency", "downlink",
"uplink",
"frequency_range", "frequency_range",
"transfer",
"polarization", "polarization",
) )
list_filter = ( list_filter = (
("polarization", MultiSelectRelatedDropdownFilter), ("polarization", MultiSelectRelatedDropdownFilter),
("sat_id", MultiSelectRelatedDropdownFilter), ("sat_id", MultiSelectRelatedDropdownFilter),
("frequency", NumericRangeFilterBuilder()), # ("frequency", NumericRangeFilterBuilder()),
"zone_name" "zone_name"
) )
search_fields = ("name",) search_fields = ("name", "sat_id__name")
ordering = ("name",) ordering = ("name",)
# def sat_name(self, obj):
# return

View File

@@ -1,6 +1,8 @@
# Generated by Django 5.2.7 on 2025-10-13 12:47 # Generated by Django 5.2.7 on 2025-10-31 13:36
import django.db.models.deletion import django.db.models.deletion
import django.db.models.expressions
import django.db.models.functions.math
import mainapp.models import mainapp.models
from django.db import migrations, models from django.db import migrations, models
@@ -19,9 +21,11 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('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='Название транспондера')), ('name', models.CharField(blank=True, max_length=30, null=True, verbose_name='Название транспондера')),
('frequency', models.FloatField(blank=True, null=True, verbose_name='Центральная частота')), ('downlink', models.FloatField(blank=True, null=True, verbose_name='Downlink')),
('frequency_range', 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='Название зоны')), ('uplink', models.FloatField(blank=True, null=True, verbose_name='Uplink')),
('zone_name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Название зоны')),
('transfer', models.GeneratedField(db_persist=True, expression=models.ExpressionWrapper(django.db.models.functions.math.Abs(django.db.models.expressions.CombinedExpression(models.F('downlink'), '-', models.F('uplink'))), output_field=models.FloatField()), null=True, output_field=models.FloatField(), 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='Поляризация')), ('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='Спутник')), ('sat_id', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='tran_satellite', to='mainapp.satellite', verbose_name='Спутник')),
], ],

View File

@@ -1,15 +1,27 @@
from django.db import models from django.db import models
from mainapp.models import Satellite, Polarization, get_default_polarization from mainapp.models import Satellite, Polarization, get_default_polarization
from django.db.models import F, ExpressionWrapper
from django.db.models.functions import Abs
class Transponders(models.Model): class Transponders(models.Model):
name = models.CharField(max_length=30, null=True, blank=True, verbose_name="Название транспондера") name = models.CharField(max_length=30, null=True, blank=True, verbose_name="Название транспондера")
frequency = models.FloatField(blank=True, null=True, verbose_name="Центральная частота") downlink = models.FloatField(blank=True, null=True, verbose_name="Downlink")
frequency_range = 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="Название зоны") uplink = models.FloatField(blank=True, null=True, verbose_name="Uplink")
zone_name = models.CharField(max_length=255, blank=True, null=True, verbose_name="Название зоны")
polarization = models.ForeignKey( polarization = models.ForeignKey(
Polarization, default=get_default_polarization, on_delete=models.SET_DEFAULT, related_name="tran_polarizations", null=True, blank=True, verbose_name="Поляризация" 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="Спутник") sat_id = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="tran_satellite", verbose_name="Спутник")
transfer =models.GeneratedField(
expression=ExpressionWrapper(
Abs(F('downlink') - F('uplink')),
output_field=models.FloatField()
),
output_field=models.FloatField(),
db_persist=True,
null=True, blank=True, verbose_name="Перенос"
)
def __str__(self): def __str__(self):
return self.name return self.name

View File

@@ -53,9 +53,15 @@
const satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { 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' attribution: 'Tiles &copy; Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'
}); });
const street_local = L.tileLayer('http://127.0.0.1:8080/styles/basic-preview/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: 'Local Tiles'
});
street_local.addTo(map);
const baseLayers = { const baseLayers = {
"Улицы": street, "Улицы": street,
"Спутник": satellite "Спутник": satellite,
"Локально": street_local
}; };
L.control.layers(baseLayers).addTo(map); L.control.layers(baseLayers).addTo(map);
map.setMaxZoom(18); map.setMaxZoom(18);

View File

@@ -3,6 +3,7 @@ import re
import json import json
from .models import Transponders from .models import Transponders
from mainapp.models import Polarization, Satellite from mainapp.models import Polarization, Satellite
from io import BytesIO
def search_satellite_on_page(data: dict, satellite_name: str): def search_satellite_on_page(data: dict, satellite_name: str):
for pos, value in data.get('page', {}).get('positions').items(): for pos, value in data.get('page', {}).get('positions').items():
@@ -90,3 +91,68 @@ def parse_transponders_from_json(filepath: str):
) )
tran_obj.save() tran_obj.save()
from lxml import etree
def parse_transponders_from_xml(data_in: BytesIO):
tree = etree.parse(data_in)
ns = {
'i': 'http://www.w3.org/2001/XMLSchema-instance',
'ns': 'http://schemas.datacontract.org/2004/07/Geolocation.Domain.Utils.Repository.SatellitesSerialization.Memos',
'tr': 'http://schemas.datacontract.org/2004/07/Geolocation.Common.Extensions'
}
satellites = tree.xpath('//ns:SatelliteMemo', namespaces=ns)
for sat in satellites[:]:
name = sat.xpath('./ns:name/text()', namespaces=ns)[0]
if name == 'X' or 'DONT USE' in name:
continue
norad = sat.xpath('./ns:norad/text()', namespaces=ns)
beams = sat.xpath('.//ns:BeamMemo', namespaces=ns)
zones = {}
for zone in beams:
zone_name = zone.xpath('./ns:name/text()', namespaces=ns)[0] if zone.xpath('./ns:name/text()', namespaces=ns) else '-'
zones[zone.xpath('./ns:id/text()', namespaces=ns)[0]] = {
"name": zone_name,
"pol": zone.xpath('./ns:polarization/text()', namespaces=ns)[0],
}
transponders = sat.xpath('.//ns:TransponderMemo', namespaces=ns)
for transponder in transponders:
tr_id = transponder.xpath('./ns:downlinkBeamId/text()', namespaces=ns)[0]
downlink_start = float(transponder.xpath('./ns:downlinkFrequency/tr:start/text()', namespaces=ns)[0])
downlink_end = float(transponder.xpath('./ns:downlinkFrequency/tr:end/text()', namespaces=ns)[0])
uplink_start = float(transponder.xpath('./ns:uplinkFrequency/tr:start/text()', namespaces=ns)[0])
uplink_end = float(transponder.xpath('./ns:uplinkFrequency/tr:end/text()', namespaces=ns)[0])
tr_data = zones[tr_id]
# p = tr_data['pol'][0] if tr_data['pol'] else '-'
match tr_data['pol']:
case 'Horizontal':
pol = 'Горизонтальная'
case 'Vertical':
pol = 'Вертикальная'
case 'CircularRight':
pol = 'Правая'
case 'CircularLeft':
pol = 'Левая'
case _:
pol = '-'
tr_name = transponder.xpath('./ns:name/text()', namespaces=ns)[0]
pol_obj, _ = Polarization.objects.get_or_create(name=pol)
sat_obj, _ = Satellite.objects.get_or_create(
name=name,
defaults={
"norad": int(norad[0]) if norad else -1
})
trans_obj, _ = Transponders.objects.get_or_create(
polarization=pol_obj,
downlink=(downlink_start+downlink_end)/2/1000000,
uplink=(uplink_start+uplink_end)/2/1000000,
frequency_range=abs(downlink_end-downlink_start)/1000000,
name=tr_name,
defaults={
"zone_name": tr_data['name'],
"sat_id": sat_obj,
}
)
trans_obj.save()

View File

@@ -72,7 +72,7 @@ class GetTransponderOnSatIdView(View):
output.append( output.append(
{ {
"name": tran.name, "name": tran.name,
"frequency": tran.frequency, "frequency": tran.downlink,
"frequency_range": tran.frequency_range, "frequency_range": tran.frequency_range,
"zone_name": tran.zone_name, "zone_name": tran.zone_name,
"polarization": tran.polarization.name "polarization": tran.polarization.name

View File

@@ -7,6 +7,7 @@ requires-python = ">=3.13"
dependencies = [ dependencies = [
"aiosqlite>=0.21.0", "aiosqlite>=0.21.0",
"bcrypt>=5.0.0", "bcrypt>=5.0.0",
"beautifulsoup4>=4.14.2",
"django>=5.2.7", "django>=5.2.7",
"django-admin-interface>=0.30.1", "django-admin-interface>=0.30.1",
"django-admin-multiple-choice-list-filter>=0.1.1", "django-admin-multiple-choice-list-filter>=0.1.1",
@@ -19,7 +20,9 @@ dependencies = [
"django-leaflet>=0.32.0", "django-leaflet>=0.32.0",
"django-map-widgets>=0.5.1", "django-map-widgets>=0.5.1",
"django-more-admin-filters>=1.13", "django-more-admin-filters>=1.13",
"gdal", "dotenv>=0.9.9",
"geopy>=2.4.1",
"gunicorn>=23.0.0",
"lxml>=6.0.2", "lxml>=6.0.2",
"matplotlib>=3.10.7", "matplotlib>=3.10.7",
"numpy>=2.3.3", "numpy>=2.3.3",
@@ -28,9 +31,12 @@ dependencies = [
"psycopg>=3.2.10", "psycopg>=3.2.10",
"redis>=6.4.0", "redis>=6.4.0",
"requests>=2.32.5", "requests>=2.32.5",
"reverse-geocoder>=1.5.1",
"scikit-learn>=1.7.2", "scikit-learn>=1.7.2",
"selenium>=4.38.0",
"setuptools>=80.9.0", "setuptools>=80.9.0",
] ]
[tool.uv.sources]
gdal = { path = "gdal-3.10.2-cp313-cp313-win_amd64.whl" } [dependency-groups]
dev = []

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-0-circle-fill" viewBox="0 0 16 16">
<path d="M8 4.951c-1.008 0-1.629 1.09-1.629 2.895v.31c0 1.81.627 2.895 1.629 2.895s1.623-1.09 1.623-2.895v-.31c0-1.8-.621-2.895-1.623-2.895"/>
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-8.012 4.158c1.858 0 2.96-1.582 2.96-3.99V7.84c0-2.426-1.079-3.996-2.936-3.996-1.864 0-2.965 1.588-2.965 3.996v.328c0 2.42 1.09 3.99 2.941 3.99"/>
</svg>

After

Width:  |  Height:  |  Size: 476 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-0-circle" viewBox="0 0 16 16">
<path d="M7.988 12.158c-1.851 0-2.941-1.57-2.941-3.99V7.84c0-2.408 1.101-3.996 2.965-3.996 1.857 0 2.935 1.57 2.935 3.996v.328c0 2.408-1.101 3.99-2.959 3.99M8 4.951c-1.008 0-1.629 1.09-1.629 2.895v.31c0 1.81.627 2.895 1.629 2.895s1.623-1.09 1.623-2.895v-.31c0-1.8-.621-2.895-1.623-2.895"/>
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8"/>
</svg>

After

Width:  |  Height:  |  Size: 507 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-0-square-fill" viewBox="0 0 16 16">
<path d="M8 4.951c-1.008 0-1.629 1.09-1.629 2.895v.31c0 1.81.627 2.895 1.629 2.895s1.623-1.09 1.623-2.895v-.31c0-1.8-.621-2.895-1.623-2.895"/>
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm5.988 12.158c-1.851 0-2.941-1.57-2.941-3.99V7.84c0-2.408 1.101-3.996 2.965-3.996 1.857 0 2.935 1.57 2.935 3.996v.328c0 2.408-1.101 3.99-2.959 3.99"/>
</svg>

After

Width:  |  Height:  |  Size: 514 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-0-square" viewBox="0 0 16 16">
<path d="M7.988 12.158c-1.851 0-2.941-1.57-2.941-3.99V7.84c0-2.408 1.101-3.996 2.965-3.996 1.857 0 2.935 1.57 2.935 3.996v.328c0 2.408-1.101 3.99-2.959 3.99M8 4.951c-1.008 0-1.629 1.09-1.629 2.895v.31c0 1.81.627 2.895 1.629 2.895s1.623-1.09 1.623-2.895v-.31c0-1.8-.621-2.895-1.623-2.895"/>
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
</svg>

After

Width:  |  Height:  |  Size: 579 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-1-circle-fill" viewBox="0 0 16 16">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M9.283 4.002H7.971L6.072 5.385v1.271l1.834-1.318h.065V12h1.312z"/>
</svg>

After

Width:  |  Height:  |  Size: 250 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-1-circle" viewBox="0 0 16 16">
<path d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8m15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0M9.283 4.002V12H7.971V5.338h-.065L6.072 6.656V5.385l1.899-1.383z"/>
</svg>

After

Width:  |  Height:  |  Size: 279 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-1-square-fill" viewBox="0 0 16 16">
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm7.283 4.002V12H7.971V5.338h-.065L6.072 6.656V5.385l1.899-1.383z"/>
</svg>

After

Width:  |  Height:  |  Size: 286 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-1-square" viewBox="0 0 16 16">
<path d="M9.283 4.002V12H7.971V5.338h-.065L6.072 6.656V5.385l1.899-1.383z"/>
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
</svg>

After

Width:  |  Height:  |  Size: 366 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-123" viewBox="0 0 16 16">
<path d="M2.873 11.297V4.142H1.699L0 5.379v1.137l1.64-1.18h.06v5.961zm3.213-5.09v-.063c0-.618.44-1.169 1.196-1.169.676 0 1.174.44 1.174 1.106 0 .624-.42 1.101-.807 1.526L4.99 10.553v.744h4.78v-.99H6.643v-.069L8.41 8.252c.65-.724 1.237-1.332 1.237-2.27C9.646 4.849 8.723 4 7.308 4c-1.573 0-2.36 1.064-2.36 2.15v.057zm6.559 1.883h.786c.823 0 1.374.481 1.379 1.179.01.707-.55 1.216-1.421 1.21-.77-.005-1.326-.419-1.379-.953h-1.095c.042 1.053.938 1.918 2.464 1.918 1.478 0 2.642-.839 2.62-2.144-.02-1.143-.922-1.651-1.551-1.714v-.063c.535-.09 1.347-.66 1.326-1.678-.026-1.053-.933-1.855-2.359-1.845-1.5.005-2.317.88-2.348 1.898h1.116c.032-.498.498-.944 1.206-.944.703 0 1.206.435 1.206 1.07.005.64-.504 1.106-1.2 1.106h-.75z"/>
</svg>

After

Width:  |  Height:  |  Size: 854 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-2-circle-fill" viewBox="0 0 16 16">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M6.646 6.24c0-.691.493-1.306 1.336-1.306.756 0 1.313.492 1.313 1.236 0 .697-.469 1.23-.902 1.705l-2.971 3.293V12h5.344v-1.107H7.268v-.077l1.974-2.22.096-.107c.688-.763 1.287-1.428 1.287-2.43 0-1.266-1.031-2.215-2.613-2.215-1.758 0-2.637 1.19-2.637 2.402v.065h1.271v-.07Z"/>
</svg>

After

Width:  |  Height:  |  Size: 457 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-2-circle" viewBox="0 0 16 16">
<path d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8m15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0M6.646 6.24v.07H5.375v-.064c0-1.213.879-2.402 2.637-2.402 1.582 0 2.613.949 2.613 2.215 0 1.002-.6 1.667-1.287 2.43l-.096.107-1.974 2.22v.077h3.498V12H5.422v-.832l2.97-3.293c.434-.475.903-1.008.903-1.705 0-.744-.557-1.236-1.313-1.236-.843 0-1.336.615-1.336 1.306"/>
</svg>

After

Width:  |  Height:  |  Size: 477 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-2-square-fill" viewBox="0 0 16 16">
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm4.646 6.24v.07H5.375v-.064c0-1.213.879-2.402 2.637-2.402 1.582 0 2.613.949 2.613 2.215 0 1.002-.6 1.667-1.287 2.43l-.096.107-1.974 2.22v.077h3.498V12H5.422v-.832l2.97-3.293c.434-.475.903-1.008.903-1.705 0-.744-.557-1.236-1.313-1.236-.843 0-1.336.615-1.336 1.306"/>
</svg>

After

Width:  |  Height:  |  Size: 484 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-2-square" viewBox="0 0 16 16">
<path d="M6.646 6.24v.07H5.375v-.064c0-1.213.879-2.402 2.637-2.402 1.582 0 2.613.949 2.613 2.215 0 1.002-.6 1.667-1.287 2.43l-.096.107-1.974 2.22v.077h3.498V12H5.422v-.832l2.97-3.293c.434-.475.903-1.008.903-1.705 0-.744-.557-1.236-1.313-1.236-.843 0-1.336.615-1.336 1.306"/>
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
</svg>

After

Width:  |  Height:  |  Size: 564 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-3-circle-fill" viewBox="0 0 16 16">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-8.082.414c.92 0 1.535.54 1.541 1.318.012.791-.615 1.36-1.588 1.354-.861-.006-1.482-.469-1.54-1.066H5.104c.047 1.177 1.05 2.144 2.754 2.144 1.653 0 2.954-.937 2.93-2.396-.023-1.278-1.031-1.846-1.734-1.916v-.07c.597-.1 1.505-.739 1.482-1.876-.03-1.177-1.043-2.074-2.637-2.062-1.675.006-2.59.984-2.625 2.12h1.248c.036-.556.557-1.054 1.348-1.054.785 0 1.348.486 1.348 1.195.006.715-.563 1.237-1.342 1.237h-.838v1.072h.879Z"/>
</svg>

After

Width:  |  Height:  |  Size: 607 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-3-circle" viewBox="0 0 16 16">
<path d="M7.918 8.414h-.879V7.342h.838c.78 0 1.348-.522 1.342-1.237 0-.709-.563-1.195-1.348-1.195-.79 0-1.312.498-1.348 1.055H5.275c.036-1.137.95-2.115 2.625-2.121 1.594-.012 2.608.885 2.637 2.062.023 1.137-.885 1.776-1.482 1.875v.07c.703.07 1.71.64 1.734 1.917.024 1.459-1.277 2.396-2.93 2.396-1.705 0-2.707-.967-2.754-2.144H6.33c.059.597.68 1.06 1.541 1.066.973.006 1.6-.563 1.588-1.354-.006-.779-.621-1.318-1.541-1.318"/>
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8"/>
</svg>

After

Width:  |  Height:  |  Size: 642 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-3-square-fill" viewBox="0 0 16 16">
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm5.918 8.414h-.879V7.342h.838c.78 0 1.348-.522 1.342-1.237 0-.709-.563-1.195-1.348-1.195-.79 0-1.312.498-1.348 1.055H5.275c.036-1.137.95-2.115 2.625-2.121 1.594-.012 2.608.885 2.637 2.062.023 1.137-.885 1.776-1.482 1.875v.07c.703.07 1.71.64 1.734 1.917.024 1.459-1.277 2.396-2.93 2.396-1.705 0-2.707-.967-2.754-2.144H6.33c.059.597.68 1.06 1.541 1.066.973.006 1.6-.563 1.588-1.354-.006-.779-.621-1.318-1.541-1.318"/>
</svg>

After

Width:  |  Height:  |  Size: 634 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-3-square" viewBox="0 0 16 16">
<path d="M7.918 8.414h-.879V7.342h.838c.78 0 1.348-.522 1.342-1.237 0-.709-.563-1.195-1.348-1.195-.79 0-1.312.498-1.348 1.055H5.275c.036-1.137.95-2.115 2.625-2.121 1.594-.012 2.608.885 2.637 2.062.023 1.137-.885 1.776-1.482 1.875v.07c.703.07 1.71.64 1.734 1.917.024 1.459-1.277 2.396-2.93 2.396-1.705 0-2.707-.967-2.754-2.144H6.33c.059.597.68 1.06 1.541 1.066.973.006 1.6-.563 1.588-1.354-.006-.779-.621-1.318-1.541-1.318"/>
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
</svg>

After

Width:  |  Height:  |  Size: 714 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-4-circle-fill" viewBox="0 0 16 16">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M7.519 5.057c-.886 1.418-1.772 2.838-2.542 4.265v1.12H8.85V12h1.26v-1.559h1.007V9.334H10.11V4.002H8.176zM6.225 9.281v.053H8.85V5.063h-.065c-.867 1.33-1.787 2.806-2.56 4.218"/>
</svg>

After

Width:  |  Height:  |  Size: 359 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-4-circle" viewBox="0 0 16 16">
<path d="M7.519 5.057q.33-.527.657-1.055h1.933v5.332h1.008v1.107H10.11V12H8.85v-1.559H4.978V9.322c.77-1.427 1.656-2.847 2.542-4.265ZM6.225 9.281v.053H8.85V5.063h-.065c-.867 1.33-1.787 2.806-2.56 4.218"/>
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8"/>
</svg>

After

Width:  |  Height:  |  Size: 421 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-4-square-fill" viewBox="0 0 16 16">
<path d="M6.225 9.281v.053H8.85V5.063h-.065c-.867 1.33-1.787 2.806-2.56 4.218"/>
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm5.519 5.057q.33-.527.657-1.055h1.933v5.332h1.008v1.107H10.11V12H8.85v-1.559H4.978V9.322c.77-1.427 1.656-2.847 2.542-4.265Z"/>
</svg>

After

Width:  |  Height:  |  Size: 428 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-4-square" viewBox="0 0 16 16">
<path d="M7.519 5.057q.33-.527.657-1.055h1.933v5.332h1.008v1.107H10.11V12H8.85v-1.559H4.978V9.322c.77-1.427 1.656-2.847 2.542-4.265ZM6.225 9.281v.053H8.85V5.063h-.065c-.867 1.33-1.787 2.806-2.56 4.218"/>
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
</svg>

After

Width:  |  Height:  |  Size: 493 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-5-circle-fill" viewBox="0 0 16 16">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-8.006 4.158c1.74 0 2.924-1.119 2.924-2.806 0-1.641-1.178-2.584-2.56-2.584-.897 0-1.442.421-1.612.68h-.064l.193-2.344h3.621V4.002H5.791L5.445 8.63h1.149c.193-.358.668-.809 1.435-.809.85 0 1.582.604 1.582 1.57 0 1.085-.779 1.682-1.57 1.682-.697 0-1.389-.31-1.53-1.031H5.276c.065 1.213 1.149 2.115 2.72 2.115Z"/>
</svg>

After

Width:  |  Height:  |  Size: 495 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-5-circle" viewBox="0 0 16 16">
<path d="M1 8a7 7 0 1 1 14 0A7 7 0 0 1 1 8m15 0A8 8 0 1 0 0 8a8 8 0 0 0 16 0m-8.006 4.158c-1.57 0-2.654-.902-2.719-2.115h1.237c.14.72.832 1.031 1.529 1.031.791 0 1.57-.597 1.57-1.681 0-.967-.732-1.57-1.582-1.57-.767 0-1.242.45-1.435.808H5.445L5.791 4h4.705v1.103H6.875l-.193 2.343h.064c.17-.258.715-.68 1.611-.68 1.383 0 2.561.944 2.561 2.585 0 1.687-1.184 2.806-2.924 2.806Z"/>
</svg>

After

Width:  |  Height:  |  Size: 514 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-5-square-fill" viewBox="0 0 16 16">
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm5.994 12.158c-1.57 0-2.654-.902-2.719-2.115h1.237c.14.72.832 1.031 1.529 1.031.791 0 1.57-.597 1.57-1.681 0-.967-.732-1.57-1.582-1.57-.767 0-1.242.45-1.435.808H5.445L5.791 4h4.705v1.103H6.875l-.193 2.343h.064c.17-.258.715-.68 1.611-.68 1.383 0 2.561.944 2.561 2.585 0 1.687-1.184 2.806-2.924 2.806Z"/>
</svg>

After

Width:  |  Height:  |  Size: 521 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-5-square" viewBox="0 0 16 16">
<path d="M7.994 12.158c-1.57 0-2.654-.902-2.719-2.115h1.237c.14.72.832 1.031 1.529 1.031.791 0 1.57-.597 1.57-1.681 0-.967-.732-1.57-1.582-1.57-.767 0-1.242.45-1.435.808H5.445L5.791 4h4.705v1.103H6.875l-.193 2.343h.064c.17-.258.715-.68 1.611-.68 1.383 0 2.561.944 2.561 2.585 0 1.687-1.184 2.806-2.924 2.806Z"/>
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
</svg>

After

Width:  |  Height:  |  Size: 601 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-6-circle-fill" viewBox="0 0 16 16">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M8.21 3.855c-1.868 0-3.116 1.395-3.116 4.407 0 1.183.228 2.039.597 2.642.569.926 1.477 1.254 2.409 1.254 1.629 0 2.847-1.013 2.847-2.783 0-1.676-1.254-2.555-2.508-2.555-1.125 0-1.752.61-1.98 1.155h-.082c-.012-1.946.727-3.036 1.805-3.036.802 0 1.213.457 1.312.815h1.29c-.06-.908-.962-1.899-2.573-1.899Zm-.099 4.008c-.92 0-1.564.65-1.564 1.576 0 1.032.703 1.635 1.558 1.635.868 0 1.553-.533 1.553-1.629 0-1.06-.744-1.582-1.547-1.582"/>
</svg>

After

Width:  |  Height:  |  Size: 617 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-6-circle" viewBox="0 0 16 16">
<path d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8m15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0M8.21 3.855c1.612 0 2.515.99 2.573 1.899H9.494c-.1-.358-.51-.815-1.312-.815-1.078 0-1.817 1.09-1.805 3.036h.082c.229-.545.855-1.155 1.98-1.155 1.254 0 2.508.88 2.508 2.555 0 1.77-1.218 2.783-2.847 2.783-.932 0-1.84-.328-2.409-1.254-.369-.603-.597-1.459-.597-2.642 0-3.012 1.248-4.407 3.117-4.407Zm-.099 4.008c-.92 0-1.564.65-1.564 1.576 0 1.032.703 1.635 1.558 1.635.868 0 1.553-.533 1.553-1.629 0-1.06-.744-1.582-1.547-1.582"/>
</svg>

After

Width:  |  Height:  |  Size: 640 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-6-square-fill" viewBox="0 0 16 16">
<path d="M8.111 7.863c-.92 0-1.564.65-1.564 1.576 0 1.032.703 1.635 1.558 1.635.868 0 1.553-.533 1.553-1.629 0-1.06-.744-1.582-1.547-1.582"/>
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm6.21 3.855c1.612 0 2.515.99 2.573 1.899H9.494c-.1-.358-.51-.815-1.312-.815-1.078 0-1.817 1.09-1.805 3.036h.082c.229-.545.855-1.155 1.98-1.155 1.254 0 2.508.88 2.508 2.555 0 1.77-1.218 2.783-2.847 2.783-.932 0-1.84-.328-2.409-1.254-.369-.603-.597-1.459-.597-2.642 0-3.012 1.248-4.407 3.117-4.407Z"/>
</svg>

After

Width:  |  Height:  |  Size: 662 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-6-square" viewBox="0 0 16 16">
<path d="M8.21 3.855c1.612 0 2.515.99 2.573 1.899H9.494c-.1-.358-.51-.815-1.312-.815-1.078 0-1.817 1.09-1.805 3.036h.082c.229-.545.855-1.155 1.98-1.155 1.254 0 2.508.88 2.508 2.555 0 1.77-1.218 2.783-2.847 2.783-.932 0-1.84-.328-2.409-1.254-.369-.603-.597-1.459-.597-2.642 0-3.012 1.248-4.407 3.117-4.407Zm-.099 4.008c-.92 0-1.564.65-1.564 1.576 0 1.032.703 1.635 1.558 1.635.868 0 1.553-.533 1.553-1.629 0-1.06-.744-1.582-1.547-1.582"/>
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
</svg>

After

Width:  |  Height:  |  Size: 727 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-7-circle-fill" viewBox="0 0 16 16">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M5.37 5.11h3.972v.07L6.025 12H7.42l3.258-6.85V4.002H5.369v1.107Z"/>
</svg>

After

Width:  |  Height:  |  Size: 251 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-7-circle" viewBox="0 0 16 16">
<path d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8m15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0M5.37 5.11V4.001h5.308V5.15L7.42 12H6.025l3.317-6.82v-.07H5.369Z"/>
</svg>

After

Width:  |  Height:  |  Size: 279 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-7-square-fill" viewBox="0 0 16 16">
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm3.37 5.11V4.001h5.308V5.15L7.42 12H6.025l3.317-6.82v-.07H5.369Z"/>
</svg>

After

Width:  |  Height:  |  Size: 286 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-7-square" viewBox="0 0 16 16">
<path d="M5.37 5.11V4.001h5.308V5.15L7.42 12H6.025l3.317-6.82v-.07H5.369Z"/>
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
</svg>

After

Width:  |  Height:  |  Size: 366 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-8-circle-fill" viewBox="0 0 16 16">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-5.03 1.803c0-1.248-.943-1.84-1.646-1.992v-.065c.598-.187 1.336-.72 1.336-1.781 0-1.225-1.084-2.121-2.654-2.121s-2.66.896-2.66 2.12c0 1.044.709 1.589 1.33 1.782v.065c-.697.152-1.647.732-1.647 2.003 0 1.39 1.19 2.344 2.953 2.344 1.77 0 2.989-.96 2.989-2.355Zm-4.347-3.71c0 .739.586 1.255 1.383 1.255s1.377-.516 1.377-1.254c0-.733-.58-1.23-1.377-1.23s-1.383.497-1.383 1.23Zm-.281 3.645c0 .838.72 1.412 1.664 1.412.943 0 1.658-.574 1.658-1.412 0-.843-.715-1.424-1.658-1.424-.944 0-1.664.58-1.664 1.424"/>
</svg>

After

Width:  |  Height:  |  Size: 686 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-8-circle" viewBox="0 0 16 16">
<path d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8m15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-5.03 1.803c0 1.394-1.218 2.355-2.988 2.355-1.763 0-2.953-.955-2.953-2.344 0-1.271.95-1.851 1.647-2.003v-.065c-.621-.193-1.33-.738-1.33-1.781 0-1.225 1.09-2.121 2.66-2.121s2.654.896 2.654 2.12c0 1.061-.738 1.595-1.336 1.782v.065c.703.152 1.647.744 1.647 1.992Zm-4.347-3.71c0 .739.586 1.255 1.383 1.255s1.377-.516 1.377-1.254c0-.733-.58-1.23-1.377-1.23s-1.383.497-1.383 1.23Zm-.281 3.645c0 .838.72 1.412 1.664 1.412.943 0 1.658-.574 1.658-1.412 0-.843-.715-1.424-1.658-1.424-.944 0-1.664.58-1.664 1.424"/>
</svg>

After

Width:  |  Height:  |  Size: 717 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-8-square-fill" viewBox="0 0 16 16">
<path d="M6.623 6.094c0 .738.586 1.254 1.383 1.254s1.377-.516 1.377-1.254c0-.733-.58-1.23-1.377-1.23s-1.383.497-1.383 1.23m-.281 3.644c0 .838.72 1.412 1.664 1.412.943 0 1.658-.574 1.658-1.412 0-.843-.715-1.424-1.658-1.424-.944 0-1.664.58-1.664 1.424"/>
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm8.97 9.803c0 1.394-1.218 2.355-2.988 2.355-1.763 0-2.953-.955-2.953-2.344 0-1.271.95-1.851 1.647-2.003v-.065c-.621-.193-1.33-.738-1.33-1.781 0-1.225 1.09-2.121 2.66-2.121s2.654.896 2.654 2.12c0 1.061-.738 1.595-1.336 1.782v.065c.703.152 1.647.744 1.647 1.992Z"/>
</svg>

After

Width:  |  Height:  |  Size: 737 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-8-square" viewBox="0 0 16 16">
<path d="M10.97 9.803c0 1.394-1.218 2.355-2.988 2.355-1.763 0-2.953-.955-2.953-2.344 0-1.271.95-1.851 1.647-2.003v-.065c-.621-.193-1.33-.738-1.33-1.781 0-1.225 1.09-2.121 2.66-2.121s2.654.896 2.654 2.12c0 1.061-.738 1.595-1.336 1.782v.065c.703.152 1.647.744 1.647 1.992Zm-4.347-3.71c0 .739.586 1.255 1.383 1.255s1.377-.516 1.377-1.254c0-.733-.58-1.23-1.377-1.23s-1.383.497-1.383 1.23Zm-.281 3.645c0 .838.72 1.412 1.664 1.412.943 0 1.658-.574 1.658-1.412 0-.843-.715-1.424-1.658-1.424-.944 0-1.664.58-1.664 1.424"/>
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
</svg>

After

Width:  |  Height:  |  Size: 804 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-9-circle-fill" viewBox="0 0 16 16">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-8.223 4.146c2.104 0 3.123-1.464 3.123-4.3 0-3.147-1.459-4.014-2.97-4.014-1.63 0-2.871 1.02-2.871 2.73 0 1.706 1.171 2.667 2.566 2.667 1.06 0 1.7-.557 1.934-1.184h.076c.047 1.67-.475 3.023-1.834 3.023-.71 0-1.149-.363-1.248-.72H5.258c.094.908.926 1.798 2.52 1.798Zm.118-3.972c.808 0 1.535-.528 1.535-1.594s-.668-1.676-1.56-1.676c-.838 0-1.517.616-1.517 1.659 0 1.072.708 1.61 1.54 1.61Z"/>
</svg>

After

Width:  |  Height:  |  Size: 574 B

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