Добавил альтернативное имя у спутника

This commit is contained in:
2025-12-01 12:19:24 +03:00
parent 01871c3e13
commit c72bf12d41
14 changed files with 720 additions and 220 deletions

View File

@@ -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}' не найден в базе данных"

View File

@@ -1,175 +1,179 @@
import logging import logging
from .parser import LyngSatParser from .parser import LyngSatParser
from .models import LyngSat from .models import LyngSat
from mainapp.models import Polarization, Standard, Modulation, Satellite from mainapp.models import Polarization, Standard, Modulation, Satellite
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def fill_lyngsat_data( def fill_lyngsat_data(
target_sats: list[str], target_sats: list[str],
regions: list[str] = None, regions: list[str] = None,
task_id: str = None, task_id: str = None,
update_progress=None update_progress=None
): ):
""" """
Заполняет данные Lyngsat для указанных спутников и регионов. Заполняет данные Lyngsat для указанных спутников и регионов.
Args: Args:
target_sats: Список названий спутников для обработки target_sats: Список названий спутников для обработки
regions: Список регионов для парсинга (по умолчанию все) regions: Список регионов для парсинга (по умолчанию все)
task_id: ID задачи Celery для логирования task_id: ID задачи Celery для логирования
update_progress: Функция для обновления прогресса (current, total, status) update_progress: Функция для обновления прогресса (current, total, status)
Returns: Returns:
dict: Статистика обработки с ключами: dict: Статистика обработки с ключами:
- total_satellites: общее количество спутников - total_satellites: общее количество спутников
- total_sources: общее количество источников - total_sources: общее количество источников
- created: количество созданных записей - created: количество созданных записей
- updated: количество обновленных записей - updated: количество обновленных записей
- errors: список ошибок - errors: список ошибок
""" """
log_prefix = f"[Task {task_id}]" if task_id else "[Lyngsat]" log_prefix = f"[Task {task_id}]" if task_id else "[Lyngsat]"
stats = { stats = {
'total_satellites': 0, 'total_satellites': 0,
'total_sources': 0, 'total_sources': 0,
'created': 0, 'created': 0,
'updated': 0, 'updated': 0,
'errors': [] 'errors': []
} }
if regions is None: if regions is None:
regions = ["europe", "asia", "america", "atlantic"] regions = ["europe", "asia", "america", "atlantic"]
logger.info(f"{log_prefix} Начало парсинга данных") logger.info(f"{log_prefix} Начало парсинга данных")
logger.info(f"{log_prefix} Спутники: {', '.join(target_sats)}") logger.info(f"{log_prefix} Спутники: {', '.join(target_sats)}")
logger.info(f"{log_prefix} Регионы: {', '.join(regions)}") logger.info(f"{log_prefix} Регионы: {', '.join(regions)}")
if update_progress: if update_progress:
update_progress(0, len(target_sats), "Инициализация парсера...") update_progress(0, len(target_sats), "Инициализация парсера...")
try: try:
parser = LyngSatParser( parser = LyngSatParser(
flaresolver_url="http://localhost:8191/v1", flaresolver_url="http://localhost:8191/v1",
target_sats=target_sats, target_sats=target_sats,
regions=regions regions=regions
) )
logger.info(f"{log_prefix} Получение данных со спутников...") logger.info(f"{log_prefix} Получение данных со спутников...")
if update_progress: if update_progress:
update_progress(0, len(target_sats), "Получение данных со спутников...") update_progress(0, len(target_sats), "Получение данных со спутников...")
lyngsat_data = parser.get_satellites_data() lyngsat_data = parser.get_satellites_data()
stats['total_satellites'] = len(lyngsat_data) stats['total_satellites'] = len(lyngsat_data)
logger.info(f"{log_prefix} Получено данных по {stats['total_satellites']} спутникам") logger.info(f"{log_prefix} Получено данных по {stats['total_satellites']} спутникам")
for idx, (sat_name, data) in enumerate(lyngsat_data.items(), 1): for idx, (sat_name, data) in enumerate(lyngsat_data.items(), 1):
logger.info(f"{log_prefix} Обработка спутника {idx}/{stats['total_satellites']}: {sat_name}") logger.info(f"{log_prefix} Обработка спутника {idx}/{stats['total_satellites']}: {sat_name}")
if update_progress: if update_progress:
update_progress(idx, stats['total_satellites'], f"Обработка {sat_name}...") update_progress(idx, stats['total_satellites'], f"Обработка {sat_name}...")
url = data['url'] url = data['url']
sources = data['sources'] sources = data['sources']
stats['total_sources'] += len(sources) stats['total_sources'] += len(sources)
logger.info(f"{log_prefix} Найдено {len(sources)} источников для {sat_name}") logger.info(f"{log_prefix} Найдено {len(sources)} источников для {sat_name}")
# Находим спутник в базе # Находим спутник в базе по имени или альтернативному имени (lowercase)
try: from django.db.models import Q
sat_obj = Satellite.objects.get(name__icontains=sat_name) sat_name_lower = sat_name.lower()
logger.debug(f"{log_prefix} Спутник {sat_name} найден в базе (ID: {sat_obj.id})") try:
except Satellite.DoesNotExist: sat_obj = Satellite.objects.get(
error_msg = f"Спутник '{sat_name}' не найден в базе данных" Q(name__icontains=sat_name_lower) | Q(alternative_name__icontains=sat_name_lower)
logger.warning(f"{log_prefix} {error_msg}") )
stats['errors'].append(error_msg) logger.debug(f"{log_prefix} Спутник {sat_name} найден в базе (ID: {sat_obj.id})")
continue except Satellite.DoesNotExist:
except Satellite.MultipleObjectsReturned: error_msg = f"Спутник '{sat_name}' не найден в базе данных"
error_msg = f"Найдено несколько спутников с именем '{sat_name}'" logger.warning(f"{log_prefix} {error_msg}")
logger.warning(f"{log_prefix} {error_msg}") stats['errors'].append(error_msg)
stats['errors'].append(error_msg) continue
continue except Satellite.MultipleObjectsReturned:
error_msg = f"Найдено несколько спутников с именем '{sat_name}'"
for source_idx, source in enumerate(sources, 1): logger.warning(f"{log_prefix} {error_msg}")
try: stats['errors'].append(error_msg)
# Парсим частоту continue
try:
freq = float(source['freq']) for source_idx, source in enumerate(sources, 1):
except (ValueError, TypeError): try:
freq = -1.0 # Парсим частоту
error_msg = f"Некорректная частота для {sat_name}: {source.get('freq')}" try:
logger.debug(f"{log_prefix} {error_msg}") freq = float(source['freq'])
stats['errors'].append(error_msg) except (ValueError, TypeError):
freq = -1.0
last_update = source['last_update'] error_msg = f"Некорректная частота для {sat_name}: {source.get('freq')}"
fec = source['metadata'].get('fec') logger.debug(f"{log_prefix} {error_msg}")
modulation_name = source['metadata'].get('modulation') stats['errors'].append(error_msg)
standard_name = source['metadata'].get('standard')
symbol_velocity = source['metadata'].get('symbol_rate') last_update = source['last_update']
polarization_name = source['pol'] fec = source['metadata'].get('fec')
channel_info = source['provider_name'] modulation_name = source['metadata'].get('modulation')
standard_name = source['metadata'].get('standard')
# Создаем или получаем связанные объекты symbol_velocity = source['metadata'].get('symbol_rate')
pol_obj, _ = Polarization.objects.get_or_create( polarization_name = source['pol']
name=polarization_name if polarization_name else "-" channel_info = source['provider_name']
)
# Создаем или получаем связанные объекты
mod_obj, _ = Modulation.objects.get_or_create( pol_obj, _ = Polarization.objects.get_or_create(
name=modulation_name if modulation_name else "-" name=polarization_name if polarization_name else "-"
) )
standard_obj, _ = Standard.objects.get_or_create( mod_obj, _ = Modulation.objects.get_or_create(
name=standard_name if standard_name else "-" name=modulation_name if modulation_name else "-"
) )
# Создаем или обновляем запись Lyngsat standard_obj, _ = Standard.objects.get_or_create(
lyng_obj, created = LyngSat.objects.update_or_create( name=standard_name if standard_name else "-"
id_satellite=sat_obj, )
frequency=freq,
polarization=pol_obj, # Создаем или обновляем запись Lyngsat
defaults={ lyng_obj, created = LyngSat.objects.update_or_create(
"modulation": mod_obj, id_satellite=sat_obj,
"standard": standard_obj, frequency=freq,
"sym_velocity": symbol_velocity if symbol_velocity else 0, polarization=pol_obj,
"channel_info": channel_info[:20] if channel_info else "", defaults={
"last_update": last_update, "modulation": mod_obj,
"fec": fec[:30] if fec else "", "standard": standard_obj,
"url": url "sym_velocity": symbol_velocity if symbol_velocity else 0,
} "channel_info": channel_info[:20] if channel_info else "",
) "last_update": last_update,
"fec": fec[:30] if fec else "",
if created: "url": url
stats['created'] += 1 }
logger.debug(f"{log_prefix} Создана запись для {sat_name} {freq} МГц") )
else:
stats['updated'] += 1 if created:
logger.debug(f"{log_prefix} Обновлена запись для {sat_name} {freq} МГц") stats['created'] += 1
logger.debug(f"{log_prefix} Создана запись для {sat_name} {freq} МГц")
# Логируем прогресс каждые 10 источников else:
if source_idx % 10 == 0: stats['updated'] += 1
logger.info(f"{log_prefix} Обработано {source_idx}/{len(sources)} источников для {sat_name}") logger.debug(f"{log_prefix} Обновлена запись для {sat_name} {freq} МГц")
except Exception as e: # Логируем прогресс каждые 10 источников
error_msg = f"Ошибка при обработке источника {sat_name}: {str(e)}" if source_idx % 10 == 0:
logger.error(f"{log_prefix} {error_msg}", exc_info=True) logger.info(f"{log_prefix} Обработано {source_idx}/{len(sources)} источников для {sat_name}")
stats['errors'].append(error_msg)
continue except Exception as e:
error_msg = f"Ошибка при обработке источника {sat_name}: {str(e)}"
logger.info(f"{log_prefix} Завершена обработка {sat_name}: создано {stats['created']}, обновлено {stats['updated']}") logger.error(f"{log_prefix} {error_msg}", exc_info=True)
stats['errors'].append(error_msg)
except Exception as e: continue
error_msg = f"Критическая ошибка: {str(e)}"
logger.error(f"{log_prefix} {error_msg}", exc_info=True) logger.info(f"{log_prefix} Завершена обработка {sat_name}: создано {stats['created']}, обновлено {stats['updated']}")
stats['errors'].append(error_msg)
except Exception as e:
logger.info(f"{log_prefix} Обработка завершена. Итого: создано {stats['created']}, обновлено {stats['updated']}, ошибок {len(stats['errors'])}") error_msg = f"Критическая ошибка: {str(e)}"
logger.error(f"{log_prefix} {error_msg}", exc_info=True)
if update_progress: stats['errors'].append(error_msg)
update_progress(stats['total_satellites'], stats['total_satellites'], "Завершено")
logger.info(f"{log_prefix} Обработка завершена. Итого: создано {stats['created']}, обновлено {stats['updated']}, ошибок {len(stats['errors'])}")
return stats
if update_progress:
update_progress(stats['total_satellites'], stats['total_satellites'], "Завершено")
def link_lyngsat_to_sources():
return stats
def link_lyngsat_to_sources():
pass pass

View File

@@ -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",)

View File

@@ -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 для множественного выбора)',

View File

@@ -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='Стандарт'),
),
]

View File

@@ -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,

View File

@@ -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>' +

View File

@@ -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>
@@ -292,18 +295,21 @@ document.addEventListener('DOMContentLoaded', function() {
{ {
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);
} }
} }
} }
@@ -321,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
@@ -837,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();
}); });

View File

@@ -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 }}

View File

@@ -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>

View File

@@ -138,7 +138,7 @@ def find_mirror_satellites(mirror_names: list) -> list:
Алгоритм: Алгоритм:
1. Для каждого имени зеркала: 1. Для каждого имени зеркала:
- Обрезать пробелы и привести к нижнему регистру - Обрезать пробелы и привести к нижнему регистру
- Найти все спутники, в имени которых содержится это имя - Найти все спутники, в имени или альтернативном имени которых содержится это имя
2. Вернуть список найденных спутников 2. Вернуть список найденных спутников
Args: Args:
@@ -147,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:
@@ -159,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)

View File

@@ -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,

View File

@@ -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 "-",

View File

@@ -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,