init commit

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

View File

24
dbapp/mapsapp/admin.py Normal file
View File

@@ -0,0 +1,24 @@
from django.contrib import admin
from .models import Transponders
from rangefilter.filters import NumericRangeFilterBuilder
from more_admin_filters import MultiSelectDropdownFilter, MultiSelectFilter, MultiSelectRelatedDropdownFilter
from import_export.admin import ImportExportActionModelAdmin
@admin.register(Transponders)
class PolarizationAdmin(ImportExportActionModelAdmin, admin.ModelAdmin):
list_display = (
"sat_id",
"name",
"zone_name",
"frequency",
"frequency_range",
"polarization",
)
list_filter = (
("polarization", MultiSelectRelatedDropdownFilter),
("sat_id", MultiSelectRelatedDropdownFilter),
("frequency", NumericRangeFilterBuilder()),
"zone_name"
)
search_fields = ("name",)
ordering = ("name",)

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

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

View File

@@ -0,0 +1,33 @@
# Generated by Django 5.2.7 on 2025-10-13 12:47
import django.db.models.deletion
import mainapp.models
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('mainapp', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Transponders',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(blank=True, max_length=30, null=True, verbose_name='Название транспондера')),
('frequency', models.FloatField(blank=True, null=True, verbose_name='Центральная частота')),
('frequency_range', models.FloatField(blank=True, null=True, verbose_name='Полоса частот')),
('zone_name', models.CharField(blank=True, max_length=60, null=True, verbose_name='Название зоны')),
('polarization', models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='tran_polarizations', to='mainapp.polarization', verbose_name='Поляризация')),
('sat_id', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='tran_satellite', to='mainapp.satellite', verbose_name='Спутник')),
],
options={
'verbose_name': 'Транспондер',
'verbose_name_plural': 'Транспондеры',
},
),
]

View File

21
dbapp/mapsapp/models.py Normal file
View File

@@ -0,0 +1,21 @@
from django.db import models
from mainapp.models import Satellite, Polarization, get_default_polarization
class Transponders(models.Model):
name = models.CharField(max_length=30, null=True, blank=True, verbose_name="Название транспондера")
frequency = models.FloatField(blank=True, null=True, verbose_name="Центральная частота")
frequency_range = models.FloatField(blank=True, null=True, verbose_name="Полоса частот")
zone_name = models.CharField(max_length=60, blank=True, null=True, verbose_name="Название зоны")
polarization = models.ForeignKey(
Polarization, default=get_default_polarization, on_delete=models.SET_DEFAULT, related_name="tran_polarizations", null=True, blank=True, verbose_name="Поляризация"
)
sat_id = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="tran_satellite", verbose_name="Спутник")
def __str__(self):
return self.name
class Meta:
verbose_name = "Транспондер"
verbose_name_plural = "Транспондеры"

View File

