Compare commits

...

3 Commits

3 changed files with 812 additions and 965 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -203,10 +203,17 @@ def find_mirror_satellites(mirror_names: list) -> list:
Алгоритм: Алгоритм:
1. Для каждого имени зеркала: 1. Для каждого имени зеркала:
- Обрезать пробелы и привести к нижнему регистру - Обрезать пробелы
- Извлечь первую часть имени (до скобки), если есть двойное имя
- Привести к нижнему регистру
- Найти все спутники, в имени или альтернативном имени которых содержится это имя - Найти все спутники, в имени или альтернативном имени которых содержится это имя
2. Вернуть список найденных спутников 2. Вернуть список найденных спутников
Примеры обработки:
- "DSN-3 (SUPERBIRD-C2)" -> "dsn-3"
- "Turksat 3A" -> "turksat 3a"
- " Amos 4 " -> "amos 4"
Args: Args:
mirror_names: список имен зеркал mirror_names: список имен зеркал
@@ -221,15 +228,26 @@ def find_mirror_satellites(mirror_names: list) -> list:
if not mirror_name or mirror_name == "-": if not mirror_name or mirror_name == "-":
continue continue
# Обрезаем пробелы и приводим к нижнему регистру # Обрезаем пробелы
mirror_name_clean = mirror_name.strip().lower() mirror_name_clean = mirror_name.strip()
if not mirror_name_clean: if not mirror_name_clean or mirror_name_clean == "-":
continue
# Извлекаем первую часть имени (до скобки), если есть двойное имя
# Например: "DSN-3 (SUPERBIRD-C2)" -> "DSN-3"
if "(" in mirror_name_clean:
mirror_name_clean = mirror_name_clean.split("(")[0].strip()
# Приводим к нижнему регистру для поиска
mirror_name_lower = mirror_name_clean.lower()
if not mirror_name_lower:
continue continue
# Ищем спутники, в имени или альтернативном имени которых содержится имя зеркала # Ищем спутники, в имени или альтернативном имени которых содержится имя зеркала
satellites = Satellite.objects.filter( satellites = Satellite.objects.filter(
Q(name__icontains=mirror_name_clean) | Q(alternative_name__icontains=mirror_name_clean) Q(name__icontains=mirror_name_lower) | Q(alternative_name__icontains=mirror_name_lower)
) )
found_satellites.extend(satellites) found_satellites.extend(satellites)
@@ -1395,27 +1413,28 @@ def kub_report(data_in: io.StringIO) -> pd.DataFrame:
from pyproj import CRS, Transformer 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° долготы. Зоны ГК (Пулково 1942) имеют EPSG коды 28404-28432 (зоны 4-32).
Центральный меридиан зоны N: (6*N - 3)° Каждая зона охватывает 6° долготы.
Args: Args:
longitude: Долгота в градусах (от -180 до 180) longitude: Долгота в градусах (от -180 до 180)
Returns: Returns:
int: Номер зоны ГК (1-60) int | None: Номер зоны ГК (4-32) или None если координаты вне зон ГК
""" """
# Нормализуем долготу к диапазону 0-360 # Нормализуем долготу к диапазону 0-360
lon_normalized = longitude if longitude >= 0 else longitude + 360 lon_normalized = longitude if longitude >= 0 else longitude + 360
# Вычисляем номер зоны (1-60) # Вычисляем номер зоны (1-60)
zone = int((lon_normalized + 6) / 6) zone = int((lon_normalized + 6) / 6)
if zone > 60:
zone = 60 # EPSG коды Пулково 1942 существуют только для зон 4-32
if zone < 1: if zone < 4 or zone > 32:
zone = 1 return None
return zone return zone
@@ -1423,14 +1442,8 @@ def get_gauss_kruger_epsg(zone: int) -> int:
""" """
Возвращает EPSG код для зоны Гаусса-Крюгера (Pulkovo 1942 / Gauss-Kruger). Возвращает EPSG код для зоны Гаусса-Крюгера (Pulkovo 1942 / Gauss-Kruger).
EPSG коды для Pulkovo 1942 GK зон:
- Зона 4: EPSG:28404
- Зона 5: EPSG:28405
- ...
- Зона N: EPSG:28400 + N
Args: Args:
zone: Номер зоны ГК (1-60) zone: Номер зоны ГК (4-32)
Returns: Returns:
int: EPSG код проекции int: EPSG код проекции
@@ -1438,13 +1451,50 @@ def get_gauss_kruger_epsg(zone: int) -> int:
return 28400 + zone 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: def transform_wgs84_to_gk(coord: tuple, zone: int = None) -> tuple:
""" """
Преобразует координаты из WGS84 (EPSG:4326) в проекцию Гаусса-Крюгера. Преобразует координаты из WGS84 в проекцию Гаусса-Крюгера.
Args: Args:
coord: Координаты в формате (longitude, latitude) в WGS84 coord: Координаты в формате (longitude, latitude) в WGS84
zone: Номер зоны ГК (если None, определяется автоматически по долготе) zone: Номер зоны ГК (если None, определяется автоматически)
Returns: Returns:
tuple: Координаты (x, y) в метрах в проекции ГК tuple: Координаты (x, y) в метрах в проекции ГК
@@ -1454,9 +1504,11 @@ def transform_wgs84_to_gk(coord: tuple, zone: int = None) -> tuple:
if zone is None: if zone is None:
zone = get_gauss_kruger_zone(lon) zone = get_gauss_kruger_zone(lon)
if zone is None:
raise ValueError(f"Координаты ({lon}, {lat}) вне зон Гаусса-Крюгера (4-32)")
epsg_gk = get_gauss_kruger_epsg(zone) epsg_gk = get_gauss_kruger_epsg(zone)
# Создаём трансформер WGS84 -> GK
transformer = Transformer.from_crs( transformer = Transformer.from_crs(
CRS.from_epsg(4326), CRS.from_epsg(4326),
CRS.from_epsg(epsg_gk), CRS.from_epsg(epsg_gk),
@@ -1469,7 +1521,7 @@ def transform_wgs84_to_gk(coord: tuple, zone: int = None) -> tuple:
def transform_gk_to_wgs84(coord: tuple, zone: int) -> tuple: def transform_gk_to_wgs84(coord: tuple, zone: int) -> tuple:
""" """
Преобразует координаты из проекции Гаусса-Крюгера в WGS84 (EPSG:4326). Преобразует координаты из проекции Гаусса-Крюгера в WGS84.
Args: Args:
coord: Координаты (x, y) в метрах в проекции ГК coord: Координаты (x, y) в метрах в проекции ГК
@@ -1481,7 +1533,6 @@ def transform_gk_to_wgs84(coord: tuple, zone: int) -> tuple:
x, y = coord x, y = coord
epsg_gk = get_gauss_kruger_epsg(zone) epsg_gk = get_gauss_kruger_epsg(zone)
# Создаём трансформер GK -> WGS84
transformer = Transformer.from_crs( transformer = Transformer.from_crs(
CRS.from_epsg(epsg_gk), CRS.from_epsg(epsg_gk),
CRS.from_epsg(4326), CRS.from_epsg(4326),
@@ -1492,37 +1543,126 @@ def transform_gk_to_wgs84(coord: tuple, zone: int) -> tuple:
return (lon, lat) 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: Args:
coord1_gk: Первая точка (x, y) в метрах coord: Координаты в формате (longitude, latitude) в WGS84
coord2_gk: Вторая точка (x, y) в метрах zone: Номер зоны UTM (если None, определяется автоматически)
is_northern: Северное полушарие (если None, определяется по широте)
Returns: Returns:
float: Расстояние в километрах tuple: Координаты (x, y) в метрах в проекции UTM
""" """
import math lon, lat = coord
x1, y1 = coord1_gk
x2, y2 = coord2_gk if zone is None:
distance_m = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) zone = get_utm_zone(lon)
return distance_m / 1000
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 average_coords_in_gk(coords: list[tuple], zone: int = None) -> tuple: def transform_utm_to_wgs84(coord: tuple, zone: int, is_northern: bool = True) -> tuple:
""" """
Вычисляет среднее арифметическое координат в проекции Гаусса-Крюгера. Преобразует координаты из проекции UTM в WGS84.
Алгоритм: Args:
1. Определяет зону ГК по первой точке (если не указана) coord: Координаты (x, y) в метрах в проекции UTM
2. Преобразует все координаты в проекцию ГК zone: Номер зоны UTM
3. Вычисляет среднее арифметическое X и Y is_northern: Северное полушарие
4. Преобразует результат обратно в WGS84
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: Args:
coords: Список координат в формате [(lon1, lat1), (lon2, lat2), ...] coords: Список координат в формате [(lon1, lat1), (lon2, lat2), ...]
zone: Номер зоны ГК (если None, определяется по первой точке)
Returns: Returns:
tuple: Средние координаты (longitude, latitude) в WGS84 tuple: Средние координаты (longitude, latitude) в WGS84
@@ -1533,19 +1673,12 @@ def average_coords_in_gk(coords: list[tuple], zone: int = None) -> tuple:
if len(coords) == 1: if len(coords) == 1:
return coords[0] return coords[0]
# Определяем зону по первой точке # Последовательно усредняем точки
if zone is None: result = coords[0]
zone = get_gauss_kruger_zone(coords[0][0]) for i in range(1, len(coords)):
result, _ = calculate_mean_coords(result, coords[i])
# Преобразуем все координаты в ГК return result
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)
def calculate_mean_coords(coord1: tuple, coord2: tuple) -> tuple[tuple, float]: def calculate_mean_coords(coord1: tuple, coord2: tuple) -> tuple[tuple, float]:

View File

@@ -1,5 +1,6 @@
""" """
Points averaging view for satellite data grouping by day/night intervals. 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 datetime import datetime, timedelta
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
@@ -8,7 +9,7 @@ from django.shortcuts import render
from django.views import View from django.views import View
from django.utils import timezone from django.utils import timezone
from ..models import ObjItem, Satellite from ..models import ObjItem, Satellite, Source
from ..utils import ( from ..utils import (
calculate_mean_coords, calculate_mean_coords,
calculate_distance_wgs84, calculate_distance_wgs84,
@@ -29,8 +30,9 @@ class PointsAveragingView(LoginRequiredMixin, View):
""" """
def get(self, request): 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( satellites = Satellite.objects.filter(
parameters__objitem__source__isnull=False,
parameters__objitem__geo_obj__coords__isnull=False parameters__objitem__geo_obj__coords__isnull=False
).distinct().order_by('name') ).distinct().order_by('name')
@@ -44,13 +46,14 @@ class PointsAveragingView(LoginRequiredMixin, View):
class PointsAveragingAPIView(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: Groups points into:
- Day: 08:00 - 19:00 - Day: 08:00 - 19:00
- Night: 19:00 - 08:00 (next day) - 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): def get(self, request):
@@ -76,9 +79,50 @@ class PointsAveragingAPIView(LoginRequiredMixin, View):
except ValueError: except ValueError:
return JsonResponse({'error': 'Неверный формат даты'}, status=400) return JsonResponse({'error': 'Неверный формат даты'}, status=400)
# Get all points for the satellite in the date range # Get all Sources for the satellite that have points in the date range
objitems = ObjItem.objects.filter( sources = Source.objects.filter(
parameter_obj__id_satellite=satellite, 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__coords__isnull=False,
geo_obj__timestamp__gte=date_from_obj, geo_obj__timestamp__gte=date_from_obj,
geo_obj__timestamp__lt=date_to_obj, geo_obj__timestamp__lt=date_to_obj,
@@ -89,16 +133,12 @@ class PointsAveragingAPIView(LoginRequiredMixin, View):
'parameter_obj__modulation', 'parameter_obj__modulation',
'parameter_obj__standard', 'parameter_obj__standard',
'geo_obj', 'geo_obj',
'source',
).prefetch_related( ).prefetch_related(
'geo_obj__mirrors' 'geo_obj__mirrors'
).order_by('geo_obj__timestamp') ).order_by('geo_obj__timestamp')
if not objitems.exists(): # Group points by day/night intervals
return JsonResponse({'error': 'Точки не найдены в указанном диапазоне'}, status=404) groups = self._group_points_by_intervals(list(objitems))
# Group points by source name and day/night intervals
groups = self._group_points_by_intervals(objitems)
# Process each group: calculate average and check for outliers # Process each group: calculate average and check for outliers
result_groups = [] result_groups = []
@@ -106,21 +146,27 @@ class PointsAveragingAPIView(LoginRequiredMixin, View):
group_result = self._process_group(group_key, points) group_result = self._process_group(group_key, points)
result_groups.append(group_result) result_groups.append(group_result)
return JsonResponse({ # Get source name from first point or use ID
'success': True, source_name = f"Источник #{source.id}"
'satellite': satellite.name, if objitems.exists():
'date_from': date_from, first_point = objitems.first()
'date_to': date_to, 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, 'groups': result_groups,
'total_groups': len(result_groups), }
})
def _group_points_by_intervals(self, objitems): 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 Day: 08:00 - 19:00
Night: 19:00 - 08:00 (next day) Night: 19:00 - 08:00 (next day)
Weekend: Friday 19:00 - Monday 08:00
""" """
groups = {} groups = {}
@@ -129,19 +175,14 @@ class PointsAveragingAPIView(LoginRequiredMixin, View):
continue continue
timestamp = timezone.localtime(objitem.geo_obj.timestamp) timestamp = timezone.localtime(objitem.geo_obj.timestamp)
# timestamp = objitem.geo_obj.timestamp
source_name = objitem.name or f"Объект #{objitem.id}"
# Determine interval # Determine interval
interval_key = self._get_interval_key(timestamp) interval_key = self._get_interval_key(timestamp)
# Create group key: (source_name, interval_key) if interval_key not in groups:
group_key = (source_name, interval_key) groups[interval_key] = []
if group_key not in groups: groups[interval_key].append(objitem)
groups[group_key] = []
groups[group_key].append(objitem)
return groups return groups
@@ -208,7 +249,7 @@ class PointsAveragingAPIView(LoginRequiredMixin, View):
return date - timedelta(days=3) return date - timedelta(days=3)
return date 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. 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 3. Iteratively add points within 56 km of current average
4. Points not within 56 km of final average are outliers 4. Points not within 56 km of final average are outliers
""" """
source_name, interval_key = group_key
# Parse interval info # Parse interval info
date_str, interval_type = interval_key.rsplit('_', 1) date_str, interval_type = interval_key.rsplit('_', 1)
interval_date = datetime.strptime(date_str, '%Y-%m-%d').date() interval_date = datetime.strptime(date_str, '%Y-%m-%d').date()
@@ -278,7 +317,7 @@ class PointsAveragingAPIView(LoginRequiredMixin, View):
}) })
# Apply clustering algorithm # 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 # Mark outliers and calculate distances
outliers = [] outliers = []
@@ -322,7 +361,7 @@ class PointsAveragingAPIView(LoginRequiredMixin, View):
# Calculate median time from valid points using timestamp_objects array # Calculate median time from valid points using timestamp_objects array
valid_timestamps = [] valid_timestamps = []
for i in valid_indices: for i in valid_indices:
if timestamp_objects[i]: if i < len(timestamp_objects) and timestamp_objects[i]:
valid_timestamps.append(timestamp_objects[i]) valid_timestamps.append(timestamp_objects[i])
median_time_str = '-' median_time_str = '-'
@@ -344,7 +383,6 @@ class PointsAveragingAPIView(LoginRequiredMixin, View):
median_time_str = timezone.localtime(median_datetime).strftime("%d.%m.%Y %H:%M") median_time_str = timezone.localtime(median_datetime).strftime("%d.%m.%Y %H:%M")
return { return {
'source_name': source_name,
'interval_key': interval_key, 'interval_key': interval_key,
'interval_label': interval_label, 'interval_label': interval_label,
'total_points': len(points_data), 'total_points': len(points_data),
@@ -353,6 +391,7 @@ class PointsAveragingAPIView(LoginRequiredMixin, View):
'has_outliers': len(outliers) > 0, 'has_outliers': len(outliers) > 0,
'avg_coordinates': avg_coords_str, 'avg_coordinates': avg_coords_str,
'avg_coord_tuple': avg_coord, 'avg_coord_tuple': avg_coord,
'avg_type': avg_type,
'avg_time': median_time_str, 'avg_time': median_time_str,
'frequency': first_point.get('frequency', '-'), 'frequency': first_point.get('frequency', '-'),
'freq_range': first_point.get('freq_range', '-'), 'freq_range': first_point.get('freq_range', '-'),
@@ -376,13 +415,13 @@ class PointsAveragingAPIView(LoginRequiredMixin, View):
If only 1 point, return it as center. If only 1 point, return it as center.
Returns: Returns:
tuple: (avg_coord, set of valid point indices) tuple: (avg_coord, set of valid point indices, avg_type)
""" """
if len(points_data) == 0: if len(points_data) == 0:
return (0, 0), set() return (0, 0), set(), "ГК"
if len(points_data) == 1: 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 # Step 1: Take first point as reference
first_coord = points_data[0]['coord_tuple'] first_coord = points_data[0]['coord_tuple']
@@ -397,35 +436,32 @@ class PointsAveragingAPIView(LoginRequiredMixin, View):
valid_indices.add(i) valid_indices.add(i)
# Step 3: Calculate average of all valid points using Gauss-Kruger projection # 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): def _calculate_average_from_indices(self, points_data, indices):
""" """
Calculate average coordinate from points at given 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: Returns:
1. Determine GK zone from the first point tuple: (avg_coord, avg_type) where avg_type is "ГК", "UTM" or "Геод"
2. Transform all coordinates to GK projection
3. Calculate arithmetic mean of X and Y
4. Transform result back to WGS84
""" """
indices_list = sorted(indices) indices_list = sorted(indices)
if not indices_list: if not indices_list:
return (0, 0) return (0, 0), "ГК"
if len(indices_list) == 1: 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 # Collect coordinates for averaging
coords = [points_data[idx]['coord_tuple'] for idx in indices_list] coords = [points_data[idx]['coord_tuple'] for idx in indices_list]
# Use Gauss-Kruger projection for averaging # Use Gauss-Kruger/UTM projection for averaging
avg_coord = average_coords_in_gk(coords) avg_coord, avg_type = average_coords_in_gk(coords)
return avg_coord return avg_coord, avg_type
class RecalculateGroupAPIView(LoginRequiredMixin, View): class RecalculateGroupAPIView(LoginRequiredMixin, View):
@@ -451,7 +487,7 @@ class RecalculateGroupAPIView(LoginRequiredMixin, View):
# If include_all is False, use only non-outlier points and apply clustering # If include_all is False, use only non-outlier points and apply clustering
if include_all: if include_all:
# Average all points - no outliers, all points are valid # 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))) valid_indices = set(range(len(points)))
else: else:
# Filter out outliers first # Filter out outliers first
@@ -461,7 +497,7 @@ class RecalculateGroupAPIView(LoginRequiredMixin, View):
return JsonResponse({'error': 'No valid points after filtering'}, status=400) return JsonResponse({'error': 'No valid points after filtering'}, status=400)
# Apply clustering algorithm # 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 # Mark outliers and calculate distances
for i, point in enumerate(points): for i, point in enumerate(points):
@@ -522,6 +558,7 @@ class RecalculateGroupAPIView(LoginRequiredMixin, View):
'success': True, 'success': True,
'avg_coordinates': avg_coords_str, 'avg_coordinates': avg_coords_str,
'avg_coord_tuple': avg_coord, 'avg_coord_tuple': avg_coord,
'avg_type': avg_type,
'total_points': len(points), 'total_points': len(points),
'valid_points_count': len(valid_points), 'valid_points_count': len(valid_points),
'outliers_count': len(outliers), 'outliers_count': len(outliers),
@@ -537,13 +574,13 @@ class RecalculateGroupAPIView(LoginRequiredMixin, View):
1. Take the first point as reference 1. Take the first point as reference
2. Find all points within 56 km of the first point 2. Find all points within 56 km of the first point
3. Calculate average of all found points using Gauss-Kruger projection 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: if len(points) == 0:
return (0, 0), set() return (0, 0), set(), "ГК"
if len(points) == 1: 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 # Step 1: Take first point as reference
first_coord = tuple(points[0]['coord_tuple']) first_coord = tuple(points[0]['coord_tuple'])
@@ -557,27 +594,30 @@ class RecalculateGroupAPIView(LoginRequiredMixin, View):
if distance <= RANGE_DISTANCE: if distance <= RANGE_DISTANCE:
valid_indices.add(i) valid_indices.add(i)
# Step 3: Calculate average of all valid points using Gauss-Kruger projection # Step 3: Calculate average of all valid points
avg_coord = self._calculate_average_from_indices(points, valid_indices) 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): def _calculate_average_from_indices(self, points, indices):
""" """
Calculate average coordinate from points at given 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) indices_list = sorted(indices)
if not indices_list: if not indices_list:
return (0, 0) return (0, 0), "ГК"
if len(indices_list) == 1: 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 # Collect coordinates for averaging
coords = [tuple(points[idx]['coord_tuple']) for idx in indices_list] coords = [tuple(points[idx]['coord_tuple']) for idx in indices_list]
# Use Gauss-Kruger projection for averaging # Use Gauss-Kruger/UTM projection for averaging
avg_coord = average_coords_in_gk(coords) avg_coord, avg_type = average_coords_in_gk(coords)
return avg_coord return avg_coord, avg_type