Compare commits
2 Commits
d521b6baad
...
c72bf12d41
| Author | SHA1 | Date | |
|---|---|---|---|
| c72bf12d41 | |||
| 01871c3e13 |
@@ -53,9 +53,13 @@ def process_single_satellite(
|
|||||||
|
|
||||||
logger.info(f"Найдено {len(sources)} источников для {sat_name}")
|
logger.info(f"Найдено {len(sources)} источников для {sat_name}")
|
||||||
|
|
||||||
# Находим спутник в базе
|
# Находим спутник в базе по имени или альтернативному имени (lowercase)
|
||||||
|
from django.db.models import Q
|
||||||
|
sat_name_lower = sat_name.lower()
|
||||||
try:
|
try:
|
||||||
sat_obj = Satellite.objects.get(name__icontains=sat_name)
|
sat_obj = Satellite.objects.get(
|
||||||
|
Q(name__icontains=sat_name_lower) | Q(alternative_name__icontains=sat_name_lower)
|
||||||
|
)
|
||||||
logger.debug(f"Спутник {sat_name} найден в базе (ID: {sat_obj.id})")
|
logger.debug(f"Спутник {sat_name} найден в базе (ID: {sat_obj.id})")
|
||||||
except Satellite.DoesNotExist:
|
except Satellite.DoesNotExist:
|
||||||
error_msg = f"Спутник '{sat_name}' не найден в базе данных"
|
error_msg = f"Спутник '{sat_name}' не найден в базе данных"
|
||||||
|
|||||||
@@ -76,9 +76,13 @@ def fill_lyngsat_data(
|
|||||||
|
|
||||||
logger.info(f"{log_prefix} Найдено {len(sources)} источников для {sat_name}")
|
logger.info(f"{log_prefix} Найдено {len(sources)} источников для {sat_name}")
|
||||||
|
|
||||||
# Находим спутник в базе
|
# Находим спутник в базе по имени или альтернативному имени (lowercase)
|
||||||
|
from django.db.models import Q
|
||||||
|
sat_name_lower = sat_name.lower()
|
||||||
try:
|
try:
|
||||||
sat_obj = Satellite.objects.get(name__icontains=sat_name)
|
sat_obj = Satellite.objects.get(
|
||||||
|
Q(name__icontains=sat_name_lower) | Q(alternative_name__icontains=sat_name_lower)
|
||||||
|
)
|
||||||
logger.debug(f"{log_prefix} Спутник {sat_name} найден в базе (ID: {sat_obj.id})")
|
logger.debug(f"{log_prefix} Спутник {sat_name} найден в базе (ID: {sat_obj.id})")
|
||||||
except Satellite.DoesNotExist:
|
except Satellite.DoesNotExist:
|
||||||
error_msg = f"Спутник '{sat_name}' не найден в базе данных"
|
error_msg = f"Спутник '{sat_name}' не найден в базе данных"
|
||||||
|
|||||||
@@ -573,6 +573,7 @@ class SatelliteAdmin(BaseAdmin):
|
|||||||
|
|
||||||
list_display = (
|
list_display = (
|
||||||
"name",
|
"name",
|
||||||
|
"alternative_name",
|
||||||
"norad",
|
"norad",
|
||||||
"international_code",
|
"international_code",
|
||||||
"undersat_point",
|
"undersat_point",
|
||||||
@@ -580,7 +581,7 @@ class SatelliteAdmin(BaseAdmin):
|
|||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
)
|
)
|
||||||
search_fields = ("name", "norad", "international_code")
|
search_fields = ("name", "alternative_name", "norad", "international_code")
|
||||||
ordering = ("name",)
|
ordering = ("name",)
|
||||||
filter_horizontal = ("band",)
|
filter_horizontal = ("band",)
|
||||||
autocomplete_fields = ("band",)
|
autocomplete_fields = ("band",)
|
||||||
|
|||||||
@@ -815,6 +815,7 @@ class SatelliteForm(forms.ModelForm):
|
|||||||
model = Satellite
|
model = Satellite
|
||||||
fields = [
|
fields = [
|
||||||
'name',
|
'name',
|
||||||
|
'alternative_name',
|
||||||
'norad',
|
'norad',
|
||||||
'international_code',
|
'international_code',
|
||||||
'band',
|
'band',
|
||||||
@@ -829,6 +830,10 @@ class SatelliteForm(forms.ModelForm):
|
|||||||
'placeholder': 'Введите название спутника',
|
'placeholder': 'Введите название спутника',
|
||||||
'required': True
|
'required': True
|
||||||
}),
|
}),
|
||||||
|
'alternative_name': forms.TextInput(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'Введите альтернативное название (необязательно)'
|
||||||
|
}),
|
||||||
'norad': forms.NumberInput(attrs={
|
'norad': forms.NumberInput(attrs={
|
||||||
'class': 'form-control',
|
'class': 'form-control',
|
||||||
'placeholder': 'Введите NORAD ID'
|
'placeholder': 'Введите NORAD ID'
|
||||||
@@ -862,6 +867,7 @@ class SatelliteForm(forms.ModelForm):
|
|||||||
}
|
}
|
||||||
labels = {
|
labels = {
|
||||||
'name': 'Название спутника',
|
'name': 'Название спутника',
|
||||||
|
'alternative_name': 'Альтернативное название',
|
||||||
'norad': 'NORAD ID',
|
'norad': 'NORAD ID',
|
||||||
'international_code': 'Международный код',
|
'international_code': 'Международный код',
|
||||||
'band': 'Диапазоны работы',
|
'band': 'Диапазоны работы',
|
||||||
@@ -872,6 +878,7 @@ class SatelliteForm(forms.ModelForm):
|
|||||||
}
|
}
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'name': 'Уникальное название спутника',
|
'name': 'Уникальное название спутника',
|
||||||
|
'alternative_name': 'Альтернативное название спутника (например, на другом языке)',
|
||||||
'norad': 'Идентификатор NORAD для отслеживания спутника',
|
'norad': 'Идентификатор NORAD для отслеживания спутника',
|
||||||
'international_code': 'Международный идентификатор спутника (например, 2011-074A)',
|
'international_code': 'Международный идентификатор спутника (например, 2011-074A)',
|
||||||
'band': 'Выберите диапазоны работы спутника (удерживайте Ctrl для множественного выбора)',
|
'band': 'Выберите диапазоны работы спутника (удерживайте Ctrl для множественного выбора)',
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-12-01 08:15
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('mainapp', '0016_alter_satellite_international_code_techanalyze'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='satellite',
|
||||||
|
name='alternative_name',
|
||||||
|
field=models.CharField(blank=True, db_index=True, help_text='Альтернативное название спутника (например, из скобок)', max_length=100, null=True, verbose_name='Альтернативное имя'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='standard',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_index=True, help_text='Стандарт передачи данных (DVB-S, DVB-S2, DVB-S2X и т.д.)', max_length=80, unique=True, verbose_name='Стандарт'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -344,6 +344,14 @@ class Satellite(models.Model):
|
|||||||
db_index=True,
|
db_index=True,
|
||||||
help_text="Название спутника",
|
help_text="Название спутника",
|
||||||
)
|
)
|
||||||
|
alternative_name = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name="Альтернативное имя",
|
||||||
|
db_index=True,
|
||||||
|
help_text="Альтернативное название спутника (например, из скобок)",
|
||||||
|
)
|
||||||
norad = models.IntegerField(
|
norad = models.IntegerField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
|
|||||||
@@ -40,9 +40,6 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{% url 'mainapp:kubsat' %}">Кубсат</a>
|
<a class="nav-link" href="{% url 'mainapp:kubsat' %}">Кубсат</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="{% url 'mainapp:points_averaging' %}">Усреднение</a>
|
|
||||||
</li>
|
|
||||||
<!-- <li class="nav-item">
|
<!-- <li class="nav-item">
|
||||||
<a class="nav-link" href="{% url 'mapsapp:3dmap' %}">3D карта</a>
|
<a class="nav-link" href="{% url 'mapsapp:3dmap' %}">3D карта</a>
|
||||||
</li> -->
|
</li> -->
|
||||||
|
|||||||
@@ -43,8 +43,13 @@ function showSatelliteModal(satelliteId) {
|
|||||||
'<div class="col-md-6"><div class="card h-100">' +
|
'<div class="col-md-6"><div class="card h-100">' +
|
||||||
'<div class="card-header bg-light"><strong><i class="bi bi-info-circle"></i> Основная информация</strong></div>' +
|
'<div class="card-header bg-light"><strong><i class="bi bi-info-circle"></i> Основная информация</strong></div>' +
|
||||||
'<div class="card-body"><table class="table table-sm table-borderless mb-0"><tbody>' +
|
'<div class="card-body"><table class="table table-sm table-borderless mb-0"><tbody>' +
|
||||||
'<tr><td class="text-muted" style="width: 40%;">Название:</td><td><strong>' + data.name + '</strong></td></tr>' +
|
'<tr><td class="text-muted" style="width: 40%;">Название:</td><td><strong>' + data.name + '</strong></td></tr>';
|
||||||
'<tr><td class="text-muted">NORAD ID:</td><td>' + data.norad + '</td></tr>' +
|
|
||||||
|
if (data.alternative_name && data.alternative_name !== '-') {
|
||||||
|
html += '<tr><td class="text-muted">Альтернативное название:</td><td><strong>' + data.alternative_name + '</strong></td></tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '<tr><td class="text-muted">NORAD ID:</td><td>' + data.norad + '</td></tr>' +
|
||||||
'<tr><td class="text-muted">Подспутниковая точка:</td><td><strong>' + data.undersat_point + '</strong></td></tr>' +
|
'<tr><td class="text-muted">Подспутниковая точка:</td><td><strong>' + data.undersat_point + '</strong></td></tr>' +
|
||||||
'<tr><td class="text-muted">Диапазоны:</td><td>' + data.bands + '</td></tr>' +
|
'<tr><td class="text-muted">Диапазоны:</td><td>' + data.bands + '</td></tr>' +
|
||||||
'</tbody></table></div></div></div>' +
|
'</tbody></table></div></div></div>' +
|
||||||
|
|||||||
@@ -180,6 +180,9 @@
|
|||||||
<button id="export-xlsx" class="btn btn-success" disabled>
|
<button id="export-xlsx" class="btn btn-success" disabled>
|
||||||
<i class="bi bi-file-earmark-excel"></i> Сохранить в Excel
|
<i class="bi bi-file-earmark-excel"></i> Сохранить в Excel
|
||||||
</button>
|
</button>
|
||||||
|
<button id="export-json" class="btn btn-info ms-2" disabled>
|
||||||
|
<i class="bi bi-filetype-json"></i> Сохранить в JSON
|
||||||
|
</button>
|
||||||
<button id="clear-table" class="btn btn-danger ms-2">
|
<button id="clear-table" class="btn btn-danger ms-2">
|
||||||
<i class="bi bi-trash"></i> Очистить таблицу
|
<i class="bi bi-trash"></i> Очистить таблицу
|
||||||
</button>
|
</button>
|
||||||
@@ -280,7 +283,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
headerWordWrap: true,
|
headerWordWrap: true,
|
||||||
columns: [
|
columns: [
|
||||||
{title: "Объект наблюдения", field: "source_name", minWidth: 180, widthGrow: 2},
|
{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: "frequency", minWidth: 100, widthGrow: 1},
|
||||||
{title: "Полоса, МГц", field: "freq_range", minWidth: 100, widthGrow: 1},
|
{title: "Полоса, МГц", field: "freq_range", minWidth: 100, widthGrow: 1},
|
||||||
{title: "Символьная скорость, БОД", field: "bod_velocity", minWidth: 120, widthGrow: 1.5},
|
{title: "Символьная скорость, БОД", field: "bod_velocity", minWidth: 120, widthGrow: 1.5},
|
||||||
@@ -288,35 +290,26 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
{title: "ОСШ", field: "snr", minWidth: 70, widthGrow: 0.8},
|
{title: "ОСШ", field: "snr", minWidth: 70, widthGrow: 0.8},
|
||||||
{title: "Зеркала", field: "mirrors", minWidth: 130, widthGrow: 1.5},
|
{title: "Зеркала", field: "mirrors", minWidth: 130, widthGrow: 1.5},
|
||||||
{title: "Усреднённые координаты", field: "avg_coordinates", minWidth: 150, widthGrow: 2},
|
{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: "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: "Действия",
|
title: "Действия",
|
||||||
field: "actions",
|
field: "actions",
|
||||||
minWidth: 100,
|
minWidth: 120,
|
||||||
widthGrow: 1,
|
widthGrow: 1,
|
||||||
hozAlign: "center",
|
hozAlign: "center",
|
||||||
formatter: function(cell, formatterParams, onRendered) {
|
formatter: function(cell, formatterParams, onRendered) {
|
||||||
const data = cell.getRow().getData();
|
const data = cell.getRow().getData();
|
||||||
const btnClass = data.has_outliers ? 'btn-warning' : 'btn-info';
|
const btnClass = data.has_outliers ? 'btn-warning' : 'btn-info';
|
||||||
return `<button class="btn btn-sm ${btnClass} btn-view-details" title="Просмотр точек"><i class="bi bi-eye"></i></button>`;
|
return `<button class="btn btn-sm ${btnClass} btn-view-details" title="Просмотр точек"><i class="bi bi-eye"></i></button>
|
||||||
|
<button class="btn btn-sm btn-danger btn-delete-row ms-1" title="Удалить строку"><i class="bi bi-trash"></i></button>`;
|
||||||
},
|
},
|
||||||
cellClick: function(e, cell) {
|
cellClick: function(e, cell) {
|
||||||
const data = cell.getRow().getData();
|
const data = cell.getRow().getData();
|
||||||
if (e.target.closest('.btn-view-details')) {
|
if (e.target.closest('.btn-view-details')) {
|
||||||
showGroupDetails(data._groupIndex);
|
showGroupDetails(data._groupIndex);
|
||||||
|
} else if (e.target.closest('.btn-delete-row')) {
|
||||||
|
deleteGroupRow(data._groupIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -334,6 +327,31 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
function updateGroupCount() {
|
function updateGroupCount() {
|
||||||
document.getElementById('group-count').textContent = allGroupsData.length;
|
document.getElementById('group-count').textContent = allGroupsData.length;
|
||||||
document.getElementById('export-xlsx').disabled = allGroupsData.length === 0;
|
document.getElementById('export-xlsx').disabled = allGroupsData.length === 0;
|
||||||
|
document.getElementById('export-json').disabled = allGroupsData.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete group row
|
||||||
|
function deleteGroupRow(groupIndex) {
|
||||||
|
if (!confirm('Удалить эту группу из таблицы?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удаляем группу из массива
|
||||||
|
allGroupsData.splice(groupIndex, 1);
|
||||||
|
|
||||||
|
// Пересчитываем индексы для оставшихся групп
|
||||||
|
allGroupsData.forEach((group, idx) => {
|
||||||
|
group._groupIndex = idx;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обновляем таблицу
|
||||||
|
table.setData(allGroupsData.map(g => ({
|
||||||
|
...g,
|
||||||
|
status: g.has_outliers ? 'outliers' : 'ok'
|
||||||
|
})));
|
||||||
|
|
||||||
|
updateGroupCount();
|
||||||
|
updateAllPointsTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show loading overlay
|
// Show loading overlay
|
||||||
@@ -504,7 +522,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Show group details modal
|
// Show group details modal
|
||||||
function showGroupDetails(groupIndex) {
|
// skipShow=true means just update content without calling modal.show()
|
||||||
|
function showGroupDetails(groupIndex, skipShow = false) {
|
||||||
currentGroupIndex = groupIndex;
|
currentGroupIndex = groupIndex;
|
||||||
const group = allGroupsData[groupIndex];
|
const group = allGroupsData[groupIndex];
|
||||||
|
|
||||||
@@ -637,10 +656,16 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show modal
|
// Show modal - use getOrCreateInstance to avoid creating multiple instances
|
||||||
const modal = new bootstrap.Modal(document.getElementById('groupDetailsModal'));
|
if (!skipShow) {
|
||||||
|
const modalElement = document.getElementById('groupDetailsModal');
|
||||||
|
let modal = bootstrap.Modal.getInstance(modalElement);
|
||||||
|
if (!modal) {
|
||||||
|
modal = new bootstrap.Modal(modalElement);
|
||||||
|
}
|
||||||
modal.show();
|
modal.show();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Remove point from group and recalculate
|
// Remove point from group and recalculate
|
||||||
async function removePointFromGroup(groupIndex, pointIndex) {
|
async function removePointFromGroup(groupIndex, pointIndex) {
|
||||||
@@ -653,6 +678,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!confirm('Удалить эту точку из выборки и пересчитать усреднение?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Remove point
|
// Remove point
|
||||||
group.points.splice(pointIndex, 1);
|
group.points.splice(pointIndex, 1);
|
||||||
|
|
||||||
@@ -664,14 +693,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
async function recalculateGroup(groupIndex, includeAll) {
|
async function recalculateGroup(groupIndex, includeAll) {
|
||||||
const group = allGroupsData[groupIndex];
|
const group = allGroupsData[groupIndex];
|
||||||
if (!group) {
|
if (!group) {
|
||||||
hideLoading();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if there are points to process
|
// Check if there are points to process
|
||||||
if (!group.points || group.points.length === 0) {
|
if (!group.points || group.points.length === 0) {
|
||||||
alert('Нет точек для пересчёта');
|
alert('Нет точек для пересчёта');
|
||||||
hideLoading();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -694,7 +721,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
alert(data.error || 'Ошибка при пересчёте');
|
alert(data.error || 'Ошибка при пересчёте');
|
||||||
hideLoading();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -705,6 +731,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
group.valid_points_count = data.valid_points_count;
|
group.valid_points_count = data.valid_points_count;
|
||||||
group.outliers_count = data.outliers_count;
|
group.outliers_count = data.outliers_count;
|
||||||
group.has_outliers = data.has_outliers;
|
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;
|
group.points = data.points;
|
||||||
|
|
||||||
// Update table
|
// Update table
|
||||||
@@ -716,9 +744,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
// Update all points table
|
// Update all points table
|
||||||
updateAllPointsTable();
|
updateAllPointsTable();
|
||||||
|
|
||||||
// Update modal if open
|
// Update modal content without calling show() again
|
||||||
if (currentGroupIndex === groupIndex) {
|
if (currentGroupIndex === groupIndex) {
|
||||||
showGroupDetails(groupIndex);
|
showGroupDetails(groupIndex, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -730,16 +758,16 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Average all points button
|
// Average all points button
|
||||||
document.getElementById('btn-average-all').addEventListener('click', function() {
|
document.getElementById('btn-average-all').addEventListener('click', async function() {
|
||||||
if (currentGroupIndex !== null) {
|
if (currentGroupIndex !== null) {
|
||||||
recalculateGroup(currentGroupIndex, true);
|
await recalculateGroup(currentGroupIndex, true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Average valid points button
|
// Average valid points button
|
||||||
document.getElementById('btn-average-valid').addEventListener('click', function() {
|
document.getElementById('btn-average-valid').addEventListener('click', async function() {
|
||||||
if (currentGroupIndex !== null) {
|
if (currentGroupIndex !== null) {
|
||||||
recalculateGroup(currentGroupIndex, false);
|
await recalculateGroup(currentGroupIndex, false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -753,7 +781,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
// Prepare summary data for export
|
// Prepare summary data for export
|
||||||
const summaryData = allGroupsData.map(group => ({
|
const summaryData = allGroupsData.map(group => ({
|
||||||
'Объект наблюдения': group.source_name,
|
'Объект наблюдения': group.source_name,
|
||||||
'Интервал': group.interval_label,
|
|
||||||
'Частота, МГц': group.frequency,
|
'Частота, МГц': group.frequency,
|
||||||
'Полоса, МГц': group.freq_range,
|
'Полоса, МГц': group.freq_range,
|
||||||
'Символьная скорость, БОД': group.bod_velocity,
|
'Символьная скорость, БОД': group.bod_velocity,
|
||||||
@@ -761,9 +788,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
'ОСШ': group.snr,
|
'ОСШ': group.snr,
|
||||||
'Зеркала': group.mirrors,
|
'Зеркала': group.mirrors,
|
||||||
'Усреднённые координаты': group.avg_coordinates,
|
'Усреднённые координаты': group.avg_coordinates,
|
||||||
'Кол-во точек': group.total_points,
|
'Медианное время': group.avg_time || '-',
|
||||||
'Выбросов': group.outliers_count,
|
'Кол-во точек': group.total_points
|
||||||
'Статус': group.has_outliers ? 'Есть выбросы' : 'OK'
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Prepare all points data for export
|
// Prepare all points data for export
|
||||||
@@ -842,6 +868,252 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
return cookieValue;
|
return cookieValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate UUID v4
|
||||||
|
function generateUUID() {
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||||
|
const r = Math.random() * 16 | 0;
|
||||||
|
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||||
|
return v.toString(16);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export to JSON
|
||||||
|
document.getElementById('export-json').addEventListener('click', function() {
|
||||||
|
if (allGroupsData.length === 0) {
|
||||||
|
alert('Нет данных для экспорта');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creator ID - можно изменить на нужное значение
|
||||||
|
const CREATOR_ID = '6fd12c90-7f17-43d9-a03e-ee14e880f757';
|
||||||
|
|
||||||
|
// Начальный объект path (фиксированный)
|
||||||
|
const pathObject = {
|
||||||
|
"tacticObjectType": "path",
|
||||||
|
"captionPosition": "right",
|
||||||
|
"points": [
|
||||||
|
{
|
||||||
|
"id": "b92b9cbb-dd27-49aa-bcb6-e89a147bc02c",
|
||||||
|
"latitude": 57,
|
||||||
|
"longitude": -13,
|
||||||
|
"altitude": 0,
|
||||||
|
"customActions": [],
|
||||||
|
"tags": { "creator": CREATOR_ID },
|
||||||
|
"tacticObjectType": "point"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "8e3666d4-4990-4cb9-9594-63ad06333489",
|
||||||
|
"latitude": 57,
|
||||||
|
"longitude": 64,
|
||||||
|
"altitude": 0,
|
||||||
|
"customActions": [],
|
||||||
|
"tags": { "creator": CREATOR_ID },
|
||||||
|
"tacticObjectType": "point"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5f137485-d2fc-443d-8507-c936f02f3569",
|
||||||
|
"latitude": 11,
|
||||||
|
"longitude": 64,
|
||||||
|
"altitude": 0,
|
||||||
|
"customActions": [],
|
||||||
|
"tags": { "creator": CREATOR_ID },
|
||||||
|
"tacticObjectType": "point"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0fb90df7-8eb0-49fa-9d00-336389171bf5",
|
||||||
|
"latitude": 11,
|
||||||
|
"longitude": -13,
|
||||||
|
"altitude": 0,
|
||||||
|
"customActions": [],
|
||||||
|
"tags": { "creator": CREATOR_ID },
|
||||||
|
"tacticObjectType": "point"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3ef12637-585e-40a4-b0ee-8f1786c89ce6",
|
||||||
|
"latitude": 57,
|
||||||
|
"longitude": -13,
|
||||||
|
"altitude": 0,
|
||||||
|
"customActions": [],
|
||||||
|
"tags": { "creator": CREATOR_ID },
|
||||||
|
"tacticObjectType": "point"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isCycle": false,
|
||||||
|
"id": "2f604051-4984-4c2f-8c4c-c0cb64008f5f",
|
||||||
|
"draggable": false,
|
||||||
|
"selectable": false,
|
||||||
|
"editable": false,
|
||||||
|
"caption": "Ограничение для работы с поверхностями",
|
||||||
|
"line": {
|
||||||
|
"color": "rgb(148,0,211)",
|
||||||
|
"thickness": 1,
|
||||||
|
"dash": "solid",
|
||||||
|
"border": null
|
||||||
|
},
|
||||||
|
"customActions": [],
|
||||||
|
"tags": { "creator": CREATOR_ID }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Результирующий массив
|
||||||
|
const result = [pathObject];
|
||||||
|
|
||||||
|
// Палитра цветов для групп (RGB формат)
|
||||||
|
const jsonGroupColors = [
|
||||||
|
"rgb(0,128,0)", // зелёный
|
||||||
|
"rgb(0,0,255)", // синий
|
||||||
|
"rgb(255,0,0)", // красный
|
||||||
|
"rgb(255,165,0)", // оранжевый
|
||||||
|
"rgb(128,0,128)", // фиолетовый
|
||||||
|
"rgb(0,128,128)", // бирюзовый
|
||||||
|
"rgb(255,20,147)", // розовый
|
||||||
|
"rgb(139,69,19)", // коричневый
|
||||||
|
"rgb(0,100,0)", // тёмно-зелёный
|
||||||
|
"rgb(70,130,180)" // стальной синий
|
||||||
|
];
|
||||||
|
|
||||||
|
// Обрабатываем каждую группу
|
||||||
|
allGroupsData.forEach((group, groupIndex) => {
|
||||||
|
// Цвет для текущей группы
|
||||||
|
const groupColor = jsonGroupColors[groupIndex % jsonGroupColors.length];
|
||||||
|
|
||||||
|
// Формируем имя для усреднённой точки с пометкой "(усредн)"
|
||||||
|
const avgName = group.source_name;
|
||||||
|
const avgTime = group.avg_time || '-';
|
||||||
|
const avgCaption = `${avgName} (усредн) - ${avgTime}`;
|
||||||
|
|
||||||
|
// Получаем координаты усреднённой точки
|
||||||
|
const avgCoord = group.avg_coord_tuple;
|
||||||
|
const avgLat = avgCoord[1];
|
||||||
|
const avgLon = avgCoord[0];
|
||||||
|
|
||||||
|
// ID для усреднённой точки (source)
|
||||||
|
const avgSourceId = generateUUID();
|
||||||
|
|
||||||
|
// Создаём source для усреднённой точки (triangle)
|
||||||
|
const avgSource = {
|
||||||
|
"tacticObjectType": "source",
|
||||||
|
"captionPosition": "right",
|
||||||
|
"id": avgSourceId,
|
||||||
|
"icon": {
|
||||||
|
"type": "triangle",
|
||||||
|
"color": groupColor
|
||||||
|
},
|
||||||
|
"caption": avgCaption,
|
||||||
|
"name": avgCaption,
|
||||||
|
"customActions": [],
|
||||||
|
"trackBehavior": {},
|
||||||
|
"bearingStyle": {
|
||||||
|
"color": groupColor,
|
||||||
|
"thickness": 2,
|
||||||
|
"dash": "solid",
|
||||||
|
"border": null
|
||||||
|
},
|
||||||
|
"bearingBehavior": {},
|
||||||
|
"tags": { "creator": CREATOR_ID }
|
||||||
|
};
|
||||||
|
result.push(avgSource);
|
||||||
|
|
||||||
|
// Создаём position для усреднённой точки
|
||||||
|
const avgPosition = {
|
||||||
|
"tacticObjectType": "position",
|
||||||
|
"id": generateUUID(),
|
||||||
|
"parentId": avgSourceId,
|
||||||
|
"timeStamp": Date.now() / 1000,
|
||||||
|
"latitude": avgLat,
|
||||||
|
"altitude": 0,
|
||||||
|
"longitude": avgLon,
|
||||||
|
"caption": "",
|
||||||
|
"tooltip": "",
|
||||||
|
"customActions": [],
|
||||||
|
"tags": {
|
||||||
|
"layers": [],
|
||||||
|
"creator": CREATOR_ID
|
||||||
|
}
|
||||||
|
};
|
||||||
|
result.push(avgPosition);
|
||||||
|
|
||||||
|
// Обрабатываем все точки группы (не выбросы)
|
||||||
|
group.points.forEach(point => {
|
||||||
|
if (point.is_outlier) return; // Пропускаем выбросы
|
||||||
|
|
||||||
|
const pointCoord = point.coord_tuple;
|
||||||
|
const pointLat = pointCoord[1];
|
||||||
|
const pointLon = pointCoord[0];
|
||||||
|
const pointName = point.name || '-';
|
||||||
|
const pointTime = point.timestamp || '-';
|
||||||
|
const pointCaption = `${pointName} - ${pointTime}`;
|
||||||
|
|
||||||
|
// ID для source точки
|
||||||
|
const pointSourceId = generateUUID();
|
||||||
|
|
||||||
|
// Создаём source для точки (circle) с тем же цветом группы
|
||||||
|
const pointSource = {
|
||||||
|
"tacticObjectType": "source",
|
||||||
|
"captionPosition": "right",
|
||||||
|
"id": pointSourceId,
|
||||||
|
"icon": {
|
||||||
|
"type": "circle",
|
||||||
|
"color": groupColor
|
||||||
|
},
|
||||||
|
"caption": pointCaption,
|
||||||
|
"name": pointCaption,
|
||||||
|
"customActions": [],
|
||||||
|
"trackBehavior": {},
|
||||||
|
"bearingStyle": {
|
||||||
|
"color": groupColor,
|
||||||
|
"thickness": 2,
|
||||||
|
"dash": "solid",
|
||||||
|
"border": null
|
||||||
|
},
|
||||||
|
"bearingBehavior": {},
|
||||||
|
"tags": { "creator": CREATOR_ID }
|
||||||
|
};
|
||||||
|
result.push(pointSource);
|
||||||
|
|
||||||
|
// Создаём position для точки
|
||||||
|
const pointPosition = {
|
||||||
|
"tacticObjectType": "position",
|
||||||
|
"id": generateUUID(),
|
||||||
|
"parentId": pointSourceId,
|
||||||
|
"timeStamp": point.timestamp_unix || (Date.now() / 1000),
|
||||||
|
"latitude": pointLat,
|
||||||
|
"altitude": 0,
|
||||||
|
"longitude": pointLon,
|
||||||
|
"caption": "",
|
||||||
|
"tooltip": "",
|
||||||
|
"customActions": [],
|
||||||
|
"tags": {
|
||||||
|
"layers": [],
|
||||||
|
"creator": CREATOR_ID
|
||||||
|
}
|
||||||
|
};
|
||||||
|
result.push(pointPosition);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Конвертируем в JSON строку
|
||||||
|
const jsonString = JSON.stringify(result, null, 2);
|
||||||
|
|
||||||
|
// Добавляем BOM для UTF-8
|
||||||
|
const BOM = '\uFEFF';
|
||||||
|
const blob = new Blob([BOM + jsonString], { type: 'application/json;charset=utf-8' });
|
||||||
|
|
||||||
|
// Генерируем имя файла
|
||||||
|
const now = new Date();
|
||||||
|
const dateStr = now.toISOString().slice(0, 10);
|
||||||
|
const filename = `averaging_${dateStr}.json`;
|
||||||
|
|
||||||
|
// Скачиваем файл
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
});
|
||||||
|
|
||||||
// Initialize
|
// Initialize
|
||||||
updateGroupCount();
|
updateGroupCount();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -86,6 +86,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.alternative_name.id_for_label }}" class="form-label">
|
||||||
|
{{ form.alternative_name.label }}
|
||||||
|
</label>
|
||||||
|
{{ form.alternative_name }}
|
||||||
|
{% if form.alternative_name.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{{ form.alternative_name.errors.0 }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if form.alternative_name.help_text %}
|
||||||
|
<div class="form-text">{{ form.alternative_name.help_text }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="{{ form.norad.id_for_label }}" class="form-label">
|
<label for="{{ form.norad.id_for_label }}" class="form-label">
|
||||||
@@ -102,9 +121,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="{{ form.international_code.id_for_label }}" class="form-label">
|
<label for="{{ form.international_code.id_for_label }}" class="form-label">
|
||||||
@@ -121,7 +138,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="{{ form.undersat_point.id_for_label }}" class="form-label">
|
<label for="{{ form.undersat_point.id_for_label }}" class="form-label">
|
||||||
@@ -138,6 +157,23 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.launch_date.id_for_label }}" class="form-label">
|
||||||
|
{{ form.launch_date.label }}
|
||||||
|
</label>
|
||||||
|
{{ form.launch_date }}
|
||||||
|
{% if form.launch_date.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{{ form.launch_date.errors.0 }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if form.launch_date.help_text %}
|
||||||
|
<div class="form-text">{{ form.launch_date.help_text }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -160,24 +196,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-12">
|
||||||
<div class="mb-3">
|
|
||||||
<label for="{{ form.launch_date.id_for_label }}" class="form-label">
|
|
||||||
{{ form.launch_date.label }}
|
|
||||||
</label>
|
|
||||||
{{ form.launch_date }}
|
|
||||||
{% if form.launch_date.errors %}
|
|
||||||
<div class="invalid-feedback d-block">
|
|
||||||
{{ form.launch_date.errors.0 }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if form.launch_date.help_text %}
|
|
||||||
<div class="form-text">{{ form.launch_date.help_text }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="{{ form.url.id_for_label }}" class="form-label">
|
<label for="{{ form.url.id_for_label }}" class="form-label">
|
||||||
{{ form.url.label }}
|
{{ form.url.label }}
|
||||||
|
|||||||
@@ -190,6 +190,16 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
|
<th scope="col" style="min-width: 150px;">
|
||||||
|
<a href="javascript:void(0)" onclick="updateSort('alternative_name')" class="text-white text-decoration-none">
|
||||||
|
Альт. название
|
||||||
|
{% if sort == 'alternative_name' %}
|
||||||
|
<i class="bi bi-arrow-up"></i>
|
||||||
|
{% elif sort == '-alternative_name' %}
|
||||||
|
<i class="bi bi-arrow-down"></i>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
<th scope="col" style="min-width: 100px;">
|
<th scope="col" style="min-width: 100px;">
|
||||||
<a href="javascript:void(0)" onclick="updateSort('norad')" class="text-white text-decoration-none">
|
<a href="javascript:void(0)" onclick="updateSort('norad')" class="text-white text-decoration-none">
|
||||||
NORAD ID
|
NORAD ID
|
||||||
@@ -274,6 +284,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="text-center">{{ satellite.id }}</td>
|
<td class="text-center">{{ satellite.id }}</td>
|
||||||
<td>{{ satellite.name }}</td>
|
<td>{{ satellite.name }}</td>
|
||||||
|
<td>{{ satellite.alternative_name|default:"-" }}</td>
|
||||||
<td>{{ satellite.norad }}</td>
|
<td>{{ satellite.norad }}</td>
|
||||||
<td>{{ satellite.international_code|default:"-" }}</td>
|
<td>{{ satellite.international_code|default:"-" }}</td>
|
||||||
<td>{{ satellite.bands }}</td>
|
<td>{{ satellite.bands }}</td>
|
||||||
@@ -307,7 +318,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="13" class="text-center text-muted">Нет данных для отображения</td>
|
<td colspan="14" class="text-center text-muted">Нет данных для отображения</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -98,6 +98,9 @@
|
|||||||
onclick="showSelectedOnMap()">
|
onclick="showSelectedOnMap()">
|
||||||
<i class="bi bi-map"></i> Карта
|
<i class="bi bi-map"></i> Карта
|
||||||
</button>
|
</button>
|
||||||
|
<a href="{% url 'mainapp:points_averaging' %}" class="btn btn-warning btn-sm" title="Усреднение точек">
|
||||||
|
<i class="bi bi-calculator"></i> Усреднение
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add to List Button -->
|
<!-- Add to List Button -->
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from datetime import datetime, time
|
|||||||
# Django imports
|
# Django imports
|
||||||
from django.contrib.gis.geos import Point
|
from django.contrib.gis.geos import Point
|
||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
# Third-party imports
|
# Third-party imports
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
@@ -137,7 +138,7 @@ def find_mirror_satellites(mirror_names: list) -> list:
|
|||||||
Алгоритм:
|
Алгоритм:
|
||||||
1. Для каждого имени зеркала:
|
1. Для каждого имени зеркала:
|
||||||
- Обрезать пробелы и привести к нижнему регистру
|
- Обрезать пробелы и привести к нижнему регистру
|
||||||
- Найти все спутники, в имени которых содержится это имя
|
- Найти все спутники, в имени или альтернативном имени которых содержится это имя
|
||||||
2. Вернуть список найденных спутников
|
2. Вернуть список найденных спутников
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -146,6 +147,8 @@ def find_mirror_satellites(mirror_names: list) -> list:
|
|||||||
Returns:
|
Returns:
|
||||||
list: список объектов Satellite
|
list: список объектов Satellite
|
||||||
"""
|
"""
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
found_satellites = []
|
found_satellites = []
|
||||||
|
|
||||||
for mirror_name in mirror_names:
|
for mirror_name in mirror_names:
|
||||||
@@ -158,9 +161,9 @@ def find_mirror_satellites(mirror_names: list) -> list:
|
|||||||
if not mirror_name_clean:
|
if not mirror_name_clean:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Ищем спутники, в имени которых содержится имя зеркала
|
# Ищем спутники, в имени или альтернативном имени которых содержится имя зеркала
|
||||||
satellites = Satellite.objects.filter(
|
satellites = Satellite.objects.filter(
|
||||||
name__icontains=mirror_name_clean
|
Q(name__icontains=mirror_name_clean) | Q(alternative_name__icontains=mirror_name_clean)
|
||||||
)
|
)
|
||||||
|
|
||||||
found_satellites.extend(satellites)
|
found_satellites.extend(satellites)
|
||||||
@@ -691,6 +694,10 @@ def get_points_from_csv(file_content, current_user=None, is_automatic=False):
|
|||||||
"mir_1",
|
"mir_1",
|
||||||
"mir_2",
|
"mir_2",
|
||||||
"mir_3",
|
"mir_3",
|
||||||
|
"mir_4",
|
||||||
|
"mir_5",
|
||||||
|
"mir_6",
|
||||||
|
"mir_7",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
df[["lat", "lon", "freq", "f_range"]] = (
|
df[["lat", "lon", "freq", "f_range"]] = (
|
||||||
@@ -719,7 +726,7 @@ def get_points_from_csv(file_content, current_user=None, is_automatic=False):
|
|||||||
sat_name = row["sat"]
|
sat_name = row["sat"]
|
||||||
|
|
||||||
# Извлекаем время для проверки дубликатов
|
# Извлекаем время для проверки дубликатов
|
||||||
timestamp = row["time"]
|
timestamp = timezone.make_aware(row["time"])
|
||||||
|
|
||||||
# Проверяем дубликаты по координатам и времени
|
# Проверяем дубликаты по координатам и времени
|
||||||
if _is_duplicate_by_coords_and_time(coord_tuple, timestamp):
|
if _is_duplicate_by_coords_and_time(coord_tuple, timestamp):
|
||||||
@@ -727,10 +734,13 @@ def get_points_from_csv(file_content, current_user=None, is_automatic=False):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Получаем или создаем объект спутника
|
# Получаем или создаем объект спутника
|
||||||
sat_obj, _ = Satellite.objects.get_or_create(
|
# sat_obj, _ = Satellite.objects.get_or_create(
|
||||||
name=sat_name, defaults={"norad": row["norad_id"]}
|
# 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
|
source = None
|
||||||
|
|
||||||
# Если is_automatic=False, работаем с Source
|
# Если is_automatic=False, работаем с Source
|
||||||
@@ -784,7 +794,7 @@ def get_points_from_csv(file_content, current_user=None, is_automatic=False):
|
|||||||
return new_sources_count
|
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 с такими же координатами и временем ГЛ.
|
Проверяет, существует ли уже ObjItem с такими же координатами и временем ГЛ.
|
||||||
|
|
||||||
@@ -934,7 +944,7 @@ def _create_objitem_from_csv_row(row, source, user_to_use, is_automatic=False):
|
|||||||
|
|
||||||
# Создаем Geo объект
|
# Создаем Geo объект
|
||||||
geo_obj, _ = Geo.objects.get_or_create(
|
geo_obj, _ = Geo.objects.get_or_create(
|
||||||
timestamp=row["time"],
|
timestamp=timezone.make_aware(row["time"]),
|
||||||
coords=Point(row["lon"], row["lat"], srid=4326),
|
coords=Point(row["lon"], row["lat"], srid=4326),
|
||||||
defaults={
|
defaults={
|
||||||
"is_average": False,
|
"is_average": False,
|
||||||
@@ -1259,6 +1269,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]:
|
def calculate_mean_coords(coord1: tuple, coord2: tuple) -> tuple[tuple, float]:
|
||||||
"""
|
"""
|
||||||
@@ -1279,6 +1445,23 @@ def calculate_mean_coords(coord1: tuple, coord2: tuple) -> tuple[tuple, float]:
|
|||||||
return (geod_direct['lon2'], geod_direct['lat2']), distance/1000
|
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(
|
def calculate_average_coords_incremental(
|
||||||
current_average: tuple, new_coord: tuple
|
current_average: tuple, new_coord: tuple
|
||||||
|
|||||||
@@ -605,6 +605,7 @@ class SatelliteDataAPIView(LoginRequiredMixin, View):
|
|||||||
data = {
|
data = {
|
||||||
'id': satellite.id,
|
'id': satellite.id,
|
||||||
'name': satellite.name,
|
'name': satellite.name,
|
||||||
|
'alternative_name': satellite.alternative_name or '-',
|
||||||
'norad': satellite.norad if satellite.norad else None,
|
'norad': satellite.norad if satellite.norad else None,
|
||||||
'bands': bands_str,
|
'bands': bands_str,
|
||||||
'undersat_point': satellite.undersat_point if satellite.undersat_point is not None else None,
|
'undersat_point': satellite.undersat_point if satellite.undersat_point is not None else None,
|
||||||
|
|||||||
@@ -9,7 +9,18 @@ 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
|
||||||
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):
|
class PointsAveragingView(LoginRequiredMixin, View):
|
||||||
@@ -117,7 +128,8 @@ class PointsAveragingAPIView(LoginRequiredMixin, View):
|
|||||||
if not objitem.geo_obj or not objitem.geo_obj.timestamp:
|
if not objitem.geo_obj or not objitem.geo_obj.timestamp:
|
||||||
continue
|
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}"
|
source_name = objitem.name or f"Объект #{objitem.id}"
|
||||||
|
|
||||||
# Determine interval
|
# Determine interval
|
||||||
@@ -177,6 +189,7 @@ class PointsAveragingAPIView(LoginRequiredMixin, View):
|
|||||||
|
|
||||||
# Collect coordinates and build points_data
|
# Collect coordinates and build points_data
|
||||||
points_data = []
|
points_data = []
|
||||||
|
timestamp_objects = [] # Store datetime objects separately
|
||||||
|
|
||||||
for objitem in points:
|
for objitem in points:
|
||||||
geo = objitem.geo_obj
|
geo = objitem.geo_obj
|
||||||
@@ -191,9 +204,14 @@ class PointsAveragingAPIView(LoginRequiredMixin, View):
|
|||||||
|
|
||||||
# Format timestamp
|
# Format timestamp
|
||||||
timestamp_str = '-'
|
timestamp_str = '-'
|
||||||
|
timestamp_unix = None
|
||||||
if geo.timestamp:
|
if geo.timestamp:
|
||||||
local_time = timezone.localtime(geo.timestamp)
|
local_time = timezone.localtime(geo.timestamp)
|
||||||
timestamp_str = local_time.strftime("%d.%m.%Y %H:%M")
|
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({
|
points_data.append({
|
||||||
'id': objitem.id,
|
'id': objitem.id,
|
||||||
@@ -204,6 +222,7 @@ class PointsAveragingAPIView(LoginRequiredMixin, View):
|
|||||||
'modulation': param.modulation.name if param and param.modulation else '-',
|
'modulation': param.modulation.name if param and param.modulation else '-',
|
||||||
'snr': f"{param.snr:.0f}" if param and param.snr else '-',
|
'snr': f"{param.snr:.0f}" if param and param.snr else '-',
|
||||||
'timestamp': timestamp_str,
|
'timestamp': timestamp_str,
|
||||||
|
'timestamp_unix': timestamp_unix,
|
||||||
'mirrors': mirrors,
|
'mirrors': mirrors,
|
||||||
'location': geo.location or '-',
|
'location': geo.location or '-',
|
||||||
'coordinates': format_coords_display(geo.coords),
|
'coordinates': format_coords_display(geo.coords),
|
||||||
@@ -221,7 +240,7 @@ class PointsAveragingAPIView(LoginRequiredMixin, View):
|
|||||||
|
|
||||||
for i, point_data in enumerate(points_data):
|
for i, point_data in enumerate(points_data):
|
||||||
coord = point_data['coord_tuple']
|
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)
|
point_data['distance_from_avg'] = round(distance, 2)
|
||||||
|
|
||||||
if i in valid_indices:
|
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)
|
# 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 {})
|
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 {
|
return {
|
||||||
'source_name': source_name,
|
'source_name': source_name,
|
||||||
'interval_key': interval_key,
|
'interval_key': interval_key,
|
||||||
@@ -251,12 +307,13 @@ 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_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', '-'),
|
||||||
'bod_velocity': first_point.get('bod_velocity', '-'),
|
'bod_velocity': first_point.get('bod_velocity', '-'),
|
||||||
'modulation': first_point.get('modulation', '-'),
|
'modulation': first_point.get('modulation', '-'),
|
||||||
'snr': first_point.get('snr', '-'),
|
'snr': first_point.get('snr', '-'),
|
||||||
'mirrors': first_point.get('mirrors', '-'),
|
'mirrors': combined_mirrors,
|
||||||
'points': points_data,
|
'points': points_data,
|
||||||
'outliers': outliers,
|
'outliers': outliers,
|
||||||
'valid_points': valid_points,
|
'valid_points': valid_points,
|
||||||
@@ -265,13 +322,12 @@ class PointsAveragingAPIView(LoginRequiredMixin, View):
|
|||||||
def _find_cluster_center(self, points_data):
|
def _find_cluster_center(self, points_data):
|
||||||
"""
|
"""
|
||||||
Find cluster center using the following algorithm:
|
Find cluster center using the following algorithm:
|
||||||
1. Find first pair of points within 56 km of each other
|
1. Take the first point as reference
|
||||||
2. Calculate their average as initial center
|
2. Find all points within 56 km of the first point
|
||||||
3. Iteratively add points within 56 km of current average
|
3. Calculate average of all found points using Gauss-Kruger projection
|
||||||
4. Return final average and indices of valid points
|
4. Return final average and indices of valid points
|
||||||
|
|
||||||
If only 1 point, return it as center.
|
If only 1 point, return it as center.
|
||||||
If no pair found within 56 km, use first point as center.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
tuple: (avg_coord, set of valid point indices)
|
tuple: (avg_coord, set of valid point indices)
|
||||||
@@ -282,69 +338,46 @@ class PointsAveragingAPIView(LoginRequiredMixin, View):
|
|||||||
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: Find first pair of points within 56 km
|
# Step 1: Take first point as reference
|
||||||
initial_pair = None
|
first_coord = points_data[0]['coord_tuple']
|
||||||
for i in range(len(points_data)):
|
valid_indices = {0}
|
||||||
for j in range(i + 1, len(points_data)):
|
|
||||||
|
# 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_i = points_data[i]['coord_tuple']
|
||||||
coord_j = points_data[j]['coord_tuple']
|
distance = calculate_distance_wgs84(first_coord, coord_i)
|
||||||
_, distance = calculate_mean_coords(coord_i, coord_j)
|
|
||||||
|
|
||||||
if distance <= RANGE_DISTANCE:
|
if distance <= RANGE_DISTANCE:
|
||||||
initial_pair = (i, j)
|
valid_indices.add(i)
|
||||||
break
|
|
||||||
if initial_pair:
|
|
||||||
break
|
|
||||||
|
|
||||||
# If no pair found within 56 km, use first point as center
|
# Step 3: Calculate average of all valid points using Gauss-Kruger projection
|
||||||
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
|
|
||||||
avg_coord = self._calculate_average_from_indices(points_data, valid_indices)
|
avg_coord = self._calculate_average_from_indices(points_data, valid_indices)
|
||||||
changed = True
|
|
||||||
|
|
||||||
return avg_coord, valid_indices
|
return avg_coord, valid_indices
|
||||||
|
|
||||||
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 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)
|
indices_list = sorted(indices)
|
||||||
if not indices_list:
|
if not indices_list:
|
||||||
return (0, 0)
|
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:]:
|
# Collect coordinates for averaging
|
||||||
coord = points_data[idx]['coord_tuple']
|
coords = [points_data[idx]['coord_tuple'] for idx in indices_list]
|
||||||
avg_coord, _ = calculate_mean_coords(avg_coord, coord)
|
|
||||||
|
# Use Gauss-Kruger projection for averaging
|
||||||
|
avg_coord = average_coords_in_gk(coords)
|
||||||
|
|
||||||
return avg_coord
|
return avg_coord
|
||||||
|
|
||||||
@@ -368,9 +401,14 @@ class RecalculateGroupAPIView(LoginRequiredMixin, View):
|
|||||||
if not points:
|
if not points:
|
||||||
return JsonResponse({'error': 'No points provided'}, status=400)
|
return JsonResponse({'error': 'No points provided'}, status=400)
|
||||||
|
|
||||||
# If include_all is True, recalculate with all points using clustering algorithm
|
# If include_all is True, average ALL points without clustering (no outliers)
|
||||||
# If include_all is False, use only non-outlier points
|
# If include_all is False, use only non-outlier points and apply clustering
|
||||||
if not include_all:
|
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)]
|
points = [p for p in points if not p.get('is_outlier', False)]
|
||||||
|
|
||||||
if not points:
|
if not points:
|
||||||
@@ -382,7 +420,7 @@ class RecalculateGroupAPIView(LoginRequiredMixin, View):
|
|||||||
# Mark outliers and calculate distances
|
# Mark outliers and calculate distances
|
||||||
for i, point in enumerate(points):
|
for i, point in enumerate(points):
|
||||||
coord = tuple(point['coord_tuple'])
|
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['distance_from_avg'] = round(distance, 2)
|
||||||
point['is_outlier'] = i not in valid_indices
|
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)]
|
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)]
|
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({
|
return JsonResponse({
|
||||||
'success': True,
|
'success': True,
|
||||||
'avg_coordinates': avg_coords_str,
|
'avg_coordinates': avg_coords_str,
|
||||||
@@ -404,15 +480,17 @@ class RecalculateGroupAPIView(LoginRequiredMixin, View):
|
|||||||
'valid_points_count': len(valid_points),
|
'valid_points_count': len(valid_points),
|
||||||
'outliers_count': len(outliers),
|
'outliers_count': len(outliers),
|
||||||
'has_outliers': len(outliers) > 0,
|
'has_outliers': len(outliers) > 0,
|
||||||
|
'mirrors': combined_mirrors,
|
||||||
|
'avg_time': median_time_str,
|
||||||
'points': points,
|
'points': points,
|
||||||
})
|
})
|
||||||
|
|
||||||
def _find_cluster_center(self, points):
|
def _find_cluster_center(self, points):
|
||||||
"""
|
"""
|
||||||
Find cluster center using the following algorithm:
|
Find cluster center using the following algorithm:
|
||||||
1. Find first pair of points within 56 km of each other
|
1. Take the first point as reference
|
||||||
2. Calculate their average as initial center
|
2. Find all points within 56 km of the first point
|
||||||
3. Iteratively add points within 56 km of current average
|
3. Calculate average of all found points using Gauss-Kruger projection
|
||||||
4. Return final average and indices of valid points
|
4. Return final average and indices of valid points
|
||||||
"""
|
"""
|
||||||
if len(points) == 0:
|
if len(points) == 0:
|
||||||
@@ -421,60 +499,39 @@ class RecalculateGroupAPIView(LoginRequiredMixin, View):
|
|||||||
if len(points) == 1:
|
if len(points) == 1:
|
||||||
return tuple(points[0]['coord_tuple']), {0}
|
return tuple(points[0]['coord_tuple']), {0}
|
||||||
|
|
||||||
# Step 1: Find first pair of points within 56 km
|
# Step 1: Take first point as reference
|
||||||
initial_pair = None
|
first_coord = tuple(points[0]['coord_tuple'])
|
||||||
for i in range(len(points)):
|
valid_indices = {0}
|
||||||
for j in range(i + 1, len(points)):
|
|
||||||
|
# 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_i = tuple(points[i]['coord_tuple'])
|
||||||
coord_j = tuple(points[j]['coord_tuple'])
|
distance = calculate_distance_wgs84(first_coord, coord_i)
|
||||||
_, distance = calculate_mean_coords(coord_i, coord_j)
|
|
||||||
|
|
||||||
if distance <= RANGE_DISTANCE:
|
if distance <= RANGE_DISTANCE:
|
||||||
initial_pair = (i, j)
|
valid_indices.add(i)
|
||||||
break
|
|
||||||
if initial_pair:
|
|
||||||
break
|
|
||||||
|
|
||||||
# If no pair found within 56 km, use first point as center
|
# Step 3: Calculate average of all valid points using Gauss-Kruger projection
|
||||||
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)
|
|
||||||
avg_coord = self._calculate_average_from_indices(points, valid_indices)
|
avg_coord = self._calculate_average_from_indices(points, valid_indices)
|
||||||
changed = True
|
|
||||||
|
|
||||||
return avg_coord, valid_indices
|
return avg_coord, valid_indices
|
||||||
|
|
||||||
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.
|
||||||
|
"""
|
||||||
indices_list = sorted(indices)
|
indices_list = sorted(indices)
|
||||||
if not indices_list:
|
if not indices_list:
|
||||||
return (0, 0)
|
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:]:
|
# Collect coordinates for averaging
|
||||||
coord = tuple(points[idx]['coord_tuple'])
|
coords = [tuple(points[idx]['coord_tuple']) for idx in indices_list]
|
||||||
avg_coord, _ = calculate_mean_coords(avg_coord, coord)
|
|
||||||
|
# Use Gauss-Kruger projection for averaging
|
||||||
|
avg_coord = average_coords_in_gk(coords)
|
||||||
|
|
||||||
return avg_coord
|
return avg_coord
|
||||||
|
|||||||
@@ -124,10 +124,11 @@ class SatelliteListView(LoginRequiredMixin, View):
|
|||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Search by name
|
# Search by name, alternative_name, or comment
|
||||||
if search_query:
|
if search_query:
|
||||||
satellites = satellites.filter(
|
satellites = satellites.filter(
|
||||||
Q(name__icontains=search_query) |
|
Q(name__icontains=search_query) |
|
||||||
|
Q(alternative_name__icontains=search_query) |
|
||||||
Q(comment__icontains=search_query)
|
Q(comment__icontains=search_query)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -137,6 +138,8 @@ class SatelliteListView(LoginRequiredMixin, View):
|
|||||||
"-id": "-id",
|
"-id": "-id",
|
||||||
"name": "name",
|
"name": "name",
|
||||||
"-name": "-name",
|
"-name": "-name",
|
||||||
|
"alternative_name": "alternative_name",
|
||||||
|
"-alternative_name": "-alternative_name",
|
||||||
"norad": "norad",
|
"norad": "norad",
|
||||||
"-norad": "-norad",
|
"-norad": "-norad",
|
||||||
"international_code": "international_code",
|
"international_code": "international_code",
|
||||||
@@ -169,6 +172,7 @@ class SatelliteListView(LoginRequiredMixin, View):
|
|||||||
processed_satellites.append({
|
processed_satellites.append({
|
||||||
'id': satellite.id,
|
'id': satellite.id,
|
||||||
'name': satellite.name or "-",
|
'name': satellite.name or "-",
|
||||||
|
'alternative_name': satellite.alternative_name or "-",
|
||||||
'norad': satellite.norad if satellite.norad else "-",
|
'norad': satellite.norad if satellite.norad else "-",
|
||||||
'international_code': satellite.international_code or "-",
|
'international_code': satellite.international_code or "-",
|
||||||
'bands': ", ".join(band_names) if band_names else "-",
|
'bands': ", ".join(band_names) if band_names else "-",
|
||||||
|
|||||||
@@ -5,12 +5,99 @@ from io import BytesIO
|
|||||||
|
|
||||||
# Third-party imports
|
# Third-party imports
|
||||||
import requests
|
import requests
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
# Local imports
|
# Local imports
|
||||||
from mainapp.models import Polarization, Satellite
|
from mainapp.models import Polarization, Satellite
|
||||||
|
|
||||||
from .models import Transponders
|
from .models import Transponders
|
||||||
|
|
||||||
|
|
||||||
|
def parse_satellite_name(full_name: str) -> tuple[str, str | None]:
|
||||||
|
"""
|
||||||
|
Парсит полное имя спутника и извлекает основное и альтернативное имя.
|
||||||
|
|
||||||
|
Альтернативное имя находится в скобках после основного названия.
|
||||||
|
Примеры:
|
||||||
|
"Koreasat 116 (ANASIS-II)" -> ("Koreasat 116", "ANASIS-II")
|
||||||
|
"Thaicom 6 (Africom 1)" -> ("Thaicom 6", "Africom 1")
|
||||||
|
"Express AM6" -> ("Express AM6", None)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
full_name: Полное имя спутника (может содержать альтернативное имя в скобках)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (основное_имя, альтернативное_имя или None)
|
||||||
|
"""
|
||||||
|
if not full_name:
|
||||||
|
return (full_name, None)
|
||||||
|
|
||||||
|
# Ищем текст в скобках в конце строки
|
||||||
|
match = re.match(r'^(.+?)\s*\(([^)]+)\)\s*$$', full_name.strip())
|
||||||
|
if match:
|
||||||
|
main_name = match.group(1).strip()
|
||||||
|
alt_name = match.group(2).strip()
|
||||||
|
return (main_name, alt_name)
|
||||||
|
|
||||||
|
return (full_name.strip(), None)
|
||||||
|
|
||||||
|
|
||||||
|
def find_satellite_by_name(name: str):
|
||||||
|
"""
|
||||||
|
Ищет спутник по имени или альтернативному имени.
|
||||||
|
|
||||||
|
Все сравнения выполняются в lowercase.
|
||||||
|
|
||||||
|
Алгоритм поиска:
|
||||||
|
1. Точное совпадение по name (lowercase)
|
||||||
|
2. Точное совпадение по alternative_name (lowercase)
|
||||||
|
3. Частичное совпадение по name (lowercase)
|
||||||
|
4. Частичное совпадение по alternative_name (lowercase)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Имя спутника для поиска
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Satellite или None: Найденный спутник или None
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Satellite.MultipleObjectsReturned: Если найдено несколько спутников
|
||||||
|
"""
|
||||||
|
if not name:
|
||||||
|
return None
|
||||||
|
|
||||||
|
name_lower = name.strip().lower()
|
||||||
|
|
||||||
|
# 1. Точное совпадение по name (lowercase)
|
||||||
|
try:
|
||||||
|
return Satellite.objects.get(name__iexact=name_lower)
|
||||||
|
except Satellite.DoesNotExist:
|
||||||
|
pass
|
||||||
|
except Satellite.MultipleObjectsReturned:
|
||||||
|
raise
|
||||||
|
|
||||||
|
# 2. Точное совпадение по alternative_name (lowercase)
|
||||||
|
try:
|
||||||
|
return Satellite.objects.get(alternative_name__iexact=name_lower)
|
||||||
|
except Satellite.DoesNotExist:
|
||||||
|
pass
|
||||||
|
except Satellite.MultipleObjectsReturned:
|
||||||
|
raise
|
||||||
|
|
||||||
|
# 3. Частичное совпадение по name или alternative_name (lowercase)
|
||||||
|
satellites = Satellite.objects.filter(
|
||||||
|
Q(name__icontains=name_lower) | Q(alternative_name__icontains=name_lower)
|
||||||
|
)
|
||||||
|
|
||||||
|
if satellites.count() == 1:
|
||||||
|
return satellites.first()
|
||||||
|
elif satellites.count() > 1:
|
||||||
|
raise Satellite.MultipleObjectsReturned(
|
||||||
|
f"Найдено несколько спутников с именем '{name_lower}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def search_satellite_on_page(data: dict, satellite_name: str):
|
def search_satellite_on_page(data: dict, satellite_name: str):
|
||||||
for pos, value in data.get('page', {}).get('positions').items():
|
for pos, value in data.get('page', {}).get('positions').items():
|
||||||
for name in value['satellites']:
|
for name in value['satellites']:
|
||||||
@@ -82,13 +169,19 @@ def parse_transponders_from_json(filepath: str, user=None):
|
|||||||
"""
|
"""
|
||||||
Парсит транспондеры из JSON файла.
|
Парсит транспондеры из JSON файла.
|
||||||
|
|
||||||
|
Если имя спутника содержит альтернативное имя в скобках, оно извлекается
|
||||||
|
и сохраняется в поле alternative_name.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
filepath: путь к JSON файлу
|
filepath: путь к JSON файлу
|
||||||
user: пользователь для установки created_by и updated_by (optional)
|
user: пользователь для установки created_by и updated_by (optional)
|
||||||
"""
|
"""
|
||||||
with open(filepath, encoding="utf-8") as jf:
|
with open(filepath, encoding="utf-8") as jf:
|
||||||
data = json.load(jf)
|
data = json.load(jf)
|
||||||
for sat_name, trans_zone in data["satellites"].items():
|
for sat_name_full, trans_zone in data["satellites"].items():
|
||||||
|
# Парсим имя спутника и альтернативное имя
|
||||||
|
main_name, alt_name = parse_satellite_name(sat_name_full)
|
||||||
|
|
||||||
for zone, trans in trans_zone.items():
|
for zone, trans in trans_zone.items():
|
||||||
for tran in trans:
|
for tran in trans:
|
||||||
f_b, f_e = tran["freq"][0].split("-")
|
f_b, f_e = tran["freq"][0].split("-")
|
||||||
@@ -96,7 +189,19 @@ def parse_transponders_from_json(filepath: str, user=None):
|
|||||||
f_range = round(abs(float(f_e) - float(f_b)), 3)
|
f_range = round(abs(float(f_e) - float(f_b)), 3)
|
||||||
|
|
||||||
pol_obj = Polarization.objects.get(name=tran["pol"])
|
pol_obj = Polarization.objects.get(name=tran["pol"])
|
||||||
sat_obj = Satellite.objects.get(name__iexact=sat_name)
|
|
||||||
|
# Ищем спутник по имени или альтернативному имени
|
||||||
|
sat_obj = find_satellite_by_name(main_name)
|
||||||
|
if not sat_obj:
|
||||||
|
# Если не найден, создаём новый с альтернативным именем
|
||||||
|
sat_obj = Satellite.objects.create(
|
||||||
|
name=main_name,
|
||||||
|
alternative_name=alt_name
|
||||||
|
)
|
||||||
|
elif alt_name and not sat_obj.alternative_name:
|
||||||
|
# Если найден, но альтернативное имя не установлено - обновляем
|
||||||
|
sat_obj.alternative_name = alt_name
|
||||||
|
sat_obj.save()
|
||||||
|
|
||||||
tran_obj, created = Transponders.objects.get_or_create(
|
tran_obj, created = Transponders.objects.get_or_create(
|
||||||
name=tran["name"],
|
name=tran["name"],
|
||||||
@@ -124,6 +229,9 @@ def parse_transponders_from_xml(data_in: BytesIO, user=None):
|
|||||||
"""
|
"""
|
||||||
Парсит транспондеры из XML файла.
|
Парсит транспондеры из XML файла.
|
||||||
|
|
||||||
|
Если имя спутника содержит альтернативное имя в скобках, оно извлекается
|
||||||
|
и сохраняется в поле alternative_name.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
data_in: BytesIO объект с XML данными
|
data_in: BytesIO объект с XML данными
|
||||||
user: пользователь для установки created_by и updated_by (optional)
|
user: пользователь для установки created_by и updated_by (optional)
|
||||||
@@ -136,9 +244,13 @@ def parse_transponders_from_xml(data_in: BytesIO, user=None):
|
|||||||
}
|
}
|
||||||
satellites = tree.xpath('//ns:SatelliteMemo', namespaces=ns)
|
satellites = tree.xpath('//ns:SatelliteMemo', namespaces=ns)
|
||||||
for sat in satellites[:]:
|
for sat in satellites[:]:
|
||||||
name = sat.xpath('./ns:name/text()', namespaces=ns)[0]
|
name_full = sat.xpath('./ns:name/text()', namespaces=ns)[0]
|
||||||
if name == 'X' or 'DONT USE' in name:
|
if name_full == 'X' or 'DONT USE' in name_full:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Парсим имя спутника и альтернативное имя
|
||||||
|
main_name, alt_name = parse_satellite_name(name_full)
|
||||||
|
|
||||||
norad = sat.xpath('./ns:norad/text()', namespaces=ns)
|
norad = sat.xpath('./ns:norad/text()', namespaces=ns)
|
||||||
beams = sat.xpath('.//ns:BeamMemo', namespaces=ns)
|
beams = sat.xpath('.//ns:BeamMemo', namespaces=ns)
|
||||||
intl_code = sat.xpath('.//ns:internationalCode/text()', namespaces=ns)
|
intl_code = sat.xpath('.//ns:internationalCode/text()', namespaces=ns)
|
||||||
@@ -158,7 +270,6 @@ def parse_transponders_from_xml(data_in: BytesIO, user=None):
|
|||||||
uplink_start = float(transponder.xpath('./ns:uplinkFrequency/tr:start/text()', namespaces=ns)[0])
|
uplink_start = float(transponder.xpath('./ns:uplinkFrequency/tr:start/text()', namespaces=ns)[0])
|
||||||
uplink_end = float(transponder.xpath('./ns:uplinkFrequency/tr:end/text()', namespaces=ns)[0])
|
uplink_end = float(transponder.xpath('./ns:uplinkFrequency/tr:end/text()', namespaces=ns)[0])
|
||||||
tr_data = zones[tr_id]
|
tr_data = zones[tr_id]
|
||||||
# p = tr_data['pol'][0] if tr_data['pol'] else '-'
|
|
||||||
match tr_data['pol']:
|
match tr_data['pol']:
|
||||||
case 'Horizontal':
|
case 'Horizontal':
|
||||||
pol = 'Горизонтальная'
|
pol = 'Горизонтальная'
|
||||||
@@ -173,13 +284,36 @@ def parse_transponders_from_xml(data_in: BytesIO, user=None):
|
|||||||
tr_name = transponder.xpath('./ns:name/text()', namespaces=ns)[0]
|
tr_name = transponder.xpath('./ns:name/text()', namespaces=ns)[0]
|
||||||
|
|
||||||
pol_obj, _ = Polarization.objects.get_or_create(name=pol)
|
pol_obj, _ = Polarization.objects.get_or_create(name=pol)
|
||||||
sat_obj, _ = Satellite.objects.get_or_create(
|
|
||||||
name=name,
|
# Ищем спутник по имени или альтернативному имени
|
||||||
defaults={
|
sat_obj = find_satellite_by_name(main_name)
|
||||||
"norad": int(norad[0]) if norad else -1,
|
if not sat_obj:
|
||||||
"international_code": intl_code[0] if intl_code else "",
|
# Если не найден, создаём новый с альтернативным именем
|
||||||
"undersat_point": sub_sat_point[0 if sub_sat_point else ""]
|
sat_obj = Satellite.objects.create(
|
||||||
})
|
name=main_name,
|
||||||
|
alternative_name=alt_name,
|
||||||
|
norad=int(norad[0]) if norad else -1,
|
||||||
|
international_code=intl_code[0] if intl_code else "",
|
||||||
|
undersat_point=float(sub_sat_point[0]) if sub_sat_point else None
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Если найден, обновляем альтернативное имя если не установлено
|
||||||
|
updated = False
|
||||||
|
if alt_name and not sat_obj.alternative_name:
|
||||||
|
sat_obj.alternative_name = alt_name
|
||||||
|
updated = True
|
||||||
|
if norad and not sat_obj.norad:
|
||||||
|
sat_obj.norad = int(norad[0])
|
||||||
|
updated = True
|
||||||
|
if intl_code and not sat_obj.international_code:
|
||||||
|
sat_obj.international_code = intl_code[0]
|
||||||
|
updated = True
|
||||||
|
if sub_sat_point and not sat_obj.undersat_point:
|
||||||
|
sat_obj.undersat_point = float(sub_sat_point[0])
|
||||||
|
updated = True
|
||||||
|
if updated:
|
||||||
|
sat_obj.save()
|
||||||
|
|
||||||
trans_obj, created = Transponders.objects.get_or_create(
|
trans_obj, created = Transponders.objects.get_or_create(
|
||||||
polarization=pol_obj,
|
polarization=pol_obj,
|
||||||
downlink=(downlink_start+downlink_end)/2/1000000,
|
downlink=(downlink_start+downlink_end)/2/1000000,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ dependencies = [
|
|||||||
"pandas>=2.3.3",
|
"pandas>=2.3.3",
|
||||||
"psycopg>=3.2.10",
|
"psycopg>=3.2.10",
|
||||||
"psycopg2-binary>=2.9.11",
|
"psycopg2-binary>=2.9.11",
|
||||||
|
"pyproj>=3.6.0",
|
||||||
"redis>=6.4.0",
|
"redis>=6.4.0",
|
||||||
"django-redis>=5.4.0",
|
"django-redis>=5.4.0",
|
||||||
"requests>=2.32.5",
|
"requests>=2.32.5",
|
||||||
|
|||||||
49
dbapp/uv.lock
generated
49
dbapp/uv.lock
generated
@@ -312,6 +312,7 @@ dependencies = [
|
|||||||
{ name = "pandas" },
|
{ name = "pandas" },
|
||||||
{ name = "psycopg" },
|
{ name = "psycopg" },
|
||||||
{ name = "psycopg2-binary" },
|
{ name = "psycopg2-binary" },
|
||||||
|
{ name = "pyproj" },
|
||||||
{ name = "redis" },
|
{ name = "redis" },
|
||||||
{ name = "requests" },
|
{ name = "requests" },
|
||||||
{ name = "selenium" },
|
{ name = "selenium" },
|
||||||
@@ -347,6 +348,7 @@ requires-dist = [
|
|||||||
{ name = "pandas", specifier = ">=2.3.3" },
|
{ name = "pandas", specifier = ">=2.3.3" },
|
||||||
{ name = "psycopg", specifier = ">=3.2.10" },
|
{ name = "psycopg", specifier = ">=3.2.10" },
|
||||||
{ name = "psycopg2-binary", specifier = ">=2.9.11" },
|
{ name = "psycopg2-binary", specifier = ">=2.9.11" },
|
||||||
|
{ name = "pyproj", specifier = ">=3.6.0" },
|
||||||
{ name = "redis", specifier = ">=6.4.0" },
|
{ name = "redis", specifier = ">=6.4.0" },
|
||||||
{ name = "requests", specifier = ">=2.32.5" },
|
{ name = "requests", specifier = ">=2.32.5" },
|
||||||
{ name = "selenium", specifier = ">=4.38.0" },
|
{ 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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "pysocks"
|
name = "pysocks"
|
||||||
version = "1.7.1"
|
version = "1.7.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user