@@ -0,0 +1,561 @@
{% extends "mapsapp/map2d_base.html" %}
{% load static %}
{% block content %}
<div class="db-objects-panel" style="position: absolute; top: 100px; z-index: 1000; background: white; padding: 10px; border: 1px solid #ccc; border-radius: 5px;">
<div class="panel-title">Объекты из базы</div>
<select id="objectSelector" class="object-select">
<option value="">— Выберите объект —</option>
{% for sat in sats %}
<option value="{{ sat.id }}">{{ sat.name }}</option>
{% endfor %}
</select>
<button id="loadObjectBtn" class="load-btn" style="display: block; width: 100%; margin-top: 10px;">Все точки</button>
<button id="loadObjectTransBtn" class="load-btn" style="display: block; width: 100%; margin-top: 10px;">Точки транспондеров</button>
<button id="clearMarkersBtn" type="button" onclick="clearAllMarkers()" style="display: block; width: 100%; margin-top: 10px;">Очистить маркеры</button>
</div>
<div class="footprint-control" style="position: absolute; top: 270px; z-index: 1000; background: white; padding: 10px; border: 1px solid #ccc; border-radius: 5px;">
<div class="panel-title">Области покрытия</div>
<div class="footprint-actions">
<button id="showAllFootprints">Показать все</button>
<button id="hideAllFootprints">Скрыть все</button>
</div>
<div id="footprintToggles"></div>
</div>
{% endblock content %}
{% block extra_js %}
<script>
function clearAllMarkers() {
if (window.mainTreeControl && window.mainTreeControl._map) {
map.removeControl(window.mainTreeControl);
delete window.mainTreeControl;
if (window.geoJsonOverlaysControl) {
delete window.geoJsonOverlaysControl;
}
}
map.eachLayer(function(layer) {
if (!(layer instanceof L.TileLayer)) {
map.removeLayer(layer);
}
});
}
let markerColors = ['red', 'green', 'orange', 'violet', 'grey', 'black', 'blue'];
function getColorIcon(color) {
return L.icon({
iconUrl: '{% static "leaflet-markers/img/marker-icon-" %}' + color + '.png',
shadowUrl: '{% static "leaflet-markers/img/marker-shadow.png" %}',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
});
}
// --- Новая функция загрузки и отображения GeoJSON ---
function loadGeoJsonForSatellite(satId) {
if (!satId) {
alert('Пожалуйста, выберите объект.');
return;
}
if (window.mainTreeControl && window.mainTreeControl._map) {
map.removeControl(window.mainTreeControl);
delete window.mainTreeControl;
}
const url = `/api/locations/${encodeURIComponent(satId)}/geojson`;
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error('Сервер вернул ошибку: ' + response.status);
}
return response.json();
})
.then(data => {
if (!data.features || data.features.length === 0) {
alert('Объекты с таким именем не найдены.');
return;
}
// --- Группировка данных по частоте ---
const groupedByFreq = {};
data.features.forEach(feature => {
const freq = feature.properties.freq/1000000;
if (!groupedByFreq[freq]) {
groupedByFreq[freq] = [];
}
groupedByFreq[freq].push(feature);
});
// --- Создание overlay слоев для L.control.layers.tree ---
const overlays = [];
let freqIndex = 0;
for (const [freq, features] of Object.entries(groupedByFreq)) {
const colorName = markerColors[freqIndex % markerColors.length];
const freqIcon = getColorIcon(colorName);
const freqGroupLayer = L.layerGroup();
const subgroup = [];
features.forEach((feature, idx) => {
const [lon, lat] = feature.geometry.coordinates;
const pointName = feature.properties.name || `Точка ${idx}`;
const marker = L.marker([lat, lon], { icon: freqIcon })
.bindPopup(`${pointName}<br>Частота: ${freq}`);
freqGroupLayer.addLayer(marker);
subgroup.push({
label: freq == -1 ? `${idx + 1} - Неизвестно` : `${idx + 1} - ${freq} МГц`,
layer: marker
});
});
// Группа для частоты
overlays.push({
label: `${features[0].properties.name} (${freq} МГц)`,
selectAllCheckbox: true,
children: subgroup,
layer: freqGroupLayer
});
freqIndex++;
}
const rootGroup = {
label: "Все точки",
selectAllCheckbox: true,
children: overlays,
layer: L.layerGroup()
};
const geoJsonControl = L.control.layers.tree(baseLayers, [rootGroup], {
collapsed: false,
autoZIndex: true
});
window.geoJsonOverlaysControl = geoJsonControl;
if (window.mainTreeControl && window.mainTreeControl._map) {
map.removeControl(window.mainTreeControl);
delete window.mainTreeControl;
}
window.mainTreeControl = geoJsonControl.addTo(map);
})
.catch(err => {
console.error('Ошибка загрузки GeoJSON:', err);
alert('Не удалось загрузить объекты: ' + err.message);
if (window.mainTreeControl && window.mainTreeControl._map) {
map.removeControl(window.mainTreeControl);
delete window.mainTreeControl;
}
});
}
function loadTranspondersPointsForSatellite(satId) {
if (!satId) {
alert('Пожалуйста, выберите объект.');
return;
}
// --- Явная очистка перед началом ---
if (window.mainTreeControl && window.mainTreeControl._map) {
console.log('Удаляем старый контрол точек');
map.removeControl(window.mainTreeControl);
delete window.mainTreeControl;
// window.geoJsonOverlaysControl также можно удалить, если он больше не нужен
if (window.geoJsonOverlaysControl) {
delete window.geoJsonOverlaysControl;
}
}
// --- Конец очистки ---
const url_points = `/api/locations/${encodeURIComponent(satId)}/geojson`;
const url_trans = `/api/transponders/${encodeURIComponent(satId)}`;
fetch(url_trans)
.then(response => {
if (!response.ok) {
throw new Error('Сервер вернул ошибку при загрузке транспондеров: ' + response.status);
}
return response.json();
})
.then(data_trans => {
console.log('Загруженные транспондеры:', data_trans);
return fetch(url_points)
.then(response => {
if (!response.ok) {
throw new Error('Сервер вернул ошибку при загрузке точек: ' + response.status);
}
return response.json();
})
.then(data_points => {
console.log('Загруженные точки:', data_points);
processAndDisplayTransponderPointsByZone(data_points, data_trans);
});
})
.catch(err => {
console.error('Ошибка загрузки транспондеров или точек:', err);
alert('Не удалось загрузить данные: ' + err.message);
// Повторная проверка на случай ошибки
if (window.mainTreeControl && window.mainTreeControl._map) {
map.removeControl(window.mainTreeControl);
delete window.mainTreeControl;
if (window.geoJsonOverlaysControl) {
delete window.geoJsonOverlaysControl;
}
}
});
}
function processAndDisplayTransponderPointsByZone(data_points, transpondersData) {
if (!data_points.features || data_points.features.length === 0) {
alert('Точки с таким идентификатором спутника не найдены.');
return;
}
if (!transpondersData || !Array.isArray(transpondersData)) {
console.error('Данные транспондеров недоступны или некорректны.');
alert('Ошибка: данные транспондеров отсутствуют.');
return;
}
// --- Функция для определения транспондера по частоте и поляризации ---
function findTransponderForPoint(pointFreqHz, pointPolarization) {
const pointFreqMhz = pointFreqHz / 1000000; // Переводим в МГц
for (const trans of transpondersData) {
if (typeof trans.frequency !== 'number' || typeof trans.frequency_range !== 'number' || typeof trans.polarization !== 'string') {
continue;
}
const centerFreq = trans.frequency;
const bandwidth = trans.frequency_range;
const halfBandwidth = bandwidth / 2;
const lowerBound = centerFreq - halfBandwidth;
const upperBound = centerFreq + halfBandwidth;
if (
pointFreqMhz >= lowerBound &&
pointFreqMhz <= upperBound &&
pointPolarization === trans.polarization
) {
return trans;
}
}
return null;
}
// --- Группировка точек по транспондерам ---
const groupedByTransponder = {};
data_points.features.forEach(feature => {
const pointFreqHz = feature.properties.freq;
let pointPolarization = feature.properties.polarization;
if (pointPolarization === undefined || pointPolarization === null) {
pointPolarization = feature.properties.pol;
}
if (pointPolarization === undefined || pointPolarization === null) {
pointPolarization = feature.properties.polar;
}
if (pointPolarization === undefined || pointPolarization === null) {
pointPolarization = feature.properties.polarisation;
}
if (pointPolarization === undefined || pointPolarization === null) {
console.warn('Точка без поляризации, игнорируется:', feature);
return;
}
const transponder = findTransponderForPoint(pointFreqHz, pointPolarization);
if (transponder) {
// --- ИСПРАВЛЕНО: используем name как уникальный идентификатор ---
const transId = transponder.name;
if (!groupedByTransponder[transId]) {
groupedByTransponder[transId] = {
transponder: transponder,
features: []
};
}
groupedByTransponder[transId].features.push(feature);
} else {
console.log(`Точка ${pointFreqHz / 1000000} МГц, ${pointPolarization} -> Не найден транспондер`);
}
});
console.log('Сгруппированные данные:', groupedByTransponder);
// --- Создание иерархии: зоны -> транспондеры -> точки (внутри слоя) ---
const zonesMap = {};
for (const [transId, groupData] of Object.entries(groupedByTransponder)) {
const trans = groupData.transponder;
const zoneName = trans.zone_name || 'Без зоны';
if (!zonesMap[zoneName]) {
zonesMap[zoneName] = [];
}
zonesMap[zoneName].push({
transponder: trans,
features: groupData.features
});
}
console.log('Сгруппировано по зонам:', zonesMap);
// --- Создание overlay слоев для L.control.layers.tree ---
const overlays = [];
let zoneIndex = 0;
for (const [zoneName, transponderGroups] of Object.entries(zonesMap)) {
const zoneGroupLayer = L.layerGroup();
const zoneChildren = [];
let transIndex = 0;
for (const transGroup of transponderGroups) {
const trans = transGroup.transponder;
const features = transGroup.features;
// Слой для одного транспондера
const transGroupLayer = L.layerGroup();
// Проходим по точкам транспондера
features.forEach(feature => {
const [lon, lat] = feature.geometry.coordinates;
const pointName = feature.properties.name || `Точка`;
const pointFreqHz = feature.properties.freq;
const pointFreqMhz = (pointFreqHz / 1000000).toFixed(2);
const pointPolarization = feature.properties.polarization || feature.properties.pol || feature.properties.polar || feature.properties.polarisation || '?';
// --- НОВОЕ: определяем цвет по частоте точки ---
// Округляем частоту до ближайшего целого Гц или, например, до 1000 Гц (1 кГц) для группировки
// const freqKey = Math.round(pointFreqHz / 1000); // Группировка по 1 кГц
const freqKey = Math.round(pointFreqHz); // Группировка по 1 Гц (можно изменить)
const colorIndex = freqKey % markerColors.length; // Индекс цвета зависит от частоты
const colorName = markerColors[colorIndex];
const pointIcon = getColorIcon(colorName); // Создаём иконку с цветом для этой точки
const marker = L.marker([lat, lon], { icon: pointIcon }) // Используем иконку точки
.bindPopup(`${pointName}<br>Частота: ${pointFreqMhz} МГц<br>Поляр.: ${pointPolarization}<br>Транспондер: ${trans.name}<br>Зона: ${trans.zone_name}`);
transGroupLayer.addLayer(marker);
});
// Добавляем транспондер в дочерние элементы зоны
// Транспондер не будет иметь фиксированного цвета, только его точки
const lowerBound = (trans.frequency - trans.frequency_range/2).toFixed(2);
const upperBound = (trans.frequency + trans.frequency_range/2).toFixed(2);
zoneChildren.push({
label: `${trans.name} (${lowerBound} - ${upperBound})`,
selectAllCheckbox: true,
layer: transGroupLayer // Этот слой содержит точки с разными цветами
});
zoneGroupLayer.addLayer(transGroupLayer);
transIndex++;
}
overlays.push({
label: zoneName,
selectAllCheckbox: true,
children: zoneChildren,
layer: zoneGroupLayer
});
zoneIndex++;
}
// --- Корневая группа ---
const rootGroup = {
label: "Все точки",
selectAllCheckbox: true,
children: overlays,
layer: L.layerGroup()
};
// --- Создаем контрол и добавляем на карту ---
const geoJsonControl = L.control.layers.tree(baseLayers, [rootGroup], {
collapsed: false,
autoZIndex: true
});
window.geoJsonOverlaysControl = geoJsonControl;
if (window.mainTreeControl && window.mainTreeControl._map) {
map.removeControl(window.mainTreeControl);
delete window.mainTreeControl;
}
window.mainTreeControl = geoJsonControl.addTo(map);
}
// --- Обработчики событий ---
document.addEventListener('DOMContentLoaded', () => {
const select = document.getElementById('objectSelector');
select.selectedIndex = 0;
const loadBtn = document.getElementById('loadObjectBtn');
const transBtn = document.getElementById('loadObjectTransBtn')
// Загружаем footprint'ы при смене выбора
select.addEventListener('change', function () {
const satId = this.value;
console.log(satId);
loadFootprintsForSatellite(satId);
});
// Загружаем GeoJSON при нажатии кнопки
loadBtn.addEventListener('click', function () {
const satId = select.value;
loadGeoJsonForSatellite(satId);
});
transBtn.addEventListener('click', function () {
const satId = select.value;
loadTranspondersPointsForSatellite(satId);
});
});
let currentFootprintLayers = {};
let currentSatelliteId = null;
const togglesContainer = document.getElementById('footprintToggles');
const showAllBtn = document.getElementById('showAllFootprints');
const hideAllBtn = document.getElementById('hideAllFootprints');
// --- Функции ---
function escapeHtml(unsafe) {
// Простая функция для экранирования HTML-символов в именах
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
function clearFootprintUIAndLayers() {
// Удаляем все текущие слои footprint'ов с карты и очищаем объект
Object.entries(currentFootprintLayers).forEach(([name, layer]) => {
if (map.hasLayer(layer)) {
map.removeLayer(layer);
}
});
currentFootprintLayers = {};
// Очищаем контейнер с чекбоксами
togglesContainer.innerHTML = '';
}
function loadFootprintsForSatellite(satId) {
// Проверка, если satId пустой - очищаем
if (!satId) {
clearFootprintUIAndLayers();
currentSatelliteId = null;
return;
}
// Сохраняем текущий ID спутника
currentSatelliteId = satId;
const url = `/api/footprint-names/${encodeURIComponent(satId)}`;
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error('Ошибка загрузки footprint\'ов: ' + response.statusText);
}
return response.json();
})
.then(footprints => {
if (!Array.isArray(footprints)) {
throw new Error('Ожидался массив footprint\'ов');
}
// Очищаем старое состояние
clearFootprintUIAndLayers();
// Создаём новые слои и чекбоксы
footprints.forEach(fp => {
// 1. Создаём тайловый слой Leaflet
// Убедитесь, что URL соответствует вашей структуре тайлов
const layer = L.tileLayer(`/tiles/${fp.name}/{z}/{x}/{y}.png`, {
minZoom: 0,
maxZoom: 21, // Установите соответствующий maxZoom
opacity: 0.7, // Установите нужную прозрачность
// attribution: 'SatBeams Rendered' // Можно добавить атрибуцию
});
// Слои изначально ДОБАВЛЕНЫ на карту (и видимы), если хотите изначально скрытыми - закомментируйте следующую строку
layer.addTo(map);
// Сохраняем слой в объекте
currentFootprintLayers[fp.name] = layer;
const safeNameAttr = encodeURIComponent(fp.name); // для data-атрибута
const safeFullName = escapeHtml(fp.fullname); // для отображения
// 2. Создаём чекбокс и метку
const label = document.createElement('label');
label.style.display = 'block';
label.style.margin = '4px 0';
// Чекбокс изначально отмечен, если слой добавлен на карту
label.innerHTML = `
<input type="checkbox"
data-footprint="${safeNameAttr}"
checked> <!-- Отмечен, так как слой добавлен -->
${safeFullName}
`;
togglesContainer.appendChild(label);
// 3. Связываем чекбокс со слоем
const checkbox = label.querySelector('input');
checkbox.addEventListener('change', function () {
const footprintName = decodeURIComponent(this.dataset.footprint);
const layer = currentFootprintLayers[footprintName];
if (layer) {
if (this.checked && !map.hasLayer(layer)) {
// Если чекбокс отмечен и слой не на карте - добавляем
map.addLayer(layer);
} else if (!this.checked && map.hasLayer(layer)) {
// Если чекбокс снят и слой на карте - удаляем
map.removeLayer(layer);
}
}
});
});
})
.catch(err => {
console.error('Ошибка загрузки footprint\'ов:', err);
alert('Не удалось загрузить области покрытия: ' + err.message);
clearFootprintUIAndLayers(); // При ошибке очищаем UI
});
}
function showAllFootprints() {
Object.entries(currentFootprintLayers).forEach(([name, layer]) => {
if (!map.hasLayer(layer)) {
map.addLayer(layer);
}
});
// Синхронизируем чекбоксы
document.querySelectorAll('#footprintToggles input[type="checkbox"]').forEach(cb => {
cb.checked = true;
});
}
function hideAllFootprints() {
Object.entries(currentFootprintLayers).forEach(([name, layer]) => {
if (map.hasLayer(layer)) {
map.removeLayer(layer);
}
});
document.querySelectorAll('#footprintToggles input[type="checkbox"]').forEach(cb => {
cb.checked = false;
});
}
// --- Обработчики событий для кнопок ---
showAllBtn.addEventListener('click', showAllFootprints);
hideAllBtn.addEventListener('click', hideAllFootprints);
</script>
{% endblock extra_js %}

