Настроил сеелери, начал привязку lyngsat

This commit is contained in:
2025-11-11 17:23:36 +03:00
parent 65e6c9a323
commit 4f21c9d7c8
110 changed files with 34270 additions and 33631 deletions

View File

@@ -1,67 +1,67 @@
# Django imports
from django.contrib import admin
# Third-party imports
from import_export.admin import ImportExportActionModelAdmin
from more_admin_filters import MultiSelectRelatedDropdownFilter
from rangefilter.filters import NumericRangeFilterBuilder
# Local imports
from .models import Transponders
# ============================================================================
# Base Admin Classes
# ============================================================================
class BaseAdmin(admin.ModelAdmin):
"""
Базовый класс для всех admin моделей mapsapp.
Предоставляет общую функциональность:
- Кнопки сохранения сверху и снизу
- Настройка количества элементов на странице
"""
save_on_top = True
list_per_page = 50
# ============================================================================
# Admin Classes
# ============================================================================
@admin.register(Transponders)
class TranspondersAdmin(ImportExportActionModelAdmin, BaseAdmin):
"""
Админ-панель для модели Transponders.
Оптимизирована для работы с транспондерами:
- Использует select_related для оптимизации запросов
- Предоставляет фильтры по спутникам, поляризации и зоне
- Поддерживает импорт/экспорт данных
"""
list_display = (
"sat_id",
"name",
"zone_name",
"downlink",
"uplink",
"frequency_range",
"transfer",
"polarization",
)
list_display_links = ("name",)
list_select_related = ("polarization", "sat_id")
list_filter = (
("polarization", MultiSelectRelatedDropdownFilter),
("sat_id", MultiSelectRelatedDropdownFilter),
("downlink", NumericRangeFilterBuilder()),
("uplink", NumericRangeFilterBuilder()),
("frequency_range", NumericRangeFilterBuilder()),
"zone_name",
)
search_fields = ("name", "sat_id__name", "zone_name")
ordering = ("name",)
autocomplete_fields = ("sat_id", "polarization")
# Django imports
from django.contrib import admin
# Third-party imports
from import_export.admin import ImportExportActionModelAdmin
from more_admin_filters import MultiSelectRelatedDropdownFilter
from rangefilter.filters import NumericRangeFilterBuilder
# Local imports
from .models import Transponders
# ============================================================================
# Base Admin Classes
# ============================================================================
class BaseAdmin(admin.ModelAdmin):
"""
Базовый класс для всех admin моделей mapsapp.
Предоставляет общую функциональность:
- Кнопки сохранения сверху и снизу
- Настройка количества элементов на странице
"""
save_on_top = True
list_per_page = 50
# ============================================================================
# Admin Classes
# ============================================================================
@admin.register(Transponders)
class TranspondersAdmin(ImportExportActionModelAdmin, BaseAdmin):
"""
Админ-панель для модели Transponders.
Оптимизирована для работы с транспондерами:
- Использует select_related для оптимизации запросов
- Предоставляет фильтры по спутникам, поляризации и зоне
- Поддерживает импорт/экспорт данных
"""
list_display = (
"sat_id",
"name",
"zone_name",
"downlink",
"uplink",
"frequency_range",
"transfer",
"polarization",
)
list_display_links = ("name",)
list_select_related = ("polarization", "sat_id")
list_filter = (
("polarization", MultiSelectRelatedDropdownFilter),
("sat_id", MultiSelectRelatedDropdownFilter),
("downlink", NumericRangeFilterBuilder()),
("uplink", NumericRangeFilterBuilder()),
("frequency_range", NumericRangeFilterBuilder()),
"zone_name",
)
search_fields = ("name", "sat_id__name", "zone_name")
ordering = ("name",)
autocomplete_fields = ("sat_id", "polarization")

View File

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

View File

@@ -1,37 +1,37 @@
# Generated by Django 5.2.7 on 2025-10-31 13:36
import django.db.models.deletion
import django.db.models.expressions
import django.db.models.functions.math
import mainapp.models
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('mainapp', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Transponders',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(blank=True, max_length=30, null=True, verbose_name='Название транспондера')),
('downlink', models.FloatField(blank=True, null=True, verbose_name='Downlink')),
('frequency_range', models.FloatField(blank=True, 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='Поляризация')),
('sat_id', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='tran_satellite', to='mainapp.satellite', verbose_name='Спутник')),
],
options={
'verbose_name': 'Транспондер',
'verbose_name_plural': 'Транспондеры',
},
),
]
# Generated by Django 5.2.7 on 2025-10-31 13:36
import django.db.models.deletion
import django.db.models.expressions
import django.db.models.functions.math
import mainapp.models
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('mainapp', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Transponders',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(blank=True, max_length=30, null=True, verbose_name='Название транспондера')),
('downlink', models.FloatField(blank=True, null=True, verbose_name='Downlink')),
('frequency_range', models.FloatField(blank=True, 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='Поляризация')),
('sat_id', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='tran_satellite', to='mainapp.satellite', verbose_name='Спутник')),
],
options={
'verbose_name': 'Транспондер',
'verbose_name_plural': 'Транспондеры',
},
),
]

View File

