Добавил геофильтры. Теперь нужен рефакторинг.
This commit is contained in:
@@ -3,6 +3,8 @@
|
|||||||
{% block title %}Список объектов{% endblock %}
|
{% block title %}Список объектов{% endblock %}
|
||||||
|
|
||||||
{% block extra_css %}
|
{% block extra_css %}
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css" />
|
||||||
<style>
|
<style>
|
||||||
.table-responsive tr.selected {
|
.table-responsive tr.selected {
|
||||||
background-color: #d4edff;
|
background-color: #d4edff;
|
||||||
@@ -22,6 +24,9 @@
|
|||||||
.btn-group .btn {
|
.btn-group .btn {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
#polygonFilterMap {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -94,6 +99,23 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Polygon Filter Button -->
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-outline-success btn-sm" type="button"
|
||||||
|
onclick="openPolygonFilterMap()">
|
||||||
|
<i class="bi bi-pentagon"></i> Фильтр по полигону
|
||||||
|
{% if polygon_coords %}
|
||||||
|
<span class="badge bg-success">✓</span>
|
||||||
|
{% endif %}
|
||||||
|
</button>
|
||||||
|
{% if polygon_coords %}
|
||||||
|
<button class="btn btn-outline-danger btn-sm" type="button"
|
||||||
|
onclick="clearPolygonFilter()" title="Очистить фильтр по полигону">
|
||||||
|
<i class="bi bi-x-circle"></i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
<div class="ms-auto">
|
<div class="ms-auto">
|
||||||
{% include 'mainapp/components/_pagination.html' with page_obj=page_obj show_info=True %}
|
{% include 'mainapp/components/_pagination.html' with page_obj=page_obj show_info=True %}
|
||||||
@@ -112,6 +134,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="offcanvas-body">
|
<div class="offcanvas-body">
|
||||||
<form method="get" id="filter-form">
|
<form method="get" id="filter-form">
|
||||||
|
<!-- Hidden field to preserve polygon filter -->
|
||||||
|
{% if polygon_coords %}
|
||||||
|
<input type="hidden" name="polygon" value="{{ polygon_coords }}">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- Satellite Selection - Multi-select -->
|
<!-- Satellite Selection - Multi-select -->
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="form-label">Спутник:</label>
|
<label class="form-label">Спутник:</label>
|
||||||
@@ -703,6 +730,167 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
|
||||||
|
<script>
|
||||||
|
// Polygon filter map variables
|
||||||
|
let polygonFilterMapInstance = null;
|
||||||
|
let drawnItems = null;
|
||||||
|
let drawControl = null;
|
||||||
|
let currentPolygon = null;
|
||||||
|
|
||||||
|
// Initialize polygon filter map
|
||||||
|
function initPolygonFilterMap() {
|
||||||
|
if (polygonFilterMapInstance) {
|
||||||
|
return; // Already initialized
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create map centered on Russia
|
||||||
|
polygonFilterMapInstance = L.map('polygonFilterMap').setView([55.7558, 37.6173], 4);
|
||||||
|
|
||||||
|
// Add OpenStreetMap tile layer
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: '© OpenStreetMap contributors',
|
||||||
|
maxZoom: 19
|
||||||
|
}).addTo(polygonFilterMapInstance);
|
||||||
|
|
||||||
|
// Initialize FeatureGroup to store drawn items
|
||||||
|
drawnItems = new L.FeatureGroup();
|
||||||
|
polygonFilterMapInstance.addLayer(drawnItems);
|
||||||
|
|
||||||
|
// Initialize draw control
|
||||||
|
drawControl = new L.Control.Draw({
|
||||||
|
position: 'topright',
|
||||||
|
draw: {
|
||||||
|
polygon: {
|
||||||
|
allowIntersection: false,
|
||||||
|
showArea: true,
|
||||||
|
drawError: {
|
||||||
|
color: '#e1e100',
|
||||||
|
message: '<strong>Ошибка:</strong> полигон не должен пересекать сам себя!'
|
||||||
|
},
|
||||||
|
shapeOptions: {
|
||||||
|
color: '#3388ff',
|
||||||
|
fillOpacity: 0.2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
polyline: false,
|
||||||
|
rectangle: {
|
||||||
|
shapeOptions: {
|
||||||
|
color: '#3388ff',
|
||||||
|
fillOpacity: 0.2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
circle: false,
|
||||||
|
circlemarker: false,
|
||||||
|
marker: false
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
featureGroup: drawnItems,
|
||||||
|
remove: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
polygonFilterMapInstance.addControl(drawControl);
|
||||||
|
|
||||||
|
// Handle polygon creation
|
||||||
|
polygonFilterMapInstance.on(L.Draw.Event.CREATED, function (event) {
|
||||||
|
const layer = event.layer;
|
||||||
|
|
||||||
|
// Remove existing polygon
|
||||||
|
drawnItems.clearLayers();
|
||||||
|
|
||||||
|
// Add new polygon
|
||||||
|
drawnItems.addLayer(layer);
|
||||||
|
currentPolygon = layer;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle polygon edit
|
||||||
|
polygonFilterMapInstance.on(L.Draw.Event.EDITED, function (event) {
|
||||||
|
const layers = event.layers;
|
||||||
|
layers.eachLayer(function (layer) {
|
||||||
|
currentPolygon = layer;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle polygon deletion
|
||||||
|
polygonFilterMapInstance.on(L.Draw.Event.DELETED, function () {
|
||||||
|
currentPolygon = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load existing polygon if present
|
||||||
|
{% if polygon_coords %}
|
||||||
|
try {
|
||||||
|
const coords = {{ polygon_coords|safe }};
|
||||||
|
if (coords && coords.length > 0) {
|
||||||
|
const latLngs = coords.map(coord => [coord[1], coord[0]]); // [lng, lat] -> [lat, lng]
|
||||||
|
const polygon = L.polygon(latLngs, {
|
||||||
|
color: '#3388ff',
|
||||||
|
fillOpacity: 0.2
|
||||||
|
});
|
||||||
|
drawnItems.addLayer(polygon);
|
||||||
|
currentPolygon = polygon;
|
||||||
|
|
||||||
|
// Fit map to polygon bounds
|
||||||
|
polygonFilterMapInstance.fitBounds(polygon.getBounds());
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error loading existing polygon:', e);
|
||||||
|
}
|
||||||
|
{% endif %}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open polygon filter map modal
|
||||||
|
function openPolygonFilterMap() {
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('polygonFilterModal'));
|
||||||
|
modal.show();
|
||||||
|
|
||||||
|
// Initialize map after modal is shown (to ensure proper rendering)
|
||||||
|
setTimeout(() => {
|
||||||
|
initPolygonFilterMap();
|
||||||
|
if (polygonFilterMapInstance) {
|
||||||
|
polygonFilterMapInstance.invalidateSize();
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear polygon on map
|
||||||
|
function clearPolygonOnMap() {
|
||||||
|
if (drawnItems) {
|
||||||
|
drawnItems.clearLayers();
|
||||||
|
currentPolygon = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply polygon filter
|
||||||
|
function applyPolygonFilter() {
|
||||||
|
if (!currentPolygon) {
|
||||||
|
alert('Пожалуйста, нарисуйте полигон на карте');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get polygon coordinates
|
||||||
|
const latLngs = currentPolygon.getLatLngs()[0]; // Get first ring for polygon
|
||||||
|
const coords = latLngs.map(latLng => [latLng.lng, latLng.lat]); // [lat, lng] -> [lng, lat]
|
||||||
|
|
||||||
|
// Close the polygon by adding first point at the end
|
||||||
|
coords.push(coords[0]);
|
||||||
|
|
||||||
|
// Add polygon coordinates to URL and reload
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
urlParams.set('polygon', JSON.stringify(coords));
|
||||||
|
urlParams.delete('page'); // Reset to first page
|
||||||
|
|
||||||
|
window.location.search = urlParams.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear polygon filter
|
||||||
|
function clearPolygonFilter() {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
urlParams.delete('polygon');
|
||||||
|
urlParams.delete('page');
|
||||||
|
window.location.search = urlParams.toString();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<script>
|
<script>
|
||||||
let lastCheckedIndex = null;
|
let lastCheckedIndex = null;
|
||||||
|
|
||||||
@@ -748,8 +936,15 @@ function showSelectedOnMap() {
|
|||||||
selectedIds.push(checkbox.value);
|
selectedIds.push(checkbox.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Redirect to the map view with selected IDs as query parameter
|
// Build URL with IDs and preserve polygon filter if present
|
||||||
const url = '{% url "mainapp:show_sources_map" %}' + '?ids=' + selectedIds.join(',');
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const polygonParam = urlParams.get('polygon');
|
||||||
|
|
||||||
|
let url = '{% url "mainapp:show_sources_map" %}' + '?ids=' + selectedIds.join(',');
|
||||||
|
if (polygonParam) {
|
||||||
|
url += '&polygon=' + encodeURIComponent(polygonParam);
|
||||||
|
}
|
||||||
|
|
||||||
window.open(url, '_blank'); // Open in a new tab
|
window.open(url, '_blank'); // Open in a new tab
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -786,6 +981,8 @@ function performSearch() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
urlParams.delete('page');
|
urlParams.delete('page');
|
||||||
|
// Preserve polygon filter
|
||||||
|
// (already in urlParams from window.location.search)
|
||||||
window.location.search = urlParams.toString();
|
window.location.search = urlParams.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -794,6 +991,8 @@ function clearSearch() {
|
|||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
urlParams.delete('search');
|
urlParams.delete('search');
|
||||||
urlParams.delete('page');
|
urlParams.delete('page');
|
||||||
|
// Preserve polygon filter
|
||||||
|
// (already in urlParams from window.location.search)
|
||||||
window.location.search = urlParams.toString();
|
window.location.search = urlParams.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -810,6 +1009,8 @@ function updateItemsPerPage() {
|
|||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
urlParams.set('items_per_page', itemsPerPage);
|
urlParams.set('items_per_page', itemsPerPage);
|
||||||
urlParams.delete('page');
|
urlParams.delete('page');
|
||||||
|
// Preserve polygon filter
|
||||||
|
// (already in urlParams from window.location.search)
|
||||||
window.location.search = urlParams.toString();
|
window.location.search = urlParams.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -829,6 +1030,8 @@ function updateSort(field) {
|
|||||||
|
|
||||||
urlParams.set('sort', newSort);
|
urlParams.set('sort', newSort);
|
||||||
urlParams.delete('page');
|
urlParams.delete('page');
|
||||||
|
// Preserve polygon filter
|
||||||
|
// (already in urlParams from window.location.search)
|
||||||
window.location.search = urlParams.toString();
|
window.location.search = urlParams.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -884,6 +1087,12 @@ function updateFilterCounter() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if polygon filter is active
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
if (urlParams.has('polygon')) {
|
||||||
|
filterCount++;
|
||||||
|
}
|
||||||
|
|
||||||
// Display the filter counter
|
// Display the filter counter
|
||||||
const counterElement = document.getElementById('filterCounter');
|
const counterElement = document.getElementById('filterCounter');
|
||||||
if (counterElement) {
|
if (counterElement) {
|
||||||
@@ -1386,4 +1595,42 @@ function showTransponderModal(transponderId) {
|
|||||||
<!-- Include the satellite modal component -->
|
<!-- Include the satellite modal component -->
|
||||||
{% include 'mainapp/components/_satellite_modal.html' %}
|
{% include 'mainapp/components/_satellite_modal.html' %}
|
||||||
|
|
||||||
|
<!-- Polygon Filter Map Modal -->
|
||||||
|
<div class="modal fade" id="polygonFilterModal" tabindex="-1" aria-labelledby="polygonFilterModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-fullscreen">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="polygonFilterModalLabel">
|
||||||
|
<i class="bi bi-pentagon"></i> Нарисуйте полигон для фильтрации
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body p-0" style="position: relative;">
|
||||||
|
<div id="polygonHelpAlert" class="alert alert-info m-2" style="position: absolute; top: 10px; left: 10px; z-index: 1000; max-width: 400px; opacity: 0.95;">
|
||||||
|
<button type="button" class="btn-close btn-sm float-end" onclick="document.getElementById('polygonHelpAlert').style.display='none'"></button>
|
||||||
|
<small>
|
||||||
|
<strong>Инструкция:</strong>
|
||||||
|
<ul class="mb-0 ps-3">
|
||||||
|
<li>Используйте инструменты справа для рисования полигона или прямоугольника</li>
|
||||||
|
<li>Кликайте по карте для создания вершин полигона</li>
|
||||||
|
<li>Замкните полигон, кликнув на первую точку</li>
|
||||||
|
<li>Нажмите "Применить фильтр" для фильтрации источников</li>
|
||||||
|
</ul>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div id="polygonFilterMap" style="height: calc(100vh - 120px); width: 100%;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||||||
|
<button type="button" class="btn btn-danger" onclick="clearPolygonOnMap()">
|
||||||
|
<i class="bi bi-trash"></i> Очистить полигон
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="applyPolygonFilter()">
|
||||||
|
<i class="bi bi-check-circle"></i> Применить фильтр
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -182,8 +182,50 @@
|
|||||||
`;
|
`;
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if polygon_coords %}
|
||||||
|
div.innerHTML += `
|
||||||
|
<div class="legend-item" style="margin-top: 8px; padding-top: 8px; border-top: 1px solid #ddd;">
|
||||||
|
<div style="width: 18px; height: 18px; margin-right: 6px; background-color: rgba(51, 136, 255, 0.2); border: 2px solid #3388ff;"></div>
|
||||||
|
<span>Область фильтра</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
return div;
|
return div;
|
||||||
};
|
};
|
||||||
legend.addTo(map);
|
legend.addTo(map);
|
||||||
|
|
||||||
|
// Добавляем полигон фильтра на карту, если он есть
|
||||||
|
{% if polygon_coords %}
|
||||||
|
try {
|
||||||
|
const polygonCoords = {{ polygon_coords|safe }};
|
||||||
|
if (polygonCoords && polygonCoords.length > 0) {
|
||||||
|
// Преобразуем координаты из [lng, lat] в [lat, lng] для Leaflet
|
||||||
|
const latLngs = polygonCoords.map(coord => [coord[1], coord[0]]);
|
||||||
|
|
||||||
|
// Создаем полигон
|
||||||
|
const filterPolygon = L.polygon(latLngs, {
|
||||||
|
color: '#3388ff',
|
||||||
|
fillColor: '#3388ff',
|
||||||
|
fillOpacity: 0.2,
|
||||||
|
weight: 2,
|
||||||
|
dashArray: '5, 5'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Добавляем полигон на карту
|
||||||
|
filterPolygon.addTo(map);
|
||||||
|
|
||||||
|
// Добавляем popup с информацией
|
||||||
|
filterPolygon.bindPopup('<strong>Область фильтра</strong><br>Отображаются только источники с точками в этой области');
|
||||||
|
|
||||||
|
// Если нет других точек, центрируем карту на полигоне
|
||||||
|
{% if not groups %}
|
||||||
|
map.fitBounds(filterPolygon.getBounds());
|
||||||
|
{% endif %}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Ошибка при отображении полигона фильтра:', e);
|
||||||
|
}
|
||||||
|
{% endif %}
|
||||||
</script>
|
</script>
|
||||||
{% endblock extra_js %}
|
{% endblock extra_js %}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from .views import (
|
|||||||
DeleteSelectedSourcesView,
|
DeleteSelectedSourcesView,
|
||||||
DeleteSelectedTranspondersView,
|
DeleteSelectedTranspondersView,
|
||||||
FillLyngsatDataView,
|
FillLyngsatDataView,
|
||||||
|
GeoPointsAPIView,
|
||||||
GetLocationsView,
|
GetLocationsView,
|
||||||
HomeView,
|
HomeView,
|
||||||
LinkLyngsatSourcesView,
|
LinkLyngsatSourcesView,
|
||||||
@@ -81,6 +82,7 @@ urlpatterns = [
|
|||||||
path('api/source/<int:source_id>/objitems/', SourceObjItemsAPIView.as_view(), name='source_objitems_api'),
|
path('api/source/<int:source_id>/objitems/', SourceObjItemsAPIView.as_view(), name='source_objitems_api'),
|
||||||
path('api/transponder/<int:transponder_id>/', TransponderDataAPIView.as_view(), name='transponder_data_api'),
|
path('api/transponder/<int:transponder_id>/', TransponderDataAPIView.as_view(), name='transponder_data_api'),
|
||||||
path('api/satellite/<int:satellite_id>/', SatelliteDataAPIView.as_view(), name='satellite_data_api'),
|
path('api/satellite/<int:satellite_id>/', SatelliteDataAPIView.as_view(), name='satellite_data_api'),
|
||||||
|
path('api/geo-points/', GeoPointsAPIView.as_view(), name='geo_points_api'),
|
||||||
path('kubsat-excel/', ProcessKubsatView.as_view(), name='kubsat_excel'),
|
path('kubsat-excel/', ProcessKubsatView.as_view(), name='kubsat_excel'),
|
||||||
path('object/create/', ObjItemCreateView.as_view(), name='objitem_create'),
|
path('object/create/', ObjItemCreateView.as_view(), name='objitem_create'),
|
||||||
path('object/<int:pk>/edit/', ObjItemUpdateView.as_view(), name='objitem_update'),
|
path('object/<int:pk>/edit/', ObjItemUpdateView.as_view(), name='objitem_update'),
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from .data_import import (
|
|||||||
ProcessKubsatView,
|
ProcessKubsatView,
|
||||||
)
|
)
|
||||||
from .api import (
|
from .api import (
|
||||||
|
GeoPointsAPIView,
|
||||||
GetLocationsView,
|
GetLocationsView,
|
||||||
LyngsatDataAPIView,
|
LyngsatDataAPIView,
|
||||||
SatelliteDataAPIView,
|
SatelliteDataAPIView,
|
||||||
@@ -70,6 +71,7 @@ __all__ = [
|
|||||||
'LinkVchSigmaView',
|
'LinkVchSigmaView',
|
||||||
'ProcessKubsatView',
|
'ProcessKubsatView',
|
||||||
# API
|
# API
|
||||||
|
'GeoPointsAPIView',
|
||||||
'GetLocationsView',
|
'GetLocationsView',
|
||||||
'LyngsatDataAPIView',
|
'LyngsatDataAPIView',
|
||||||
'SatelliteDataAPIView',
|
'SatelliteDataAPIView',
|
||||||
|
|||||||
@@ -472,6 +472,52 @@ class TransponderDataAPIView(LoginRequiredMixin, View):
|
|||||||
return JsonResponse({'error': str(e)}, status=500)
|
return JsonResponse({'error': str(e)}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
class GeoPointsAPIView(LoginRequiredMixin, View):
|
||||||
|
"""API endpoint for getting all geo points for polygon filter visualization."""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
from ..models import Geo
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Limit to reasonable number of points to avoid performance issues
|
||||||
|
limit = int(request.GET.get('limit', 10000))
|
||||||
|
limit = min(limit, 50000) # Max 50k points
|
||||||
|
|
||||||
|
# Get all Geo objects with coordinates
|
||||||
|
geo_objs = Geo.objects.filter(
|
||||||
|
coords__isnull=False
|
||||||
|
).select_related(
|
||||||
|
'objitem',
|
||||||
|
'objitem__source'
|
||||||
|
)[:limit]
|
||||||
|
|
||||||
|
points = []
|
||||||
|
for geo_obj in geo_objs:
|
||||||
|
if not geo_obj.coords:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Get source_id if available
|
||||||
|
source_id = None
|
||||||
|
if hasattr(geo_obj, 'objitem') and geo_obj.objitem:
|
||||||
|
if hasattr(geo_obj.objitem, 'source') and geo_obj.objitem.source:
|
||||||
|
source_id = geo_obj.objitem.source.id
|
||||||
|
|
||||||
|
points.append({
|
||||||
|
'id': geo_obj.id,
|
||||||
|
'lat': geo_obj.coords.y,
|
||||||
|
'lng': geo_obj.coords.x,
|
||||||
|
'source_id': source_id or '-'
|
||||||
|
})
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'points': points,
|
||||||
|
'total': len(points),
|
||||||
|
'limited': len(points) >= limit
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({'error': str(e)}, status=500)
|
||||||
|
|
||||||
|
|
||||||
class SatelliteDataAPIView(LoginRequiredMixin, View):
|
class SatelliteDataAPIView(LoginRequiredMixin, View):
|
||||||
"""API endpoint for getting Satellite data."""
|
"""API endpoint for getting Satellite data."""
|
||||||
|
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ class ShowSourcesMapView(LoginRequiredMixin, View):
|
|||||||
"""View for displaying selected sources on map."""
|
"""View for displaying selected sources on map."""
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
|
import json
|
||||||
from ..models import Source
|
from ..models import Source
|
||||||
|
|
||||||
ids = request.GET.get("ids", "")
|
ids = request.GET.get("ids", "")
|
||||||
@@ -168,8 +169,18 @@ class ShowSourcesMapView(LoginRequiredMixin, View):
|
|||||||
else:
|
else:
|
||||||
return redirect("mainapp:home")
|
return redirect("mainapp:home")
|
||||||
|
|
||||||
|
# Get polygon filter from URL if present
|
||||||
|
polygon_coords_str = request.GET.get("polygon", "").strip()
|
||||||
|
polygon_coords = None
|
||||||
|
if polygon_coords_str:
|
||||||
|
try:
|
||||||
|
polygon_coords = json.loads(polygon_coords_str)
|
||||||
|
except (json.JSONDecodeError, ValueError, TypeError):
|
||||||
|
polygon_coords = None
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"groups": groups,
|
"groups": groups,
|
||||||
|
"polygon_coords": json.dumps(polygon_coords) if polygon_coords else None,
|
||||||
}
|
}
|
||||||
return render(request, "mainapp/source_map.html", context)
|
return render(request, "mainapp/source_map.html", context)
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
"""
|
"""
|
||||||
Source related views.
|
Source related views.
|
||||||
"""
|
"""
|
||||||
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
||||||
|
from django.contrib.gis.geos import Point, Polygon as GEOSPolygon
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.db.models import Count, Q
|
from django.db.models import Count, Q
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
@@ -62,6 +64,23 @@ class SourceListView(LoginRequiredMixin, View):
|
|||||||
snr_min = request.GET.get("snr_min", "").strip()
|
snr_min = request.GET.get("snr_min", "").strip()
|
||||||
snr_max = request.GET.get("snr_max", "").strip()
|
snr_max = request.GET.get("snr_max", "").strip()
|
||||||
|
|
||||||
|
# Get polygon filter
|
||||||
|
polygon_coords_str = request.GET.get("polygon", "").strip()
|
||||||
|
polygon_coords = None
|
||||||
|
polygon_geom = None
|
||||||
|
|
||||||
|
if polygon_coords_str:
|
||||||
|
try:
|
||||||
|
polygon_coords = json.loads(polygon_coords_str)
|
||||||
|
if polygon_coords and len(polygon_coords) >= 4:
|
||||||
|
# Create GEOS Polygon from coordinates
|
||||||
|
# Coordinates are in [lng, lat] format
|
||||||
|
polygon_geom = GEOSPolygon(polygon_coords, srid=4326)
|
||||||
|
except (json.JSONDecodeError, ValueError, TypeError) as e:
|
||||||
|
# Invalid polygon data, ignore
|
||||||
|
polygon_coords = None
|
||||||
|
polygon_geom = None
|
||||||
|
|
||||||
# Get all satellites for filter
|
# Get all satellites for filter
|
||||||
satellites = (
|
satellites = (
|
||||||
Satellite.objects.filter(parameters__objitem__source__isnull=False)
|
Satellite.objects.filter(parameters__objitem__source__isnull=False)
|
||||||
@@ -210,6 +229,11 @@ class SourceListView(LoginRequiredMixin, View):
|
|||||||
objitem_filter_q &= Q(source_objitems__geo_obj__mirrors__id__in=selected_mirrors)
|
objitem_filter_q &= Q(source_objitems__geo_obj__mirrors__id__in=selected_mirrors)
|
||||||
has_objitem_filter = True
|
has_objitem_filter = True
|
||||||
|
|
||||||
|
# Add polygon filter
|
||||||
|
if polygon_geom:
|
||||||
|
objitem_filter_q &= Q(source_objitems__geo_obj__coords__within=polygon_geom)
|
||||||
|
has_objitem_filter = True
|
||||||
|
|
||||||
# Get all Source objects with query optimization
|
# Get all Source objects with query optimization
|
||||||
# Using annotate to count ObjItems efficiently (single query with GROUP BY)
|
# Using annotate to count ObjItems efficiently (single query with GROUP BY)
|
||||||
# Using prefetch_related for reverse ForeignKey relationships to avoid N+1 queries
|
# Using prefetch_related for reverse ForeignKey relationships to avoid N+1 queries
|
||||||
@@ -443,6 +467,12 @@ class SourceListView(LoginRequiredMixin, View):
|
|||||||
source_objitems__geo_obj__mirrors__id__in=selected_mirrors
|
source_objitems__geo_obj__mirrors__id__in=selected_mirrors
|
||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
|
# Filter by polygon
|
||||||
|
if polygon_geom:
|
||||||
|
sources = sources.filter(
|
||||||
|
source_objitems__geo_obj__coords__within=polygon_geom
|
||||||
|
).distinct()
|
||||||
|
|
||||||
# Apply sorting
|
# Apply sorting
|
||||||
valid_sort_fields = {
|
valid_sort_fields = {
|
||||||
"id": "id",
|
"id": "id",
|
||||||
@@ -540,6 +570,8 @@ class SourceListView(LoginRequiredMixin, View):
|
|||||||
objitems_to_display = objitems_to_display.filter(geo_obj__mirrors__id__in=selected_mirrors)
|
objitems_to_display = objitems_to_display.filter(geo_obj__mirrors__id__in=selected_mirrors)
|
||||||
if search_by_name:
|
if search_by_name:
|
||||||
objitems_to_display = objitems_to_display.filter(name__icontains=search_query)
|
objitems_to_display = objitems_to_display.filter(name__icontains=search_query)
|
||||||
|
if polygon_geom:
|
||||||
|
objitems_to_display = objitems_to_display.filter(geo_obj__coords__within=polygon_geom)
|
||||||
|
|
||||||
# Use annotated count (consistent with filtering)
|
# Use annotated count (consistent with filtering)
|
||||||
objitem_count = source.objitem_count
|
objitem_count = source.objitem_count
|
||||||
@@ -652,6 +684,7 @@ class SourceListView(LoginRequiredMixin, View):
|
|||||||
int(x) if isinstance(x, str) else x for x in selected_mirrors if (isinstance(x, int) or (isinstance(x, str) and x.isdigit()))
|
int(x) if isinstance(x, str) else x for x in selected_mirrors if (isinstance(x, int) or (isinstance(x, str) and x.isdigit()))
|
||||||
],
|
],
|
||||||
'object_infos': object_infos,
|
'object_infos': object_infos,
|
||||||
|
'polygon_coords': json.dumps(polygon_coords) if polygon_coords else None,
|
||||||
'full_width_page': True,
|
'full_width_page': True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user