View File

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

View File

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

3
dbapp/mapsapp/tests.py Normal file
View File

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

17
dbapp/mapsapp/urls.py Normal file
View File

@@ -0,0 +1,17 @@
from django.conf import settings
from django.conf.urls.static import static
from django.urls import path
from . import views
urlpatterns = [
path('3dmap', views.cesium_map, name='3dmap'),
path('2dmap', views.leaflet_map, name='2dmap'),
path('api/footprint-names/<int:sat_id>', views.get_footprints, name="footprint_names"),
path('api/transponders/<int:sat_id>', views.get_transponder_on_satid, name='transponders_data'),
path('tiles/<str:footprint_name>/<int:z>/<int:x>/<int:y>.png', views.tile_proxy, name='tile_proxy'),
# path('', views.home_page, name='home'),
# path('excel-data', views.load_excel_data, name='load_excel_data'),
# path('satellites', views.add_satellites, name='add_sats'),
]

92
dbapp/mapsapp/utils.py Normal file
View File

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

68
dbapp/mapsapp/views.py Normal file
View File

@@ -0,0 +1,68 @@
from django.shortcuts import render
from django.http import JsonResponse
import requests
from django.core import serializers
from django.http import HttpResponse, HttpResponseNotFound
from django.views.decorators.cache import cache_page
from django.views.decorators.http import require_GET
from mainapp.models import Satellite
from .models import Transponders
from .utils import get_band_names
def cesium_map(request):
sats = Satellite.objects.all()
return render(request, 'mapsapp/map3d.html', {'sats': sats})
def get_footprints(request, sat_id):
try:
sat_name = Satellite.objects.get(id=sat_id).name
footprint_names = get_band_names(sat_name)
return JsonResponse(footprint_names, safe=False)
except Exception as e:
return JsonResponse({"error": str(e)}, status=400)
@require_GET
@cache_page(60 * 60 * 24)
def tile_proxy(request, footprint_name, z, x, y):
if not footprint_name.replace('-', '').replace('_', '').isalnum():
return HttpResponse("Invalid footprint name", status=400)
url = f"https://static.satbeams.com/tiles/{footprint_name}/{z}/{x}/{y}.png"
try:
resp = requests.get(url, timeout=10)
if resp.status_code == 200:
response = HttpResponse(resp.content, content_type='image/png')
response["Access-Control-Allow-Origin"] = "*"
return response
else:
return HttpResponseNotFound("Tile not found")
except Exception as e:
return HttpResponse(f"Proxy error: {e}", status=500)
def leaflet_map(request):
sats = Satellite.objects.all()
trans = Transponders.objects.all()
return render(request, 'mapsapp/map2d.html', {'sats': sats, 'trans': trans})
def get_transponder_on_satid(request, sat_id):
trans = Transponders.objects.filter(sat_id=sat_id)
output = []
for tran in trans:
output.append(
{
"name": tran.name,
"frequency": tran.frequency,
"frequency_range": tran.frequency_range,
"zone_name": tran.zone_name,
"polarization": tran.polarization.name
}
)
if not trans:
return JsonResponse({'error': 'Объектов не найдено'}, status=400)
return JsonResponse(output, safe=False)