Поправил общую карту с footprintaми
This commit is contained in:
@@ -17,6 +17,8 @@
|
|||||||
транспондеров</button>
|
транспондеров</button>
|
||||||
<button id="clearMarkersBtn" type="button" onclick="clearAllMarkers()"
|
<button id="clearMarkersBtn" type="button" onclick="clearAllMarkers()"
|
||||||
style="display: block; width: 100%; margin-top: 10px;">Очистить маркеры</button>
|
style="display: block; width: 100%; margin-top: 10px;">Очистить маркеры</button>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footprint-control"
|
<div class="footprint-control"
|
||||||
@@ -30,68 +32,392 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
||||||
{% block extra_css %}
|
|
||||||
<style>
|
|
||||||
.panel-title {
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
.object-select {
|
|
||||||
width: 100%;
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
.load-btn {
|
|
||||||
padding: 5px 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.footprint-actions {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
.footprint-actions button {
|
|
||||||
margin-right: 5px;
|
|
||||||
padding: 3px 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.layers-control {
|
|
||||||
background: white;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
box-shadow: 0 0 0 2px rgba(0,0,0,.1);
|
|
||||||
font-size: 12px;
|
|
||||||
max-height: 400px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
.layers-control h6 {
|
|
||||||
margin: 0 0 8px 0;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.layer-group {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
padding-left: 10px;
|
|
||||||
border-left: 2px solid #ddd;
|
|
||||||
}
|
|
||||||
.layer-group-title {
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.layer-item {
|
|
||||||
margin: 3px 0;
|
|
||||||
}
|
|
||||||
.layer-item label {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock extra_css %}
|
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script>
|
<script>
|
||||||
const markerColors = ['#e41a1c', '#4daf4a', '#ff7f00', '#984ea3', '#999999', '#000000', '#377eb8'];
|
function clearAllMarkers() {
|
||||||
let markersData = [];
|
if (window.mainTreeControl && window.mainTreeControl._map) {
|
||||||
let layersControlContainer = null;
|
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 currentFootprintLayers = {};
|
||||||
let currentSatelliteId = null;
|
let currentSatelliteId = null;
|
||||||
|
|
||||||
@@ -99,249 +425,142 @@
|
|||||||
const showAllBtn = document.getElementById('showAllFootprints');
|
const showAllBtn = document.getElementById('showAllFootprints');
|
||||||
const hideAllBtn = document.getElementById('hideAllFootprints');
|
const hideAllBtn = document.getElementById('hideAllFootprints');
|
||||||
|
|
||||||
function clearAllMarkers() {
|
// --- Функции ---
|
||||||
markersData.forEach((group, groupIndex) => {
|
|
||||||
const sourceId = `markers-${groupIndex}`;
|
|
||||||
const layerId = `markers-layer-${groupIndex}`;
|
|
||||||
const innerLayerId = `markers-layer-${groupIndex}-inner`;
|
|
||||||
if (map.getLayer(innerLayerId)) map.removeLayer(innerLayerId);
|
|
||||||
if (map.getLayer(layerId)) map.removeLayer(layerId);
|
|
||||||
if (map.getSource(sourceId)) map.removeSource(sourceId);
|
|
||||||
});
|
|
||||||
markersData = [];
|
|
||||||
if (layersControlContainer) {
|
|
||||||
layersControlContainer.remove();
|
|
||||||
layersControlContainer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addMarkersToMap() {
|
function escapeHtml(unsafe) {
|
||||||
markersData.forEach((group, groupIndex) => {
|
// Простая функция для экранирования HTML-символов в именах
|
||||||
const sourceId = `markers-${groupIndex}`;
|
return unsafe
|
||||||
const layerId = `markers-layer-${groupIndex}`;
|
.replace(/&/g, "&")
|
||||||
if (map.getSource(sourceId)) return;
|
.replace(/</g, "<")
|
||||||
const geojson = {
|
.replace(/>/g, ">")
|
||||||
type: 'FeatureCollection',
|
.replace(/"/g, """)
|
||||||
features: group.features.map(f => ({
|
.replace(/'/g, "'");
|
||||||
type: 'Feature',
|
|
||||||
geometry: { type: 'Point', coordinates: f.coordinates },
|
|
||||||
properties: { name: f.name, freq: f.freq, color: group.color, groupName: group.name }
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
map.addSource(sourceId, { type: 'geojson', data: geojson });
|
|
||||||
map.addLayer({
|
|
||||||
id: layerId, type: 'circle', source: sourceId,
|
|
||||||
paint: { 'circle-radius': 8, 'circle-color': group.color, 'circle-stroke-width': 2, 'circle-stroke-color': '#ffffff' }
|
|
||||||
});
|
|
||||||
map.addLayer({
|
|
||||||
id: `${layerId}-inner`, type: 'circle', source: sourceId,
|
|
||||||
paint: { 'circle-radius': 3, 'circle-color': '#ffffff' }
|
|
||||||
});
|
|
||||||
map.on('click', layerId, (e) => {
|
|
||||||
const props = e.features[0].properties;
|
|
||||||
const coords = e.features[0].geometry.coordinates.slice();
|
|
||||||
new maplibregl.Popup().setLngLat(coords)
|
|
||||||
.setHTML(`<div class="popup-title">${props.name}</div><div class="popup-info">Частота: ${props.freq}</div>`)
|
|
||||||
.addTo(map);
|
|
||||||
});
|
|
||||||
map.on('mouseenter', layerId, () => { map.getCanvas().style.cursor = 'pointer'; });
|
|
||||||
map.on('mouseleave', layerId, () => { map.getCanvas().style.cursor = ''; });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function createLayersControl() {
|
|
||||||
if (layersControlContainer) layersControlContainer.remove();
|
|
||||||
layersControlContainer = document.createElement('div');
|
|
||||||
layersControlContainer.className = 'layers-control';
|
|
||||||
layersControlContainer.style.cssText = 'position:absolute;top:10px;left:10px;z-index:1000;';
|
|
||||||
const title = document.createElement('h6');
|
|
||||||
title.textContent = 'Все точки';
|
|
||||||
layersControlContainer.appendChild(title);
|
|
||||||
markersData.forEach((group, groupIndex) => {
|
|
||||||
const groupDiv = document.createElement('div');
|
|
||||||
groupDiv.className = 'layer-group';
|
|
||||||
const groupTitle = document.createElement('div');
|
|
||||||
groupTitle.className = 'layer-group-title';
|
|
||||||
const groupCheckbox = document.createElement('input');
|
|
||||||
groupCheckbox.type = 'checkbox';
|
|
||||||
groupCheckbox.checked = true;
|
|
||||||
groupCheckbox.addEventListener('change', (e) => {
|
|
||||||
const visibility = e.target.checked ? 'visible' : 'none';
|
|
||||||
const layerId = `markers-layer-${groupIndex}`;
|
|
||||||
if (map.getLayer(layerId)) {
|
|
||||||
map.setLayoutProperty(layerId, 'visibility', visibility);
|
|
||||||
map.setLayoutProperty(`${layerId}-inner`, 'visibility', visibility);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const colorSpan = document.createElement('span');
|
|
||||||
colorSpan.style.cssText = `display:inline-block;width:12px;height:12px;border-radius:50%;background-color:${group.color};margin-right:5px;`;
|
|
||||||
groupTitle.appendChild(groupCheckbox);
|
|
||||||
groupTitle.appendChild(colorSpan);
|
|
||||||
groupTitle.appendChild(document.createTextNode(` ${group.name} (${group.features.length})`));
|
|
||||||
groupDiv.appendChild(groupTitle);
|
|
||||||
layersControlContainer.appendChild(groupDiv);
|
|
||||||
});
|
|
||||||
document.body.appendChild(layersControlContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
function fitBoundsToMarkers() {
|
|
||||||
const allCoords = [];
|
|
||||||
markersData.forEach(group => group.features.forEach(f => allCoords.push(f.coordinates)));
|
|
||||||
if (allCoords.length > 0) {
|
|
||||||
const bounds = allCoords.reduce((b, c) => b.extend(c), new maplibregl.LngLatBounds(allCoords[0], allCoords[0]));
|
|
||||||
map.fitBounds(bounds, { padding: 50 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadGeoJsonForSatellite(satId) {
|
|
||||||
if (!satId) { alert('Пожалуйста, выберите объект.'); return; }
|
|
||||||
clearAllMarkers();
|
|
||||||
fetch(`/api/locations/${encodeURIComponent(satId)}/geojson`)
|
|
||||||
.then(r => { if (!r.ok) throw new Error('Ошибка: ' + r.status); return r.json(); })
|
|
||||||
.then(data => {
|
|
||||||
if (!data.features || data.features.length === 0) { alert('Объекты не найдены.'); return; }
|
|
||||||
const groupedByFreq = {};
|
|
||||||
data.features.forEach(f => {
|
|
||||||
const freq = f.properties.freq / 1000000;
|
|
||||||
if (!groupedByFreq[freq]) groupedByFreq[freq] = [];
|
|
||||||
groupedByFreq[freq].push(f);
|
|
||||||
});
|
|
||||||
let freqIndex = 0;
|
|
||||||
for (const [freq, features] of Object.entries(groupedByFreq)) {
|
|
||||||
markersData.push({
|
|
||||||
name: `${features[0].properties.name} (${freq} МГц)`,
|
|
||||||
color: markerColors[freqIndex % markerColors.length],
|
|
||||||
features: features.map((f, idx) => ({
|
|
||||||
name: f.properties.name || `Точка ${idx}`,
|
|
||||||
freq: freq == -1 ? 'Неизвестно' : `${freq} МГц`,
|
|
||||||
coordinates: f.geometry.coordinates
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
freqIndex++;
|
|
||||||
}
|
|
||||||
addMarkersToMap();
|
|
||||||
createLayersControl();
|
|
||||||
fitBoundsToMarkers();
|
|
||||||
})
|
|
||||||
.catch(err => { console.error(err); alert('Ошибка: ' + err.message); });
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadTranspondersPointsForSatellite(satId) {
|
|
||||||
if (!satId) { alert('Пожалуйста, выберите объект.'); return; }
|
|
||||||
clearAllMarkers();
|
|
||||||
Promise.all([
|
|
||||||
fetch(`/api/transponders/${encodeURIComponent(satId)}`).then(r => { if (!r.ok) throw new Error('Ошибка транспондеров'); return r.json(); }),
|
|
||||||
fetch(`/api/locations/${encodeURIComponent(satId)}/geojson`).then(r => { if (!r.ok) throw new Error('Ошибка точек'); return r.json(); })
|
|
||||||
]).then(([trans, points]) => processAndDisplayTransponderPointsByZone(points, trans))
|
|
||||||
.catch(err => { console.error(err); alert('Ошибка: ' + err.message); });
|
|
||||||
}
|
|
||||||
|
|
||||||
function processAndDisplayTransponderPointsByZone(data_points, transpondersData) {
|
|
||||||
if (!data_points.features?.length) { alert('Точки не найдены.'); return; }
|
|
||||||
if (!Array.isArray(transpondersData)) { alert('Данные транспондеров отсутствуют.'); return; }
|
|
||||||
function findTransponder(freqHz, pol) {
|
|
||||||
const freqMhz = freqHz / 1000000;
|
|
||||||
for (const t of transpondersData) {
|
|
||||||
if (typeof t.frequency !== 'number') continue;
|
|
||||||
const half = (t.frequency_range || 0) / 2;
|
|
||||||
if (freqMhz >= t.frequency - half && freqMhz <= t.frequency + half && pol === t.polarization) return t;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const grouped = {};
|
|
||||||
data_points.features.forEach(f => {
|
|
||||||
const pol = f.properties.polarization || f.properties.pol || f.properties.polar || f.properties.polarisation;
|
|
||||||
if (!pol) return;
|
|
||||||
const trans = findTransponder(f.properties.freq, pol);
|
|
||||||
if (trans) {
|
|
||||||
if (!grouped[trans.name]) grouped[trans.name] = { transponder: trans, features: [] };
|
|
||||||
grouped[trans.name].features.push(f);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const zones = {};
|
|
||||||
for (const [, g] of Object.entries(grouped)) {
|
|
||||||
const zone = g.transponder.zone_name || 'Без зоны';
|
|
||||||
if (!zones[zone]) zones[zone] = [];
|
|
||||||
zones[zone].push(g);
|
|
||||||
}
|
|
||||||
let idx = 0;
|
|
||||||
for (const [zone, groups] of Object.entries(zones)) {
|
|
||||||
groups.forEach((g, ti) => {
|
|
||||||
const t = g.transponder;
|
|
||||||
const lb = (t.frequency - (t.frequency_range || 0) / 2).toFixed(2);
|
|
||||||
const ub = (t.frequency + (t.frequency_range || 0) / 2).toFixed(2);
|
|
||||||
markersData.push({
|
|
||||||
name: `${zone} - ${t.name} (${lb} - ${ub})`,
|
|
||||||
color: markerColors[(idx + ti) % markerColors.length],
|
|
||||||
features: g.features.map(f => ({
|
|
||||||
name: f.properties.name || 'Точка',
|
|
||||||
freq: `${(f.properties.freq / 1000000).toFixed(2)} МГц`,
|
|
||||||
coordinates: f.geometry.coordinates
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
});
|
|
||||||
idx++;
|
|
||||||
}
|
|
||||||
addMarkersToMap();
|
|
||||||
createLayersControl();
|
|
||||||
fitBoundsToMarkers();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearFootprintUIAndLayers() {
|
function clearFootprintUIAndLayers() {
|
||||||
Object.keys(currentFootprintLayers).forEach(name => {
|
// Удаляем все текущие слои footprint'ов с карты и очищаем объект
|
||||||
const layerId = `footprint-${name}`, sourceId = `footprint-source-${name}`;
|
Object.entries(currentFootprintLayers).forEach(([name, layer]) => {
|
||||||
if (map.getLayer(layerId)) map.removeLayer(layerId);
|
if (map.hasLayer(layer)) {
|
||||||
if (map.getSource(sourceId)) map.removeSource(sourceId);
|
map.removeLayer(layer);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
currentFootprintLayers = {};
|
currentFootprintLayers = {};
|
||||||
|
|
||||||
|
// Очищаем контейнер с чекбоксами
|
||||||
togglesContainer.innerHTML = '';
|
togglesContainer.innerHTML = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadFootprintsForSatellite(satId) {
|
function loadFootprintsForSatellite(satId) {
|
||||||
if (!satId) { clearFootprintUIAndLayers(); currentSatelliteId = null; return; }
|
// Проверка, если satId пустой - очищаем
|
||||||
|
if (!satId) {
|
||||||
|
clearFootprintUIAndLayers();
|
||||||
|
currentSatelliteId = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем текущий ID спутника
|
||||||
currentSatelliteId = satId;
|
currentSatelliteId = satId;
|
||||||
fetch(`/api/footprint-names/${encodeURIComponent(satId)}`)
|
|
||||||
.then(r => { if (!r.ok) throw new Error('Ошибка'); return r.json(); })
|
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 => {
|
.then(footprints => {
|
||||||
if (!Array.isArray(footprints)) throw new Error('Неверный формат');
|
if (!Array.isArray(footprints)) {
|
||||||
|
throw new Error('Ожидался массив footprint\'ов');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Очищаем старое состояние
|
||||||
clearFootprintUIAndLayers();
|
clearFootprintUIAndLayers();
|
||||||
|
|
||||||
|
// Создаём новые слои и чекбоксы
|
||||||
footprints.forEach(fp => {
|
footprints.forEach(fp => {
|
||||||
const sourceId = `footprint-source-${fp.name}`, layerId = `footprint-${fp.name}`;
|
// 1. Создаём тайловый слой Leaflet
|
||||||
map.addSource(sourceId, { type: 'raster', tiles: [`/tiles/${fp.name}/{z}/{x}/{y}.png`], tileSize: 256 });
|
// Убедитесь, что URL соответствует вашей структуре тайлов
|
||||||
map.addLayer({ id: layerId, type: 'raster', source: sourceId, paint: { 'raster-opacity': 0.7 } });
|
const layer = L.tileLayer(`/tiles/${fp.name}/{z}/{x}/{y}.png`, {
|
||||||
currentFootprintLayers[fp.name] = layerId;
|
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');
|
const label = document.createElement('label');
|
||||||
label.style.cssText = 'display:block;margin:4px 0;';
|
label.style.display = 'block';
|
||||||
label.innerHTML = `<input type="checkbox" data-footprint="${encodeURIComponent(fp.name)}" checked> ${fp.fullname.replace(/</g,'<')}`;
|
label.style.margin = '4px 0';
|
||||||
|
// Чекбокс изначально отмечен, если слой добавлен на карту
|
||||||
|
label.innerHTML = `
|
||||||
|
<input type="checkbox"
|
||||||
|
data-footprint="${safeNameAttr}"
|
||||||
|
checked> <!-- Отмечен, так как слой добавлен -->
|
||||||
|
${safeFullName}
|
||||||
|
`;
|
||||||
togglesContainer.appendChild(label);
|
togglesContainer.appendChild(label);
|
||||||
label.querySelector('input').addEventListener('change', function() {
|
|
||||||
const lid = currentFootprintLayers[decodeURIComponent(this.dataset.footprint)];
|
// 3. Связываем чекбокс со слоем
|
||||||
if (lid && map.getLayer(lid)) map.setLayoutProperty(lid, 'visibility', this.checked ? 'visible' : 'none');
|
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(err); alert('Ошибка: ' + err.message); clearFootprintUIAndLayers(); });
|
.catch(err => {
|
||||||
|
console.error('Ошибка загрузки footprint\'ов:', err);
|
||||||
|
alert('Не удалось загрузить области покрытия: ' + err.message);
|
||||||
|
clearFootprintUIAndLayers(); // При ошибке очищаем UI
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function showAllFootprints() {
|
function showAllFootprints() {
|
||||||
Object.values(currentFootprintLayers).forEach(lid => { if (map.getLayer(lid)) map.setLayoutProperty(lid, 'visibility', 'visible'); });
|
Object.entries(currentFootprintLayers).forEach(([name, layer]) => {
|
||||||
document.querySelectorAll('#footprintToggles input').forEach(cb => cb.checked = true);
|
if (!map.hasLayer(layer)) {
|
||||||
|
map.addLayer(layer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Синхронизируем чекбоксы
|
||||||
|
document.querySelectorAll('#footprintToggles input[type="checkbox"]').forEach(cb => {
|
||||||
|
cb.checked = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideAllFootprints() {
|
function hideAllFootprints() {
|
||||||
Object.values(currentFootprintLayers).forEach(lid => { if (map.getLayer(lid)) map.setLayoutProperty(lid, 'visibility', 'none'); });
|
Object.entries(currentFootprintLayers).forEach(([name, layer]) => {
|
||||||
document.querySelectorAll('#footprintToggles input').forEach(cb => cb.checked = false);
|
if (map.hasLayer(layer)) {
|
||||||
|
map.removeLayer(layer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.querySelectorAll('#footprintToggles input[type="checkbox"]').forEach(cb => {
|
||||||
|
cb.checked = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onStyleChange = function() { addMarkersToMap(); if (currentSatelliteId) loadFootprintsForSatellite(currentSatelliteId); };
|
// --- Обработчики событий для кнопок ---
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const select = document.getElementById('objectSelector');
|
|
||||||
select.selectedIndex = 0;
|
|
||||||
select.addEventListener('change', function() { loadFootprintsForSatellite(this.value); });
|
|
||||||
document.getElementById('loadObjectBtn').addEventListener('click', () => loadGeoJsonForSatellite(select.value));
|
|
||||||
document.getElementById('loadObjectTransBtn').addEventListener('click', () => loadTranspondersPointsForSatellite(select.value));
|
|
||||||
});
|
|
||||||
showAllBtn.addEventListener('click', showAllFootprints);
|
showAllBtn.addEventListener('click', showAllFootprints);
|
||||||
hideAllBtn.addEventListener('click', hideAllFootprints);
|
hideAllBtn.addEventListener('click', hideAllFootprints);
|
||||||
</script>
|
</script>
|
||||||
{% endblock extra_js %}
|
{% endblock extra_js %}
|
||||||
@@ -22,7 +22,7 @@ def search_satellite_on_page(data: dict, satellite_name: str):
|
|||||||
|
|
||||||
def get_footprint_data(position: str = 62) -> dict:
|
def get_footprint_data(position: str = 62) -> dict:
|
||||||
"""Возвращает словарь с данным по footprint для спутников на выбранной долготе"""
|
"""Возвращает словарь с данным по footprint для спутников на выбранной долготе"""
|
||||||
response = requests.get(f"https://www.satbeams.com/footprints?position={position}", verify=False)
|
response = requests.get(f"https://www.satbeams.com/footprints?position={position}", verify=True)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
match = re.search(r'var data = ({.*?});', response.text, re.DOTALL)
|
match = re.search(r'var data = ({.*?});', response.text, re.DOTALL)
|
||||||
if match:
|
if match:
|
||||||
|
|||||||
@@ -1,169 +1,169 @@
|
|||||||
# Standard library imports
|
# Standard library imports
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.http import HttpResponse, HttpResponseNotFound, JsonResponse
|
from django.http import HttpResponse, HttpResponseNotFound, JsonResponse
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from django.views.decorators.cache import cache_page
|
from django.views.decorators.cache import cache_page
|
||||||
from django.views.decorators.http import require_GET
|
from django.views.decorators.http import require_GET
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
|
|
||||||
# Third-party imports
|
# Third-party imports
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
# Local imports
|
# Local imports
|
||||||
from mainapp.models import Satellite
|
from mainapp.models import Satellite
|
||||||
from .models import Transponders
|
from .models import Transponders
|
||||||
from .utils import get_band_names
|
from .utils import get_band_names
|
||||||
|
|
||||||
|
|
||||||
class CesiumMapView(LoginRequiredMixin, TemplateView):
|
class CesiumMapView(LoginRequiredMixin, TemplateView):
|
||||||
"""
|
"""
|
||||||
Представление для отображения 3D карты с использованием Cesium.
|
Представление для отображения 3D карты с использованием Cesium.
|
||||||
|
|
||||||
Отображает спутники и их зоны покрытия на интерактивной 3D карте.
|
Отображает спутники и их зоны покрытия на интерактивной 3D карте.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
template_name = "mapsapp/map3d.html"
|
template_name = "mapsapp/map3d.html"
|
||||||
|
|
||||||
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
# Оптимизированный запрос - загружаем только необходимые поля
|
# Оптимизированный запрос - загружаем только необходимые поля
|
||||||
# Фильтруем спутники, у которых есть параметры с привязанными объектами
|
# Фильтруем спутники, у которых есть параметры с привязанными объектами
|
||||||
context["sats"] = (
|
context["sats"] = (
|
||||||
Satellite.objects.filter(parameters__objitem__isnull=False)
|
Satellite.objects.filter(parameters__objitem__isnull=False)
|
||||||
.distinct()
|
.distinct()
|
||||||
.only("id", "name")
|
.only("id", "name")
|
||||||
.order_by("name")
|
.order_by("name")
|
||||||
)
|
)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class GetFootprintsView(LoginRequiredMixin, View):
|
class GetFootprintsView(LoginRequiredMixin, View):
|
||||||
"""
|
"""
|
||||||
API для получения зон покрытия (footprints) спутника.
|
API для получения зон покрытия (footprints) спутника.
|
||||||
|
|
||||||
Возвращает список названий зон покрытия для указанного спутника.
|
Возвращает список названий зон покрытия для указанного спутника.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self, request, sat_id):
|
def get(self, request, sat_id):
|
||||||
try:
|
try:
|
||||||
# Оптимизированный запрос - загружаем только поле name
|
# Оптимизированный запрос - загружаем только поле name
|
||||||
sat_name = Satellite.objects.only("name").get(id=sat_id).name
|
sat_name = Satellite.objects.only("name").get(id=sat_id).name
|
||||||
footprint_names = get_band_names(sat_name)
|
footprint_names = get_band_names(sat_name)
|
||||||
|
|
||||||
return JsonResponse(footprint_names, safe=False)
|
return JsonResponse(footprint_names, safe=False)
|
||||||
except Satellite.DoesNotExist:
|
except Satellite.DoesNotExist:
|
||||||
return JsonResponse({"error": "Спутник не найден"}, status=404)
|
return JsonResponse({"error": "Спутник не найден"}, status=404)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return JsonResponse({"error": str(e)}, status=500)
|
return JsonResponse({"error": str(e)}, status=500)
|
||||||
|
|
||||||
|
|
||||||
class TileProxyView(View):
|
class TileProxyView(View):
|
||||||
"""
|
"""
|
||||||
Прокси для загрузки тайлов карты покрытия спутников.
|
Прокси для загрузки тайлов карты покрытия спутников.
|
||||||
|
|
||||||
Кэширует тайлы на 7 дней для улучшения производительности.
|
Кэширует тайлы на 7 дней для улучшения производительности.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Константы
|
# Константы
|
||||||
TILE_BASE_URL = "https://static.satbeams.com/tiles"
|
TILE_BASE_URL = "https://static.satbeams.com/tiles"
|
||||||
CACHE_DURATION = 60 * 60 * 24 * 7 # 7 дней
|
CACHE_DURATION = 60 * 60 * 24 * 7 # 7 дней
|
||||||
REQUEST_TIMEOUT = 10 # секунд
|
REQUEST_TIMEOUT = 10 # секунд
|
||||||
|
|
||||||
@method_decorator(require_GET)
|
@method_decorator(require_GET)
|
||||||
@method_decorator(cache_page(CACHE_DURATION))
|
@method_decorator(cache_page(CACHE_DURATION))
|
||||||
def dispatch(self, *args, **kwargs):
|
def dispatch(self, *args, **kwargs):
|
||||||
return super().dispatch(*args, **kwargs)
|
return super().dispatch(*args, **kwargs)
|
||||||
|
|
||||||
def get(self, request, footprint_name, z, x, y):
|
def get(self, request, footprint_name, z, x, y):
|
||||||
# Валидация имени footprint
|
# Валидация имени footprint
|
||||||
if not footprint_name.replace("-", "").replace("_", "").isalnum():
|
if not footprint_name.replace("-", "").replace("_", "").isalnum():
|
||||||
return HttpResponse("Invalid footprint name", status=400)
|
return HttpResponse("Invalid footprint name", status=400)
|
||||||
|
|
||||||
url = f"{self.TILE_BASE_URL}/{footprint_name}/{z}/{x}/{y}.png"
|
url = f"{self.TILE_BASE_URL}/{footprint_name}/{z}/{x}/{y}.png"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
resp = requests.get(url, timeout=self.REQUEST_TIMEOUT)
|
resp = requests.get(url, timeout=self.REQUEST_TIMEOUT)
|
||||||
if resp.status_code == 200:
|
if resp.status_code == 200:
|
||||||
response = HttpResponse(resp.content, content_type="image/png")
|
response = HttpResponse(resp.content, content_type="image/png")
|
||||||
response["Access-Control-Allow-Origin"] = "*"
|
response["Access-Control-Allow-Origin"] = "*"
|
||||||
response["Cache-Control"] = f"public, max-age={self.CACHE_DURATION}"
|
response["Cache-Control"] = f"public, max-age={self.CACHE_DURATION}"
|
||||||
return response
|
return response
|
||||||
else:
|
else:
|
||||||
return HttpResponseNotFound("Tile not found")
|
return HttpResponseNotFound("Tile not found")
|
||||||
except requests.Timeout:
|
except requests.Timeout:
|
||||||
return HttpResponse("Request timeout", status=504)
|
return HttpResponse("Request timeout", status=504)
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
return HttpResponse(f"Proxy error: {e}", status=500)
|
return HttpResponse(f"Proxy error: {e}", status=500)
|
||||||
|
|
||||||
|
|
||||||
class LeafletMapView(LoginRequiredMixin, TemplateView):
|
class LeafletMapView(LoginRequiredMixin, TemplateView):
|
||||||
"""
|
"""
|
||||||
Представление для отображения 2D карты с использованием Leaflet.
|
Представление для отображения 2D карты с использованием Leaflet.
|
||||||
|
|
||||||
Отображает спутники и транспондеры на интерактивной 2D карте.
|
Отображает спутники и транспондеры на интерактивной 2D карте.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
template_name = "mapsapp/map2d.html"
|
template_name = "mapsapp/map2d.html"
|
||||||
|
|
||||||
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
# Оптимизированные запросы - загружаем только необходимые поля
|
# Оптимизированные запросы - загружаем только необходимые поля
|
||||||
# Фильтруем спутники, у которых есть параметры с привязанными объектами
|
# Фильтруем спутники, у которых есть параметры с привязанными объектами
|
||||||
context["sats"] = (
|
context["sats"] = (
|
||||||
Satellite.objects.filter(parameters__objitem__isnull=False)
|
Satellite.objects.filter(parameters__objitem__isnull=False)
|
||||||
.distinct()
|
.distinct()
|
||||||
.only("id", "name")
|
.only("id", "name")
|
||||||
.order_by("name")
|
.order_by("name")
|
||||||
)
|
)
|
||||||
|
|
||||||
context["trans"] = Transponders.objects.select_related(
|
context["trans"] = Transponders.objects.select_related(
|
||||||
"sat_id", "polarization"
|
"sat_id", "polarization"
|
||||||
).only(
|
).only(
|
||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"sat_id__name",
|
"sat_id__name",
|
||||||
"polarization__name",
|
"polarization__name",
|
||||||
"downlink",
|
"downlink",
|
||||||
"frequency_range",
|
"frequency_range",
|
||||||
"zone_name",
|
"zone_name",
|
||||||
)
|
)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class GetTransponderOnSatIdView(LoginRequiredMixin, View):
|
class GetTransponderOnSatIdView(LoginRequiredMixin, View):
|
||||||
"""
|
"""
|
||||||
API для получения транспондеров спутника.
|
API для получения транспондеров спутника.
|
||||||
|
|
||||||
Возвращает список транспондеров для указанного спутника с оптимизированными запросами.
|
Возвращает список транспондеров для указанного спутника с оптимизированными запросами.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self, request, sat_id):
|
def get(self, request, sat_id):
|
||||||
# Оптимизированный запрос с select_related и only
|
# Оптимизированный запрос с select_related и only
|
||||||
trans = (
|
trans = (
|
||||||
Transponders.objects.filter(sat_id=sat_id)
|
Transponders.objects.filter(sat_id=sat_id)
|
||||||
.select_related("polarization")
|
.select_related("polarization")
|
||||||
.only(
|
.only(
|
||||||
"name", "downlink", "frequency_range", "zone_name", "polarization__name"
|
"name", "downlink", "frequency_range", "zone_name", "polarization__name"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if not trans.exists():
|
if not trans.exists():
|
||||||
return JsonResponse({"error": "Объектов не найдено"}, status=404)
|
return JsonResponse({"error": "Объектов не найдено"}, status=404)
|
||||||
|
|
||||||
# Используем list comprehension для лучшей производительности
|
# Используем list comprehension для лучшей производительности
|
||||||
output = [
|
output = [
|
||||||
{
|
{
|
||||||
"name": tran.name,
|
"name": tran.name,
|
||||||
"frequency": tran.downlink,
|
"frequency": tran.downlink,
|
||||||
"frequency_range": tran.frequency_range,
|
"frequency_range": tran.frequency_range,
|
||||||
"zone_name": tran.zone_name,
|
"zone_name": tran.zone_name,
|
||||||
"polarization": tran.polarization.name if tran.polarization else "-",
|
"polarization": tran.polarization.name if tran.polarization else "-",
|
||||||
}
|
}
|
||||||
for tran in trans
|
for tran in trans
|
||||||
]
|
]
|
||||||
|
|
||||||
return JsonResponse(output, safe=False)
|
return JsonResponse(output, safe=False)
|
||||||
|
|||||||
Reference in New Issue
Block a user