Добавил альтернативное имя у спутника
This commit is contained in:
@@ -53,9 +53,13 @@ def process_single_satellite(
|
||||
|
||||
logger.info(f"Найдено {len(sources)} источников для {sat_name}")
|
||||
|
||||
# Находим спутник в базе
|
||||
# Находим спутник в базе по имени или альтернативному имени (lowercase)
|
||||
from django.db.models import Q
|
||||
sat_name_lower = sat_name.lower()
|
||||
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})")
|
||||
except Satellite.DoesNotExist:
|
||||
error_msg = f"Спутник '{sat_name}' не найден в базе данных"
|
||||
|
||||
@@ -76,9 +76,13 @@ def fill_lyngsat_data(
|
||||
|
||||
logger.info(f"{log_prefix} Найдено {len(sources)} источников для {sat_name}")
|
||||
|
||||
# Находим спутник в базе
|
||||
# Находим спутник в базе по имени или альтернативному имени (lowercase)
|
||||
from django.db.models import Q
|
||||
sat_name_lower = sat_name.lower()
|
||||
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})")
|
||||
except Satellite.DoesNotExist:
|
||||
error_msg = f"Спутник '{sat_name}' не найден в базе данных"
|
||||
|
||||
@@ -573,6 +573,7 @@ class SatelliteAdmin(BaseAdmin):
|
||||
|
||||
list_display = (
|
||||
"name",
|
||||
"alternative_name",
|
||||
"norad",
|
||||
"international_code",
|
||||
"undersat_point",
|
||||
@@ -580,7 +581,7 @@ class SatelliteAdmin(BaseAdmin):
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
search_fields = ("name", "norad", "international_code")
|
||||
search_fields = ("name", "alternative_name", "norad", "international_code")
|
||||
ordering = ("name",)
|
||||
filter_horizontal = ("band",)
|
||||
autocomplete_fields = ("band",)
|
||||
|
||||
@@ -815,6 +815,7 @@ class SatelliteForm(forms.ModelForm):
|
||||
model = Satellite
|
||||
fields = [
|
||||
'name',
|
||||
'alternative_name',
|
||||
'norad',
|
||||
'international_code',
|
||||
'band',
|
||||
@@ -829,6 +830,10 @@ class SatelliteForm(forms.ModelForm):
|
||||
'placeholder': 'Введите название спутника',
|
||||
'required': True
|
||||
}),
|
||||
'alternative_name': forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'Введите альтернативное название (необязательно)'
|
||||
}),
|
||||
'norad': forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'Введите NORAD ID'
|
||||
@@ -862,6 +867,7 @@ class SatelliteForm(forms.ModelForm):
|
||||
}
|
||||
labels = {
|
||||
'name': 'Название спутника',
|
||||
'alternative_name': 'Альтернативное название',
|
||||
'norad': 'NORAD ID',
|
||||
'international_code': 'Международный код',
|
||||
'band': 'Диапазоны работы',
|
||||
@@ -872,6 +878,7 @@ class SatelliteForm(forms.ModelForm):
|
||||
}
|
||||
help_texts = {
|
||||
'name': 'Уникальное название спутника',
|
||||
'alternative_name': 'Альтернативное название спутника (например, на другом языке)',
|
||||
'norad': 'Идентификатор NORAD для отслеживания спутника',
|
||||
'international_code': 'Международный идентификатор спутника (например, 2011-074A)',
|
||||
'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,
|
||||
help_text="Название спутника",
|
||||
)
|
||||
alternative_name = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Альтернативное имя",
|
||||
db_index=True,
|
||||
help_text="Альтернативное название спутника (например, из скобок)",
|
||||
)
|
||||
norad = models.IntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
|
||||
@@ -43,8 +43,13 @@ function showSatelliteModal(satelliteId) {
|
||||
'<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-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">NORAD ID:</td><td>' + data.norad + '</td></tr>' +
|
||||
'<tr><td class="text-muted" style="width: 40%;">Название:</td><td><strong>' + data.name + '</strong></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>' + data.bands + '</td></tr>' +
|
||||
'</tbody></table></div></div></div>' +
|
||||
|
||||
@@ -180,6 +180,9 @@
|
||||
<button id="export-xlsx" class="btn btn-success" disabled>
|
||||
<i class="bi bi-file-earmark-excel"></i> Сохранить в Excel
|
||||
</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">
|
||||
<i class="bi bi-trash"></i> Очистить таблицу
|
||||
</button>
|
||||
@@ -292,18 +295,21 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
{
|
||||
title: "Действия",
|
||||
field: "actions",
|
||||
minWidth: 100,
|
||||
minWidth: 120,
|
||||
widthGrow: 1,
|
||||
hozAlign: "center",
|
||||
formatter: function(cell, formatterParams, onRendered) {
|
||||
const data = cell.getRow().getData();
|
||||
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) {
|
||||
const data = cell.getRow().getData();
|
||||
if (e.target.closest('.btn-view-details')) {
|
||||
showGroupDetails(data._groupIndex);
|
||||
} else if (e.target.closest('.btn-delete-row')) {
|
||||
deleteGroupRow(data._groupIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -321,6 +327,31 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
function updateGroupCount() {
|
||||
document.getElementById('group-count').textContent = allGroupsData.length;
|
||||
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
|
||||
@@ -837,6 +868,252 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
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
|
||||
updateGroupCount();
|
||||
});
|
||||
|
||||
@@ -86,6 +86,25 @@
|
||||
</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="mb-3">
|
||||
<label for="{{ form.norad.id_for_label }}" class="form-label">
|
||||
@@ -102,9 +121,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.international_code.id_for_label }}" class="form-label">
|
||||
@@ -121,7 +138,9 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.undersat_point.id_for_label }}" class="form-label">
|
||||
@@ -138,6 +157,23 @@
|
||||
{% endif %}
|
||||
</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 class="row">
|
||||
@@ -160,24 +196,7 @@
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<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 class="col-md-6">
|
||||
<div class="col-md-12">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.url.id_for_label }}" class="form-label">
|
||||
{{ form.url.label }}
|
||||
|
||||
@@ -190,6 +190,16 @@
|
||||
{% endif %}
|
||||
</a>
|
||||
</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;">
|
||||
<a href="javascript:void(0)" onclick="updateSort('norad')" class="text-white text-decoration-none">
|
||||
NORAD ID
|
||||
@@ -274,6 +284,7 @@
|
||||
</td>
|
||||
<td class="text-center">{{ satellite.id }}</td>
|
||||
<td>{{ satellite.name }}</td>
|
||||
<td>{{ satellite.alternative_name|default:"-" }}</td>
|
||||
<td>{{ satellite.norad }}</td>
|
||||
<td>{{ satellite.international_code|default:"-" }}</td>
|
||||
<td>{{ satellite.bands }}</td>
|
||||
@@ -307,7 +318,7 @@
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="13" class="text-center text-muted">Нет данных для отображения</td>
|
||||
<td colspan="14" class="text-center text-muted">Нет данных для отображения</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@@ -138,7 +138,7 @@ def find_mirror_satellites(mirror_names: list) -> list:
|
||||
Алгоритм:
|
||||
1. Для каждого имени зеркала:
|
||||
- Обрезать пробелы и привести к нижнему регистру
|
||||
- Найти все спутники, в имени которых содержится это имя
|
||||
- Найти все спутники, в имени или альтернативном имени которых содержится это имя
|
||||
2. Вернуть список найденных спутников
|
||||
|
||||
Args:
|
||||
@@ -147,6 +147,8 @@ def find_mirror_satellites(mirror_names: list) -> list:
|
||||
Returns:
|
||||
list: список объектов Satellite
|
||||
"""
|
||||
from django.db.models import Q
|
||||
|
||||
found_satellites = []
|
||||
|
||||
for mirror_name in mirror_names:
|
||||
@@ -159,9 +161,9 @@ def find_mirror_satellites(mirror_names: list) -> list:
|
||||
if not mirror_name_clean:
|
||||
continue
|
||||
|
||||
# Ищем спутники, в имени которых содержится имя зеркала
|
||||
# Ищем спутники, в имени или альтернативном имени которых содержится имя зеркала
|
||||
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)
|
||||
|
||||
@@ -605,6 +605,7 @@ class SatelliteDataAPIView(LoginRequiredMixin, View):
|
||||
data = {
|
||||
'id': satellite.id,
|
||||
'name': satellite.name,
|
||||
'alternative_name': satellite.alternative_name or '-',
|
||||
'norad': satellite.norad if satellite.norad else None,
|
||||
'bands': bands_str,
|
||||
'undersat_point': satellite.undersat_point if satellite.undersat_point is not None else None,
|
||||
|
||||
@@ -124,10 +124,11 @@ class SatelliteListView(LoginRequiredMixin, View):
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# Search by name
|
||||
# Search by name, alternative_name, or comment
|
||||
if search_query:
|
||||
satellites = satellites.filter(
|
||||
Q(name__icontains=search_query) |
|
||||
Q(alternative_name__icontains=search_query) |
|
||||
Q(comment__icontains=search_query)
|
||||
)
|
||||
|
||||
@@ -137,6 +138,8 @@ class SatelliteListView(LoginRequiredMixin, View):
|
||||
"-id": "-id",
|
||||
"name": "name",
|
||||
"-name": "-name",
|
||||
"alternative_name": "alternative_name",
|
||||
"-alternative_name": "-alternative_name",
|
||||
"norad": "norad",
|
||||
"-norad": "-norad",
|
||||
"international_code": "international_code",
|
||||
@@ -169,6 +172,7 @@ class SatelliteListView(LoginRequiredMixin, View):
|
||||
processed_satellites.append({
|
||||
'id': satellite.id,
|
||||
'name': satellite.name or "-",
|
||||
'alternative_name': satellite.alternative_name or "-",
|
||||
'norad': satellite.norad if satellite.norad else "-",
|
||||
'international_code': satellite.international_code or "-",
|
||||
'bands': ", ".join(band_names) if band_names else "-",
|
||||
|
||||
@@ -5,12 +5,99 @@ from io import BytesIO
|
||||
|
||||
# Third-party imports
|
||||
import requests
|
||||
from django.db.models import Q
|
||||
|
||||
# Local imports
|
||||
from mainapp.models import Polarization, Satellite
|
||||
|
||||
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):
|
||||
for pos, value in data.get('page', {}).get('positions').items():
|
||||
for name in value['satellites']:
|
||||
@@ -82,13 +169,19 @@ def parse_transponders_from_json(filepath: str, user=None):
|
||||
"""
|
||||
Парсит транспондеры из JSON файла.
|
||||
|
||||
Если имя спутника содержит альтернативное имя в скобках, оно извлекается
|
||||
и сохраняется в поле alternative_name.
|
||||
|
||||
Args:
|
||||
filepath: путь к JSON файлу
|
||||
user: пользователь для установки created_by и updated_by (optional)
|
||||
"""
|
||||
with open(filepath, encoding="utf-8") as 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 tran in trans:
|
||||
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)
|
||||
|
||||
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(
|
||||
name=tran["name"],
|
||||
@@ -124,6 +229,9 @@ def parse_transponders_from_xml(data_in: BytesIO, user=None):
|
||||
"""
|
||||
Парсит транспондеры из XML файла.
|
||||
|
||||
Если имя спутника содержит альтернативное имя в скобках, оно извлекается
|
||||
и сохраняется в поле alternative_name.
|
||||
|
||||
Args:
|
||||
data_in: BytesIO объект с XML данными
|
||||
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)
|
||||
for sat in satellites[:]:
|
||||
name = sat.xpath('./ns:name/text()', namespaces=ns)[0]
|
||||
if name == 'X' or 'DONT USE' in name:
|
||||
name_full = sat.xpath('./ns:name/text()', namespaces=ns)[0]
|
||||
if name_full == 'X' or 'DONT USE' in name_full:
|
||||
continue
|
||||
|
||||
# Парсим имя спутника и альтернативное имя
|
||||
main_name, alt_name = parse_satellite_name(name_full)
|
||||
|
||||
norad = sat.xpath('./ns:norad/text()', namespaces=ns)
|
||||
beams = sat.xpath('.//ns:BeamMemo', 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_end = float(transponder.xpath('./ns:uplinkFrequency/tr:end/text()', namespaces=ns)[0])
|
||||
tr_data = zones[tr_id]
|
||||
# p = tr_data['pol'][0] if tr_data['pol'] else '-'
|
||||
match tr_data['pol']:
|
||||
case 'Horizontal':
|
||||
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]
|
||||
|
||||
pol_obj, _ = Polarization.objects.get_or_create(name=pol)
|
||||
sat_obj, _ = Satellite.objects.get_or_create(
|
||||
name=name,
|
||||
defaults={
|
||||
"norad": int(norad[0]) if norad else -1,
|
||||
"international_code": intl_code[0] if intl_code else "",
|
||||
"undersat_point": sub_sat_point[0 if sub_sat_point else ""]
|
||||
})
|
||||
|
||||
# Ищем спутник по имени или альтернативному имени
|
||||
sat_obj = find_satellite_by_name(main_name)
|
||||
if not sat_obj:
|
||||
# Если не найден, создаём новый с альтернативным именем
|
||||
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(
|
||||
polarization=pol_obj,
|
||||
downlink=(downlink_start+downlink_end)/2/1000000,
|
||||
|
||||
Reference in New Issue
Block a user