From a18071b7ecbd18b37a1a17fea2aa84e9a7dd3be8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=BE=D1=88=D0=BA=D0=B8=D0=BD=20=D0=A1=D0=B5=D1=80?= =?UTF-8?q?=D0=B3=D0=B5=D0=B9?= Date: Tue, 2 Dec 2025 11:47:47 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=BE=D0=BC=D0=B5=D0=BD=D1=8F=D0=BB=20?= =?UTF-8?q?=D1=83=D1=81=D1=80=D0=B5=D0=B4=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../templates/mainapp/points_averaging.html | 790 ++++++------------ dbapp/mainapp/utils.py | 221 +++-- dbapp/mainapp/views/points_averaging.py | 64 +- 3 files changed, 465 insertions(+), 610 deletions(-) diff --git a/dbapp/mainapp/templates/mainapp/points_averaging.html b/dbapp/mainapp/templates/mainapp/points_averaging.html index 3d09924..5e9c744 100644 --- a/dbapp/mainapp/templates/mainapp/points_averaging.html +++ b/dbapp/mainapp/templates/mainapp/points_averaging.html @@ -22,30 +22,37 @@ border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } - .source-card { - border: 1px solid #dee2e6; - border-radius: 8px; - margin-bottom: 15px; - overflow: hidden; + #sources-table { + margin-top: 15px; + font-size: 12px; } - .source-header { - background: #f8f9fa; - padding: 12px 15px; - border-bottom: 1px solid #dee2e6; - cursor: pointer; - display: flex; - justify-content: space-between; + #sources-table .tabulator-header { + font-size: 12px; + } + #sources-table .tabulator-cell { + font-size: 12px; + padding: 6px 4px; + } + .btn-group-custom { + margin-top: 15px; + } + .loading-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(255,255,255,0.8); + display: none; + justify-content: center; align-items: center; + z-index: 9999; } - .source-header:hover { - background: #e9ecef; + .loading-overlay.active { + display: flex; } - .source-header h5 { - margin: 0; - font-size: 16px; - } - .source-body { - padding: 15px; + .modal-xl { + max-width: 95%; } .group-card { border: 1px solid #e9ecef; @@ -86,11 +93,11 @@ padding: 10px; } .points-table { - font-size: 12px; + font-size: 11px; width: 100%; } .points-table th, .points-table td { - padding: 6px 8px; + padding: 5px 6px; border: 1px solid #dee2e6; } .points-table th { @@ -103,57 +110,9 @@ .points-table tr.valid { background-color: #d4edda !important; } - .btn-group-custom { - margin-top: 15px; + .source-has-outliers { + background-color: #fff3cd !important; } - .loading-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(255,255,255,0.8); - display: none; - justify-content: center; - align-items: center; - z-index: 9999; - } - .loading-overlay.active { - display: flex; - } - .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; - } - #summary-table { - font-size: 12px; - } - #summary-table .tabulator-header { - font-size: 12px; - } - #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 %} @@ -195,9 +154,9 @@
-
+
-
Источники 0
+
Источники 0
- -
-
- Нет данных. Выберите спутник и диапазон дат, затем нажмите 'Загрузить данные'. +
+
+
+ + + @@ -235,17 +200,10 @@ {% endblock %} diff --git a/dbapp/mainapp/utils.py b/dbapp/mainapp/utils.py index 29ff99d..bf59c67 100644 --- a/dbapp/mainapp/utils.py +++ b/dbapp/mainapp/utils.py @@ -1413,27 +1413,28 @@ def kub_report(data_in: io.StringIO) -> pd.DataFrame: from pyproj import CRS, Transformer -def get_gauss_kruger_zone(longitude: float) -> int: +def get_gauss_kruger_zone(longitude: float) -> int | None: """ Определяет номер зоны Гаусса-Крюгера по долготе. - Зоны ГК нумеруются от 1 до 60, каждая зона охватывает 6° долготы. - Центральный меридиан зоны N: (6*N - 3)° + Зоны ГК (Пулково 1942) имеют EPSG коды 28404-28432 (зоны 4-32). + Каждая зона охватывает 6° долготы. Args: longitude: Долгота в градусах (от -180 до 180) Returns: - int: Номер зоны ГК (1-60) + int | None: Номер зоны ГК (4-32) или None если координаты вне зон ГК """ # Нормализуем долготу к диапазону 0-360 lon_normalized = longitude if longitude >= 0 else longitude + 360 # Вычисляем номер зоны (1-60) zone = int((lon_normalized + 6) / 6) - if zone > 60: - zone = 60 - if zone < 1: - zone = 1 + + # EPSG коды Пулково 1942 существуют только для зон 4-32 + if zone < 4 or zone > 32: + return None + return zone @@ -1441,14 +1442,8 @@ def get_gauss_kruger_epsg(zone: int) -> int: """ Возвращает EPSG код для зоны Гаусса-Крюгера (Pulkovo 1942 / Gauss-Kruger). - EPSG коды для Pulkovo 1942 GK зон: - - Зона 4: EPSG:28404 - - Зона 5: EPSG:28405 - - ... - - Зона N: EPSG:28400 + N - Args: - zone: Номер зоны ГК (1-60) + zone: Номер зоны ГК (4-32) Returns: int: EPSG код проекции @@ -1456,13 +1451,50 @@ def get_gauss_kruger_epsg(zone: int) -> int: return 28400 + zone +def get_utm_zone(longitude: float) -> int: + """ + Определяет номер зоны UTM по долготе. + + UTM зоны нумеруются от 1 до 60, каждая зона охватывает 6° долготы. + + Args: + longitude: Долгота в градусах (от -180 до 180) + + Returns: + int: Номер зоны UTM (1-60) + """ + zone = int((longitude + 180) / 6) + 1 + if zone > 60: + zone = 60 + if zone < 1: + zone = 1 + return zone + + +def get_utm_epsg(zone: int, is_northern: bool = True) -> int: + """ + Возвращает EPSG код для зоны UTM (WGS 84 / UTM). + + Args: + zone: Номер зоны UTM (1-60) + is_northern: True для северного полушария, False для южного + + Returns: + int: EPSG код проекции + """ + if is_northern: + return 32600 + zone + else: + return 32700 + zone + + def transform_wgs84_to_gk(coord: tuple, zone: int = None) -> tuple: """ - Преобразует координаты из WGS84 (EPSG:4326) в проекцию Гаусса-Крюгера. + Преобразует координаты из WGS84 в проекцию Гаусса-Крюгера. Args: coord: Координаты в формате (longitude, latitude) в WGS84 - zone: Номер зоны ГК (если None, определяется автоматически по долготе) + zone: Номер зоны ГК (если None, определяется автоматически) Returns: tuple: Координаты (x, y) в метрах в проекции ГК @@ -1472,9 +1504,11 @@ def transform_wgs84_to_gk(coord: tuple, zone: int = None) -> tuple: if zone is None: zone = get_gauss_kruger_zone(lon) + if zone is None: + raise ValueError(f"Координаты ({lon}, {lat}) вне зон Гаусса-Крюгера (4-32)") + epsg_gk = get_gauss_kruger_epsg(zone) - # Создаём трансформер WGS84 -> GK transformer = Transformer.from_crs( CRS.from_epsg(4326), CRS.from_epsg(epsg_gk), @@ -1487,7 +1521,7 @@ def transform_wgs84_to_gk(coord: tuple, zone: int = None) -> tuple: def transform_gk_to_wgs84(coord: tuple, zone: int) -> tuple: """ - Преобразует координаты из проекции Гаусса-Крюгера в WGS84 (EPSG:4326). + Преобразует координаты из проекции Гаусса-Крюгера в WGS84. Args: coord: Координаты (x, y) в метрах в проекции ГК @@ -1499,7 +1533,6 @@ def transform_gk_to_wgs84(coord: tuple, zone: int) -> tuple: x, y = coord epsg_gk = get_gauss_kruger_epsg(zone) - # Создаём трансформер GK -> WGS84 transformer = Transformer.from_crs( CRS.from_epsg(epsg_gk), CRS.from_epsg(4326), @@ -1510,37 +1543,126 @@ def transform_gk_to_wgs84(coord: tuple, zone: int) -> tuple: return (lon, lat) -def calculate_distance_gk(coord1_gk: tuple, coord2_gk: tuple) -> float: +def transform_wgs84_to_utm(coord: tuple, zone: int = None, is_northern: bool = None) -> tuple: """ - Вычисляет расстояние между двумя точками в проекции ГК (в километрах). + Преобразует координаты из WGS84 в проекцию UTM. Args: - coord1_gk: Первая точка (x, y) в метрах - coord2_gk: Вторая точка (x, y) в метрах + coord: Координаты в формате (longitude, latitude) в WGS84 + zone: Номер зоны UTM (если None, определяется автоматически) + is_northern: Северное полушарие (если None, определяется по широте) Returns: - float: Расстояние в километрах + tuple: Координаты (x, y) в метрах в проекции UTM """ - import math - x1, y1 = coord1_gk - x2, y2 = coord2_gk - distance_m = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) - return distance_m / 1000 - - -def average_coords_in_gk(coords: list[tuple], zone: int = None) -> tuple: - """ - Вычисляет среднее арифметическое координат в проекции Гаусса-Крюгера. + lon, lat = coord - Алгоритм: - 1. Определяет зону ГК по первой точке (если не указана) - 2. Преобразует все координаты в проекцию ГК - 3. Вычисляет среднее арифметическое X и Y - 4. Преобразует результат обратно в WGS84 + if zone is None: + zone = get_utm_zone(lon) + + if is_northern is None: + is_northern = lat >= 0 + + epsg_utm = get_utm_epsg(zone, is_northern) + + transformer = Transformer.from_crs( + CRS.from_epsg(4326), + CRS.from_epsg(epsg_utm), + always_xy=True + ) + + x, y = transformer.transform(lon, lat) + return (x, y) + + +def transform_utm_to_wgs84(coord: tuple, zone: int, is_northern: bool = True) -> tuple: + """ + Преобразует координаты из проекции UTM в WGS84. + + Args: + coord: Координаты (x, y) в метрах в проекции UTM + zone: Номер зоны UTM + is_northern: Северное полушарие + + Returns: + tuple: Координаты (longitude, latitude) в WGS84 + """ + x, y = coord + epsg_utm = get_utm_epsg(zone, is_northern) + + transformer = Transformer.from_crs( + CRS.from_epsg(epsg_utm), + CRS.from_epsg(4326), + always_xy=True + ) + + lon, lat = transformer.transform(x, y) + return (lon, lat) + + +def average_coords_in_gk(coords: list[tuple], zone: int = None) -> tuple[tuple, str]: + """ + Вычисляет среднее арифметическое координат в проекции. + + Приоритет: + 1. Гаусс-Крюгер (Пулково 1942) для зон 4-32 + 2. UTM для координат вне зон ГК + 3. Геодезическое усреднение как последний fallback + + Args: + coords: Список координат в формате [(lon1, lat1), (lon2, lat2), ...] + zone: Номер зоны (если None, определяется по первой точке) + + Returns: + tuple: (координаты (lon, lat), тип_усреднения) + тип_усреднения: "ГК" | "UTM" | "Геод" + """ + if not coords: + return (0, 0), "ГК" + + if len(coords) == 1: + return coords[0], "ГК" + + first_lon, first_lat = coords[0] + + # Пытаемся использовать Гаусс-Крюгер + if zone is None: + gk_zone = get_gauss_kruger_zone(first_lon) + else: + gk_zone = zone if 4 <= zone <= 32 else None + + # Если координаты в зонах ГК (4-32), используем ГК + if gk_zone is not None: + try: + coords_projected = [transform_wgs84_to_gk(c, gk_zone) for c in coords] + avg_x = sum(c[0] for c in coords_projected) / len(coords_projected) + avg_y = sum(c[1] for c in coords_projected) / len(coords_projected) + return transform_gk_to_wgs84((avg_x, avg_y), gk_zone), "ГК" + except Exception: + pass # Fallback на UTM + + # Fallback на UTM для координат вне зон ГК + try: + utm_zone = get_utm_zone(first_lon) + is_northern = first_lat >= 0 + + coords_utm = [transform_wgs84_to_utm(c, utm_zone, is_northern) for c in coords] + avg_x = sum(c[0] for c in coords_utm) / len(coords_utm) + avg_y = sum(c[1] for c in coords_utm) / len(coords_utm) + return transform_utm_to_wgs84((avg_x, avg_y), utm_zone, is_northern), "UTM" + except Exception: + # Последний fallback - геодезическое усреднение + return _average_coords_geodesic(coords), "Геод" + + +def _average_coords_geodesic(coords: list[tuple]) -> tuple: + """ + Вычисляет среднее координат через последовательное геодезическое усреднение. + + Используется как fallback при ошибках проекции. Args: coords: Список координат в формате [(lon1, lat1), (lon2, lat2), ...] - zone: Номер зоны ГК (если None, определяется по первой точке) Returns: tuple: Средние координаты (longitude, latitude) в WGS84 @@ -1551,19 +1673,12 @@ def average_coords_in_gk(coords: list[tuple], zone: int = None) -> tuple: if len(coords) == 1: return coords[0] - # Определяем зону по первой точке - if zone is None: - zone = get_gauss_kruger_zone(coords[0][0]) + # Последовательно усредняем точки + result = coords[0] + for i in range(1, len(coords)): + result, _ = calculate_mean_coords(result, coords[i]) - # Преобразуем все координаты в ГК - coords_gk = [transform_wgs84_to_gk(c, zone) for c in coords] - - # Вычисляем среднее арифметическое - avg_x = sum(c[0] for c in coords_gk) / len(coords_gk) - avg_y = sum(c[1] for c in coords_gk) / len(coords_gk) - - # Преобразуем обратно в WGS84 - return transform_gk_to_wgs84((avg_x, avg_y), zone) + return result def calculate_mean_coords(coord1: tuple, coord2: tuple) -> tuple[tuple, float]: diff --git a/dbapp/mainapp/views/points_averaging.py b/dbapp/mainapp/views/points_averaging.py index 9363251..f0510bb 100644 --- a/dbapp/mainapp/views/points_averaging.py +++ b/dbapp/mainapp/views/points_averaging.py @@ -317,7 +317,7 @@ class PointsAveragingAPIView(LoginRequiredMixin, View): }) # Apply clustering algorithm - avg_coord, valid_indices = self._find_cluster_center(points_data) + avg_coord, valid_indices, avg_type = self._find_cluster_center(points_data) # Mark outliers and calculate distances outliers = [] @@ -391,6 +391,7 @@ class PointsAveragingAPIView(LoginRequiredMixin, View): 'has_outliers': len(outliers) > 0, 'avg_coordinates': avg_coords_str, 'avg_coord_tuple': avg_coord, + 'avg_type': avg_type, 'avg_time': median_time_str, 'frequency': first_point.get('frequency', '-'), 'freq_range': first_point.get('freq_range', '-'), @@ -414,13 +415,13 @@ class PointsAveragingAPIView(LoginRequiredMixin, View): If only 1 point, return it as center. Returns: - tuple: (avg_coord, set of valid point indices) + tuple: (avg_coord, set of valid point indices, avg_type) """ if len(points_data) == 0: - return (0, 0), set() + return (0, 0), set(), "ГК" if len(points_data) == 1: - return points_data[0]['coord_tuple'], {0} + return points_data[0]['coord_tuple'], {0}, "ГК" # Step 1: Take first point as reference first_coord = points_data[0]['coord_tuple'] @@ -435,35 +436,32 @@ class PointsAveragingAPIView(LoginRequiredMixin, View): valid_indices.add(i) # Step 3: Calculate average of all valid points using Gauss-Kruger projection - avg_coord = self._calculate_average_from_indices(points_data, valid_indices) + avg_coord, avg_type = self._calculate_average_from_indices(points_data, valid_indices) - return avg_coord, valid_indices + return avg_coord, valid_indices, avg_type def _calculate_average_from_indices(self, points_data, indices): """ Calculate average coordinate from points at given indices. - Uses arithmetic averaging in Gauss-Kruger projection. + Uses arithmetic averaging in Gauss-Kruger or UTM projection. - Algorithm: - 1. Determine GK zone from the first point - 2. Transform all coordinates to GK projection - 3. Calculate arithmetic mean of X and Y - 4. Transform result back to WGS84 + Returns: + tuple: (avg_coord, avg_type) where avg_type is "ГК", "UTM" or "Геод" """ indices_list = sorted(indices) if not indices_list: - return (0, 0) + return (0, 0), "ГК" if len(indices_list) == 1: - return points_data[indices_list[0]]['coord_tuple'] + return points_data[indices_list[0]]['coord_tuple'], "ГК" # Collect coordinates for averaging coords = [points_data[idx]['coord_tuple'] for idx in indices_list] - # Use Gauss-Kruger projection for averaging - avg_coord = average_coords_in_gk(coords) + # Use Gauss-Kruger/UTM projection for averaging + avg_coord, avg_type = average_coords_in_gk(coords) - return avg_coord + return avg_coord, avg_type class RecalculateGroupAPIView(LoginRequiredMixin, View): @@ -489,7 +487,7 @@ class RecalculateGroupAPIView(LoginRequiredMixin, View): # If include_all is False, use only non-outlier points and apply clustering if include_all: # Average all points - no outliers, all points are valid - avg_coord = self._calculate_average_from_indices(points, set(range(len(points)))) + avg_coord, avg_type = self._calculate_average_from_indices(points, set(range(len(points)))) valid_indices = set(range(len(points))) else: # Filter out outliers first @@ -499,7 +497,7 @@ class RecalculateGroupAPIView(LoginRequiredMixin, View): return JsonResponse({'error': 'No valid points after filtering'}, status=400) # Apply clustering algorithm - avg_coord, valid_indices = self._find_cluster_center(points) + avg_coord, valid_indices, avg_type = self._find_cluster_center(points) # Mark outliers and calculate distances for i, point in enumerate(points): @@ -560,6 +558,7 @@ class RecalculateGroupAPIView(LoginRequiredMixin, View): 'success': True, 'avg_coordinates': avg_coords_str, 'avg_coord_tuple': avg_coord, + 'avg_type': avg_type, 'total_points': len(points), 'valid_points_count': len(valid_points), 'outliers_count': len(outliers), @@ -575,13 +574,13 @@ class RecalculateGroupAPIView(LoginRequiredMixin, View): 1. Take the first point as reference 2. Find all points within 56 km of the first point 3. Calculate average of all found points using Gauss-Kruger projection - 4. Return final average and indices of valid points + 4. Return final average, indices of valid points, and averaging type """ if len(points) == 0: - return (0, 0), set() + return (0, 0), set(), "ГК" if len(points) == 1: - return tuple(points[0]['coord_tuple']), {0} + return tuple(points[0]['coord_tuple']), {0}, "ГК" # Step 1: Take first point as reference first_coord = tuple(points[0]['coord_tuple']) @@ -595,27 +594,30 @@ class RecalculateGroupAPIView(LoginRequiredMixin, View): if distance <= RANGE_DISTANCE: valid_indices.add(i) - # Step 3: Calculate average of all valid points using Gauss-Kruger projection - avg_coord = self._calculate_average_from_indices(points, valid_indices) + # Step 3: Calculate average of all valid points + avg_coord, avg_type = self._calculate_average_from_indices(points, valid_indices) - return avg_coord, valid_indices + return avg_coord, valid_indices, avg_type def _calculate_average_from_indices(self, points, indices): """ Calculate average coordinate from points at given indices. - Uses arithmetic averaging in Gauss-Kruger projection. + Uses arithmetic averaging in Gauss-Kruger or UTM projection. + + Returns: + tuple: (avg_coord, avg_type) """ indices_list = sorted(indices) if not indices_list: - return (0, 0) + return (0, 0), "ГК" if len(indices_list) == 1: - return tuple(points[indices_list[0]]['coord_tuple']) + return tuple(points[indices_list[0]]['coord_tuple']), "ГК" # Collect coordinates for averaging coords = [tuple(points[idx]['coord_tuple']) for idx in indices_list] - # Use Gauss-Kruger projection for averaging - avg_coord = average_coords_in_gk(coords) + # Use Gauss-Kruger/UTM projection for averaging + avg_coord, avg_type = average_coords_in_gk(coords) - return avg_coord + return avg_coord, avg_type