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

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}")
# Находим спутник в базе
# Находим спутник в базе по имени или альтернативному имени (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}' не найден в базе данных"

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -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();
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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