Усредение точек в проекции ГК
This commit is contained in:
@@ -40,9 +40,6 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'mainapp:kubsat' %}">Кубсат</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'mainapp:points_averaging' %}">Усреднение</a>
|
||||
</li>
|
||||
<!-- <li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'mapsapp:3dmap' %}">3D карта</a>
|
||||
</li> -->
|
||||
|
||||
@@ -280,7 +280,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
headerWordWrap: true,
|
||||
columns: [
|
||||
{title: "Объект наблюдения", field: "source_name", minWidth: 180, widthGrow: 2},
|
||||
{title: "Интервал", field: "interval_label", minWidth: 150, widthGrow: 1.5},
|
||||
{title: "Частота, МГц", field: "frequency", minWidth: 100, widthGrow: 1},
|
||||
{title: "Полоса, МГц", field: "freq_range", minWidth: 100, widthGrow: 1},
|
||||
{title: "Символьная скорость, БОД", field: "bod_velocity", minWidth: 120, widthGrow: 1.5},
|
||||
@@ -288,20 +287,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
{title: "ОСШ", field: "snr", minWidth: 70, widthGrow: 0.8},
|
||||
{title: "Зеркала", field: "mirrors", minWidth: 130, widthGrow: 1.5},
|
||||
{title: "Усреднённые координаты", field: "avg_coordinates", minWidth: 150, widthGrow: 2},
|
||||
{title: "Медианное время", field: "avg_time", minWidth: 120, widthGrow: 1},
|
||||
{title: "Кол-во точек", field: "total_points", minWidth: 80, widthGrow: 0.8, hozAlign: "center"},
|
||||
{
|
||||
title: "Статус",
|
||||
field: "status",
|
||||
minWidth: 120,
|
||||
widthGrow: 1,
|
||||
formatter: function(cell, formatterParams, onRendered) {
|
||||
const data = cell.getRow().getData();
|
||||
if (data.has_outliers) {
|
||||
return `<span class="outlier-warning"><i class="bi bi-exclamation-triangle"></i> Выбросы (${data.outliers_count})</span>`;
|
||||
}
|
||||
return '<span class="text-success"><i class="bi bi-check-circle"></i> OK</span>';
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Действия",
|
||||
field: "actions",
|
||||
@@ -504,7 +491,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
|
||||
// Show group details modal
|
||||
function showGroupDetails(groupIndex) {
|
||||
// skipShow=true means just update content without calling modal.show()
|
||||
function showGroupDetails(groupIndex, skipShow = false) {
|
||||
currentGroupIndex = groupIndex;
|
||||
const group = allGroupsData[groupIndex];
|
||||
|
||||
@@ -637,9 +625,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
});
|
||||
|
||||
// Show modal
|
||||
const modal = new bootstrap.Modal(document.getElementById('groupDetailsModal'));
|
||||
modal.show();
|
||||
// Show modal - use getOrCreateInstance to avoid creating multiple instances
|
||||
if (!skipShow) {
|
||||
const modalElement = document.getElementById('groupDetailsModal');
|
||||
let modal = bootstrap.Modal.getInstance(modalElement);
|
||||
if (!modal) {
|
||||
modal = new bootstrap.Modal(modalElement);
|
||||
}
|
||||
modal.show();
|
||||
}
|
||||
}
|
||||
|
||||
// Remove point from group and recalculate
|
||||
@@ -653,6 +647,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm('Удалить эту точку из выборки и пересчитать усреднение?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove point
|
||||
group.points.splice(pointIndex, 1);
|
||||
|
||||
@@ -664,14 +662,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
async function recalculateGroup(groupIndex, includeAll) {
|
||||
const group = allGroupsData[groupIndex];
|
||||
if (!group) {
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if there are points to process
|
||||
if (!group.points || group.points.length === 0) {
|
||||
alert('Нет точек для пересчёта');
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -694,7 +690,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
if (!response.ok) {
|
||||
alert(data.error || 'Ошибка при пересчёте');
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -705,6 +700,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
group.valid_points_count = data.valid_points_count;
|
||||
group.outliers_count = data.outliers_count;
|
||||
group.has_outliers = data.has_outliers;
|
||||
group.mirrors = data.mirrors || group.mirrors;
|
||||
group.avg_time = data.avg_time || group.avg_time;
|
||||
group.points = data.points;
|
||||
|
||||
// Update table
|
||||
@@ -716,9 +713,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
// Update all points table
|
||||
updateAllPointsTable();
|
||||
|
||||
// Update modal if open
|
||||
// Update modal content without calling show() again
|
||||
if (currentGroupIndex === groupIndex) {
|
||||
showGroupDetails(groupIndex);
|
||||
showGroupDetails(groupIndex, true);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
@@ -730,16 +727,16 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
|
||||
// Average all points button
|
||||
document.getElementById('btn-average-all').addEventListener('click', function() {
|
||||
document.getElementById('btn-average-all').addEventListener('click', async function() {
|
||||
if (currentGroupIndex !== null) {
|
||||
recalculateGroup(currentGroupIndex, true);
|
||||
await recalculateGroup(currentGroupIndex, true);
|
||||
}
|
||||
});
|
||||
|
||||
// Average valid points button
|
||||
document.getElementById('btn-average-valid').addEventListener('click', function() {
|
||||
document.getElementById('btn-average-valid').addEventListener('click', async function() {
|
||||
if (currentGroupIndex !== null) {
|
||||
recalculateGroup(currentGroupIndex, false);
|
||||
await recalculateGroup(currentGroupIndex, false);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -753,7 +750,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
// Prepare summary data for export
|
||||
const summaryData = allGroupsData.map(group => ({
|
||||
'Объект наблюдения': group.source_name,
|
||||
'Интервал': group.interval_label,
|
||||
'Частота, МГц': group.frequency,
|
||||
'Полоса, МГц': group.freq_range,
|
||||
'Символьная скорость, БОД': group.bod_velocity,
|
||||
@@ -761,9 +757,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
'ОСШ': group.snr,
|
||||
'Зеркала': group.mirrors,
|
||||
'Усреднённые координаты': group.avg_coordinates,
|
||||
'Кол-во точек': group.total_points,
|
||||
'Выбросов': group.outliers_count,
|
||||
'Статус': group.has_outliers ? 'Есть выбросы' : 'OK'
|
||||
'Медианное время': group.avg_time || '-',
|
||||
'Кол-во точек': group.total_points
|
||||
}));
|
||||
|
||||
// Prepare all points data for export
|
||||
|
||||
@@ -98,6 +98,9 @@
|
||||
onclick="showSelectedOnMap()">
|
||||
<i class="bi bi-map"></i> Карта
|
||||
</button>
|
||||
<a href="{% url 'mainapp:points_averaging' %}" class="btn btn-warning btn-sm" title="Усреднение точек">
|
||||
<i class="bi bi-calculator"></i> Усреднение
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Add to List Button -->
|
||||
|
||||
@@ -7,6 +7,7 @@ from datetime import datetime, time
|
||||
# Django imports
|
||||
from django.contrib.gis.geos import Point
|
||||
from django.db.models import F
|
||||
from django.utils import timezone
|
||||
|
||||
# Third-party imports
|
||||
import pandas as pd
|
||||
@@ -691,6 +692,10 @@ def get_points_from_csv(file_content, current_user=None, is_automatic=False):
|
||||
"mir_1",
|
||||
"mir_2",
|
||||
"mir_3",
|
||||
"mir_4",
|
||||
"mir_5",
|
||||
"mir_6",
|
||||
"mir_7",
|
||||
],
|
||||
)
|
||||
df[["lat", "lon", "freq", "f_range"]] = (
|
||||
@@ -719,7 +724,7 @@ def get_points_from_csv(file_content, current_user=None, is_automatic=False):
|
||||
sat_name = row["sat"]
|
||||
|
||||
# Извлекаем время для проверки дубликатов
|
||||
timestamp = row["time"]
|
||||
timestamp = timezone.make_aware(row["time"])
|
||||
|
||||
# Проверяем дубликаты по координатам и времени
|
||||
if _is_duplicate_by_coords_and_time(coord_tuple, timestamp):
|
||||
@@ -727,10 +732,13 @@ def get_points_from_csv(file_content, current_user=None, is_automatic=False):
|
||||
continue
|
||||
|
||||
# Получаем или создаем объект спутника
|
||||
sat_obj, _ = Satellite.objects.get_or_create(
|
||||
name=sat_name, defaults={"norad": row["norad_id"]}
|
||||
)
|
||||
# sat_obj, _ = Satellite.objects.get_or_create(
|
||||
# name=sat_name, defaults={"norad": row["norad_id"]}
|
||||
# )
|
||||
|
||||
sat_obj, _ = Satellite.objects.get_or_create(
|
||||
norad=row["norad_id"], defaults={"name": sat_name}
|
||||
)
|
||||
source = None
|
||||
|
||||
# Если is_automatic=False, работаем с Source
|
||||
@@ -784,7 +792,7 @@ def get_points_from_csv(file_content, current_user=None, is_automatic=False):
|
||||
return new_sources_count
|
||||
|
||||
|
||||
def _is_duplicate_by_coords_and_time(coord_tuple, timestamp, tolerance_km=0.1):
|
||||
def _is_duplicate_by_coords_and_time(coord_tuple, timestamp, tolerance_km=0.001):
|
||||
"""
|
||||
Проверяет, существует ли уже ObjItem с такими же координатами и временем ГЛ.
|
||||
|
||||
@@ -934,7 +942,7 @@ def _create_objitem_from_csv_row(row, source, user_to_use, is_automatic=False):
|
||||
|
||||
# Создаем Geo объект
|
||||
geo_obj, _ = Geo.objects.get_or_create(
|
||||
timestamp=row["time"],
|
||||
timestamp=timezone.make_aware(row["time"]),
|
||||
coords=Point(row["lon"], row["lat"], srid=4326),
|
||||
defaults={
|
||||
"is_average": False,
|
||||
@@ -1259,6 +1267,162 @@ def kub_report(data_in: io.StringIO) -> pd.DataFrame:
|
||||
# Утилиты для работы с координатами
|
||||
# ============================================================================
|
||||
|
||||
# Импорт pyproj для работы с проекциями
|
||||
from pyproj import CRS, Transformer
|
||||
|
||||
|
||||
def get_gauss_kruger_zone(longitude: float) -> int:
|
||||
"""
|
||||
Определяет номер зоны Гаусса-Крюгера по долготе.
|
||||
|
||||
Зоны ГК нумеруются от 1 до 60, каждая зона охватывает 6° долготы.
|
||||
Центральный меридиан зоны N: (6*N - 3)°
|
||||
|
||||
Args:
|
||||
longitude: Долгота в градусах (от -180 до 180)
|
||||
|
||||
Returns:
|
||||
int: Номер зоны ГК (1-60)
|
||||
"""
|
||||
# Нормализуем долготу к диапазону 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
|
||||
return zone
|
||||
|
||||
|
||||
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)
|
||||
|
||||
Returns:
|
||||
int: EPSG код проекции
|
||||
"""
|
||||
return 28400 + zone
|
||||
|
||||
|
||||
def transform_wgs84_to_gk(coord: tuple, zone: int = None) -> tuple:
|
||||
"""
|
||||
Преобразует координаты из WGS84 (EPSG:4326) в проекцию Гаусса-Крюгера.
|
||||
|
||||
Args:
|
||||
coord: Координаты в формате (longitude, latitude) в WGS84
|
||||
zone: Номер зоны ГК (если None, определяется автоматически по долготе)
|
||||
|
||||
Returns:
|
||||
tuple: Координаты (x, y) в метрах в проекции ГК
|
||||
"""
|
||||
lon, lat = coord
|
||||
|
||||
if zone is None:
|
||||
zone = get_gauss_kruger_zone(lon)
|
||||
|
||||
epsg_gk = get_gauss_kruger_epsg(zone)
|
||||
|
||||
# Создаём трансформер WGS84 -> GK
|
||||
transformer = Transformer.from_crs(
|
||||
CRS.from_epsg(4326),
|
||||
CRS.from_epsg(epsg_gk),
|
||||
always_xy=True
|
||||
)
|
||||
|
||||
x, y = transformer.transform(lon, lat)
|
||||
return (x, y)
|
||||
|
||||
|
||||
def transform_gk_to_wgs84(coord: tuple, zone: int) -> tuple:
|
||||
"""
|
||||
Преобразует координаты из проекции Гаусса-Крюгера в WGS84 (EPSG:4326).
|
||||
|
||||
Args:
|
||||
coord: Координаты (x, y) в метрах в проекции ГК
|
||||
zone: Номер зоны ГК
|
||||
|
||||
Returns:
|
||||
tuple: Координаты (longitude, latitude) в WGS84
|
||||
"""
|
||||
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),
|
||||
always_xy=True
|
||||
)
|
||||
|
||||
lon, lat = transformer.transform(x, y)
|
||||
return (lon, lat)
|
||||
|
||||
|
||||
def calculate_distance_gk(coord1_gk: tuple, coord2_gk: tuple) -> float:
|
||||
"""
|
||||
Вычисляет расстояние между двумя точками в проекции ГК (в километрах).
|
||||
|
||||
Args:
|
||||
coord1_gk: Первая точка (x, y) в метрах
|
||||
coord2_gk: Вторая точка (x, y) в метрах
|
||||
|
||||
Returns:
|
||||
float: Расстояние в километрах
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
Вычисляет среднее арифметическое координат в проекции Гаусса-Крюгера.
|
||||
|
||||
Алгоритм:
|
||||
1. Определяет зону ГК по первой точке (если не указана)
|
||||
2. Преобразует все координаты в проекцию ГК
|
||||
3. Вычисляет среднее арифметическое X и Y
|
||||
4. Преобразует результат обратно в WGS84
|
||||
|
||||
Args:
|
||||
coords: Список координат в формате [(lon1, lat1), (lon2, lat2), ...]
|
||||
zone: Номер зоны ГК (если None, определяется по первой точке)
|
||||
|
||||
Returns:
|
||||
tuple: Средние координаты (longitude, latitude) в WGS84
|
||||
"""
|
||||
if not coords:
|
||||
return (0, 0)
|
||||
|
||||
if len(coords) == 1:
|
||||
return coords[0]
|
||||
|
||||
# Определяем зону по первой точке
|
||||
if zone is None:
|
||||
zone = get_gauss_kruger_zone(coords[0][0])
|
||||
|
||||
# Преобразуем все координаты в ГК
|
||||
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]:
|
||||
"""
|
||||
@@ -1279,6 +1443,23 @@ def calculate_mean_coords(coord1: tuple, coord2: tuple) -> tuple[tuple, float]:
|
||||
return (geod_direct['lon2'], geod_direct['lat2']), distance/1000
|
||||
|
||||
|
||||
def calculate_distance_wgs84(coord1: tuple, coord2: tuple) -> float:
|
||||
"""
|
||||
Вычисляет расстояние между двумя точками в WGS84 (в километрах).
|
||||
|
||||
Args:
|
||||
coord1: Первая точка (longitude, latitude)
|
||||
coord2: Вторая точка (longitude, latitude)
|
||||
|
||||
Returns:
|
||||
float: Расстояние в километрах
|
||||
"""
|
||||
lon1, lat1 = coord1
|
||||
lon2, lat2 = coord2
|
||||
geod_inv = Geodesic.WGS84.Inverse(lat1, lon1, lat2, lon2)
|
||||
return geod_inv['s12'] / 1000
|
||||
|
||||
|
||||
|
||||
def calculate_average_coords_incremental(
|
||||
current_average: tuple, new_coord: tuple
|
||||
|
||||
@@ -9,7 +9,18 @@ from django.views import View
|
||||
from django.utils import timezone
|
||||
|
||||
from ..models import ObjItem, Satellite
|
||||
from ..utils import calculate_mean_coords, format_frequency, format_symbol_rate, format_coords_display, RANGE_DISTANCE
|
||||
from ..utils import (
|
||||
calculate_mean_coords,
|
||||
calculate_distance_wgs84,
|
||||
format_frequency,
|
||||
format_symbol_rate,
|
||||
format_coords_display,
|
||||
RANGE_DISTANCE,
|
||||
get_gauss_kruger_zone,
|
||||
transform_wgs84_to_gk,
|
||||
transform_gk_to_wgs84,
|
||||
average_coords_in_gk,
|
||||
)
|
||||
|
||||
|
||||
class PointsAveragingView(LoginRequiredMixin, View):
|
||||
@@ -117,7 +128,8 @@ class PointsAveragingAPIView(LoginRequiredMixin, View):
|
||||
if not objitem.geo_obj or not objitem.geo_obj.timestamp:
|
||||
continue
|
||||
|
||||
timestamp = 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
|
||||
@@ -177,6 +189,7 @@ class PointsAveragingAPIView(LoginRequiredMixin, View):
|
||||
|
||||
# Collect coordinates and build points_data
|
||||
points_data = []
|
||||
timestamp_objects = [] # Store datetime objects separately
|
||||
|
||||
for objitem in points:
|
||||
geo = objitem.geo_obj
|
||||
@@ -191,9 +204,14 @@ class PointsAveragingAPIView(LoginRequiredMixin, View):
|
||||
|
||||
# Format timestamp
|
||||
timestamp_str = '-'
|
||||
timestamp_unix = None
|
||||
if geo.timestamp:
|
||||
local_time = timezone.localtime(geo.timestamp)
|
||||
timestamp_str = local_time.strftime("%d.%m.%Y %H:%M")
|
||||
timestamp_unix = geo.timestamp.timestamp()
|
||||
timestamp_objects.append(geo.timestamp)
|
||||
else:
|
||||
timestamp_objects.append(None)
|
||||
|
||||
points_data.append({
|
||||
'id': objitem.id,
|
||||
@@ -204,6 +222,7 @@ class PointsAveragingAPIView(LoginRequiredMixin, View):
|
||||
'modulation': param.modulation.name if param and param.modulation else '-',
|
||||
'snr': f"{param.snr:.0f}" if param and param.snr else '-',
|
||||
'timestamp': timestamp_str,
|
||||
'timestamp_unix': timestamp_unix,
|
||||
'mirrors': mirrors,
|
||||
'location': geo.location or '-',
|
||||
'coordinates': format_coords_display(geo.coords),
|
||||
@@ -221,7 +240,7 @@ class PointsAveragingAPIView(LoginRequiredMixin, View):
|
||||
|
||||
for i, point_data in enumerate(points_data):
|
||||
coord = point_data['coord_tuple']
|
||||
_, distance = calculate_mean_coords(avg_coord, coord)
|
||||
distance = calculate_distance_wgs84(avg_coord, coord)
|
||||
point_data['distance_from_avg'] = round(distance, 2)
|
||||
|
||||
if i in valid_indices:
|
||||
@@ -241,6 +260,43 @@ class PointsAveragingAPIView(LoginRequiredMixin, View):
|
||||
# Get common parameters from first valid point (or first point if no valid)
|
||||
first_point = valid_points[0] if valid_points else (points_data[0] if points_data else {})
|
||||
|
||||
# Collect all unique mirrors from valid points
|
||||
all_mirrors = set()
|
||||
for point in valid_points:
|
||||
mirrors_str = point.get('mirrors', '-')
|
||||
if mirrors_str and mirrors_str != '-':
|
||||
# Split by comma and add each mirror
|
||||
for mirror in mirrors_str.split(','):
|
||||
mirror = mirror.strip()
|
||||
if mirror and mirror != '-':
|
||||
all_mirrors.add(mirror)
|
||||
|
||||
combined_mirrors = ', '.join(sorted(all_mirrors)) if all_mirrors else '-'
|
||||
|
||||
# Calculate median time from valid points using timestamp_objects array
|
||||
valid_timestamps = []
|
||||
for i in valid_indices:
|
||||
if timestamp_objects[i]:
|
||||
valid_timestamps.append(timestamp_objects[i])
|
||||
|
||||
median_time_str = '-'
|
||||
if valid_timestamps:
|
||||
# Sort timestamps and get median
|
||||
sorted_timestamps = sorted(valid_timestamps, key=lambda ts: ts.timestamp())
|
||||
n = len(sorted_timestamps)
|
||||
|
||||
if n % 2 == 1:
|
||||
# Odd number of timestamps - take middle one
|
||||
median_datetime = sorted_timestamps[n // 2]
|
||||
else:
|
||||
# Even number of timestamps - take average of two middle ones
|
||||
mid1 = sorted_timestamps[n // 2 - 1]
|
||||
mid2 = sorted_timestamps[n // 2]
|
||||
avg_seconds = (mid1.timestamp() + mid2.timestamp()) / 2
|
||||
median_datetime = datetime.fromtimestamp(avg_seconds, tz=mid1.tzinfo)
|
||||
|
||||
median_time_str = timezone.localtime(median_datetime).strftime("%d.%m.%Y %H:%M")
|
||||
|
||||
return {
|
||||
'source_name': source_name,
|
||||
'interval_key': interval_key,
|
||||
@@ -251,12 +307,13 @@ class PointsAveragingAPIView(LoginRequiredMixin, View):
|
||||
'has_outliers': len(outliers) > 0,
|
||||
'avg_coordinates': avg_coords_str,
|
||||
'avg_coord_tuple': avg_coord,
|
||||
'avg_time': median_time_str,
|
||||
'frequency': first_point.get('frequency', '-'),
|
||||
'freq_range': first_point.get('freq_range', '-'),
|
||||
'bod_velocity': first_point.get('bod_velocity', '-'),
|
||||
'modulation': first_point.get('modulation', '-'),
|
||||
'snr': first_point.get('snr', '-'),
|
||||
'mirrors': first_point.get('mirrors', '-'),
|
||||
'mirrors': combined_mirrors,
|
||||
'points': points_data,
|
||||
'outliers': outliers,
|
||||
'valid_points': valid_points,
|
||||
@@ -265,13 +322,12 @@ class PointsAveragingAPIView(LoginRequiredMixin, View):
|
||||
def _find_cluster_center(self, points_data):
|
||||
"""
|
||||
Find cluster center using the following algorithm:
|
||||
1. Find first pair of points within 56 km of each other
|
||||
2. Calculate their average as initial center
|
||||
3. Iteratively add points within 56 km of current average
|
||||
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
|
||||
|
||||
If only 1 point, return it as center.
|
||||
If no pair found within 56 km, use first point as center.
|
||||
|
||||
Returns:
|
||||
tuple: (avg_coord, set of valid point indices)
|
||||
@@ -282,69 +338,46 @@ class PointsAveragingAPIView(LoginRequiredMixin, View):
|
||||
if len(points_data) == 1:
|
||||
return points_data[0]['coord_tuple'], {0}
|
||||
|
||||
# Step 1: Find first pair of points within 56 km
|
||||
initial_pair = None
|
||||
for i in range(len(points_data)):
|
||||
for j in range(i + 1, len(points_data)):
|
||||
coord_i = points_data[i]['coord_tuple']
|
||||
coord_j = points_data[j]['coord_tuple']
|
||||
_, distance = calculate_mean_coords(coord_i, coord_j)
|
||||
|
||||
if distance <= RANGE_DISTANCE:
|
||||
initial_pair = (i, j)
|
||||
break
|
||||
if initial_pair:
|
||||
break
|
||||
# Step 1: Take first point as reference
|
||||
first_coord = points_data[0]['coord_tuple']
|
||||
valid_indices = {0}
|
||||
|
||||
# If no pair found within 56 km, use first point as center
|
||||
if not initial_pair:
|
||||
# All points are outliers except the first one
|
||||
return points_data[0]['coord_tuple'], {0}
|
||||
# Step 2: Find all points within 56 km of the first point
|
||||
for i in range(1, len(points_data)):
|
||||
coord_i = points_data[i]['coord_tuple']
|
||||
distance = calculate_distance_wgs84(first_coord, coord_i)
|
||||
|
||||
if distance <= RANGE_DISTANCE:
|
||||
valid_indices.add(i)
|
||||
|
||||
# Step 2: Calculate initial average from the pair
|
||||
i, j = initial_pair
|
||||
coord_i = points_data[i]['coord_tuple']
|
||||
coord_j = points_data[j]['coord_tuple']
|
||||
avg_coord, _ = calculate_mean_coords(coord_i, coord_j)
|
||||
|
||||
valid_indices = {i, j}
|
||||
|
||||
# Step 3: Iteratively add points within 56 km of current average
|
||||
# Keep iterating until no new points are added
|
||||
changed = True
|
||||
while changed:
|
||||
changed = False
|
||||
for k in range(len(points_data)):
|
||||
if k in valid_indices:
|
||||
continue
|
||||
|
||||
coord_k = points_data[k]['coord_tuple']
|
||||
_, distance = calculate_mean_coords(avg_coord, coord_k)
|
||||
|
||||
if distance <= RANGE_DISTANCE:
|
||||
# Add point to cluster and recalculate average
|
||||
valid_indices.add(k)
|
||||
|
||||
# Recalculate average with all valid points
|
||||
avg_coord = self._calculate_average_from_indices(points_data, valid_indices)
|
||||
changed = True
|
||||
# Step 3: Calculate average of all valid points using Gauss-Kruger projection
|
||||
avg_coord = self._calculate_average_from_indices(points_data, valid_indices)
|
||||
|
||||
return avg_coord, valid_indices
|
||||
|
||||
def _calculate_average_from_indices(self, points_data, indices):
|
||||
"""
|
||||
Calculate average coordinate from points at given indices.
|
||||
Uses incremental averaging.
|
||||
Uses arithmetic averaging in Gauss-Kruger 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
|
||||
"""
|
||||
indices_list = sorted(indices)
|
||||
if not indices_list:
|
||||
return (0, 0)
|
||||
|
||||
avg_coord = points_data[indices_list[0]]['coord_tuple']
|
||||
if len(indices_list) == 1:
|
||||
return points_data[indices_list[0]]['coord_tuple']
|
||||
|
||||
for idx in indices_list[1:]:
|
||||
coord = points_data[idx]['coord_tuple']
|
||||
avg_coord, _ = calculate_mean_coords(avg_coord, coord)
|
||||
# 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)
|
||||
|
||||
return avg_coord
|
||||
|
||||
@@ -368,21 +401,26 @@ class RecalculateGroupAPIView(LoginRequiredMixin, View):
|
||||
if not points:
|
||||
return JsonResponse({'error': 'No points provided'}, status=400)
|
||||
|
||||
# If include_all is True, recalculate with all points using clustering algorithm
|
||||
# If include_all is False, use only non-outlier points
|
||||
if not include_all:
|
||||
# If include_all is True, average ALL points without clustering (no outliers)
|
||||
# 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))))
|
||||
valid_indices = set(range(len(points)))
|
||||
else:
|
||||
# Filter out outliers first
|
||||
points = [p for p in points if not p.get('is_outlier', False)]
|
||||
|
||||
if not points:
|
||||
return JsonResponse({'error': 'No valid points after filtering'}, status=400)
|
||||
|
||||
# Apply clustering algorithm
|
||||
avg_coord, valid_indices = self._find_cluster_center(points)
|
||||
|
||||
if not points:
|
||||
return JsonResponse({'error': 'No valid points after filtering'}, status=400)
|
||||
|
||||
# Apply clustering algorithm
|
||||
avg_coord, valid_indices = self._find_cluster_center(points)
|
||||
|
||||
# Mark outliers and calculate distances
|
||||
for i, point in enumerate(points):
|
||||
coord = tuple(point['coord_tuple'])
|
||||
_, distance = calculate_mean_coords(avg_coord, coord)
|
||||
distance = calculate_distance_wgs84(avg_coord, coord)
|
||||
point['distance_from_avg'] = round(distance, 2)
|
||||
point['is_outlier'] = i not in valid_indices
|
||||
|
||||
@@ -396,6 +434,44 @@ class RecalculateGroupAPIView(LoginRequiredMixin, View):
|
||||
outliers = [p for p in points if p.get('is_outlier', False)]
|
||||
valid_points = [p for p in points if not p.get('is_outlier', False)]
|
||||
|
||||
# Collect all unique mirrors from valid points
|
||||
all_mirrors = set()
|
||||
for point in valid_points:
|
||||
mirrors_str = point.get('mirrors', '-')
|
||||
if mirrors_str and mirrors_str != '-':
|
||||
for mirror in mirrors_str.split(','):
|
||||
mirror = mirror.strip()
|
||||
if mirror and mirror != '-':
|
||||
all_mirrors.add(mirror)
|
||||
|
||||
combined_mirrors = ', '.join(sorted(all_mirrors)) if all_mirrors else '-'
|
||||
|
||||
# Calculate median time from valid points using timestamp_unix
|
||||
valid_timestamps_unix = []
|
||||
for point in valid_points:
|
||||
if point.get('timestamp_unix'):
|
||||
valid_timestamps_unix.append(point['timestamp_unix'])
|
||||
|
||||
median_time_str = '-'
|
||||
if valid_timestamps_unix:
|
||||
from datetime import datetime
|
||||
# Sort timestamps and get median
|
||||
sorted_timestamps = sorted(valid_timestamps_unix)
|
||||
n = len(sorted_timestamps)
|
||||
|
||||
if n % 2 == 1:
|
||||
# Odd number of timestamps - take middle one
|
||||
median_unix = sorted_timestamps[n // 2]
|
||||
else:
|
||||
# Even number of timestamps - take average of two middle ones
|
||||
mid1 = sorted_timestamps[n // 2 - 1]
|
||||
mid2 = sorted_timestamps[n // 2]
|
||||
median_unix = (mid1 + mid2) / 2
|
||||
|
||||
# Convert Unix timestamp to datetime
|
||||
median_datetime = datetime.fromtimestamp(median_unix, tz=timezone.get_current_timezone())
|
||||
median_time_str = timezone.localtime(median_datetime).strftime("%d.%m.%Y %H:%M")
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'avg_coordinates': avg_coords_str,
|
||||
@@ -404,15 +480,17 @@ class RecalculateGroupAPIView(LoginRequiredMixin, View):
|
||||
'valid_points_count': len(valid_points),
|
||||
'outliers_count': len(outliers),
|
||||
'has_outliers': len(outliers) > 0,
|
||||
'mirrors': combined_mirrors,
|
||||
'avg_time': median_time_str,
|
||||
'points': points,
|
||||
})
|
||||
|
||||
def _find_cluster_center(self, points):
|
||||
"""
|
||||
Find cluster center using the following algorithm:
|
||||
1. Find first pair of points within 56 km of each other
|
||||
2. Calculate their average as initial center
|
||||
3. Iteratively add points within 56 km of current average
|
||||
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
|
||||
"""
|
||||
if len(points) == 0:
|
||||
@@ -421,60 +499,39 @@ class RecalculateGroupAPIView(LoginRequiredMixin, View):
|
||||
if len(points) == 1:
|
||||
return tuple(points[0]['coord_tuple']), {0}
|
||||
|
||||
# Step 1: Find first pair of points within 56 km
|
||||
initial_pair = None
|
||||
for i in range(len(points)):
|
||||
for j in range(i + 1, len(points)):
|
||||
coord_i = tuple(points[i]['coord_tuple'])
|
||||
coord_j = tuple(points[j]['coord_tuple'])
|
||||
_, distance = calculate_mean_coords(coord_i, coord_j)
|
||||
|
||||
if distance <= RANGE_DISTANCE:
|
||||
initial_pair = (i, j)
|
||||
break
|
||||
if initial_pair:
|
||||
break
|
||||
# Step 1: Take first point as reference
|
||||
first_coord = tuple(points[0]['coord_tuple'])
|
||||
valid_indices = {0}
|
||||
|
||||
# If no pair found within 56 km, use first point as center
|
||||
if not initial_pair:
|
||||
return tuple(points[0]['coord_tuple']), {0}
|
||||
# Step 2: Find all points within 56 km of the first point
|
||||
for i in range(1, len(points)):
|
||||
coord_i = tuple(points[i]['coord_tuple'])
|
||||
distance = calculate_distance_wgs84(first_coord, coord_i)
|
||||
|
||||
if distance <= RANGE_DISTANCE:
|
||||
valid_indices.add(i)
|
||||
|
||||
# Step 2: Calculate initial average from the pair
|
||||
i, j = initial_pair
|
||||
coord_i = tuple(points[i]['coord_tuple'])
|
||||
coord_j = tuple(points[j]['coord_tuple'])
|
||||
avg_coord, _ = calculate_mean_coords(coord_i, coord_j)
|
||||
|
||||
valid_indices = {i, j}
|
||||
|
||||
# Step 3: Iteratively add points within 56 km of current average
|
||||
changed = True
|
||||
while changed:
|
||||
changed = False
|
||||
for k in range(len(points)):
|
||||
if k in valid_indices:
|
||||
continue
|
||||
|
||||
coord_k = tuple(points[k]['coord_tuple'])
|
||||
_, distance = calculate_mean_coords(avg_coord, coord_k)
|
||||
|
||||
if distance <= RANGE_DISTANCE:
|
||||
valid_indices.add(k)
|
||||
avg_coord = self._calculate_average_from_indices(points, valid_indices)
|
||||
changed = True
|
||||
# Step 3: Calculate average of all valid points using Gauss-Kruger projection
|
||||
avg_coord = self._calculate_average_from_indices(points, valid_indices)
|
||||
|
||||
return avg_coord, valid_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.
|
||||
"""
|
||||
indices_list = sorted(indices)
|
||||
if not indices_list:
|
||||
return (0, 0)
|
||||
|
||||
avg_coord = tuple(points[indices_list[0]]['coord_tuple'])
|
||||
if len(indices_list) == 1:
|
||||
return tuple(points[indices_list[0]]['coord_tuple'])
|
||||
|
||||
for idx in indices_list[1:]:
|
||||
coord = tuple(points[idx]['coord_tuple'])
|
||||
avg_coord, _ = calculate_mean_coords(avg_coord, coord)
|
||||
# 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)
|
||||
|
||||
return avg_coord
|
||||
|
||||
Reference in New Issue
Block a user