diff --git a/dbapp/mainapp/templates/mainapp/points_averaging.html b/dbapp/mainapp/templates/mainapp/points_averaging.html index ffa083d..3d09924 100644 --- a/dbapp/mainapp/templates/mainapp/points_averaging.html +++ b/dbapp/mainapp/templates/mainapp/points_averaging.html @@ -22,53 +22,90 @@ border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } - #result-table { - margin-top: 20px; + .source-card { + border: 1px solid #dee2e6; + border-radius: 8px; + margin-bottom: 15px; + overflow: hidden; + } + .source-header { + background: #f8f9fa; + padding: 12px 15px; + border-bottom: 1px solid #dee2e6; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + } + .source-header:hover { + background: #e9ecef; + } + .source-header h5 { + margin: 0; + font-size: 16px; + } + .source-body { + padding: 15px; + } + .group-card { + border: 1px solid #e9ecef; + border-radius: 6px; + margin-bottom: 10px; + background: #fff; + } + .group-header { + background: #f8f9fa; + padding: 10px 12px; + border-bottom: 1px solid #e9ecef; + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 10px; + } + .group-header.has-outliers { + background: #fff3cd; + } + .group-info { + display: flex; + flex-wrap: wrap; + gap: 15px; + align-items: center; + } + .group-info-item { + font-size: 13px; + } + .group-info-item strong { + color: #495057; + } + .group-actions { + display: flex; + gap: 5px; + } + .group-body { + padding: 10px; + } + .points-table { font-size: 12px; + width: 100%; } - #result-table .tabulator-header { - font-size: 12px; - white-space: normal; - word-wrap: break-word; + .points-table th, .points-table td { + padding: 6px 8px; + border: 1px solid #dee2e6; } - #result-table .tabulator-header .tabulator-col { - white-space: normal; - word-wrap: break-word; - height: auto; - min-height: 40px; + .points-table th { + background: #f8f9fa; + font-weight: 600; } - #result-table .tabulator-header .tabulator-col-content { - white-space: normal; - word-wrap: break-word; - padding: 6px 4px; + .points-table tr.outlier { + background-color: #ffcccc !important; } - #result-table .tabulator-cell { - font-size: 12px; - padding: 6px 4px; + .points-table tr.valid { + background-color: #d4edda !important; } .btn-group-custom { margin-top: 15px; } - .outlier-row { - background-color: #ffcccc !important; - } - .outlier-warning { - color: #dc3545; - font-weight: bold; - } - .group-header { - background-color: #e9ecef; - padding: 10px; - margin-bottom: 10px; - border-radius: 4px; - } - .points-detail-table { - margin-top: 10px; - font-size: 11px; - } - .modal-xl { - max-width: 95%; - } .loading-overlay { position: fixed; top: 0; @@ -84,53 +121,39 @@ .loading-overlay.active { display: flex; } - /* Цвета для групп */ - .group-color-0 { background-color: #cce5ff !important; } - .group-color-1 { background-color: #d4edda !important; } - .group-color-2 { background-color: #fff3cd !important; } - .group-color-3 { background-color: #f8d7da !important; } - .group-color-4 { background-color: #d1ecf1 !important; } - .group-color-5 { background-color: #e2d5f1 !important; } - .group-color-6 { background-color: #ffeeba !important; } - .group-color-7 { background-color: #c3e6cb !important; } - .group-color-8 { background-color: #bee5eb !important; } - .group-color-9 { background-color: #f5c6cb !important; } - - .all-points-section { + .badge-count { + font-size: 12px; + padding: 4px 8px; + } + .collapse-icon { + transition: transform 0.2s; + } + .collapsed .collapse-icon { + transform: rotate(-90deg); + } + .summary-table { margin-top: 20px; } - #all-points-table { - margin-top: 10px; - font-size: 11px; + #summary-table { + font-size: 12px; } - #all-points-table .tabulator-header { - font-size: 11px; + #summary-table .tabulator-header { + font-size: 12px; } - #all-points-table .tabulator-cell { - font-size: 11px; - padding: 4px 3px; - } - .legend-item { - display: inline-flex; - align-items: center; - margin-right: 15px; - margin-bottom: 5px; - } - .legend-color { - width: 20px; - height: 20px; - margin-right: 5px; - border: 1px solid #ccc; - border-radius: 3px; - } - .legend-container { - padding: 10px; - background: #f8f9fa; - border-radius: 4px; - margin-bottom: 10px; - max-height: 100px; - overflow-y: auto; + #summary-table .tabulator-cell { + font-size: 12px; + padding: 6px 4px; } + .source-color-0 { border-left: 4px solid #0d6efd; } + .source-color-1 { border-left: 4px solid #198754; } + .source-color-2 { border-left: 4px solid #ffc107; } + .source-color-3 { border-left: 4px solid #dc3545; } + .source-color-4 { border-left: 4px solid #0dcaf0; } + .source-color-5 { border-left: 4px solid #6f42c1; } + .source-color-6 { border-left: 4px solid #fd7e14; } + .source-color-7 { border-left: 4px solid #20c997; } + .source-color-8 { border-left: 4px solid #6c757d; } + .source-color-9 { border-left: 4px solid #d63384; } {% endblock %} @@ -142,7 +165,7 @@
-

