init commit
This commit is contained in:
0
dbapp/mapsapp/__init__.py
Normal file
0
dbapp/mapsapp/__init__.py
Normal file
24
dbapp/mapsapp/admin.py
Normal file
24
dbapp/mapsapp/admin.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from django.contrib import admin
|
||||
from .models import Transponders
|
||||
from rangefilter.filters import NumericRangeFilterBuilder
|
||||
from more_admin_filters import MultiSelectDropdownFilter, MultiSelectFilter, MultiSelectRelatedDropdownFilter
|
||||
from import_export.admin import ImportExportActionModelAdmin
|
||||
|
||||
@admin.register(Transponders)
|
||||
class PolarizationAdmin(ImportExportActionModelAdmin, admin.ModelAdmin):
|
||||
list_display = (
|
||||
"sat_id",
|
||||
"name",
|
||||
"zone_name",
|
||||
"frequency",
|
||||
"frequency_range",
|
||||
"polarization",
|
||||
)
|
||||
list_filter = (
|
||||
("polarization", MultiSelectRelatedDropdownFilter),
|
||||
("sat_id", MultiSelectRelatedDropdownFilter),
|
||||
("frequency", NumericRangeFilterBuilder()),
|
||||
"zone_name"
|
||||
)
|
||||
search_fields = ("name",)
|
||||
ordering = ("name",)
|
||||
6
dbapp/mapsapp/apps.py
Normal file
6
dbapp/mapsapp/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class MapsappConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'mapsapp'
|
||||
33
dbapp/mapsapp/migrations/0001_initial.py
Normal file
33
dbapp/mapsapp/migrations/0001_initial.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-13 12:47
|
||||
|
||||
import django.db.models.deletion
|
||||
import mainapp.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Transponders',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(blank=True, max_length=30, null=True, verbose_name='Название транспондера')),
|
||||
('frequency', models.FloatField(blank=True, null=True, verbose_name='Центральная частота')),
|
||||
('frequency_range', models.FloatField(blank=True, null=True, verbose_name='Полоса частот')),
|
||||
('zone_name', models.CharField(blank=True, max_length=60, null=True, verbose_name='Название зоны')),
|
||||
('polarization', models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='tran_polarizations', to='mainapp.polarization', verbose_name='Поляризация')),
|
||||
('sat_id', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='tran_satellite', to='mainapp.satellite', verbose_name='Спутник')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Транспондер',
|
||||
'verbose_name_plural': 'Транспондеры',
|
||||
},
|
||||
),
|
||||
]
|
||||
0
dbapp/mapsapp/migrations/__init__.py
Normal file
0
dbapp/mapsapp/migrations/__init__.py
Normal file
21
dbapp/mapsapp/models.py
Normal file
21
dbapp/mapsapp/models.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from django.db import models
|
||||
from mainapp.models import Satellite, Polarization, get_default_polarization
|
||||
|
||||
class Transponders(models.Model):
|
||||
name = models.CharField(max_length=30, null=True, blank=True, verbose_name="Название транспондера")
|
||||
frequency = models.FloatField(blank=True, null=True, verbose_name="Центральная частота")
|
||||
frequency_range = models.FloatField(blank=True, null=True, verbose_name="Полоса частот")
|
||||
zone_name = models.CharField(max_length=60, blank=True, null=True, verbose_name="Название зоны")
|
||||
polarization = models.ForeignKey(
|
||||
Polarization, default=get_default_polarization, on_delete=models.SET_DEFAULT, related_name="tran_polarizations", null=True, blank=True, verbose_name="Поляризация"
|
||||
)
|
||||
sat_id = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="tran_satellite", verbose_name="Спутник")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Транспондер"
|
||||
verbose_name_plural = "Транспондеры"
|
||||
|
||||
|
||||
561
dbapp/mapsapp/templates/mapsapp/map2d.html
Normal file
561
dbapp/mapsapp/templates/mapsapp/map2d.html
Normal file
@@ -0,0 +1,561 @@
|
||||
{% extends "mapsapp/map2d_base.html" %}
|
||||
{% load static %}
|
||||
{% block content %}
|
||||
|
||||
<div class="db-objects-panel" style="position: absolute; top: 100px; z-index: 1000; background: white; padding: 10px; border: 1px solid #ccc; border-radius: 5px;">
|
||||
<div class="panel-title">Объекты из базы</div>
|
||||
<select id="objectSelector" class="object-select">
|
||||
<option value="">— Выберите объект —</option>
|
||||
{% for sat in sats %}
|
||||
<option value="{{ sat.id }}">{{ sat.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button id="loadObjectBtn" class="load-btn" style="display: block; width: 100%; margin-top: 10px;">Все точки</button>
|
||||
<button id="loadObjectTransBtn" class="load-btn" style="display: block; width: 100%; margin-top: 10px;">Точки транспондеров</button>
|
||||
<button id="clearMarkersBtn" type="button" onclick="clearAllMarkers()" style="display: block; width: 100%; margin-top: 10px;">Очистить маркеры</button>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="footprint-control" style="position: absolute; top: 270px; z-index: 1000; background: white; padding: 10px; border: 1px solid #ccc; border-radius: 5px;">
|
||||
<div class="panel-title">Области покрытия</div>
|
||||
<div class="footprint-actions">
|
||||
<button id="showAllFootprints">Показать все</button>
|
||||
<button id="hideAllFootprints">Скрыть все</button>
|
||||
</div>
|
||||
<div id="footprintToggles"></div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
function clearAllMarkers() {
|
||||
if (window.mainTreeControl && window.mainTreeControl._map) {
|
||||
map.removeControl(window.mainTreeControl);
|
||||
delete window.mainTreeControl;
|
||||
if (window.geoJsonOverlaysControl) {
|
||||
delete window.geoJsonOverlaysControl;
|
||||
}
|
||||
}
|
||||
map.eachLayer(function(layer) {
|
||||
if (!(layer instanceof L.TileLayer)) {
|
||||
map.removeLayer(layer);
|
||||
}
|
||||
});
|
||||
}
|
||||
let markerColors = ['red', 'green', 'orange', 'violet', 'grey', 'black', 'blue'];
|
||||
|
||||
function getColorIcon(color) {
|
||||
return L.icon({
|
||||
iconUrl: '{% static "leaflet-markers/img/marker-icon-" %}' + color + '.png',
|
||||
shadowUrl: '{% static "leaflet-markers/img/marker-shadow.png" %}',
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
}
|
||||
|
||||
// --- Новая функция загрузки и отображения GeoJSON ---
|
||||
function loadGeoJsonForSatellite(satId) {
|
||||
if (!satId) {
|
||||
alert('Пожалуйста, выберите объект.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.mainTreeControl && window.mainTreeControl._map) {
|
||||
map.removeControl(window.mainTreeControl);
|
||||
delete window.mainTreeControl;
|
||||
}
|
||||
|
||||
const url = `/api/locations/${encodeURIComponent(satId)}/geojson`;
|
||||
|
||||
fetch(url)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Сервер вернул ошибку: ' + response.status);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (!data.features || data.features.length === 0) {
|
||||
alert('Объекты с таким именем не найдены.');
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Группировка данных по частоте ---
|
||||
const groupedByFreq = {};
|
||||
data.features.forEach(feature => {
|
||||
const freq = feature.properties.freq/1000000;
|
||||
if (!groupedByFreq[freq]) {
|
||||
groupedByFreq[freq] = [];
|
||||
}
|
||||
groupedByFreq[freq].push(feature);
|
||||
});
|
||||
|
||||
// --- Создание overlay слоев для L.control.layers.tree ---
|
||||
const overlays = [];
|
||||
let freqIndex = 0;
|
||||
|
||||
for (const [freq, features] of Object.entries(groupedByFreq)) {
|
||||
const colorName = markerColors[freqIndex % markerColors.length];
|
||||
const freqIcon = getColorIcon(colorName);
|
||||
const freqGroupLayer = L.layerGroup();
|
||||
const subgroup = [];
|
||||
|
||||
features.forEach((feature, idx) => {
|
||||
const [lon, lat] = feature.geometry.coordinates;
|
||||
const pointName = feature.properties.name || `Точка ${idx}`;
|
||||
const marker = L.marker([lat, lon], { icon: freqIcon })
|
||||
.bindPopup(`${pointName}<br>Частота: ${freq}`);
|
||||
freqGroupLayer.addLayer(marker);
|
||||
|
||||
subgroup.push({
|
||||
label: freq == -1 ? `${idx + 1} - Неизвестно` : `${idx + 1} - ${freq} МГц`,
|
||||
layer: marker
|
||||
});
|
||||
});
|
||||
|
||||
// Группа для частоты
|
||||
overlays.push({
|
||||
label: `${features[0].properties.name} (${freq} МГц)`,
|
||||
selectAllCheckbox: true,
|
||||
children: subgroup,
|
||||
layer: freqGroupLayer
|
||||
});
|
||||
|
||||
freqIndex++;
|
||||
}
|
||||
const rootGroup = {
|
||||
label: "Все точки",
|
||||
selectAllCheckbox: true,
|
||||
children: overlays,
|
||||
layer: L.layerGroup()
|
||||
};
|
||||
|
||||
const geoJsonControl = L.control.layers.tree(baseLayers, [rootGroup], {
|
||||
collapsed: false,
|
||||
autoZIndex: true
|
||||
});
|
||||
|
||||
window.geoJsonOverlaysControl = geoJsonControl;
|
||||
if (window.mainTreeControl && window.mainTreeControl._map) {
|
||||
map.removeControl(window.mainTreeControl);
|
||||
delete window.mainTreeControl;
|
||||
}
|
||||
window.mainTreeControl = geoJsonControl.addTo(map);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Ошибка загрузки GeoJSON:', err);
|
||||
alert('Не удалось загрузить объекты: ' + err.message);
|
||||
if (window.mainTreeControl && window.mainTreeControl._map) {
|
||||
map.removeControl(window.mainTreeControl);
|
||||
delete window.mainTreeControl;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadTranspondersPointsForSatellite(satId) {
|
||||
if (!satId) {
|
||||
alert('Пожалуйста, выберите объект.');
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Явная очистка перед началом ---
|
||||
if (window.mainTreeControl && window.mainTreeControl._map) {
|
||||
console.log('Удаляем старый контрол точек');
|
||||
map.removeControl(window.mainTreeControl);
|
||||
delete window.mainTreeControl;
|
||||
// window.geoJsonOverlaysControl также можно удалить, если он больше не нужен
|
||||
if (window.geoJsonOverlaysControl) {
|
||||
delete window.geoJsonOverlaysControl;
|
||||
}
|
||||
}
|
||||
// --- Конец очистки ---
|
||||
|
||||
const url_points = `/api/locations/${encodeURIComponent(satId)}/geojson`;
|
||||
const url_trans = `/api/transponders/${encodeURIComponent(satId)}`;
|
||||
|
||||
fetch(url_trans)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Сервер вернул ошибку при загрузке транспондеров: ' + response.status);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data_trans => {
|
||||
console.log('Загруженные транспондеры:', data_trans);
|
||||
|
||||
return fetch(url_points)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Сервер вернул ошибку при загрузке точек: ' + response.status);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data_points => {
|
||||
console.log('Загруженные точки:', data_points);
|
||||
processAndDisplayTransponderPointsByZone(data_points, data_trans);
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Ошибка загрузки транспондеров или точек:', err);
|
||||
alert('Не удалось загрузить данные: ' + err.message);
|
||||
// Повторная проверка на случай ошибки
|
||||
if (window.mainTreeControl && window.mainTreeControl._map) {
|
||||
map.removeControl(window.mainTreeControl);
|
||||
delete window.mainTreeControl;
|
||||
if (window.geoJsonOverlaysControl) {
|
||||
delete window.geoJsonOverlaysControl;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function processAndDisplayTransponderPointsByZone(data_points, transpondersData) {
|
||||
if (!data_points.features || data_points.features.length === 0) {
|
||||
alert('Точки с таким идентификатором спутника не найдены.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!transpondersData || !Array.isArray(transpondersData)) {
|
||||
console.error('Данные транспондеров недоступны или некорректны.');
|
||||
alert('Ошибка: данные транспондеров отсутствуют.');
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Функция для определения транспондера по частоте и поляризации ---
|
||||
function findTransponderForPoint(pointFreqHz, pointPolarization) {
|
||||
const pointFreqMhz = pointFreqHz / 1000000; // Переводим в МГц
|
||||
for (const trans of transpondersData) {
|
||||
if (typeof trans.frequency !== 'number' || typeof trans.frequency_range !== 'number' || typeof trans.polarization !== 'string') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const centerFreq = trans.frequency;
|
||||
const bandwidth = trans.frequency_range;
|
||||
const halfBandwidth = bandwidth / 2;
|
||||
const lowerBound = centerFreq - halfBandwidth;
|
||||
const upperBound = centerFreq + halfBandwidth;
|
||||
|
||||
if (
|
||||
pointFreqMhz >= lowerBound &&
|
||||
pointFreqMhz <= upperBound &&
|
||||
pointPolarization === trans.polarization
|
||||
) {
|
||||
return trans;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- Группировка точек по транспондерам ---
|
||||
const groupedByTransponder = {};
|
||||
data_points.features.forEach(feature => {
|
||||
const pointFreqHz = feature.properties.freq;
|
||||
let pointPolarization = feature.properties.polarization;
|
||||
if (pointPolarization === undefined || pointPolarization === null) {
|
||||
pointPolarization = feature.properties.pol;
|
||||
}
|
||||
if (pointPolarization === undefined || pointPolarization === null) {
|
||||
pointPolarization = feature.properties.polar;
|
||||
}
|
||||
if (pointPolarization === undefined || pointPolarization === null) {
|
||||
pointPolarization = feature.properties.polarisation;
|
||||
}
|
||||
if (pointPolarization === undefined || pointPolarization === null) {
|
||||
console.warn('Точка без поляризации, игнорируется:', feature);
|
||||
return;
|
||||
}
|
||||
|
||||
const transponder = findTransponderForPoint(pointFreqHz, pointPolarization);
|
||||
|
||||
if (transponder) {
|
||||
// --- ИСПРАВЛЕНО: используем name как уникальный идентификатор ---
|
||||
const transId = transponder.name;
|
||||
if (!groupedByTransponder[transId]) {
|
||||
groupedByTransponder[transId] = {
|
||||
transponder: transponder,
|
||||
features: []
|
||||
};
|
||||
}
|
||||
groupedByTransponder[transId].features.push(feature);
|
||||
} else {
|
||||
console.log(`Точка ${pointFreqHz / 1000000} МГц, ${pointPolarization} -> Не найден транспондер`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Сгруппированные данные:', groupedByTransponder);
|
||||
|
||||
// --- Создание иерархии: зоны -> транспондеры -> точки (внутри слоя) ---
|
||||
const zonesMap = {};
|
||||
|
||||
for (const [transId, groupData] of Object.entries(groupedByTransponder)) {
|
||||
const trans = groupData.transponder;
|
||||
const zoneName = trans.zone_name || 'Без зоны';
|
||||
|
||||
if (!zonesMap[zoneName]) {
|
||||
zonesMap[zoneName] = [];
|
||||
}
|
||||
zonesMap[zoneName].push({
|
||||
transponder: trans,
|
||||
features: groupData.features
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Сгруппировано по зонам:', zonesMap);
|
||||
|
||||
// --- Создание overlay слоев для L.control.layers.tree ---
|
||||
const overlays = [];
|
||||
let zoneIndex = 0;
|
||||
|
||||
for (const [zoneName, transponderGroups] of Object.entries(zonesMap)) {
|
||||
const zoneGroupLayer = L.layerGroup();
|
||||
const zoneChildren = [];
|
||||
|
||||
let transIndex = 0;
|
||||
for (const transGroup of transponderGroups) {
|
||||
const trans = transGroup.transponder;
|
||||
const features = transGroup.features;
|
||||
|
||||
// Слой для одного транспондера
|
||||
const transGroupLayer = L.layerGroup();
|
||||
|
||||
// Проходим по точкам транспондера
|
||||
features.forEach(feature => {
|
||||
const [lon, lat] = feature.geometry.coordinates;
|
||||
const pointName = feature.properties.name || `Точка`;
|
||||
const pointFreqHz = feature.properties.freq;
|
||||
const pointFreqMhz = (pointFreqHz / 1000000).toFixed(2);
|
||||
const pointPolarization = feature.properties.polarization || feature.properties.pol || feature.properties.polar || feature.properties.polarisation || '?';
|
||||
|
||||
// --- НОВОЕ: определяем цвет по частоте точки ---
|
||||
// Округляем частоту до ближайшего целого Гц или, например, до 1000 Гц (1 кГц) для группировки
|
||||
// const freqKey = Math.round(pointFreqHz / 1000); // Группировка по 1 кГц
|
||||
const freqKey = Math.round(pointFreqHz); // Группировка по 1 Гц (можно изменить)
|
||||
const colorIndex = freqKey % markerColors.length; // Индекс цвета зависит от частоты
|
||||
const colorName = markerColors[colorIndex];
|
||||
const pointIcon = getColorIcon(colorName); // Создаём иконку с цветом для этой точки
|
||||
|
||||
const marker = L.marker([lat, lon], { icon: pointIcon }) // Используем иконку точки
|
||||
.bindPopup(`${pointName}<br>Частота: ${pointFreqMhz} МГц<br>Поляр.: ${pointPolarization}<br>Транспондер: ${trans.name}<br>Зона: ${trans.zone_name}`);
|
||||
transGroupLayer.addLayer(marker);
|
||||
});
|
||||
|
||||
// Добавляем транспондер в дочерние элементы зоны
|
||||
// Транспондер не будет иметь фиксированного цвета, только его точки
|
||||
const lowerBound = (trans.frequency - trans.frequency_range/2).toFixed(2);
|
||||
const upperBound = (trans.frequency + trans.frequency_range/2).toFixed(2);
|
||||
zoneChildren.push({
|
||||
label: `${trans.name} (${lowerBound} - ${upperBound})`,
|
||||
selectAllCheckbox: true,
|
||||
layer: transGroupLayer // Этот слой содержит точки с разными цветами
|
||||
});
|
||||
|
||||
zoneGroupLayer.addLayer(transGroupLayer);
|
||||
transIndex++;
|
||||
}
|
||||
|
||||
overlays.push({
|
||||
label: zoneName,
|
||||
selectAllCheckbox: true,
|
||||
children: zoneChildren,
|
||||
layer: zoneGroupLayer
|
||||
});
|
||||
|
||||
zoneIndex++;
|
||||
}
|
||||
|
||||
// --- Корневая группа ---
|
||||
const rootGroup = {
|
||||
label: "Все точки",
|
||||
selectAllCheckbox: true,
|
||||
children: overlays,
|
||||
layer: L.layerGroup()
|
||||
};
|
||||
|
||||
// --- Создаем контрол и добавляем на карту ---
|
||||
const geoJsonControl = L.control.layers.tree(baseLayers, [rootGroup], {
|
||||
collapsed: false,
|
||||
autoZIndex: true
|
||||
});
|
||||
|
||||
window.geoJsonOverlaysControl = geoJsonControl;
|
||||
if (window.mainTreeControl && window.mainTreeControl._map) {
|
||||
map.removeControl(window.mainTreeControl);
|
||||
delete window.mainTreeControl;
|
||||
}
|
||||
window.mainTreeControl = geoJsonControl.addTo(map);
|
||||
}
|
||||
|
||||
// --- Обработчики событий ---
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const select = document.getElementById('objectSelector');
|
||||
select.selectedIndex = 0;
|
||||
const loadBtn = document.getElementById('loadObjectBtn');
|
||||
const transBtn = document.getElementById('loadObjectTransBtn')
|
||||
|
||||
// Загружаем footprint'ы при смене выбора
|
||||
select.addEventListener('change', function () {
|
||||
const satId = this.value;
|
||||
console.log(satId);
|
||||
loadFootprintsForSatellite(satId);
|
||||
});
|
||||
|
||||
// Загружаем GeoJSON при нажатии кнопки
|
||||
loadBtn.addEventListener('click', function () {
|
||||
const satId = select.value;
|
||||
loadGeoJsonForSatellite(satId);
|
||||
});
|
||||
|
||||
transBtn.addEventListener('click', function () {
|
||||
const satId = select.value;
|
||||
loadTranspondersPointsForSatellite(satId);
|
||||
});
|
||||
});
|
||||
let currentFootprintLayers = {};
|
||||
let currentSatelliteId = null;
|
||||
|
||||
const togglesContainer = document.getElementById('footprintToggles');
|
||||
const showAllBtn = document.getElementById('showAllFootprints');
|
||||
const hideAllBtn = document.getElementById('hideAllFootprints');
|
||||
|
||||
// --- Функции ---
|
||||
|
||||
function escapeHtml(unsafe) {
|
||||
// Простая функция для экранирования HTML-символов в именах
|
||||
return unsafe
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function clearFootprintUIAndLayers() {
|
||||
// Удаляем все текущие слои footprint'ов с карты и очищаем объект
|
||||
Object.entries(currentFootprintLayers).forEach(([name, layer]) => {
|
||||
if (map.hasLayer(layer)) {
|
||||
map.removeLayer(layer);
|
||||
}
|
||||
});
|
||||
currentFootprintLayers = {};
|
||||
|
||||
// Очищаем контейнер с чекбоксами
|
||||
togglesContainer.innerHTML = '';
|
||||
}
|
||||
|
||||
function loadFootprintsForSatellite(satId) {
|
||||
// Проверка, если satId пустой - очищаем
|
||||
if (!satId) {
|
||||
clearFootprintUIAndLayers();
|
||||
currentSatelliteId = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Сохраняем текущий ID спутника
|
||||
currentSatelliteId = satId;
|
||||
|
||||
const url = `/api/footprint-names/${encodeURIComponent(satId)}`;
|
||||
|
||||
fetch(url)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Ошибка загрузки footprint\'ов: ' + response.statusText);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(footprints => {
|
||||
if (!Array.isArray(footprints)) {
|
||||
throw new Error('Ожидался массив footprint\'ов');
|
||||
}
|
||||
|
||||
// Очищаем старое состояние
|
||||
clearFootprintUIAndLayers();
|
||||
|
||||
// Создаём новые слои и чекбоксы
|
||||
footprints.forEach(fp => {
|
||||
// 1. Создаём тайловый слой Leaflet
|
||||
// Убедитесь, что URL соответствует вашей структуре тайлов
|
||||
const layer = L.tileLayer(`/tiles/${fp.name}/{z}/{x}/{y}.png`, {
|
||||
minZoom: 0,
|
||||
maxZoom: 21, // Установите соответствующий maxZoom
|
||||
opacity: 0.7, // Установите нужную прозрачность
|
||||
// attribution: 'SatBeams Rendered' // Можно добавить атрибуцию
|
||||
});
|
||||
|
||||
// Слои изначально ДОБАВЛЕНЫ на карту (и видимы), если хотите изначально скрытыми - закомментируйте следующую строку
|
||||
layer.addTo(map);
|
||||
|
||||
// Сохраняем слой в объекте
|
||||
currentFootprintLayers[fp.name] = layer;
|
||||
|
||||
const safeNameAttr = encodeURIComponent(fp.name); // для data-атрибута
|
||||
const safeFullName = escapeHtml(fp.fullname); // для отображения
|
||||
|
||||
// 2. Создаём чекбокс и метку
|
||||
const label = document.createElement('label');
|
||||
label.style.display = 'block';
|
||||
label.style.margin = '4px 0';
|
||||
// Чекбокс изначально отмечен, если слой добавлен на карту
|
||||
label.innerHTML = `
|
||||
<input type="checkbox"
|
||||
data-footprint="${safeNameAttr}"
|
||||
checked> <!-- Отмечен, так как слой добавлен -->
|
||||
${safeFullName}
|
||||
`;
|
||||
togglesContainer.appendChild(label);
|
||||
|
||||
// 3. Связываем чекбокс со слоем
|
||||
const checkbox = label.querySelector('input');
|
||||
checkbox.addEventListener('change', function () {
|
||||
const footprintName = decodeURIComponent(this.dataset.footprint);
|
||||
const layer = currentFootprintLayers[footprintName];
|
||||
if (layer) {
|
||||
if (this.checked && !map.hasLayer(layer)) {
|
||||
// Если чекбокс отмечен и слой не на карте - добавляем
|
||||
map.addLayer(layer);
|
||||
} else if (!this.checked && map.hasLayer(layer)) {
|
||||
// Если чекбокс снят и слой на карте - удаляем
|
||||
map.removeLayer(layer);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Ошибка загрузки footprint\'ов:', err);
|
||||
alert('Не удалось загрузить области покрытия: ' + err.message);
|
||||
clearFootprintUIAndLayers(); // При ошибке очищаем UI
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function showAllFootprints() {
|
||||
Object.entries(currentFootprintLayers).forEach(([name, layer]) => {
|
||||
if (!map.hasLayer(layer)) {
|
||||
map.addLayer(layer);
|
||||
}
|
||||
});
|
||||
// Синхронизируем чекбоксы
|
||||
document.querySelectorAll('#footprintToggles input[type="checkbox"]').forEach(cb => {
|
||||
cb.checked = true;
|
||||
});
|
||||
}
|
||||
|
||||
function hideAllFootprints() {
|
||||
Object.entries(currentFootprintLayers).forEach(([name, layer]) => {
|
||||
if (map.hasLayer(layer)) {
|
||||
map.removeLayer(layer);
|
||||
}
|
||||
});
|
||||
document.querySelectorAll('#footprintToggles input[type="checkbox"]').forEach(cb => {
|
||||
cb.checked = false;
|
||||
});
|
||||
}
|
||||
|
||||
// --- Обработчики событий для кнопок ---
|
||||
showAllBtn.addEventListener('click', showAllFootprints);
|
||||
hideAllBtn.addEventListener('click', hideAllFootprints);
|
||||
</script>
|
||||
{% endblock extra_js %}
|
||||
74
dbapp/mapsapp/templates/mapsapp/map2d_base.html
Normal file
74
dbapp/mapsapp/templates/mapsapp/map2d_base.html
Normal file
@@ -0,0 +1,74 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
||||
<meta charset="utf-8" />
|
||||
<title>{% block title %}Карта{% endblock %}</title>
|
||||
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
|
||||
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
|
||||
<link href="{% static 'leaflet/leaflet.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'leaflet-measure/leaflet-measure.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'leaflet-tree/L.Control.Layers.Tree.css' %}" rel="stylesheet">
|
||||
{% block extra_css %}{% endblock %}
|
||||
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
#map {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="map"></div>
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
<script src="{% static 'leaflet/leaflet.js' %}"></script>
|
||||
<script src="{% static 'leaflet-measure/leaflet-measure.ru.js' %}"></script>
|
||||
<script src="{% static 'leaflet-tree/L.Control.Layers.Tree.js' %}"></script>
|
||||
{% comment %} <script src="{% static 'leaflet-tree/LayersTree.js' %}"></script> {% endcomment %}
|
||||
|
||||
<script>
|
||||
let map = L.map('map').setView([0, 0], 2);
|
||||
L.control.scale({
|
||||
imperial: false,
|
||||
metric: true}).addTo(map);
|
||||
map.attributionControl.setPrefix(false);
|
||||
const street = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
});
|
||||
street.addTo(map);
|
||||
const satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
||||
attribution: 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'
|
||||
});
|
||||
const baseLayers = {
|
||||
"Улицы": street,
|
||||
"Спутник": satellite
|
||||
};
|
||||
L.control.layers(baseLayers).addTo(map);
|
||||
map.setMaxZoom(18);
|
||||
map.setMinZoom(0);
|
||||
L.control.measure({ primaryLengthUnit: 'kilometers' }).addTo(map);
|
||||
|
||||
{% comment %} let imageUrl = '{% static "mapsapp/assets/world_map.jpg" %}';
|
||||
let imageBounds = [[-82, -180], [82, 180]];
|
||||
|
||||
L.imageOverlay(imageUrl, imageBounds).addTo(map); {% endcomment %}
|
||||
|
||||
</script>
|
||||
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
115
dbapp/mapsapp/templates/mapsapp/map3d.html
Normal file
115
dbapp/mapsapp/templates/mapsapp/map3d.html
Normal file
@@ -0,0 +1,115 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
|
||||
<title>Cesium Map Editor</title>
|
||||
<script src="{% static 'cesium/Cesium.js' %}"></script>
|
||||
<link href="{% static 'cesium/Widgets/widgets.css' %}" rel="stylesheet">
|
||||
<link rel="stylesheet" href="{% static 'mapsapp/style.css' %}">
|
||||
</head>
|
||||
<body>
|
||||
<div id="cesiumContainer"></div>
|
||||
<input type="file" id="fileInput" accept=".geojson,.json,.kml" style="display: none;" />
|
||||
<!-- Панель инструментов -->
|
||||
<div class="toolbar">
|
||||
<!-- Группа 1: Режимы рисования -->
|
||||
<div class="toolbar-section">
|
||||
<div class="section-title">Рисование</div>
|
||||
<div class="toolbar-group">
|
||||
<button id="selectMode" class="tool-btn active" title="Режим выделения (S)">
|
||||
<span>🔍</span> Выделение
|
||||
</button>
|
||||
<button id="markerMode" class="tool-btn" title="Добавить маркер (M)">
|
||||
<span>📌</span> Маркер
|
||||
</button>
|
||||
<button id="polygonMode" class="tool-btn" title="Рисовать полигон (P)">
|
||||
<span>⬢</span> Полигон
|
||||
</button>
|
||||
<button id="polylineMode" class="tool-btn" title="Рисовать линию (L)">
|
||||
<span>〰️</span> Линия
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Группа 2: Импорт/Экспорт -->
|
||||
<div class="toolbar-section">
|
||||
<div class="section-title">Импорт/экспорт всех объектов</div>
|
||||
<div class="toolbar-group">
|
||||
<button id="importBtn" class="tool-btn" title="Импортировать GeoJSON или KML">
|
||||
<span>📥</span> Импорт
|
||||
</button>
|
||||
<button id="exportBtn" class="tool-btn" title="Экспортировать в GeoJSON или KML">
|
||||
<span>📤</span> Экспорт
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Группа 3: Действия -->
|
||||
<div class="toolbar-section">
|
||||
<div class="section-title">Действия</div>
|
||||
<div class="toolbar-group">
|
||||
<button id="deleteSelected" class="tool-btn danger" title="Удалить выделенное (Del)">
|
||||
<span>🗑️</span> Удалить
|
||||
</button>
|
||||
<button id="clearAll" class="tool-btn danger" title="Очистить всё">
|
||||
<span>🧹</span> Очистить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Строка состояния -->
|
||||
<div class="status-bar">
|
||||
<span id="modeStatus">Режим: Выделение</span>
|
||||
<span id="coordinates" style="color: #eeeeeeff; font-size: 11px;"></span>
|
||||
<span id="hint">Нажмите ESC для отмены</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Блок выбора объектов из БД -->
|
||||
<div class="db-objects-panel">
|
||||
<div class="panel-title">Объекты из базы</div>
|
||||
<select id="objectSelector" class="object-select">
|
||||
<option value="">— Выберите объект —</option>
|
||||
{% for sat in sats %}
|
||||
<option value="{{sat.id}}">{{sat.name}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button id="loadObjectBtn" class="load-btn">Загрузить на карту</button>
|
||||
</div>
|
||||
|
||||
<div class="footprint-control">
|
||||
<div class="panel-title">Области покрытия</div>
|
||||
<div class="footprint-actions">
|
||||
<button id="showAllFootprints">Показать все</button>
|
||||
<button id="hideAllFootprints">Скрыть все</button>
|
||||
</div>
|
||||
<div id="footprintToggles"></div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно для описания -->
|
||||
<div id="descriptionModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h3>Добавить описание</h3>
|
||||
<textarea id="descriptionInput" placeholder="Введите описание объекта..."></textarea>
|
||||
<div class="modal-buttons">
|
||||
<button id="confirmDescription">Сохранить</button>
|
||||
<button id="cancelDescription">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="exportModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h3>Экспорт данных</h3>
|
||||
<p>Выберите формат для экспорта всех объектов:</p>
|
||||
<div class="modal-buttons" style="justify-content: center; gap: 15px; margin-top: 20px;">
|
||||
<button id="exportGeoJson">GeoJSON</button>
|
||||
<button id="exportKml">KML</button>
|
||||
<button id="cancelExport">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="{% static 'mapsapp/main.js' %}"></script>
|
||||
</body>
|
||||
</html>
|
||||
3
dbapp/mapsapp/tests.py
Normal file
3
dbapp/mapsapp/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
17
dbapp/mapsapp/urls.py
Normal file
17
dbapp/mapsapp/urls.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('3dmap', views.cesium_map, name='3dmap'),
|
||||
path('2dmap', views.leaflet_map, name='2dmap'),
|
||||
path('api/footprint-names/<int:sat_id>', views.get_footprints, name="footprint_names"),
|
||||
path('api/transponders/<int:sat_id>', views.get_transponder_on_satid, name='transponders_data'),
|
||||
path('tiles/<str:footprint_name>/<int:z>/<int:x>/<int:y>.png', views.tile_proxy, name='tile_proxy'),
|
||||
# path('', views.home_page, name='home'),
|
||||
# path('excel-data', views.load_excel_data, name='load_excel_data'),
|
||||
# path('satellites', views.add_satellites, name='add_sats'),
|
||||
|
||||
]
|
||||
92
dbapp/mapsapp/utils.py
Normal file
92
dbapp/mapsapp/utils.py
Normal file
@@ -0,0 +1,92 @@
|
||||
import requests
|
||||
import re
|
||||
import json
|
||||
from .models import Transponders
|
||||
from mainapp.models import Polarization, Satellite
|
||||
|
||||
def search_satellite_on_page(data: dict, satellite_name: str):
|
||||
for pos, value in data.get('page', {}).get('positions').items():
|
||||
for name in value['satellites']:
|
||||
if name['other_names'] is None:
|
||||
name['other_names'] = ''
|
||||
if satellite_name.lower() in name['name'].lower() or satellite_name.lower() in name['other_names'].lower():
|
||||
return pos, name['id']
|
||||
return '', ''
|
||||
|
||||
def get_footprint_data(position: str = 62) -> dict:
|
||||
"""Возвращает словарь с данным по footprint для спутников на выбранной долготе"""
|
||||
response = requests.get(f"https://www.satbeams.com/footprints?position={position}")
|
||||
response.raise_for_status()
|
||||
match = re.search(r'var data = ({.*?});', response.text, re.DOTALL)
|
||||
if match:
|
||||
json_str = match.group(1)
|
||||
try:
|
||||
data = json.loads(json_str)
|
||||
return data.get("page", {}).get("footprint_data", {}).get("beams",[])
|
||||
except json.JSONDecodeError as e:
|
||||
print("Ошибка парсинга JSON:", e)
|
||||
else:
|
||||
print("Нужных данных не найдено")
|
||||
return {}
|
||||
|
||||
|
||||
|
||||
def get_all_page_data(url:str = 'https://www.satbeams.com/footprints') -> dict:
|
||||
"""Возвращает словарь с данными по всем спутникам на странице"""
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
match = re.search(r'var data = ({.*?});', response.text, re.DOTALL)
|
||||
if match:
|
||||
json_str = match.group(1)
|
||||
try:
|
||||
data = json.loads(json_str)
|
||||
# Файл json на диске для достоверности
|
||||
with open('data.json', 'w') as jf:
|
||||
json.dump(data, jf, indent=2)
|
||||
return data
|
||||
except json.JSONDecodeError as e:
|
||||
print("Ошибка парсинга JSON:", e)
|
||||
else:
|
||||
print("Нужных данных не найдено")
|
||||
return {}
|
||||
|
||||
|
||||
def get_names_footprints_for_satellite(footprint_data: dict, sat_id: str) -> list[str]:
|
||||
names = []
|
||||
for beam in footprint_data:
|
||||
if 'ku' in beam['band'].lower() and sat_id in beam['satellite_id']:
|
||||
names.append(
|
||||
{
|
||||
"name": beam['name'],
|
||||
"fullname": beam['fullname'][8:]
|
||||
}
|
||||
)
|
||||
return names
|
||||
|
||||
|
||||
def get_band_names(satellite_name: str) -> list[str]:
|
||||
data = get_all_page_data()
|
||||
pos, sat_id = search_satellite_on_page(data, satellite_name)
|
||||
footprints = get_footprint_data(pos)
|
||||
names = get_names_footprints_for_satellite(footprints, sat_id)
|
||||
return names
|
||||
|
||||
def parse_transponders_from_json(filepath: str):
|
||||
with open(filepath, encoding="utf-8") as jf:
|
||||
data = json.load(jf)
|
||||
for sat_name, trans_zone in data["satellites"].items():
|
||||
for zone, trans in trans_zone.items():
|
||||
for tran in trans:
|
||||
f_b, f_e = tran["freq"][0].split("-")
|
||||
f = round((float(f_b) + float(f_e))/2, 3)
|
||||
f_range = round(abs(float(f_e) - float(f_b)), 3)
|
||||
tran_obj = Transponders.objects.create(
|
||||
name=tran["name"],
|
||||
frequency=f,
|
||||
frequency_range=f_range,
|
||||
zone_name=zone,
|
||||
polarization=Polarization.objects.get(name=tran["pol"]),
|
||||
sat_id=Satellite.objects.get(name__iexact=sat_name)
|
||||
)
|
||||
tran_obj.save()
|
||||
|
||||
68
dbapp/mapsapp/views.py
Normal file
68
dbapp/mapsapp/views.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from django.shortcuts import render
|
||||
from django.http import JsonResponse
|
||||
import requests
|
||||
from django.core import serializers
|
||||
from django.http import HttpResponse, HttpResponseNotFound
|
||||
from django.views.decorators.cache import cache_page
|
||||
from django.views.decorators.http import require_GET
|
||||
from mainapp.models import Satellite
|
||||
from .models import Transponders
|
||||
from .utils import get_band_names
|
||||
|
||||
def cesium_map(request):
|
||||
sats = Satellite.objects.all()
|
||||
|
||||
return render(request, 'mapsapp/map3d.html', {'sats': sats})
|
||||
|
||||
def get_footprints(request, sat_id):
|
||||
try:
|
||||
sat_name = Satellite.objects.get(id=sat_id).name
|
||||
footprint_names = get_band_names(sat_name)
|
||||
|
||||
return JsonResponse(footprint_names, safe=False)
|
||||
except Exception as e:
|
||||
return JsonResponse({"error": str(e)}, status=400)
|
||||
|
||||
|
||||
@require_GET
|
||||
@cache_page(60 * 60 * 24)
|
||||
def tile_proxy(request, footprint_name, z, x, y):
|
||||
if not footprint_name.replace('-', '').replace('_', '').isalnum():
|
||||
return HttpResponse("Invalid footprint name", status=400)
|
||||
|
||||
url = f"https://static.satbeams.com/tiles/{footprint_name}/{z}/{x}/{y}.png"
|
||||
|
||||
try:
|
||||
resp = requests.get(url, timeout=10)
|
||||
if resp.status_code == 200:
|
||||
response = HttpResponse(resp.content, content_type='image/png')
|
||||
response["Access-Control-Allow-Origin"] = "*"
|
||||
return response
|
||||
else:
|
||||
return HttpResponseNotFound("Tile not found")
|
||||
except Exception as e:
|
||||
return HttpResponse(f"Proxy error: {e}", status=500)
|
||||
|
||||
def leaflet_map(request):
|
||||
sats = Satellite.objects.all()
|
||||
trans = Transponders.objects.all()
|
||||
return render(request, 'mapsapp/map2d.html', {'sats': sats, 'trans': trans})
|
||||
|
||||
|
||||
def get_transponder_on_satid(request, sat_id):
|
||||
trans = Transponders.objects.filter(sat_id=sat_id)
|
||||
output = []
|
||||
for tran in trans:
|
||||
output.append(
|
||||
{
|
||||
"name": tran.name,
|
||||
"frequency": tran.frequency,
|
||||
"frequency_range": tran.frequency_range,
|
||||
"zone_name": tran.zone_name,
|
||||
"polarization": tran.polarization.name
|
||||
}
|
||||
)
|
||||
if not trans:
|
||||
return JsonResponse({'error': 'Объектов не найдено'}, status=400)
|
||||
|
||||
return JsonResponse(output, safe=False)
|
||||
Reference in New Issue
Block a user