diff --git a/dbapp/mainapp/templates/mainapp/source_list.html b/dbapp/mainapp/templates/mainapp/source_list.html index 1368672..43fa753 100644 --- a/dbapp/mainapp/templates/mainapp/source_list.html +++ b/dbapp/mainapp/templates/mainapp/source_list.html @@ -3,6 +3,8 @@ {% block title %}Список объектов{% endblock %} {% block extra_css %} + + {% endblock %} @@ -94,6 +99,23 @@ + +
+ + {% if polygon_coords %} + + {% endif %} +
+
{% include 'mainapp/components/_pagination.html' with page_obj=page_obj show_info=True %} @@ -112,6 +134,11 @@
+ + {% if polygon_coords %} + + {% endif %} +
@@ -703,6 +730,167 @@ {% endblock %} {% block extra_js %} + + + {% endblock extra_js %} diff --git a/dbapp/mainapp/urls.py b/dbapp/mainapp/urls.py index 8198552..17a77f6 100644 --- a/dbapp/mainapp/urls.py +++ b/dbapp/mainapp/urls.py @@ -11,6 +11,7 @@ from .views import ( DeleteSelectedSourcesView, DeleteSelectedTranspondersView, FillLyngsatDataView, + GeoPointsAPIView, GetLocationsView, HomeView, LinkLyngsatSourcesView, @@ -81,6 +82,7 @@ urlpatterns = [ path('api/source//objitems/', SourceObjItemsAPIView.as_view(), name='source_objitems_api'), path('api/transponder//', TransponderDataAPIView.as_view(), name='transponder_data_api'), path('api/satellite//', 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('object/create/', ObjItemCreateView.as_view(), name='objitem_create'), path('object//edit/', ObjItemUpdateView.as_view(), name='objitem_update'), diff --git a/dbapp/mainapp/views/__init__.py b/dbapp/mainapp/views/__init__.py index f30671a..b156ee8 100644 --- a/dbapp/mainapp/views/__init__.py +++ b/dbapp/mainapp/views/__init__.py @@ -18,6 +18,7 @@ from .data_import import ( ProcessKubsatView, ) from .api import ( + GeoPointsAPIView, GetLocationsView, LyngsatDataAPIView, SatelliteDataAPIView, @@ -70,6 +71,7 @@ __all__ = [ 'LinkVchSigmaView', 'ProcessKubsatView', # API + 'GeoPointsAPIView', 'GetLocationsView', 'LyngsatDataAPIView', 'SatelliteDataAPIView', diff --git a/dbapp/mainapp/views/api.py b/dbapp/mainapp/views/api.py index a4be17e..d77a1dd 100644 --- a/dbapp/mainapp/views/api.py +++ b/dbapp/mainapp/views/api.py @@ -472,6 +472,52 @@ class TransponderDataAPIView(LoginRequiredMixin, View): 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): """API endpoint for getting Satellite data.""" diff --git a/dbapp/mainapp/views/map.py b/dbapp/mainapp/views/map.py index 9ac519a..57a0d71 100644 --- a/dbapp/mainapp/views/map.py +++ b/dbapp/mainapp/views/map.py @@ -126,6 +126,7 @@ class ShowSourcesMapView(LoginRequiredMixin, View): """View for displaying selected sources on map.""" def get(self, request): + import json from ..models import Source ids = request.GET.get("ids", "") @@ -168,8 +169,18 @@ class ShowSourcesMapView(LoginRequiredMixin, View): else: 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 = { "groups": groups, + "polygon_coords": json.dumps(polygon_coords) if polygon_coords else None, } return render(request, "mainapp/source_map.html", context) diff --git a/dbapp/mainapp/views/source.py b/dbapp/mainapp/views/source.py index b531d01..5ab368d 100644 --- a/dbapp/mainapp/views/source.py +++ b/dbapp/mainapp/views/source.py @@ -1,10 +1,12 @@ """ Source related views. """ +import json from datetime import datetime from django.contrib import messages 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.db.models import Count, Q from django.http import JsonResponse @@ -62,6 +64,23 @@ class SourceListView(LoginRequiredMixin, View): snr_min = request.GET.get("snr_min", "").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 satellites = ( 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) 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 # Using annotate to count ObjItems efficiently (single query with GROUP BY) # 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 ).distinct() + # Filter by polygon + if polygon_geom: + sources = sources.filter( + source_objitems__geo_obj__coords__within=polygon_geom + ).distinct() + # Apply sorting valid_sort_fields = { "id": "id", @@ -540,6 +570,8 @@ class SourceListView(LoginRequiredMixin, View): objitems_to_display = objitems_to_display.filter(geo_obj__mirrors__id__in=selected_mirrors) if search_by_name: 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) 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())) ], 'object_infos': object_infos, + 'polygon_coords': json.dumps(polygon_coords) if polygon_coords else None, 'full_width_page': True, }