@@ -1,64 +1,64 @@
# Generated by Django 5.2.7 on 2025-11-07 20:58
import django.core.validators
import django.db.models.deletion
import mainapp.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0006_alter_customuser_options_alter_geo_options_and_more'),
('mapsapp', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='transponders',
options={'ordering': ['sat_id', 'downlink'], 'verbose_name': 'Транспондер', 'verbose_name_plural': 'Транспондеры'},
),
migrations.AlterField(
model_name='transponders',
name='downlink',
field=models.FloatField(blank=True, help_text='Частота downlink в МГц (0-50000)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50000)], verbose_name='Downlink'),
),
migrations.AlterField(
model_name='transponders',
name='frequency_range',
field=models.FloatField(blank=True, help_text='Полоса частот в МГц (0-1000)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000)], verbose_name='Полоса'),
),
migrations.AlterField(
model_name='transponders',
name='name',
field=models.CharField(blank=True, db_index=True, help_text='Название транспондера', max_length=30, null=True, verbose_name='Название транспондера'),
),
migrations.AlterField(
model_name='transponders',
name='polarization',
field=models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, help_text='Поляризация сигнала', null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='tran_polarizations', to='mainapp.polarization', verbose_name='Поляризация'),
),
migrations.AlterField(
model_name='transponders',
name='sat_id',
field=models.ForeignKey(help_text='Спутник, которому принадлежит транспондер', on_delete=django.db.models.deletion.PROTECT, related_name='tran_satellite', to='mainapp.satellite', verbose_name='Спутник'),
),
migrations.AlterField(
model_name='transponders',
name='uplink',
field=models.FloatField(blank=True, help_text='Частота uplink в МГц (0-50000)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50000)], verbose_name='Uplink'),
),
migrations.AlterField(
model_name='transponders',
name='zone_name',
field=models.CharField(blank=True, db_index=True, help_text='Название зоны покрытия транспондера', max_length=255, null=True, verbose_name='Название зоны'),
),
migrations.AddIndex(
model_name='transponders',
index=models.Index(fields=['sat_id', 'downlink'], name='mapsapp_tra_sat_id__3e3fd7_idx'),
),
migrations.AddIndex(
model_name='transponders',
index=models.Index(fields=['sat_id', 'zone_name'], name='mapsapp_tra_sat_id__305ae7_idx'),
),
]
# Generated by Django 5.2.7 on 2025-11-07 20:58
import django.core.validators
import django.db.models.deletion
import mainapp.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0006_alter_customuser_options_alter_geo_options_and_more'),
('mapsapp', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='transponders',
options={'ordering': ['sat_id', 'downlink'], 'verbose_name': 'Транспондер', 'verbose_name_plural': 'Транспондеры'},
),
migrations.AlterField(
model_name='transponders',
name='downlink',
field=models.FloatField(blank=True, help_text='Частота downlink в МГц (0-50000)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50000)], verbose_name='Downlink'),
),
migrations.AlterField(
model_name='transponders',
name='frequency_range',
field=models.FloatField(blank=True, help_text='Полоса частот в МГц (0-1000)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000)], verbose_name='Полоса'),
),
migrations.AlterField(
model_name='transponders',
name='name',
field=models.CharField(blank=True, db_index=True, help_text='Название транспондера', max_length=30, null=True, verbose_name='Название транспондера'),
),
migrations.AlterField(
model_name='transponders',
name='polarization',
field=models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, help_text='Поляризация сигнала', null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='tran_polarizations', to='mainapp.polarization', verbose_name='Поляризация'),
),
migrations.AlterField(
model_name='transponders',
name='sat_id',
field=models.ForeignKey(help_text='Спутник, которому принадлежит транспондер', on_delete=django.db.models.deletion.PROTECT, related_name='tran_satellite', to='mainapp.satellite', verbose_name='Спутник'),
),
migrations.AlterField(
model_name='transponders',
name='uplink',
field=models.FloatField(blank=True, help_text='Частота uplink в МГц (0-50000)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(50000)], verbose_name='Uplink'),
),
migrations.AlterField(
model_name='transponders',
name='zone_name',
field=models.CharField(blank=True, db_index=True, help_text='Название зоны покрытия транспондера', max_length=255, null=True, verbose_name='Название зоны'),
),
migrations.AddIndex(
model_name='transponders',
index=models.Index(fields=['sat_id', 'downlink'], name='mapsapp_tra_sat_id__3e3fd7_idx'),
),
migrations.AddIndex(
model_name='transponders',
index=models.Index(fields=['sat_id', 'zone_name'], name='mapsapp_tra_sat_id__305ae7_idx'),
),
]

View File

@@ -1,117 +1,117 @@
# Django imports
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import ExpressionWrapper, F
from django.db.models.functions import Abs
# Local imports
from mainapp.models import Polarization, Satellite, get_default_polarization
class Transponders(models.Model):
"""
Модель транспондера спутника.
Хранит информацию о частотах uplink/downlink, зоне покрытия и поляризации.
"""
# Основные поля
name = models.CharField(
max_length=30,
null=True,
blank=True,
verbose_name="Название транспондера",
db_index=True,
help_text="Название транспондера"
)
downlink = models.FloatField(
blank=True,
null=True,
verbose_name="Downlink",
validators=[MinValueValidator(0), MaxValueValidator(50000)],
help_text="Частота downlink в МГц (0-50000)"
)
frequency_range = models.FloatField(
blank=True,
null=True,
verbose_name="Полоса",
validators=[MinValueValidator(0), MaxValueValidator(1000)],
help_text="Полоса частот в МГц (0-1000)"
)
uplink = models.FloatField(
blank=True,
null=True,
verbose_name="Uplink",
validators=[MinValueValidator(0), MaxValueValidator(50000)],
help_text="Частота uplink в МГц (0-50000)"
)
zone_name = models.CharField(
max_length=255,
blank=True,
null=True,
verbose_name="Название зоны",
db_index=True,
help_text="Название зоны покрытия транспондера"
)
# Связи
polarization = models.ForeignKey(
Polarization,
default=get_default_polarization,
on_delete=models.SET_DEFAULT,
related_name="tran_polarizations",
null=True,
blank=True,
verbose_name="Поляризация",
help_text="Поляризация сигнала"
)
sat_id = models.ForeignKey(
Satellite,
on_delete=models.PROTECT,
related_name="tran_satellite",
verbose_name="Спутник",
db_index=True,
help_text="Спутник, которому принадлежит транспондер"
)
# Вычисляемые поля
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 clean(self):
"""Валидация на уровне модели"""
super().clean()
# Проверка что downlink и uplink заданы
if self.downlink and self.uplink:
# Обычно uplink выше downlink для спутниковой связи
if self.uplink < self.downlink:
raise ValidationError({
'uplink': 'Частота uplink обычно выше частоты downlink'
})
def __str__(self):
if self.name:
return self.name
return f"Транспондер {self.sat_id.name if self.sat_id else 'Unknown'}"
class Meta:
verbose_name = "Транспондер"
verbose_name_plural = "Транспондеры"
ordering = ['sat_id', 'downlink']
indexes = [
models.Index(fields=['sat_id', 'downlink']),
models.Index(fields=['sat_id', 'zone_name']),
]
# Django imports
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import ExpressionWrapper, F
from django.db.models.functions import Abs
# Local imports
from mainapp.models import Polarization, Satellite, get_default_polarization
class Transponders(models.Model):
"""
Модель транспондера спутника.
Хранит информацию о частотах uplink/downlink, зоне покрытия и поляризации.
"""
# Основные поля
name = models.CharField(
max_length=30,
null=True,
blank=True,
verbose_name="Название транспондера",
db_index=True,
help_text="Название транспондера"
)
downlink = models.FloatField(
blank=True,
null=True,
verbose_name="Downlink",
validators=[MinValueValidator(0), MaxValueValidator(50000)],
help_text="Частота downlink в МГц (0-50000)"
)
frequency_range = models.FloatField(
blank=True,
null=True,
verbose_name="Полоса",
validators=[MinValueValidator(0), MaxValueValidator(1000)],
help_text="Полоса частот в МГц (0-1000)"
)
uplink = models.FloatField(
blank=True,
null=True,
verbose_name="Uplink",
validators=[MinValueValidator(0), MaxValueValidator(50000)],
help_text="Частота uplink в МГц (0-50000)"
)
zone_name = models.CharField(
max_length=255,
blank=True,
null=True,
verbose_name="Название зоны",
db_index=True,
help_text="Название зоны покрытия транспондера"
)
# Связи
polarization = models.ForeignKey(
Polarization,
default=get_default_polarization,
on_delete=models.SET_DEFAULT,
related_name="tran_polarizations",
null=True,
blank=True,
verbose_name="Поляризация",
help_text="Поляризация сигнала"
)
sat_id = models.ForeignKey(
Satellite,
on_delete=models.PROTECT,
related_name="tran_satellite",
verbose_name="Спутник",
db_index=True,
help_text="Спутник, которому принадлежит транспондер"
)
# Вычисляемые поля
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 clean(self):
"""Валидация на уровне модели"""
super().clean()
# Проверка что downlink и uplink заданы
if self.downlink and self.uplink:
# Обычно uplink выше downlink для спутниковой связи
if self.uplink < self.downlink:
raise ValidationError({
'uplink': 'Частота uplink обычно выше частоты downlink'
})
def __str__(self):
if self.name:
return self.name
return f"Транспондер {self.sat_id.name if self.sat_id else 'Unknown'}"
class Meta:
verbose_name = "Транспондер"
verbose_name_plural = "Транспондеры"
ordering = ['sat_id', 'downlink']
indexes = [
models.Index(fields=['sat_id', 'downlink']),
models.Index(fields=['sat_id', 'zone_name']),
]

File diff suppressed because it is too large Load Diff

View File

@@ -1,83 +1,83 @@
{% load static %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>{% block title %}Карта{% endblock %}</title>
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
<!-- Leaflet CSS -->
<link href="{% static 'leaflet/leaflet.css' %}" rel="stylesheet">
<link href="{% static 'leaflet-measure/leaflet-measure.css' %}" rel="stylesheet">
<link href="{% static 'leaflet-tree/L.Control.Layers.Tree.css' %}" rel="stylesheet">
<!-- Extra CSS -->
{% block extra_css %}{% endblock %}
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
overflow: hidden;
}
#map {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
</style>
</head>
<body>
<div id="map"></div>
{% block content %}
{% endblock %}
<!-- Leaflet JavaScript -->
<script src="{% static 'leaflet/leaflet.js' %}"></script>
<script src="{% static 'leaflet-measure/leaflet-measure.ru.js' %}"></script>
<script src="{% static 'leaflet-tree/L.Control.Layers.Tree.js' %}"></script>
<script>
let map = L.map('map').setView([0, 0], 2);
L.control.scale({
imperial: false,
metric: true}).addTo(map);
map.attributionControl.setPrefix(false);
const street = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
});
street.addTo(map);
const satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: 'Tiles &copy; Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'
});
const 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 = {
"Улицы": street,
"Спутник": satellite,
"Локально": street_local
};
L.control.layers(baseLayers).addTo(map);
map.setMaxZoom(18);
map.setMinZoom(0);
L.control.measure({ primaryLengthUnit: 'kilometers' }).addTo(map);
{% comment %} let imageUrl = '{% static "mapsapp/assets/world_map.jpg" %}';
let imageBounds = [[-82, -180], [82, 180]];
L.imageOverlay(imageUrl, imageBounds).addTo(map); {% endcomment %}
</script>
{% block extra_js %}{% endblock %}
</body>
{% load static %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>{% block title %}Карта{% endblock %}</title>
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
<!-- Leaflet CSS -->
<link href="{% static 'leaflet/leaflet.css' %}" rel="stylesheet">
<link href="{% static 'leaflet-measure/leaflet-measure.css' %}" rel="stylesheet">
<link href="{% static 'leaflet-tree/L.Control.Layers.Tree.css' %}" rel="stylesheet">
<!-- Extra CSS -->
{% block extra_css %}{% endblock %}
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
overflow: hidden;
}
#map {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
</style>
</head>
<body>
<div id="map"></div>
{% block content %}
{% endblock %}
<!-- Leaflet JavaScript -->
<script src="{% static 'leaflet/leaflet.js' %}"></script>
<script src="{% static 'leaflet-measure/leaflet-measure.ru.js' %}"></script>
<script src="{% static 'leaflet-tree/L.Control.Layers.Tree.js' %}"></script>
<script>
let map = L.map('map').setView([0, 0], 2);
L.control.scale({
imperial: false,
metric: true}).addTo(map);
map.attributionControl.setPrefix(false);
const street = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
});
street.addTo(map);
const satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: 'Tiles &copy; Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'
});
const 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 = {
"Улицы": street,
"Спутник": satellite,
"Локально": street_local
};
L.control.layers(baseLayers).addTo(map);
map.setMaxZoom(18);
map.setMinZoom(0);
L.control.measure({ primaryLengthUnit: 'kilometers' }).addTo(map);
{% comment %} let imageUrl = '{% static "mapsapp/assets/world_map.jpg" %}';
let imageBounds = [[-82, -180], [82, 180]];
L.imageOverlay(imageUrl, imageBounds).addTo(map); {% endcomment %}
</script>
{% block extra_js %}{% endblock %}
</body>
</html>

