Поправил общую карту с footprintaми

This commit is contained in:
2025-11-27 17:36:23 +03:00
parent eba19126ef
commit 908e11879d
3 changed files with 666 additions and 447 deletions

View File

@@ -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, "&amp;")
if (map.getSource(sourceId)) return; .replace(/</g, "<")
const geojson = { .replace(/>/g, ">")
type: 'FeatureCollection', .replace(/"/g, "&quot;")
features: group.features.map(f => ({ .replace(/'/g, "&#039;");
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,'&lt;')}`; 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 %}

View File

@@ -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:

View File

@@ -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)