Усреднение точек спутников

+

Усреднение точек по источникам

@@ -165,16 +188,16 @@
-
+
-
Результаты усреднения 0
+
Источники 0
-
-
+ +
+
+ Нет данных. Выберите спутник и диапазон дат, затем нажмите 'Загрузить данные'. +
+
- - -
- - - @@ -265,105 +235,427 @@ {% endblock %} diff --git a/dbapp/mainapp/views/points_averaging.py b/dbapp/mainapp/views/points_averaging.py index bfea7a3..9363251 100644 --- a/dbapp/mainapp/views/points_averaging.py +++ b/dbapp/mainapp/views/points_averaging.py @@ -1,5 +1,6 @@ """ Points averaging view for satellite data grouping by day/night intervals. +Groups points by Source, then by time intervals within each Source. """ from datetime import datetime, timedelta from django.contrib.auth.mixins import LoginRequiredMixin @@ -8,7 +9,7 @@ from django.shortcuts import render from django.views import View from django.utils import timezone -from ..models import ObjItem, Satellite +from ..models import ObjItem, Satellite, Source from ..utils import ( calculate_mean_coords, calculate_distance_wgs84, @@ -29,8 +30,9 @@ class PointsAveragingView(LoginRequiredMixin, View): """ def get(self, request): - # Get satellites that have points with geo data + # Get satellites that have sources with points with geo data satellites = Satellite.objects.filter( + parameters__objitem__source__isnull=False, parameters__objitem__geo_obj__coords__isnull=False ).distinct().order_by('name') @@ -44,13 +46,14 @@ class PointsAveragingView(LoginRequiredMixin, View): class PointsAveragingAPIView(LoginRequiredMixin, View): """ - API endpoint for grouping and averaging points by day/night intervals. + API endpoint for grouping and averaging points by Source and day/night intervals. Groups points into: - Day: 08:00 - 19:00 - Night: 19:00 - 08:00 (next day) + - Weekend: Friday 19:00 - Monday 08:00 - For each group, calculates average coordinates and checks for outliers (>56 km). + For each group within each Source, calculates average coordinates and checks for outliers (>56 km). """ def get(self, request): @@ -76,9 +79,50 @@ class PointsAveragingAPIView(LoginRequiredMixin, View): except ValueError: return JsonResponse({'error': 'Неверный формат даты'}, status=400) - # Get all points for the satellite in the date range - objitems = ObjItem.objects.filter( - parameter_obj__id_satellite=satellite, + # Get all Sources for the satellite that have points in the date range + sources = Source.objects.filter( + source_objitems__parameter_obj__id_satellite=satellite, + source_objitems__geo_obj__coords__isnull=False, + source_objitems__geo_obj__timestamp__gte=date_from_obj, + source_objitems__geo_obj__timestamp__lt=date_to_obj, + ).distinct().prefetch_related( + 'source_objitems', + 'source_objitems__geo_obj', + 'source_objitems__geo_obj__mirrors', + 'source_objitems__parameter_obj', + 'source_objitems__parameter_obj__polarization', + 'source_objitems__parameter_obj__modulation', + 'source_objitems__parameter_obj__standard', + ) + + if not sources.exists(): + return JsonResponse({'error': 'Источники не найдены в указанном диапазоне'}, status=404) + + # Process each source + result_sources = [] + for source in sources: + source_data = self._process_source(source, date_from_obj, date_to_obj) + if source_data['groups']: # Only add if has groups with points + result_sources.append(source_data) + + if not result_sources: + return JsonResponse({'error': 'Точки не найдены в указанном диапазоне'}, status=404) + + return JsonResponse({ + 'success': True, + 'satellite': satellite.name, + 'date_from': date_from, + 'date_to': date_to, + 'sources': result_sources, + 'total_sources': len(result_sources), + }) + + def _process_source(self, source, date_from_obj, date_to_obj): + """ + Process a single Source: get its points and group them by time intervals. + """ + # Get all points for this source in the date range + objitems = source.source_objitems.filter( geo_obj__coords__isnull=False, geo_obj__timestamp__gte=date_from_obj, geo_obj__timestamp__lt=date_to_obj, @@ -89,16 +133,12 @@ class PointsAveragingAPIView(LoginRequiredMixin, View): 'parameter_obj__modulation', 'parameter_obj__standard', 'geo_obj', - 'source', ).prefetch_related( 'geo_obj__mirrors' ).order_by('geo_obj__timestamp') - if not objitems.exists(): - return JsonResponse({'error': 'Точки не найдены в указанном диапазоне'}, status=404) - - # Group points by source name and day/night intervals - groups = self._group_points_by_intervals(objitems) + # Group points by day/night intervals + groups = self._group_points_by_intervals(list(objitems)) # Process each group: calculate average and check for outliers result_groups = [] @@ -106,21 +146,27 @@ class PointsAveragingAPIView(LoginRequiredMixin, View): group_result = self._process_group(group_key, points) result_groups.append(group_result) - return JsonResponse({ - 'success': True, - 'satellite': satellite.name, - 'date_from': date_from, - 'date_to': date_to, + # Get source name from first point or use ID + source_name = f"Источник #{source.id}" + if objitems.exists(): + first_point = objitems.first() + if first_point.name: + source_name = first_point.name + + return { + 'source_id': source.id, + 'source_name': source_name, + 'total_points': sum(len(g['points']) for g in result_groups), 'groups': result_groups, - 'total_groups': len(result_groups), - }) + } def _group_points_by_intervals(self, objitems): """ - Group points by source name and day/night intervals. + Group points by day/night intervals. Day: 08:00 - 19:00 Night: 19:00 - 08:00 (next day) + Weekend: Friday 19:00 - Monday 08:00 """ groups = {} @@ -129,19 +175,14 @@ class PointsAveragingAPIView(LoginRequiredMixin, View): continue timestamp = timezone.localtime(objitem.geo_obj.timestamp) - # timestamp = objitem.geo_obj.timestamp - source_name = objitem.name or f"Объект #{objitem.id}" # Determine interval interval_key = self._get_interval_key(timestamp) - # Create group key: (source_name, interval_key) - group_key = (source_name, interval_key) + if interval_key not in groups: + groups[interval_key] = [] - if group_key not in groups: - groups[group_key] = [] - - groups[group_key].append(objitem) + groups[interval_key].append(objitem) return groups @@ -208,7 +249,7 @@ class PointsAveragingAPIView(LoginRequiredMixin, View): return date - timedelta(days=3) return date - def _process_group(self, group_key, points): + def _process_group(self, interval_key, points): """ Process a group of points: calculate average and check for outliers. @@ -218,8 +259,6 @@ class PointsAveragingAPIView(LoginRequiredMixin, View): 3. Iteratively add points within 56 km of current average 4. Points not within 56 km of final average are outliers """ - source_name, interval_key = group_key - # Parse interval info date_str, interval_type = interval_key.rsplit('_', 1) interval_date = datetime.strptime(date_str, '%Y-%m-%d').date() @@ -322,7 +361,7 @@ class PointsAveragingAPIView(LoginRequiredMixin, View): # Calculate median time from valid points using timestamp_objects array valid_timestamps = [] for i in valid_indices: - if timestamp_objects[i]: + if i < len(timestamp_objects) and timestamp_objects[i]: valid_timestamps.append(timestamp_objects[i]) median_time_str = '-' @@ -344,7 +383,6 @@ class PointsAveragingAPIView(LoginRequiredMixin, View): median_time_str = timezone.localtime(median_datetime).strftime("%d.%m.%Y %H:%M") return { - 'source_name': source_name, 'interval_key': interval_key, 'interval_label': interval_label, 'total_points': len(points_data),