View File

@@ -1,118 +1,118 @@
{% load static %}
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
<title>Cesium Map Editor</title>
<!-- Cesium Library -->
<script src="{% static 'cesium/Cesium.js' %}" defer></script>
<link href="{% static 'cesium/Widgets/widgets.css' %}" rel="stylesheet">
<!-- Custom Styles -->
<link rel="stylesheet" href="{% static 'mapsapp/style.css' %}">
</head>
<body>
<div id="cesiumContainer"></div>
<input type="file" id="fileInput" accept=".geojson,.json,.kml" style="display: none;" />
<!-- Панель инструментов -->
<div class="toolbar">
<!-- Группа 1: Режимы рисования -->
<div class="toolbar-section">
<div class="section-title">Рисование</div>
<div class="toolbar-group">
<button id="selectMode" class="tool-btn active" title="Режим выделения (S)">
<span>🔍</span> Выделение
</button>
<button id="markerMode" class="tool-btn" title="Добавить маркер (M)">
<span>📌</span> Маркер
</button>
<button id="polygonMode" class="tool-btn" title="Рисовать полигон (P)">
<span></span> Полигон
</button>
<button id="polylineMode" class="tool-btn" title="Рисовать линию (L)">
<span>〰️</span> Линия
</button>
</div>
</div>
<!-- Группа 2: Импорт/Экспорт -->
<div class="toolbar-section">
<div class="section-title">Импорт/экспорт всех объектов</div>
<div class="toolbar-group">
<button id="importBtn" class="tool-btn" title="Импортировать GeoJSON или KML">
<span>📥</span> Импорт
</button>
<button id="exportBtn" class="tool-btn" title="Экспортировать в GeoJSON или KML">
<span>📤</span> Экспорт
</button>
</div>
</div>
<!-- Группа 3: Действия -->
<div class="toolbar-section">
<div class="section-title">Действия</div>
<div class="toolbar-group">
<button id="deleteSelected" class="tool-btn danger" title="Удалить выделенное (Del)">
<span>🗑️</span> Удалить
</button>
<button id="clearAll" class="tool-btn danger" title="Очистить всё">
<span>🧹</span> Очистить
</button>
</div>
</div>
<!-- Строка состояния -->
<div class="status-bar">
<span id="modeStatus">Режим: Выделение</span>
<span id="coordinates" style="color: #eeeeeeff; font-size: 11px;"></span>
<span id="hint">Нажмите ESC для отмены</span>
</div>
</div>
<!-- Блок выбора объектов из БД -->
<div class="db-objects-panel">
<div class="panel-title">Объекты из базы</div>
<select id="objectSelector" class="object-select">
<option value="">— Выберите объект —</option>
{% for sat in sats %}
<option value="{{sat.id}}">{{sat.name}}</option>
{% endfor %}
</select>
<button id="loadObjectBtn" class="load-btn">Загрузить на карту</button>
</div>
<div class="footprint-control">
<div class="panel-title">Области покрытия</div>
<div class="footprint-actions">
<button id="showAllFootprints">Показать все</button>
<button id="hideAllFootprints">Скрыть все</button>
</div>
<div id="footprintToggles"></div>
</div>
<!-- Модальное окно для описания -->
<div id="descriptionModal" class="modal">
<div class="modal-content">
<h3>Добавить описание</h3>
<textarea id="descriptionInput" placeholder="Введите описание объекта..."></textarea>
<div class="modal-buttons">
<button id="confirmDescription">Сохранить</button>
<button id="cancelDescription">Отмена</button>
</div>
</div>
</div>
<div id="exportModal" class="modal">
<div class="modal-content">
<h3>Экспорт данных</h3>
<p>Выберите формат для экспорта всех объектов:</p>
<div class="modal-buttons" style="justify-content: center; gap: 15px; margin-top: 20px;">
<button id="exportGeoJson">GeoJSON</button>
<button id="exportKml">KML</button>
<button id="cancelExport">Отмена</button>
</div>
</div>
</div>
<script src="{% static 'mapsapp/main.js' %}"></script>
</body>
{% load static %}
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
<title>Cesium Map Editor</title>
<!-- Cesium Library -->
<script src="{% static 'cesium/Cesium.js' %}" defer></script>
<link href="{% static 'cesium/Widgets/widgets.css' %}" rel="stylesheet">
<!-- Custom Styles -->
<link rel="stylesheet" href="{% static 'mapsapp/style.css' %}">
</head>
<body>
<div id="cesiumContainer"></div>
<input type="file" id="fileInput" accept=".geojson,.json,.kml" style="display: none;" />
<!-- Панель инструментов -->
<div class="toolbar">
<!-- Группа 1: Режимы рисования -->
<div class="toolbar-section">
<div class="section-title">Рисование</div>
<div class="toolbar-group">
<button id="selectMode" class="tool-btn active" title="Режим выделения (S)">
<span>🔍</span> Выделение
</button>
<button id="markerMode" class="tool-btn" title="Добавить маркер (M)">
<span>📌</span> Маркер
</button>
<button id="polygonMode" class="tool-btn" title="Рисовать полигон (P)">
<span></span> Полигон
</button>
<button id="polylineMode" class="tool-btn" title="Рисовать линию (L)">
<span>〰️</span> Линия
</button>
</div>
</div>
<!-- Группа 2: Импорт/Экспорт -->
<div class="toolbar-section">
<div class="section-title">Импорт/экспорт всех объектов</div>
<div class="toolbar-group">
<button id="importBtn" class="tool-btn" title="Импортировать GeoJSON или KML">
<span>📥</span> Импорт
</button>
<button id="exportBtn" class="tool-btn" title="Экспортировать в GeoJSON или KML">
<span>📤</span> Экспорт
</button>
</div>
</div>
<!-- Группа 3: Действия -->
<div class="toolbar-section">
<div class="section-title">Действия</div>
<div class="toolbar-group">
<button id="deleteSelected" class="tool-btn danger" title="Удалить выделенное (Del)">
<span>🗑️</span> Удалить
</button>
<button id="clearAll" class="tool-btn danger" title="Очистить всё">
<span>🧹</span> Очистить
</button>
</div>
</div>
<!-- Строка состояния -->
<div class="status-bar">
<span id="modeStatus">Режим: Выделение</span>
<span id="coordinates" style="color: #eeeeeeff; font-size: 11px;"></span>
<span id="hint">Нажмите ESC для отмены</span>
</div>
</div>
<!-- Блок выбора объектов из БД -->
<div class="db-objects-panel">
<div class="panel-title">Объекты из базы</div>
<select id="objectSelector" class="object-select">
<option value="">— Выберите объект —</option>
{% for sat in sats %}
<option value="{{sat.id}}">{{sat.name}}</option>
{% endfor %}
</select>
<button id="loadObjectBtn" class="load-btn">Загрузить на карту</button>
</div>
<div class="footprint-control">
<div class="panel-title">Области покрытия</div>
<div class="footprint-actions">
<button id="showAllFootprints">Показать все</button>
<button id="hideAllFootprints">Скрыть все</button>
</div>
<div id="footprintToggles"></div>
</div>
<!-- Модальное окно для описания -->
<div id="descriptionModal" class="modal">
<div class="modal-content">
<h3>Добавить описание</h3>
<textarea id="descriptionInput" placeholder="Введите описание объекта..."></textarea>
<div class="modal-buttons">
<button id="confirmDescription">Сохранить</button>
<button id="cancelDescription">Отмена</button>
</div>
</div>
</div>
<div id="exportModal" class="modal">
<div class="modal-content">
<h3>Экспорт данных</h3>
<p>Выберите формат для экспорта всех объектов:</p>
<div class="modal-buttons" style="justify-content: center; gap: 15px; margin-top: 20px;">
<button id="exportGeoJson">GeoJSON</button>
<button id="exportKml">KML</button>
<button id="cancelExport">Отмена</button>
</div>
</div>
</div>
<script src="{% static 'mapsapp/main.js' %}"></script>
</body>
</html>

