Усредение точек в проекции ГК
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,10 +625,16 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
});
|
||||
|
||||
// Show modal
|
||||
const modal = new bootstrap.Modal(document.getElementById('groupDetailsModal'));
|
||||
// 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
|
||||
async function removePointFromGroup(groupIndex, pointIndex) {
|
||||
@@ -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)):
|
||||
# Step 1: Take first point as reference
|
||||
first_coord = points_data[0]['coord_tuple']
|
||||
valid_indices = {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']
|
||||
coord_j = points_data[j]['coord_tuple']
|
||||
_, distance = calculate_mean_coords(coord_i, coord_j)
|
||||
distance = calculate_distance_wgs84(first_coord, coord_i)
|
||||
|
||||
if distance <= RANGE_DISTANCE:
|
||||
initial_pair = (i, j)
|
||||
break
|
||||
if initial_pair:
|
||||
break
|
||||
valid_indices.add(i)
|
||||
|
||||
# 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: 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
|
||||
# Step 3: Calculate average of all valid points using Gauss-Kruger projection
|
||||
avg_coord = self._calculate_average_from_indices(points_data, valid_indices)
|
||||
changed = True
|
||||
|
||||
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,9 +401,14 @@ 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:
|
||||
@@ -382,7 +420,7 @@ class RecalculateGroupAPIView(LoginRequiredMixin, View):
|
||||
# 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)):
|
||||
# Step 1: Take first point as reference
|
||||
first_coord = tuple(points[0]['coord_tuple'])
|
||||
valid_indices = {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'])
|
||||
coord_j = tuple(points[j]['coord_tuple'])
|
||||
_, distance = calculate_mean_coords(coord_i, coord_j)
|
||||
distance = calculate_distance_wgs84(first_coord, coord_i)
|
||||
|
||||
if distance <= RANGE_DISTANCE:
|
||||
initial_pair = (i, j)
|
||||
break
|
||||
if initial_pair:
|
||||
break
|
||||
valid_indices.add(i)
|
||||
|
||||
# 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: 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)
|
||||
# Step 3: Calculate average of all valid points using Gauss-Kruger projection
|
||||
avg_coord = self._calculate_average_from_indices(points, valid_indices)
|
||||
changed = True
|
||||
|
||||
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
|
||||
|
||||
@@ -30,6 +30,7 @@ dependencies = [
|
||||
"pandas>=2.3.3",
|
||||
"psycopg>=3.2.10",
|
||||
"psycopg2-binary>=2.9.11",
|
||||
"pyproj>=3.6.0",
|
||||
"redis>=6.4.0",
|
||||
"django-redis>=5.4.0",
|
||||
"requests>=2.32.5",
|
||||
|
||||
49
dbapp/uv.lock
generated
49
dbapp/uv.lock
generated
@@ -312,6 +312,7 @@ dependencies = [
|
||||
{ name = "pandas" },
|
||||
{ name = "psycopg" },
|
||||
{ name = "psycopg2-binary" },
|
||||
{ name = "pyproj" },
|
||||
{ name = "redis" },
|
||||
{ name = "requests" },
|
||||
{ name = "selenium" },
|
||||
@@ -347,6 +348,7 @@ requires-dist = [
|
||||
{ name = "pandas", specifier = ">=2.3.3" },
|
||||
{ name = "psycopg", specifier = ">=3.2.10" },
|
||||
{ name = "psycopg2-binary", specifier = ">=2.9.11" },
|
||||
{ name = "pyproj", specifier = ">=3.6.0" },
|
||||
{ name = "redis", specifier = ">=6.4.0" },
|
||||
{ name = "requests", specifier = ">=2.32.5" },
|
||||
{ name = "selenium", specifier = ">=4.38.0" },
|
||||
@@ -938,6 +940,53 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyproj"
|
||||
version = "3.7.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/04/90/67bd7260b4ea9b8b20b4f58afef6c223ecb3abf368eb4ec5bc2cdef81b49/pyproj-3.7.2.tar.gz", hash = "sha256:39a0cf1ecc7e282d1d30f36594ebd55c9fae1fda8a2622cee5d100430628f88c", size = 226279, upload-time = "2025-08-14T12:05:42.18Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/be/14/faf1b90d267cea68d7e70662e7f88cefdb1bc890bd596c74b959e0517a72/pyproj-3.7.2-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:19466e529b1b15eeefdf8ff26b06fa745856c044f2f77bf0edbae94078c1dfa1", size = 6214580, upload-time = "2025-08-14T12:04:28.804Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/48/da9a45b184d375f62667f62eba0ca68569b0bd980a0bb7ffcc1d50440520/pyproj-3.7.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:c79b9b84c4a626c5dc324c0d666be0bfcebd99f7538d66e8898c2444221b3da7", size = 4615388, upload-time = "2025-08-14T12:04:30.553Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/e7/d2b459a4a64bca328b712c1b544e109df88e5c800f7c143cfbc404d39bfb/pyproj-3.7.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ceecf374cacca317bc09e165db38ac548ee3cad07c3609442bd70311c59c21aa", size = 9628455, upload-time = "2025-08-14T12:04:32.435Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/85/c2b1706e51942de19076eff082f8495e57d5151364e78b5bef4af4a1d94a/pyproj-3.7.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5141a538ffdbe4bfd157421828bb2e07123a90a7a2d6f30fa1462abcfb5ce681", size = 9514269, upload-time = "2025-08-14T12:04:34.599Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/38/07a9b89ae7467872f9a476883a5bad9e4f4d1219d31060f0f2b282276cbe/pyproj-3.7.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f000841e98ea99acbb7b8ca168d67773b0191de95187228a16110245c5d954d5", size = 10808437, upload-time = "2025-08-14T12:04:36.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/56/fda1daeabbd39dec5b07f67233d09f31facb762587b498e6fc4572be9837/pyproj-3.7.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8115faf2597f281a42ab608ceac346b4eb1383d3b45ab474fd37341c4bf82a67", size = 10745540, upload-time = "2025-08-14T12:04:38.568Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/90/c793182cbba65a39a11db2ac6b479fe76c59e6509ae75e5744c344a0da9d/pyproj-3.7.2-cp313-cp313-win32.whl", hash = "sha256:f18c0579dd6be00b970cb1a6719197fceecc407515bab37da0066f0184aafdf3", size = 5896506, upload-time = "2025-08-14T12:04:41.059Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/0f/747974129cf0d800906f81cd25efd098c96509026e454d4b66868779ab04/pyproj-3.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:bb41c29d5f60854b1075853fe80c58950b398d4ebb404eb532536ac8d2834ed7", size = 6310195, upload-time = "2025-08-14T12:04:42.974Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/64/fc7598a53172c4931ec6edf5228280663063150625d3f6423b4c20f9daff/pyproj-3.7.2-cp313-cp313-win_arm64.whl", hash = "sha256:2b617d573be4118c11cd96b8891a0b7f65778fa7733ed8ecdb297a447d439100", size = 6230748, upload-time = "2025-08-14T12:04:44.491Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/f0/611dd5cddb0d277f94b7af12981f56e1441bf8d22695065d4f0df5218498/pyproj-3.7.2-cp313-cp313t-macosx_13_0_x86_64.whl", hash = "sha256:d27b48f0e81beeaa2b4d60c516c3a1cfbb0c7ff6ef71256d8e9c07792f735279", size = 6241729, upload-time = "2025-08-14T12:04:46.274Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/93/40bd4a6c523ff9965e480870611aed7eda5aa2c6128c6537345a2b77b542/pyproj-3.7.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:55a3610d75023c7b1c6e583e48ef8f62918e85a2ae81300569d9f104d6684bb6", size = 4652497, upload-time = "2025-08-14T12:04:48.203Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/ae/7150ead53c117880b35e0d37960d3138fe640a235feb9605cb9386f50bb0/pyproj-3.7.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:8d7349182fa622696787cc9e195508d2a41a64765da9b8a6bee846702b9e6220", size = 9942610, upload-time = "2025-08-14T12:04:49.652Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/17/7a4a7eafecf2b46ab64e5c08176c20ceb5844b503eaa551bf12ccac77322/pyproj-3.7.2-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:d230b186eb876ed4f29a7c5ee310144c3a0e44e89e55f65fb3607e13f6db337c", size = 9692390, upload-time = "2025-08-14T12:04:51.731Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/55/ae18f040f6410f0ea547a21ada7ef3e26e6c82befa125b303b02759c0e9d/pyproj-3.7.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:237499c7862c578d0369e2b8ac56eec550e391a025ff70e2af8417139dabb41c", size = 11047596, upload-time = "2025-08-14T12:04:53.748Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/2e/d3fff4d2909473f26ae799f9dda04caa322c417a51ff3b25763f7d03b233/pyproj-3.7.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8c225f5978abd506fd9a78eaaf794435e823c9156091cabaab5374efb29d7f69", size = 10896975, upload-time = "2025-08-14T12:04:55.875Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/bc/8fc7d3963d87057b7b51ebe68c1e7c51c23129eee5072ba6b86558544a46/pyproj-3.7.2-cp313-cp313t-win32.whl", hash = "sha256:2da731876d27639ff9d2d81c151f6ab90a1546455fabd93368e753047be344a2", size = 5953057, upload-time = "2025-08-14T12:04:58.466Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/27/ea9809966cc47d2d51e6d5ae631ea895f7c7c7b9b3c29718f900a8f7d197/pyproj-3.7.2-cp313-cp313t-win_amd64.whl", hash = "sha256:f54d91ae18dd23b6c0ab48126d446820e725419da10617d86a1b69ada6d881d3", size = 6375414, upload-time = "2025-08-14T12:04:59.861Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/f8/1ef0129fba9a555c658e22af68989f35e7ba7b9136f25758809efec0cd6e/pyproj-3.7.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fc52ba896cfc3214dc9f9ca3c0677a623e8fdd096b257c14a31e719d21ff3fdd", size = 6262501, upload-time = "2025-08-14T12:05:01.39Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/17/c2b050d3f5b71b6edd0d96ae16c990fdc42a5f1366464a5c2772146de33a/pyproj-3.7.2-cp314-cp314-macosx_13_0_x86_64.whl", hash = "sha256:2aaa328605ace41db050d06bac1adc11f01b71fe95c18661497763116c3a0f02", size = 6214541, upload-time = "2025-08-14T12:05:03.166Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/68/68ada9c8aea96ded09a66cfd9bf87aa6db8c2edebe93f5bf9b66b0143fbc/pyproj-3.7.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:35dccbce8201313c596a970fde90e33605248b66272595c061b511c8100ccc08", size = 4617456, upload-time = "2025-08-14T12:05:04.563Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/e4/4c50ceca7d0e937977866b02cb64e6ccf4df979a5871e521f9e255df6073/pyproj-3.7.2-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:25b0b7cb0042444c29a164b993c45c1b8013d6c48baa61dc1160d834a277e83b", size = 9615590, upload-time = "2025-08-14T12:05:06.094Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/1e/ada6fb15a1d75b5bd9b554355a69a798c55a7dcc93b8d41596265c1772e3/pyproj-3.7.2-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:85def3a6388e9ba51f964619aa002a9d2098e77c6454ff47773bb68871024281", size = 9474960, upload-time = "2025-08-14T12:05:07.973Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/07/9d48ad0a8db36e16f842f2c8a694c1d9d7dcf9137264846bef77585a71f3/pyproj-3.7.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b1bccefec3875ab81eabf49059e2b2ea77362c178b66fd3528c3e4df242f1516", size = 10799478, upload-time = "2025-08-14T12:05:14.102Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/cf/2f812b529079f72f51ff2d6456b7fef06c01735e5cfd62d54ffb2b548028/pyproj-3.7.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d5371ca114d6990b675247355a801925814eca53e6c4b2f1b5c0a956336ee36e", size = 10710030, upload-time = "2025-08-14T12:05:16.317Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/9b/4626a19e1f03eba4c0e77b91a6cf0f73aa9cb5d51a22ee385c22812bcc2c/pyproj-3.7.2-cp314-cp314-win32.whl", hash = "sha256:77f066626030f41be543274f5ac79f2a511fe89860ecd0914f22131b40a0ec25", size = 5991181, upload-time = "2025-08-14T12:05:19.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/b2/5a6610554306a83a563080c2cf2c57565563eadd280e15388efa00fb5b33/pyproj-3.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:5a964da1696b8522806f4276ab04ccfff8f9eb95133a92a25900697609d40112", size = 6434721, upload-time = "2025-08-14T12:05:21.022Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/ce/6c910ea2e1c74ef673c5d48c482564b8a7824a44c4e35cca2e765b68cfcc/pyproj-3.7.2-cp314-cp314-win_arm64.whl", hash = "sha256:e258ab4dbd3cf627809067c0ba8f9884ea76c8e5999d039fb37a1619c6c3e1f6", size = 6363821, upload-time = "2025-08-14T12:05:22.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/e4/5532f6f7491812ba782a2177fe9de73fd8e2912b59f46a1d056b84b9b8f2/pyproj-3.7.2-cp314-cp314t-macosx_13_0_x86_64.whl", hash = "sha256:bbbac2f930c6d266f70ec75df35ef851d96fdb3701c674f42fd23a9314573b37", size = 6241773, upload-time = "2025-08-14T12:05:24.577Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/1f/0938c3f2bbbef1789132d1726d9b0e662f10cfc22522743937f421ad664e/pyproj-3.7.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:b7544e0a3d6339dc9151e9c8f3ea62a936ab7cc446a806ec448bbe86aebb979b", size = 4652537, upload-time = "2025-08-14T12:05:26.391Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/a8/488b1ed47d25972f33874f91f09ca8f2227902f05f63a2b80dc73e7b1c97/pyproj-3.7.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:f7f5133dca4c703e8acadf6f30bc567d39a42c6af321e7f81975c2518f3ed357", size = 9940864, upload-time = "2025-08-14T12:05:27.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/cc/7f4c895d0cb98e47b6a85a6d79eaca03eb266129eed2f845125c09cf31ff/pyproj-3.7.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:5aff3343038d7426aa5076f07feb88065f50e0502d1b0d7c22ddfdd2c75a3f81", size = 9688868, upload-time = "2025-08-14T12:05:30.425Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/b7/c7e306b8bb0f071d9825b753ee4920f066c40fbfcce9372c4f3cfb2fc4ed/pyproj-3.7.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b0552178c61f2ac1c820d087e8ba6e62b29442debddbb09d51c4bf8acc84d888", size = 11045910, upload-time = "2025-08-14T12:05:32.507Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/fb/538a4d2df695980e2dde5c04d965fbdd1fe8c20a3194dc4aaa3952a4d1be/pyproj-3.7.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:47d87db2d2c436c5fd0409b34d70bb6cdb875cca2ebe7a9d1c442367b0ab8d59", size = 10895724, upload-time = "2025-08-14T12:05:35.465Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/8b/a3f0618b03957de9db5489a04558a8826f43906628bb0b766033aa3b5548/pyproj-3.7.2-cp314-cp314t-win32.whl", hash = "sha256:c9b6f1d8ad3e80a0ee0903a778b6ece7dca1d1d40f6d114ae01bc8ddbad971aa", size = 6056848, upload-time = "2025-08-14T12:05:37.553Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/56/413240dd5149dd3291eda55aa55a659da4431244a2fd1319d0ae89407cfb/pyproj-3.7.2-cp314-cp314t-win_amd64.whl", hash = "sha256:1914e29e27933ba6f9822663ee0600f169014a2859f851c054c88cf5ea8a333c", size = 6517676, upload-time = "2025-08-14T12:05:39.126Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/73/a7141a1a0559bf1a7aa42a11c879ceb19f02f5c6c371c6d57fd86cefd4d1/pyproj-3.7.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d9d25bae416a24397e0d85739f84d323b55f6511e45a522dd7d7eae70d10c7e4", size = 6391844, upload-time = "2025-08-14T12:05:40.745Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pysocks"
|
||||
version = "1.7.1"
|
||||
|
||||
Reference in New Issue
Block a user