View File

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

View File

@@ -1,14 +1,14 @@
from django.conf import settings
from django.conf.urls.static import static
from django.urls import path
from . import views
app_name = 'mapsapp'
urlpatterns = [
path('3dmap', views.CesiumMapView.as_view(), name='3dmap'),
path('2dmap', views.LeafletMapView.as_view(), name='2dmap'),
path('api/footprint-names/<int:sat_id>', views.GetFootprintsView.as_view(), name="footprint_names"),
path('api/transponders/<int:sat_id>', views.GetTransponderOnSatIdView.as_view(), name='transponders_data'),
path('tiles/<str:footprint_name>/<int:z>/<int:x>/<int:y>.png', views.TileProxyView.as_view(), name='tile_proxy'),
from django.conf import settings
from django.conf.urls.static import static
from django.urls import path
from . import views
app_name = 'mapsapp'
urlpatterns = [
path('3dmap', views.CesiumMapView.as_view(), name='3dmap'),
path('2dmap', views.LeafletMapView.as_view(), name='2dmap'),
path('api/footprint-names/<int:sat_id>', views.GetFootprintsView.as_view(), name="footprint_names"),
path('api/transponders/<int:sat_id>', views.GetTransponderOnSatIdView.as_view(), name='transponders_data'),
path('tiles/<str:footprint_name>/<int:z>/<int:x>/<int:y>.png', views.TileProxyView.as_view(), name='tile_proxy'),
]

View File

@@ -1,165 +1,165 @@
# Standard library imports
import json
import re
from io import BytesIO
# Third-party imports
import requests
# Local imports
from mainapp.models import Polarization, Satellite
from .models import Transponders
def search_satellite_on_page(data: dict, satellite_name: str):
for pos, value in data.get('page', {}).get('positions').items():
for name in value['satellites']:
if name['other_names'] is None:
name['other_names'] = ''
if satellite_name.lower() in name['name'].lower() or satellite_name.lower() in name['other_names'].lower():
return pos, name['id']
return '', ''
def get_footprint_data(position: str = 62) -> dict:
"""Возвращает словарь с данным по footprint для спутников на выбранной долготе"""
response = requests.get(f"https://www.satbeams.com/footprints?position={position}")
response.raise_for_status()
match = re.search(r'var data = ({.*?});', response.text, re.DOTALL)
if match:
json_str = match.group(1)
try:
data = json.loads(json_str)
return data.get("page", {}).get("footprint_data", {}).get("beams",[])
except json.JSONDecodeError as e:
print("Ошибка парсинга JSON:", e)
else:
print("Нужных данных не найдено")
return {}
def get_all_page_data(url:str = 'https://www.satbeams.com/footprints') -> dict:
"""Возвращает словарь с данными по всем спутникам на странице"""
response = requests.get(url)
response.raise_for_status()
match = re.search(r'var data = ({.*?});', response.text, re.DOTALL)
if match:
json_str = match.group(1)
try:
data = json.loads(json_str)
# Файл json на диске для достоверности
with open('data.json', 'w') as jf:
json.dump(data, jf, indent=2)
return data
except json.JSONDecodeError as e:
print("Ошибка парсинга JSON:", e)
else:
print("Нужных данных не найдено")
return {}
def get_names_footprints_for_satellite(footprint_data: dict, sat_id: str) -> list[str]:
names = []
for beam in footprint_data:
if 'ku' in beam['band'].lower() and sat_id in beam['satellite_id']:
names.append(
{
"name": beam['name'],
"fullname": beam['fullname'][8:]
}
)
return names
def get_band_names(satellite_name: str) -> list[str]:
data = get_all_page_data()
pos, sat_id = search_satellite_on_page(data, satellite_name)
footprints = get_footprint_data(pos)
names = get_names_footprints_for_satellite(footprints, sat_id)
return names
def parse_transponders_from_json(filepath: str):
with open(filepath, encoding="utf-8") as jf:
data = json.load(jf)
for sat_name, trans_zone in data["satellites"].items():
for zone, trans in trans_zone.items():
for tran in trans:
f_b, f_e = tran["freq"][0].split("-")
f = round((float(f_b) + float(f_e))/2, 3)
f_range = round(abs(float(f_e) - float(f_b)), 3)
tran_obj = Transponders.objects.create(
name=tran["name"],
frequency=f,
frequency_range=f_range,
zone_name=zone,
polarization=Polarization.objects.get(name=tran["pol"]),
sat_id=Satellite.objects.get(name__iexact=sat_name)
)
tran_obj.save()
# Third-party imports (additional)
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()
# Standard library imports
import json
import re
from io import BytesIO
# Third-party imports
import requests
# Local imports
from mainapp.models import Polarization, Satellite
from .models import Transponders
def search_satellite_on_page(data: dict, satellite_name: str):
for pos, value in data.get('page', {}).get('positions').items():
for name in value['satellites']:
if name['other_names'] is None:
name['other_names'] = ''
if satellite_name.lower() in name['name'].lower() or satellite_name.lower() in name['other_names'].lower():
return pos, name['id']
return '', ''
def get_footprint_data(position: str = 62) -> dict:
"""Возвращает словарь с данным по footprint для спутников на выбранной долготе"""
response = requests.get(f"https://www.satbeams.com/footprints?position={position}")
response.raise_for_status()
match = re.search(r'var data = ({.*?});', response.text, re.DOTALL)
if match:
json_str = match.group(1)
try:
data = json.loads(json_str)
return data.get("page", {}).get("footprint_data", {}).get("beams",[])
except json.JSONDecodeError as e:
print("Ошибка парсинга JSON:", e)
else:
print("Нужных данных не найдено")
return {}
def get_all_page_data(url:str = 'https://www.satbeams.com/footprints') -> dict:
"""Возвращает словарь с данными по всем спутникам на странице"""
response = requests.get(url)
response.raise_for_status()
match = re.search(r'var data = ({.*?});', response.text, re.DOTALL)
if match:
json_str = match.group(1)
try:
data = json.loads(json_str)
# Файл json на диске для достоверности
with open('data.json', 'w') as jf:
json.dump(data, jf, indent=2)
return data
except json.JSONDecodeError as e:
print("Ошибка парсинга JSON:", e)
else:
print("Нужных данных не найдено")
return {}
def get_names_footprints_for_satellite(footprint_data: dict, sat_id: str) -> list[str]:
names = []
for beam in footprint_data:
if 'ku' in beam['band'].lower() and sat_id in beam['satellite_id']:
names.append(
{
"name": beam['name'],
"fullname": beam['fullname'][8:]
}
)
return names
def get_band_names(satellite_name: str) -> list[str]:
data = get_all_page_data()
pos, sat_id = search_satellite_on_page(data, satellite_name)
footprints = get_footprint_data(pos)
names = get_names_footprints_for_satellite(footprints, sat_id)
return names
def parse_transponders_from_json(filepath: str):
with open(filepath, encoding="utf-8") as jf:
data = json.load(jf)
for sat_name, trans_zone in data["satellites"].items():
for zone, trans in trans_zone.items():
for tran in trans:
f_b, f_e = tran["freq"][0].split("-")
f = round((float(f_b) + float(f_e))/2, 3)
f_range = round(abs(float(f_e) - float(f_b)), 3)
tran_obj = Transponders.objects.create(
name=tran["name"],
frequency=f,
frequency_range=f_range,
zone_name=zone,
polarization=Polarization.objects.get(name=tran["pol"]),
sat_id=Satellite.objects.get(name__iexact=sat_name)
)
tran_obj.save()
# Third-party imports (additional)
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

@@ -1,148 +1,148 @@
# Standard library imports
from typing import Any, Dict
# Django imports
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponse, HttpResponseNotFound, JsonResponse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.cache import cache_page
from django.views.decorators.http import require_GET
from django.views.generic import TemplateView
# Third-party imports
import requests
# Local imports
from mainapp.models import Satellite
from .models import Transponders
from .utils import get_band_names
class CesiumMapView(LoginRequiredMixin, TemplateView):
"""
Представление для отображения 3D карты с использованием Cesium.
Отображает спутники и их зоны покрытия на интерактивной 3D карте.
"""
template_name = 'mapsapp/map3d.html'
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(**kwargs)
# Оптимизированный запрос - загружаем только необходимые поля
context['sats'] = Satellite.objects.filter(
parameters__objitems__isnull=False
).distinct().only('id', 'name').order_by('name')
return context
class GetFootprintsView(LoginRequiredMixin, View):
"""
API для получения зон покрытия (footprints) спутника.
Возвращает список названий зон покрытия для указанного спутника.
"""
def get(self, request, sat_id):
try:
# Оптимизированный запрос - загружаем только поле name
sat_name = Satellite.objects.only('name').get(id=sat_id).name
footprint_names = get_band_names(sat_name)
return JsonResponse(footprint_names, safe=False)
except Satellite.DoesNotExist:
return JsonResponse({"error": "Спутник не найден"}, status=404)
except Exception as e:
return JsonResponse({"error": str(e)}, status=500)
class TileProxyView(View):
"""
Прокси для загрузки тайлов карты покрытия спутников.
Кэширует тайлы на 7 дней для улучшения производительности.
"""
# Константы
TILE_BASE_URL = "https://static.satbeams.com/tiles"
CACHE_DURATION = 60 * 60 * 24 * 7 # 7 дней
REQUEST_TIMEOUT = 10 # секунд
@method_decorator(require_GET)
@method_decorator(cache_page(CACHE_DURATION))
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)
def get(self, request, footprint_name, z, x, y):
# Валидация имени footprint
if not footprint_name.replace('-', '').replace('_', '').isalnum():
return HttpResponse("Invalid footprint name", status=400)
url = f"{self.TILE_BASE_URL}/{footprint_name}/{z}/{x}/{y}.png"
try:
resp = requests.get(url, timeout=self.REQUEST_TIMEOUT)
if resp.status_code == 200:
response = HttpResponse(resp.content, content_type='image/png')
response["Access-Control-Allow-Origin"] = "*"
response["Cache-Control"] = f"public, max-age={self.CACHE_DURATION}"
return response
else:
return HttpResponseNotFound("Tile not found")
except requests.Timeout:
return HttpResponse("Request timeout", status=504)
except requests.RequestException as e:
return HttpResponse(f"Proxy error: {e}", status=500)
class LeafletMapView(LoginRequiredMixin, TemplateView):
"""
Представление для отображения 2D карты с использованием Leaflet.
Отображает спутники и транспондеры на интерактивной 2D карте.
"""
template_name = 'mapsapp/map2d.html'
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(**kwargs)
# Оптимизированные запросы - загружаем только необходимые поля
context['sats'] = Satellite.objects.filter(
parameters__objitems__isnull=False
).distinct().only('id', 'name').order_by('name')
context['trans'] = Transponders.objects.select_related(
'sat_id', 'polarization'
).only(
'id', 'name', 'sat_id__name', 'polarization__name',
'downlink', 'frequency_range', 'zone_name'
)
return context
class GetTransponderOnSatIdView(LoginRequiredMixin, View):
"""
API для получения транспондеров спутника.
Возвращает список транспондеров для указанного спутника с оптимизированными запросами.
"""
def get(self, request, sat_id):
# Оптимизированный запрос с select_related и only
trans = Transponders.objects.filter(
sat_id=sat_id
).select_related('polarization').only(
'name', 'downlink', 'frequency_range',
'zone_name', 'polarization__name'
)
if not trans.exists():
return JsonResponse({'error': 'Объектов не найдено'}, status=404)
# Используем list comprehension для лучшей производительности
output = [
{
"name": tran.name,
"frequency": tran.downlink,
"frequency_range": tran.frequency_range,
"zone_name": tran.zone_name,
"polarization": tran.polarization.name if tran.polarization else "-"
}
for tran in trans
]
# Standard library imports
from typing import Any, Dict
# Django imports
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponse, HttpResponseNotFound, JsonResponse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.cache import cache_page
from django.views.decorators.http import require_GET
from django.views.generic import TemplateView
# Third-party imports
import requests
# Local imports
from mainapp.models import Satellite
from .models import Transponders
from .utils import get_band_names
class CesiumMapView(LoginRequiredMixin, TemplateView):
"""
Представление для отображения 3D карты с использованием Cesium.
Отображает спутники и их зоны покрытия на интерактивной 3D карте.
"""
template_name = 'mapsapp/map3d.html'
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(**kwargs)
# Оптимизированный запрос - загружаем только необходимые поля
context['sats'] = Satellite.objects.filter(
parameters__objitems__isnull=False
).distinct().only('id', 'name').order_by('name')
return context
class GetFootprintsView(LoginRequiredMixin, View):
"""
API для получения зон покрытия (footprints) спутника.
Возвращает список названий зон покрытия для указанного спутника.
"""
def get(self, request, sat_id):
try:
# Оптимизированный запрос - загружаем только поле name
sat_name = Satellite.objects.only('name').get(id=sat_id).name
footprint_names = get_band_names(sat_name)
return JsonResponse(footprint_names, safe=False)
except Satellite.DoesNotExist:
return JsonResponse({"error": "Спутник не найден"}, status=404)
except Exception as e:
return JsonResponse({"error": str(e)}, status=500)
class TileProxyView(View):
"""
Прокси для загрузки тайлов карты покрытия спутников.
Кэширует тайлы на 7 дней для улучшения производительности.
"""
# Константы
TILE_BASE_URL = "https://static.satbeams.com/tiles"
CACHE_DURATION = 60 * 60 * 24 * 7 # 7 дней
REQUEST_TIMEOUT = 10 # секунд
@method_decorator(require_GET)
@method_decorator(cache_page(CACHE_DURATION))
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)
def get(self, request, footprint_name, z, x, y):
# Валидация имени footprint
if not footprint_name.replace('-', '').replace('_', '').isalnum():
return HttpResponse("Invalid footprint name", status=400)
url = f"{self.TILE_BASE_URL}/{footprint_name}/{z}/{x}/{y}.png"
try:
resp = requests.get(url, timeout=self.REQUEST_TIMEOUT)
if resp.status_code == 200:
response = HttpResponse(resp.content, content_type='image/png')
response["Access-Control-Allow-Origin"] = "*"
response["Cache-Control"] = f"public, max-age={self.CACHE_DURATION}"
return response
else:
return HttpResponseNotFound("Tile not found")
except requests.Timeout:
return HttpResponse("Request timeout", status=504)
except requests.RequestException as e:
return HttpResponse(f"Proxy error: {e}", status=500)
class LeafletMapView(LoginRequiredMixin, TemplateView):
"""
Представление для отображения 2D карты с использованием Leaflet.
Отображает спутники и транспондеры на интерактивной 2D карте.
"""
template_name = 'mapsapp/map2d.html'
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(**kwargs)
# Оптимизированные запросы - загружаем только необходимые поля
context['sats'] = Satellite.objects.filter(
parameters__objitems__isnull=False
).distinct().only('id', 'name').order_by('name')
context['trans'] = Transponders.objects.select_related(
'sat_id', 'polarization'
).only(
'id', 'name', 'sat_id__name', 'polarization__name',
'downlink', 'frequency_range', 'zone_name'
)
return context
class GetTransponderOnSatIdView(LoginRequiredMixin, View):
"""
API для получения транспондеров спутника.
Возвращает список транспондеров для указанного спутника с оптимизированными запросами.
"""
def get(self, request, sat_id):
# Оптимизированный запрос с select_related и only
trans = Transponders.objects.filter(
sat_id=sat_id
).select_related('polarization').only(
'name', 'downlink', 'frequency_range',
'zone_name', 'polarization__name'
)
if not trans.exists():
return JsonResponse({'error': 'Объектов не найдено'}, status=404)
# Используем list comprehension для лучшей производительности
output = [
{
"name": tran.name,
"frequency": tran.downlink,
"frequency_range": tran.frequency_range,
"zone_name": tran.zone_name,
"polarization": tran.polarization.name if tran.polarization else "-"
}
for tran in trans
]
return JsonResponse(output, safe=False)