Добавил разделение по исчтоника и поправил функцию импорта из Excel csv
This commit is contained in:
99
dbapp/fix_objitems_without_source.py
Normal file
99
dbapp/fix_objitems_without_source.py
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
"""
|
||||||
|
Скрипт для исправления ObjItems без связи с Source.
|
||||||
|
|
||||||
|
Для каждого ObjItem без source:
|
||||||
|
1. Получить координаты из geo_obj
|
||||||
|
2. Найти ближайший Source (по coords_average)
|
||||||
|
3. Если расстояние <= 0.5 градуса, связать ObjItem с этим Source
|
||||||
|
4. Иначе создать новый Source с coords_average = координаты geo_obj
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import django
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dbapp.settings")
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from mainapp.models import ObjItem, Source, CustomUser
|
||||||
|
from django.contrib.gis.geos import Point
|
||||||
|
from django.contrib.gis.measure import D
|
||||||
|
from django.contrib.gis.db.models.functions import Distance
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_distance_degrees(coord1, coord2):
|
||||||
|
"""Вычисляет расстояние между двумя координатами в градусах."""
|
||||||
|
import math
|
||||||
|
|
||||||
|
lon1, lat1 = coord1
|
||||||
|
lon2, lat2 = coord2
|
||||||
|
|
||||||
|
return math.sqrt((lon2 - lon1) ** 2 + (lat2 - lat1) ** 2)
|
||||||
|
|
||||||
|
|
||||||
|
def fix_objitems_without_source():
|
||||||
|
"""Исправляет ObjItems без связи с Source."""
|
||||||
|
|
||||||
|
# Получаем пользователя по умолчанию
|
||||||
|
default_user = CustomUser.objects.get(id=1)
|
||||||
|
|
||||||
|
# Получаем все ObjItems без source
|
||||||
|
objitems_without_source = ObjItem.objects.filter(source__isnull=True)
|
||||||
|
total_count = objitems_without_source.count()
|
||||||
|
|
||||||
|
print(f"Найдено {total_count} ObjItems без source")
|
||||||
|
|
||||||
|
if total_count == 0:
|
||||||
|
print("Нечего исправлять!")
|
||||||
|
return
|
||||||
|
|
||||||
|
fixed_count = 0
|
||||||
|
new_sources_count = 0
|
||||||
|
|
||||||
|
for objitem in objitems_without_source:
|
||||||
|
# Проверяем, есть ли geo_obj
|
||||||
|
if not hasattr(objitem, 'geo_obj') or not objitem.geo_obj or not objitem.geo_obj.coords:
|
||||||
|
print(f"ObjItem {objitem.id} не имеет geo_obj или координат, пропускаем")
|
||||||
|
continue
|
||||||
|
|
||||||
|
geo_coords = objitem.geo_obj.coords
|
||||||
|
coord_tuple = (geo_coords.x, geo_coords.y)
|
||||||
|
|
||||||
|
# Ищем ближайший Source
|
||||||
|
sources_with_coords = Source.objects.filter(coords_average__isnull=False)
|
||||||
|
|
||||||
|
closest_source = None
|
||||||
|
min_distance = float('inf')
|
||||||
|
|
||||||
|
for source in sources_with_coords:
|
||||||
|
source_coord = (source.coords_average.x, source.coords_average.y)
|
||||||
|
distance = calculate_distance_degrees(coord_tuple, source_coord)
|
||||||
|
|
||||||
|
if distance < min_distance:
|
||||||
|
min_distance = distance
|
||||||
|
closest_source = source
|
||||||
|
|
||||||
|
# Если нашли близкий Source (расстояние <= 0.5 градуса)
|
||||||
|
if closest_source and min_distance <= 0.5:
|
||||||
|
objitem.source = closest_source
|
||||||
|
objitem.save()
|
||||||
|
print(f"ObjItem {objitem.id} связан с Source {closest_source.id} (расстояние: {min_distance:.4f}°)")
|
||||||
|
fixed_count += 1
|
||||||
|
else:
|
||||||
|
# Создаем новый Source
|
||||||
|
new_source = Source.objects.create(
|
||||||
|
coords_average=Point(coord_tuple, srid=4326),
|
||||||
|
created_by=default_user
|
||||||
|
)
|
||||||
|
objitem.source = new_source
|
||||||
|
objitem.save()
|
||||||
|
print(f"ObjItem {objitem.id} связан с новым Source {new_source.id}")
|
||||||
|
fixed_count += 1
|
||||||
|
new_sources_count += 1
|
||||||
|
|
||||||
|
print(f"\nГотово!")
|
||||||
|
print(f"Исправлено ObjItems: {fixed_count}")
|
||||||
|
print(f"Создано новых Source: {new_sources_count}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
fix_objitems_without_source()
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-11-12 19:41
|
||||||
|
|
||||||
|
import django.contrib.gis.db.models.fields
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('mainapp', '0002_initial'),
|
||||||
|
('mapsapp', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='source',
|
||||||
|
name='coords_average',
|
||||||
|
field=django.contrib.gis.db.models.fields.PointField(blank=True, help_text='Усреднённые координаты, полученные от в ходе геолокации (WGS84)', null=True, srid=4326, verbose_name='Координаты ГЛ'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='objitem',
|
||||||
|
name='source',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='source_objitems', to='mainapp.source', verbose_name='ИРИ'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='objitem',
|
||||||
|
name='transponder',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='Транспондер, с помощью которого была получена точка', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transponder_objitems', to='mapsapp.transponders', verbose_name='Транспондер'),
|
||||||
|
),
|
||||||
|
]
|
||||||
453
dbapp/mainapp/templates/mainapp/source_list.html
Normal file
453
dbapp/mainapp/templates/mainapp/source_list.html
Normal file
@@ -0,0 +1,453 @@
|
|||||||
|
{% extends 'mainapp/base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Список источников{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.table-responsive tr.selected {
|
||||||
|
background-color: #d4edff;
|
||||||
|
}
|
||||||
|
.sticky-top {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid px-3">
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<h2>Список источников</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toolbar -->
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex flex-wrap align-items-center gap-3">
|
||||||
|
<!-- Search bar -->
|
||||||
|
<div style="min-width: 200px; flex-grow: 1; max-width: 400px;">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" id="toolbar-search" class="form-control" placeholder="Поиск по ID..."
|
||||||
|
value="{{ search_query|default:'' }}">
|
||||||
|
<button type="button" class="btn btn-outline-primary"
|
||||||
|
onclick="performSearch()">Найти</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary"
|
||||||
|
onclick="clearSearch()">Очистить</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Items per page select -->
|
||||||
|
<div>
|
||||||
|
<label for="items-per-page" class="form-label mb-0">Показать:</label>
|
||||||
|
<select name="items_per_page" id="items-per-page"
|
||||||
|
class="form-select form-select-sm d-inline-block" style="width: auto;"
|
||||||
|
onchange="updateItemsPerPage()">
|
||||||
|
{% for option in available_items_per_page %}
|
||||||
|
<option value="{{ option }}" {% if option == items_per_page %}selected{% endif %}>
|
||||||
|
{{ option }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter Toggle Button -->
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="offcanvas"
|
||||||
|
data-bs-target="#offcanvasFilters" aria-controls="offcanvasFilters">
|
||||||
|
<i class="bi bi-funnel"></i> Фильтры
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
<div class="ms-auto">
|
||||||
|
{% include 'mainapp/components/_pagination.html' with page_obj=page_obj show_info=True %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Offcanvas Filter Panel -->
|
||||||
|
<div class="offcanvas offcanvas-start" tabindex="-1" id="offcanvasFilters" aria-labelledby="offcanvasFiltersLabel">
|
||||||
|
<div class="offcanvas-header">
|
||||||
|
<h5 class="offcanvas-title" id="offcanvasFiltersLabel">Фильтры</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Закрыть"></button>
|
||||||
|
</div>
|
||||||
|
<div class="offcanvas-body">
|
||||||
|
<form method="get" id="filter-form">
|
||||||
|
<!-- Coordinates Average Filter -->
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label">Усредненные координаты:</label>
|
||||||
|
<div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input class="form-check-input" type="checkbox" name="has_coords_average" id="has_coords_average_1"
|
||||||
|
value="1" {% if has_coords_average == '1' %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="has_coords_average_1">Есть</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input class="form-check-input" type="checkbox" name="has_coords_average" id="has_coords_average_0"
|
||||||
|
value="0" {% if has_coords_average == '0' %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="has_coords_average_0">Нет</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kubsat Coordinates Filter -->
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label">Координаты Кубсата:</label>
|
||||||
|
<div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input class="form-check-input" type="checkbox" name="has_coords_kupsat" id="has_coords_kupsat_1"
|
||||||
|
value="1" {% if has_coords_kupsat == '1' %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="has_coords_kupsat_1">Есть</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input class="form-check-input" type="checkbox" name="has_coords_kupsat" id="has_coords_kupsat_0"
|
||||||
|
value="0" {% if has_coords_kupsat == '0' %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="has_coords_kupsat_0">Нет</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Valid Coordinates Filter -->
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label">Координаты оперативников:</label>
|
||||||
|
<div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input class="form-check-input" type="checkbox" name="has_coords_valid" id="has_coords_valid_1"
|
||||||
|
value="1" {% if has_coords_valid == '1' %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="has_coords_valid_1">Есть</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input class="form-check-input" type="checkbox" name="has_coords_valid" id="has_coords_valid_0"
|
||||||
|
value="0" {% if has_coords_valid == '0' %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="has_coords_valid_0">Нет</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Reference Coordinates Filter -->
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label">Координаты справочные:</label>
|
||||||
|
<div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input class="form-check-input" type="checkbox" name="has_coords_reference" id="has_coords_reference_1"
|
||||||
|
value="1" {% if has_coords_reference == '1' %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="has_coords_reference_1">Есть</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input class="form-check-input" type="checkbox" name="has_coords_reference" id="has_coords_reference_0"
|
||||||
|
value="0" {% if has_coords_reference == '0' %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="has_coords_reference_0">Нет</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ObjItem Count Filter -->
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label">Количество ObjItem:</label>
|
||||||
|
<input type="number" name="objitem_count_min" class="form-control form-control-sm mb-1"
|
||||||
|
placeholder="От" value="{{ objitem_count_min|default:'' }}">
|
||||||
|
<input type="number" name="objitem_count_max" class="form-control form-control-sm"
|
||||||
|
placeholder="До" value="{{ objitem_count_max|default:'' }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Filter -->
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label">Дата создания:</label>
|
||||||
|
<input type="date" name="date_from" id="date_from" class="form-control form-control-sm mb-1"
|
||||||
|
placeholder="От" value="{{ date_from|default:'' }}">
|
||||||
|
<input type="date" name="date_to" id="date_to" class="form-control form-control-sm"
|
||||||
|
placeholder="До" value="{{ date_to|default:'' }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Apply Filters and Reset Buttons -->
|
||||||
|
<div class="d-grid gap-2 mt-2">
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm">Применить</button>
|
||||||
|
<a href="?" class="btn btn-secondary btn-sm">Сбросить</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Table -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive" style="max-height: 75vh; overflow-y: auto;">
|
||||||
|
<table class="table table-striped table-hover table-sm" style="font-size: 0.85rem;">
|
||||||
|
<thead class="table-dark sticky-top">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="text-center" style="min-width: 60px;">
|
||||||
|
<a href="javascript:void(0)" onclick="updateSort('id')" class="text-white text-decoration-none">
|
||||||
|
ID
|
||||||
|
{% if sort == 'id' %}
|
||||||
|
<i class="bi bi-arrow-up"></i>
|
||||||
|
{% elif sort == '-id' %}
|
||||||
|
<i class="bi bi-arrow-down"></i>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<th scope="col" style="min-width: 150px;">Усредненные координаты</th>
|
||||||
|
<th scope="col" style="min-width: 150px;">Координаты Кубсата</th>
|
||||||
|
<th scope="col" style="min-width: 150px;">Координаты оперативников</th>
|
||||||
|
<th scope="col" style="min-width: 150px;">Координаты справочные</th>
|
||||||
|
<th scope="col" class="text-center" style="min-width: 100px;">
|
||||||
|
<a href="javascript:void(0)" onclick="updateSort('objitem_count')" class="text-white text-decoration-none">
|
||||||
|
Кол-во ObjItem
|
||||||
|
{% if sort == 'objitem_count' %}
|
||||||
|
<i class="bi bi-arrow-up"></i>
|
||||||
|
{% elif sort == '-objitem_count' %}
|
||||||
|
<i class="bi bi-arrow-down"></i>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<th scope="col" style="min-width: 120px;">
|
||||||
|
<a href="javascript:void(0)" onclick="updateSort('created_at')" class="text-white text-decoration-none">
|
||||||
|
Создано
|
||||||
|
{% if sort == 'created_at' %}
|
||||||
|
<i class="bi bi-arrow-up"></i>
|
||||||
|
{% elif sort == '-created_at' %}
|
||||||
|
<i class="bi bi-arrow-down"></i>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<th scope="col" style="min-width: 120px;">
|
||||||
|
<a href="javascript:void(0)" onclick="updateSort('updated_at')" class="text-white text-decoration-none">
|
||||||
|
Обновлено
|
||||||
|
{% if sort == 'updated_at' %}
|
||||||
|
<i class="bi bi-arrow-up"></i>
|
||||||
|
{% elif sort == '-updated_at' %}
|
||||||
|
<i class="bi bi-arrow-down"></i>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="text-center" style="min-width: 100px;">Детали</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for source in processed_sources %}
|
||||||
|
<tr>
|
||||||
|
<td class="text-center">{{ source.id }}</td>
|
||||||
|
<td>{{ source.coords_average }}</td>
|
||||||
|
<td>{{ source.coords_kupsat }}</td>
|
||||||
|
<td>{{ source.coords_valid }}</td>
|
||||||
|
<td>{{ source.coords_reference }}</td>
|
||||||
|
<td class="text-center">{{ source.objitem_count }}</td>
|
||||||
|
<td>{{ source.created_at|date:"d.m.Y H:i" }}</td>
|
||||||
|
<td>{{ source.updated_at|date:"d.m.Y H:i" }}</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||||
|
onclick="showSourceDetails({{ source.id }})">
|
||||||
|
<i class="bi bi-eye"></i> Показать
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="9" class="text-center text-muted">Нет данных для отображения</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal for Source Details -->
|
||||||
|
<div class="modal fade" id="sourceDetailsModal" tabindex="-1" aria-labelledby="sourceDetailsModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-xl">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="sourceDetailsModalLabel">Детали источника #<span id="modalSourceId"></span></h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div id="modalLoadingSpinner" class="text-center py-4">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Загрузка...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="modalErrorMessage" class="alert alert-danger" style="display: none;"></div>
|
||||||
|
<div id="modalContent" style="display: none;">
|
||||||
|
<h6>Связанные объекты (<span id="objitemCount">0</span>):</h6>
|
||||||
|
<div class="table-responsive" style="max-height: 60vh; overflow-y: auto;">
|
||||||
|
<table class="table table-striped table-hover table-sm">
|
||||||
|
<thead class="table-light sticky-top">
|
||||||
|
<tr>
|
||||||
|
<th>Имя</th>
|
||||||
|
<th>Спутник</th>
|
||||||
|
<th>Частота, МГц</th>
|
||||||
|
<th>Полоса, МГц</th>
|
||||||
|
<th>Поляризация</th>
|
||||||
|
<th>Сим. скорость, БОД</th>
|
||||||
|
<th>Модуляция</th>
|
||||||
|
<th>ОСШ</th>
|
||||||
|
<th>Время ГЛ</th>
|
||||||
|
<th>Местоположение</th>
|
||||||
|
<th>Координаты ГЛ</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="objitemTableBody">
|
||||||
|
<!-- Data will be loaded here via AJAX -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="modalNoData" class="text-center text-muted py-4" style="display: none;">
|
||||||
|
Нет связанных объектов
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
// Search functionality
|
||||||
|
function performSearch() {
|
||||||
|
const searchValue = document.getElementById('toolbar-search').value.trim();
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
|
if (searchValue) {
|
||||||
|
urlParams.set('search', searchValue);
|
||||||
|
} else {
|
||||||
|
urlParams.delete('search');
|
||||||
|
}
|
||||||
|
|
||||||
|
urlParams.delete('page');
|
||||||
|
window.location.search = urlParams.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSearch() {
|
||||||
|
document.getElementById('toolbar-search').value = '';
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
urlParams.delete('search');
|
||||||
|
urlParams.delete('page');
|
||||||
|
window.location.search = urlParams.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Enter key in search input
|
||||||
|
document.getElementById('toolbar-search').addEventListener('keypress', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
performSearch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Items per page functionality
|
||||||
|
function updateItemsPerPage() {
|
||||||
|
const itemsPerPage = document.getElementById('items-per-page').value;
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
urlParams.set('items_per_page', itemsPerPage);
|
||||||
|
urlParams.delete('page');
|
||||||
|
window.location.search = urlParams.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sorting functionality
|
||||||
|
function updateSort(field) {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const currentSort = urlParams.get('sort');
|
||||||
|
|
||||||
|
let newSort;
|
||||||
|
if (currentSort === field) {
|
||||||
|
newSort = '-' + field;
|
||||||
|
} else if (currentSort === '-' + field) {
|
||||||
|
newSort = field;
|
||||||
|
} else {
|
||||||
|
newSort = field;
|
||||||
|
}
|
||||||
|
|
||||||
|
urlParams.set('sort', newSort);
|
||||||
|
urlParams.delete('page');
|
||||||
|
window.location.search = urlParams.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show source details in modal
|
||||||
|
function showSourceDetails(sourceId) {
|
||||||
|
// Update modal title
|
||||||
|
document.getElementById('modalSourceId').textContent = sourceId;
|
||||||
|
|
||||||
|
// Show loading spinner, hide content and error
|
||||||
|
document.getElementById('modalLoadingSpinner').style.display = 'block';
|
||||||
|
document.getElementById('modalContent').style.display = 'none';
|
||||||
|
document.getElementById('modalNoData').style.display = 'none';
|
||||||
|
document.getElementById('modalErrorMessage').style.display = 'none';
|
||||||
|
|
||||||
|
// Open modal
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('sourceDetailsModal'));
|
||||||
|
modal.show();
|
||||||
|
|
||||||
|
// Fetch data from API
|
||||||
|
fetch(`/api/source/${sourceId}/objitems/`)
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 404) {
|
||||||
|
throw new Error('Источник не найден');
|
||||||
|
} else {
|
||||||
|
throw new Error('Ошибка при загрузке данных');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
// Hide loading spinner
|
||||||
|
document.getElementById('modalLoadingSpinner').style.display = 'none';
|
||||||
|
|
||||||
|
if (data.objitems && data.objitems.length > 0) {
|
||||||
|
// Show content
|
||||||
|
document.getElementById('modalContent').style.display = 'block';
|
||||||
|
document.getElementById('objitemCount').textContent = data.objitems.length;
|
||||||
|
|
||||||
|
// Populate table
|
||||||
|
const tbody = document.getElementById('objitemTableBody');
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
data.objitems.forEach(objitem => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
row.innerHTML = `
|
||||||
|
<td>${objitem.name}</td>
|
||||||
|
<td>${objitem.satellite_name}</td>
|
||||||
|
<td>${objitem.frequency}</td>
|
||||||
|
<td>${objitem.freq_range}</td>
|
||||||
|
<td>${objitem.polarization}</td>
|
||||||
|
<td>${objitem.bod_velocity}</td>
|
||||||
|
<td>${objitem.modulation}</td>
|
||||||
|
<td>${objitem.snr}</td>
|
||||||
|
<td>${objitem.geo_timestamp}</td>
|
||||||
|
<td>${objitem.geo_location}</td>
|
||||||
|
<td>${objitem.geo_coords}</td>
|
||||||
|
`;
|
||||||
|
tbody.appendChild(row);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Show no data message
|
||||||
|
document.getElementById('modalNoData').style.display = 'block';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
// Hide loading spinner
|
||||||
|
document.getElementById('modalLoadingSpinner').style.display = 'none';
|
||||||
|
|
||||||
|
// Show error message
|
||||||
|
const errorDiv = document.getElementById('modalErrorMessage');
|
||||||
|
errorDiv.textContent = error.message;
|
||||||
|
errorDiv.style.display = 'block';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -6,7 +6,7 @@ from . import views
|
|||||||
app_name = 'mainapp'
|
app_name = 'mainapp'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', views.HomePageView.as_view(), name='home'), # Home page that redirects based on auth
|
path('', views.SourceListView.as_view(), name='home'), # Source list page
|
||||||
path('objitems/', views.ObjItemListView.as_view(), name='objitem_list'), # Objects list page
|
path('objitems/', views.ObjItemListView.as_view(), name='objitem_list'), # Objects list page
|
||||||
path('actions/', views.ActionsPageView.as_view(), name='actions'), # Move actions to a separate page
|
path('actions/', views.ActionsPageView.as_view(), name='actions'), # Move actions to a separate page
|
||||||
path('excel-data', views.LoadExcelDataView.as_view(), name='load_excel_data'),
|
path('excel-data', views.LoadExcelDataView.as_view(), name='load_excel_data'),
|
||||||
@@ -23,6 +23,7 @@ urlpatterns = [
|
|||||||
path('link-lyngsat/', views.LinkLyngsatSourcesView.as_view(), name='link_lyngsat'),
|
path('link-lyngsat/', views.LinkLyngsatSourcesView.as_view(), name='link_lyngsat'),
|
||||||
path('api/lyngsat/<int:lyngsat_id>/', views.LyngsatDataAPIView.as_view(), name='lyngsat_data_api'),
|
path('api/lyngsat/<int:lyngsat_id>/', views.LyngsatDataAPIView.as_view(), name='lyngsat_data_api'),
|
||||||
path('api/sigma-parameter/<int:parameter_id>/', views.SigmaParameterDataAPIView.as_view(), name='sigma_parameter_data_api'),
|
path('api/sigma-parameter/<int:parameter_id>/', views.SigmaParameterDataAPIView.as_view(), name='sigma_parameter_data_api'),
|
||||||
|
path('api/source/<int:source_id>/objitems/', views.SourceObjItemsAPIView.as_view(), name='source_objitems_api'),
|
||||||
path('kubsat-excel/', views.ProcessKubsatView.as_view(), name='kubsat_excel'),
|
path('kubsat-excel/', views.ProcessKubsatView.as_view(), name='kubsat_excel'),
|
||||||
path('object/create/', views.ObjItemCreateView.as_view(), name='objitem_create'),
|
path('object/create/', views.ObjItemCreateView.as_view(), name='objitem_create'),
|
||||||
path('object/<int:pk>/edit/', views.ObjItemUpdateView.as_view(), name='objitem_update'),
|
path('object/<int:pk>/edit/', views.ObjItemUpdateView.as_view(), name='objitem_update'),
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ from .models import (
|
|||||||
Polarization,
|
Polarization,
|
||||||
Satellite,
|
Satellite,
|
||||||
SigmaParameter,
|
SigmaParameter,
|
||||||
|
Source,
|
||||||
Standard,
|
Standard,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -78,139 +79,233 @@ def remove_str(s: str):
|
|||||||
|
|
||||||
|
|
||||||
def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
|
def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
|
||||||
|
"""
|
||||||
|
Импортирует данные из DataFrame с группировкой близких координат.
|
||||||
|
|
||||||
|
Алгоритм:
|
||||||
|
1. Извлечь все координаты и данные строк из DataFrame
|
||||||
|
2. Создать список необработанных записей (координата + данные строки)
|
||||||
|
3. Пока список не пуст:
|
||||||
|
a. Взять первую запись из списка
|
||||||
|
b. Создать новый Source с coords_average = эта координата
|
||||||
|
c. Создать ObjItem для этой записи и связать с Source
|
||||||
|
d. Удалить запись из списка
|
||||||
|
e. Для каждой оставшейся записи в списке:
|
||||||
|
- Вычислить расстояние от её координаты до coords_average
|
||||||
|
- Если расстояние <= 0.5 градуса:
|
||||||
|
* Вычислить новое среднее ИНКРЕМЕНТАЛЬНО:
|
||||||
|
new_avg = (coords_average + current_coord) / 2
|
||||||
|
* Обновить coords_average в Source
|
||||||
|
* Создать ObjItem для этой записи и связать с Source
|
||||||
|
* Удалить запись из списка
|
||||||
|
- Иначе: пропустить и проверить следующую запись
|
||||||
|
4. Сохранить все изменения в БД
|
||||||
|
|
||||||
|
Важно: Среднее вычисляется инкрементально - каждая новая точка
|
||||||
|
усредняется с текущим средним, а не со всеми точками кластера.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
df: DataFrame с данными
|
||||||
|
sat: объект Satellite
|
||||||
|
current_user: текущий пользователь (optional)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: количество созданных Source
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
df.rename(columns={"Модуляция ": "Модуляция"}, inplace=True)
|
df.rename(columns={"Модуляция ": "Модуляция"}, inplace=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
|
|
||||||
consts = get_all_constants()
|
consts = get_all_constants()
|
||||||
df.fillna(-1, inplace=True)
|
df.fillna(-1, inplace=True)
|
||||||
for stroka in df.iterrows():
|
|
||||||
geo_point = Point(coords_transform(stroka[1]["Координаты"]), srid=4326)
|
# Шаг 1: Извлечь все координаты и данные строк из DataFrame
|
||||||
valid_point = None
|
unprocessed_records = []
|
||||||
kupsat_point = None
|
|
||||||
|
for idx, row in df.iterrows():
|
||||||
try:
|
try:
|
||||||
if (
|
# Извлекаем координату
|
||||||
stroka[1]["Координаты объекта"] != -1
|
coord_tuple = coords_transform(row["Координаты"])
|
||||||
and stroka[1]["Координаты Кубсата"] != "+"
|
|
||||||
):
|
# Сохраняем запись с координатой и данными строки
|
||||||
if (
|
unprocessed_records.append({"coord": coord_tuple, "row": row, "index": idx})
|
||||||
"ИРИ" not in stroka[1]["Координаты объекта"]
|
except Exception as e:
|
||||||
and "БЛА" not in stroka[1]["Координаты объекта"]
|
print(f"Ошибка при обработке строки {idx}: {e}")
|
||||||
):
|
continue
|
||||||
valid_point = list(
|
|
||||||
map(
|
user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
|
||||||
float,
|
source_count = 0
|
||||||
stroka[1]["Координаты объекта"]
|
|
||||||
.replace(",", ".")
|
# Шаг 3: Цикл обработки пока список не пуст
|
||||||
.split(". "),
|
while unprocessed_records:
|
||||||
)
|
# Шаг 3a: Взять первую запись из списка
|
||||||
)
|
first_record = unprocessed_records.pop(0)
|
||||||
valid_point = Point(valid_point[1], valid_point[0], srid=4326)
|
first_coord = first_record["coord"]
|
||||||
if (
|
|
||||||
stroka[1]["Координаты Кубсата"] != -1
|
# Шаг 3b: Создать новый Source с coords_average = эта координата
|
||||||
and stroka[1]["Координаты Кубсата"] != "+"
|
source = Source.objects.create(
|
||||||
):
|
coords_average=Point(first_coord, srid=4326), created_by=user_to_use
|
||||||
kupsat_point = list(
|
)
|
||||||
map(
|
source_count += 1
|
||||||
float,
|
|
||||||
stroka[1]["Координаты Кубсата"].replace(",", ".").split(". "),
|
# Шаг 3c: Создать ObjItem для этой записи и связать с Source
|
||||||
)
|
_create_objitem_from_row(first_record["row"], sat, source, user_to_use, consts)
|
||||||
|
|
||||||
|
# Шаг 3e: Для каждой оставшейся записи в списке
|
||||||
|
records_to_remove = []
|
||||||
|
|
||||||
|
for i, record in enumerate(unprocessed_records):
|
||||||
|
current_coord = record["coord"]
|
||||||
|
|
||||||
|
# Вычислить расстояние от координаты до coords_average
|
||||||
|
current_avg = (source.coords_average.x, source.coords_average.y)
|
||||||
|
distance = calculate_distance_degrees(current_avg, current_coord)
|
||||||
|
|
||||||
|
# Если расстояние <= 0.5 градуса
|
||||||
|
if distance <= 0.5:
|
||||||
|
# Вычислить новое среднее ИНКРЕМЕНТАЛЬНО
|
||||||
|
new_avg = calculate_average_coords_incremental(
|
||||||
|
current_avg, current_coord
|
||||||
)
|
)
|
||||||
kupsat_point = Point(kupsat_point[1], kupsat_point[0], srid=4326)
|
|
||||||
except KeyError:
|
|
||||||
print("В таблице нет столбцов с координатами кубсата")
|
|
||||||
try:
|
|
||||||
polarization_obj, _ = Polarization.objects.get_or_create(
|
|
||||||
name=stroka[1]["Поляризация"].strip()
|
|
||||||
)
|
|
||||||
except KeyError:
|
|
||||||
polarization_obj, _ = Polarization.objects.get_or_create(name="-")
|
|
||||||
freq = remove_str(stroka[1]["Частота, МГц"])
|
|
||||||
freq_line = remove_str(stroka[1]["Полоса, МГц"])
|
|
||||||
v = remove_str(stroka[1]["Символьная скорость, БОД"])
|
|
||||||
try:
|
|
||||||
mod_obj, _ = Modulation.objects.get_or_create(
|
|
||||||
name=stroka[1]["Модуляция"].strip()
|
|
||||||
)
|
|
||||||
except AttributeError:
|
|
||||||
mod_obj, _ = Modulation.objects.get_or_create(name="-")
|
|
||||||
snr = remove_str(stroka[1]["ОСШ"])
|
|
||||||
date = stroka[1]["Дата"].date()
|
|
||||||
time_ = stroka[1]["Время"]
|
|
||||||
if isinstance(time_, str):
|
|
||||||
time_ = time_.strip()
|
|
||||||
time_ = time(0, 0, 0)
|
|
||||||
timestamp = datetime.combine(date, time_)
|
|
||||||
current_mirrors = []
|
|
||||||
mirror_1 = stroka[1]["Зеркало 1"].strip().split("\n")
|
|
||||||
mirror_2 = stroka[1]["Зеркало 2"].strip().split("\n")
|
|
||||||
if len(mirror_1) > 1:
|
|
||||||
for mir in mirror_1:
|
|
||||||
mir_obj, _ = Mirror.objects.get_or_create(name=mir.strip())
|
|
||||||
current_mirrors.append(mir.strip())
|
|
||||||
elif mirror_1[0] not in consts[3]:
|
|
||||||
mir_obj, _ = Mirror.objects.get_or_create(name=mirror_1[0].strip())
|
|
||||||
current_mirrors.append(mirror_1[0].strip())
|
|
||||||
if len(mirror_2) > 1:
|
|
||||||
for mir in mirror_2:
|
|
||||||
mir_obj, _ = Mirror.objects.get_or_create(name=mir.strip())
|
|
||||||
current_mirrors.append(mir.strip())
|
|
||||||
elif mirror_2[0] not in consts[3]:
|
|
||||||
mir_obj, _ = Mirror.objects.get_or_create(name=mirror_2[0].strip())
|
|
||||||
current_mirrors.append(mirror_2[0].strip())
|
|
||||||
location = stroka[1]["Местоопределение"].strip()
|
|
||||||
comment = stroka[1]["Комментарий"]
|
|
||||||
source = stroka[1]["Объект наблюдения"]
|
|
||||||
user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
|
|
||||||
|
|
||||||
geo, _ = Geo.objects.get_or_create(
|
# Обновить coords_average в Source
|
||||||
timestamp=timestamp,
|
source.coords_average = Point(new_avg, srid=4326)
|
||||||
coords=geo_point,
|
source.save()
|
||||||
defaults={
|
|
||||||
"coords_kupsat": kupsat_point,
|
|
||||||
"coords_valid": valid_point,
|
|
||||||
"location": location,
|
|
||||||
"comment": comment,
|
|
||||||
"is_average": (comment != -1.0),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
geo.save()
|
|
||||||
geo.mirrors.set(Mirror.objects.filter(name__in=current_mirrors))
|
|
||||||
|
|
||||||
# Check if ObjItem with same geo already exists
|
# Создать ObjItem для этой записи и связать с Source
|
||||||
existing_obj_item = ObjItem.objects.filter(geo_obj=geo).first()
|
_create_objitem_from_row(
|
||||||
if existing_obj_item:
|
record["row"], sat, source, user_to_use, consts
|
||||||
# Check if parameter with same values exists for this object
|
)
|
||||||
if (
|
|
||||||
hasattr(existing_obj_item, 'parameter_obj') and
|
# Пометить запись для удаления
|
||||||
existing_obj_item.parameter_obj and
|
records_to_remove.append(i)
|
||||||
existing_obj_item.parameter_obj.id_satellite == sat and
|
|
||||||
existing_obj_item.parameter_obj.polarization == polarization_obj and
|
# Удалить обработанные записи из списка (в обратном порядке, чтобы не сбить индексы)
|
||||||
existing_obj_item.parameter_obj.frequency == freq and
|
for i in reversed(records_to_remove):
|
||||||
existing_obj_item.parameter_obj.freq_range == freq_line and
|
unprocessed_records.pop(i)
|
||||||
existing_obj_item.parameter_obj.bod_velocity == v and
|
|
||||||
existing_obj_item.parameter_obj.modulation == mod_obj and
|
return source_count
|
||||||
existing_obj_item.parameter_obj.snr == snr
|
|
||||||
):
|
|
||||||
# Skip creating duplicate
|
def _create_objitem_from_row(row, sat, source, user_to_use, consts):
|
||||||
continue
|
"""
|
||||||
|
Вспомогательная функция для создания ObjItem из строки DataFrame.
|
||||||
# Create new ObjItem and Parameter
|
|
||||||
obj_item = ObjItem.objects.create(name=source, created_by=user_to_use)
|
Args:
|
||||||
|
row: строка DataFrame
|
||||||
vch_load_obj = Parameter.objects.create(
|
sat: объект Satellite
|
||||||
id_satellite=sat,
|
source: объект Source для связи
|
||||||
polarization=polarization_obj,
|
user_to_use: пользователь для created_by
|
||||||
frequency=freq,
|
consts: константы из get_all_constants()
|
||||||
freq_range=freq_line,
|
"""
|
||||||
bod_velocity=v,
|
# Извлекаем координату
|
||||||
modulation=mod_obj,
|
geo_point = Point(coords_transform(row["Координаты"]), srid=4326)
|
||||||
snr=snr,
|
|
||||||
objitem=obj_item
|
# Обработка поляризации
|
||||||
|
try:
|
||||||
|
polarization_obj, _ = Polarization.objects.get_or_create(
|
||||||
|
name=row["Поляризация"].strip()
|
||||||
)
|
)
|
||||||
|
except KeyError:
|
||||||
geo.objitem = obj_item
|
polarization_obj, _ = Polarization.objects.get_or_create(name="-")
|
||||||
geo.save()
|
|
||||||
|
# Обработка ВЧ параметров
|
||||||
|
freq = remove_str(row["Частота, МГц"])
|
||||||
|
freq_line = remove_str(row["Полоса, МГц"])
|
||||||
|
v = remove_str(row["Символьная скорость, БОД"])
|
||||||
|
|
||||||
|
try:
|
||||||
|
mod_obj, _ = Modulation.objects.get_or_create(name=row["Модуляция"].strip())
|
||||||
|
except AttributeError:
|
||||||
|
mod_obj, _ = Modulation.objects.get_or_create(name="-")
|
||||||
|
|
||||||
|
snr = remove_str(row["ОСШ"])
|
||||||
|
|
||||||
|
# Обработка времени
|
||||||
|
date = row["Дата"].date()
|
||||||
|
time_ = row["Время"]
|
||||||
|
if isinstance(time_, str):
|
||||||
|
time_ = time_.strip()
|
||||||
|
time_ = time(0, 0, 0)
|
||||||
|
timestamp = datetime.combine(date, time_)
|
||||||
|
|
||||||
|
# Обработка зеркал
|
||||||
|
current_mirrors = []
|
||||||
|
mirror_1 = row["Зеркало 1"].strip().split("\n")
|
||||||
|
mirror_2 = row["Зеркало 2"].strip().split("\n")
|
||||||
|
|
||||||
|
if len(mirror_1) > 1:
|
||||||
|
for mir in mirror_1:
|
||||||
|
Mirror.objects.get_or_create(name=mir.strip())
|
||||||
|
current_mirrors.append(mir.strip())
|
||||||
|
elif mirror_1[0] not in consts[3]:
|
||||||
|
Mirror.objects.get_or_create(name=mirror_1[0].strip())
|
||||||
|
current_mirrors.append(mirror_1[0].strip())
|
||||||
|
|
||||||
|
if len(mirror_2) > 1:
|
||||||
|
for mir in mirror_2:
|
||||||
|
Mirror.objects.get_or_create(name=mir.strip())
|
||||||
|
current_mirrors.append(mir.strip())
|
||||||
|
elif mirror_2[0] not in consts[3]:
|
||||||
|
Mirror.objects.get_or_create(name=mirror_2[0].strip())
|
||||||
|
current_mirrors.append(mirror_2[0].strip())
|
||||||
|
|
||||||
|
location = row["Местоопределение"].strip()
|
||||||
|
comment = row["Комментарий"]
|
||||||
|
source_name = row["Объект наблюдения"]
|
||||||
|
|
||||||
|
# Создаем Geo объект (БЕЗ coords_kupsat и coords_valid)
|
||||||
|
geo, _ = Geo.objects.get_or_create(
|
||||||
|
timestamp=timestamp,
|
||||||
|
coords=geo_point,
|
||||||
|
defaults={
|
||||||
|
"location": location,
|
||||||
|
"comment": comment,
|
||||||
|
"is_average": (comment != -1.0),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
geo.save()
|
||||||
|
geo.mirrors.set(Mirror.objects.filter(name__in=current_mirrors))
|
||||||
|
|
||||||
|
# Проверяем, существует ли уже ObjItem с таким же geo
|
||||||
|
existing_obj_item = ObjItem.objects.filter(geo_obj=geo).first()
|
||||||
|
if existing_obj_item:
|
||||||
|
# Проверяем, существует ли parameter с такими же значениями
|
||||||
|
if (
|
||||||
|
hasattr(existing_obj_item, "parameter_obj")
|
||||||
|
and existing_obj_item.parameter_obj
|
||||||
|
and existing_obj_item.parameter_obj.id_satellite == sat
|
||||||
|
and existing_obj_item.parameter_obj.polarization == polarization_obj
|
||||||
|
and existing_obj_item.parameter_obj.frequency == freq
|
||||||
|
and existing_obj_item.parameter_obj.freq_range == freq_line
|
||||||
|
and existing_obj_item.parameter_obj.bod_velocity == v
|
||||||
|
and existing_obj_item.parameter_obj.modulation == mod_obj
|
||||||
|
and existing_obj_item.parameter_obj.snr == snr
|
||||||
|
):
|
||||||
|
# Пропускаем создание дубликата
|
||||||
|
return
|
||||||
|
|
||||||
|
# Создаем новый ObjItem и связываем с Source
|
||||||
|
obj_item = ObjItem.objects.create(
|
||||||
|
name=source_name, source=source, created_by=user_to_use
|
||||||
|
)
|
||||||
|
|
||||||
|
# Создаем Parameter
|
||||||
|
Parameter.objects.create(
|
||||||
|
id_satellite=sat,
|
||||||
|
polarization=polarization_obj,
|
||||||
|
frequency=freq,
|
||||||
|
freq_range=freq_line,
|
||||||
|
bod_velocity=v,
|
||||||
|
modulation=mod_obj,
|
||||||
|
snr=snr,
|
||||||
|
objitem=obj_item,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Связываем geo с objitem
|
||||||
|
geo.objitem = obj_item
|
||||||
|
geo.save()
|
||||||
|
|
||||||
|
|
||||||
def add_satellite_list():
|
def add_satellite_list():
|
||||||
@@ -281,6 +376,33 @@ def get_point_from_json(filepath: str):
|
|||||||
|
|
||||||
|
|
||||||
def get_points_from_csv(file_content, current_user=None):
|
def get_points_from_csv(file_content, current_user=None):
|
||||||
|
"""
|
||||||
|
Импортирует данные из CSV с группировкой близких координат.
|
||||||
|
|
||||||
|
Улучшенный алгоритм с учетом существующих Source:
|
||||||
|
1. Извлечь все координаты и данные строк из DataFrame
|
||||||
|
2. Создать список необработанных записей (координата + данные строки)
|
||||||
|
3. Получить все существующие Source из БД
|
||||||
|
4. Для каждой записи:
|
||||||
|
a. Проверить, существует ли дубликат (координаты + частота)
|
||||||
|
b. Если дубликат найден, пропустить запись
|
||||||
|
c. Найти ближайший существующий Source (расстояние <= 0.5 градуса)
|
||||||
|
d. Если найден:
|
||||||
|
- Обновить coords_average этого Source (инкрементально)
|
||||||
|
- Создать ObjItem и связать с этим Source
|
||||||
|
e. Если не найден:
|
||||||
|
- Создать новый Source
|
||||||
|
- Создать ObjItem и связать с новым Source
|
||||||
|
- Добавить новый Source в список существующих
|
||||||
|
5. Сохранить все изменения в БД
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_content: содержимое CSV файла
|
||||||
|
current_user: текущий пользователь (optional)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: количество созданных Source
|
||||||
|
"""
|
||||||
df = pd.read_csv(
|
df = pd.read_csv(
|
||||||
io.StringIO(file_content),
|
io.StringIO(file_content),
|
||||||
sep=";",
|
sep=";",
|
||||||
@@ -308,68 +430,196 @@ def get_points_from_csv(file_content, current_user=None):
|
|||||||
.astype(float)
|
.astype(float)
|
||||||
)
|
)
|
||||||
df["time"] = pd.to_datetime(df["time"], format="%d.%m.%Y %H:%M:%S")
|
df["time"] = pd.to_datetime(df["time"], format="%d.%m.%Y %H:%M:%S")
|
||||||
for row in df.iterrows():
|
|
||||||
row = row[1]
|
|
||||||
match row["obj"].split(" ")[-1]:
|
|
||||||
case "V":
|
|
||||||
pol = "Вертикальная"
|
|
||||||
case "H":
|
|
||||||
pol = "Горизонтальная"
|
|
||||||
case "R":
|
|
||||||
pol = "Правая"
|
|
||||||
case "L":
|
|
||||||
pol = "Левая"
|
|
||||||
case _:
|
|
||||||
pol = "-"
|
|
||||||
pol_obj, _ = Polarization.objects.get_or_create(name=pol)
|
|
||||||
sat_obj, _ = Satellite.objects.get_or_create(
|
|
||||||
name=row["sat"], defaults={"norad": row["norad_id"]}
|
|
||||||
)
|
|
||||||
mir_1_obj, _ = Mirror.objects.get_or_create(name=row["mir_1"])
|
|
||||||
mir_2_obj, _ = Mirror.objects.get_or_create(name=row["mir_2"])
|
|
||||||
mir_lst = [row["mir_1"], row["mir_2"]]
|
|
||||||
if not pd.isna(row["mir_3"]):
|
|
||||||
mir_3_obj, _ = Mirror.objects.get_or_create(name=row["mir_3"])
|
|
||||||
user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
|
|
||||||
|
|
||||||
geo_obj, _ = Geo.objects.get_or_create(
|
# Шаг 1: Извлечь все координаты и данные строк из DataFrame
|
||||||
timestamp=row["time"],
|
records = []
|
||||||
coords=Point(row["lon"], row["lat"], srid=4326),
|
|
||||||
defaults={
|
|
||||||
"is_average": False,
|
|
||||||
# 'id_user_add': user_to_use,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
geo_obj.mirrors.set(Mirror.objects.filter(name__in=mir_lst))
|
|
||||||
|
|
||||||
# Check if ObjItem with same geo already exists
|
for idx, row in df.iterrows():
|
||||||
existing_obj_item = ObjItem.objects.filter(geo_obj=geo_obj).first()
|
try:
|
||||||
if existing_obj_item:
|
# Извлекаем координату из колонок lat и lon
|
||||||
# Check if parameter with same values exists for this object
|
coord_tuple = (row["lon"], row["lat"])
|
||||||
if (
|
|
||||||
hasattr(existing_obj_item, 'parameter_obj') and
|
# Сохраняем запись с координатой и данными строки
|
||||||
existing_obj_item.parameter_obj and
|
records.append({"coord": coord_tuple, "row": row, "index": idx})
|
||||||
existing_obj_item.parameter_obj.id_satellite == sat_obj and
|
except Exception as e:
|
||||||
existing_obj_item.parameter_obj.polarization == pol_obj and
|
print(f"Ошибка при обработке строки {idx}: {e}")
|
||||||
existing_obj_item.parameter_obj.frequency == row["freq"] and
|
continue
|
||||||
existing_obj_item.parameter_obj.freq_range == row["f_range"]
|
|
||||||
):
|
user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
|
||||||
# Skip creating duplicate
|
|
||||||
continue
|
# Шаг 3: Получить все существующие Source из БД
|
||||||
|
existing_sources = list(Source.objects.filter(coords_average__isnull=False))
|
||||||
|
new_sources_count = 0
|
||||||
|
added_count = 0
|
||||||
|
skipped_count = 0
|
||||||
|
|
||||||
|
# Шаг 4: Обработка каждой записи
|
||||||
|
for record in records:
|
||||||
|
current_coord = record["coord"]
|
||||||
|
row = record["row"]
|
||||||
|
|
||||||
# Create new ObjItem and Parameter
|
# Шаг 4a: Проверить, существует ли дубликат (координаты + частота)
|
||||||
obj_item = ObjItem.objects.create(name=row["obj"], created_by=user_to_use)
|
if _is_duplicate_objitem(current_coord, row["freq"], row["f_range"]):
|
||||||
|
skipped_count += 1
|
||||||
|
continue
|
||||||
|
|
||||||
vch_load_obj = Parameter.objects.create(
|
# Шаг 4c: Найти ближайший существующий Source
|
||||||
id_satellite=sat_obj,
|
closest_source = None
|
||||||
polarization=pol_obj,
|
min_distance = float('inf')
|
||||||
frequency=row["freq"],
|
|
||||||
freq_range=row["f_range"],
|
|
||||||
objitem=obj_item
|
|
||||||
)
|
|
||||||
|
|
||||||
geo_obj.objitem = obj_item
|
for source in existing_sources:
|
||||||
geo_obj.save()
|
source_coord = (source.coords_average.x, source.coords_average.y)
|
||||||
|
distance = calculate_distance_degrees(source_coord, current_coord)
|
||||||
|
|
||||||
|
if distance < min_distance:
|
||||||
|
min_distance = distance
|
||||||
|
closest_source = source
|
||||||
|
|
||||||
|
# Шаг 4d: Если найден близкий Source (расстояние <= 0.5 градуса)
|
||||||
|
if closest_source and min_distance <= 0.5:
|
||||||
|
# Обновить coords_average инкрементально
|
||||||
|
current_avg = (closest_source.coords_average.x, closest_source.coords_average.y)
|
||||||
|
new_avg = calculate_average_coords_incremental(current_avg, current_coord)
|
||||||
|
closest_source.coords_average = Point(new_avg, srid=4326)
|
||||||
|
closest_source.save()
|
||||||
|
|
||||||
|
# Создать ObjItem и связать с существующим Source
|
||||||
|
_create_objitem_from_csv_row(row, closest_source, user_to_use)
|
||||||
|
added_count += 1
|
||||||
|
else:
|
||||||
|
# Шаг 4e: Создать новый Source
|
||||||
|
new_source = Source.objects.create(
|
||||||
|
coords_average=Point(current_coord, srid=4326),
|
||||||
|
created_by=user_to_use
|
||||||
|
)
|
||||||
|
new_sources_count += 1
|
||||||
|
|
||||||
|
# Создать ObjItem и связать с новым Source
|
||||||
|
_create_objitem_from_csv_row(row, new_source, user_to_use)
|
||||||
|
added_count += 1
|
||||||
|
|
||||||
|
# Добавить новый Source в список существующих
|
||||||
|
existing_sources.append(new_source)
|
||||||
|
|
||||||
|
print(f"Импорт завершен: создано {new_sources_count} новых источников, "
|
||||||
|
f"добавлено {added_count} точек, пропущено {skipped_count} дубликатов")
|
||||||
|
|
||||||
|
return new_sources_count
|
||||||
|
|
||||||
|
|
||||||
|
def _is_duplicate_objitem(coord_tuple, frequency, freq_range, tolerance=0.001):
|
||||||
|
"""
|
||||||
|
Проверяет, существует ли уже ObjItem с такими же координатами и частотой.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
coord_tuple: кортеж (lon, lat) координат
|
||||||
|
frequency: частота в МГц
|
||||||
|
freq_range: полоса частот в МГц
|
||||||
|
tolerance: допуск для сравнения координат в градусах (по умолчанию 0.001 ≈ 100м)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True если дубликат найден, False иначе
|
||||||
|
"""
|
||||||
|
# Ищем ObjItems с близкими координатами через geo_obj
|
||||||
|
nearby_objitems = ObjItem.objects.filter(
|
||||||
|
geo_obj__coords__isnull=False
|
||||||
|
).select_related('parameter_obj', 'geo_obj')
|
||||||
|
|
||||||
|
for objitem in nearby_objitems:
|
||||||
|
if not objitem.geo_obj or not objitem.geo_obj.coords:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Проверяем расстояние между координатами
|
||||||
|
geo_coord = (objitem.geo_obj.coords.x, objitem.geo_obj.coords.y)
|
||||||
|
distance = calculate_distance_degrees(coord_tuple, geo_coord)
|
||||||
|
|
||||||
|
if distance <= tolerance:
|
||||||
|
# Координаты совпадают, проверяем частоту
|
||||||
|
if hasattr(objitem, 'parameter_obj') and objitem.parameter_obj:
|
||||||
|
param = objitem.parameter_obj
|
||||||
|
# Проверяем совпадение частоты с небольшим допуском (0.1 МГц)
|
||||||
|
if (abs(param.frequency - frequency) < 0.1 and
|
||||||
|
abs(param.freq_range - freq_range) < 0.1):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _create_objitem_from_csv_row(row, source, user_to_use):
|
||||||
|
"""
|
||||||
|
Вспомогательная функция для создания ObjItem из строки CSV DataFrame.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
row: строка DataFrame
|
||||||
|
source: объект Source для связи
|
||||||
|
user_to_use: пользователь для created_by
|
||||||
|
"""
|
||||||
|
# Определяем поляризацию
|
||||||
|
match row["obj"].split(" ")[-1]:
|
||||||
|
case "V":
|
||||||
|
pol = "Вертикальная"
|
||||||
|
case "H":
|
||||||
|
pol = "Горизонтальная"
|
||||||
|
case "R":
|
||||||
|
pol = "Правая"
|
||||||
|
case "L":
|
||||||
|
pol = "Левая"
|
||||||
|
case _:
|
||||||
|
pol = "-"
|
||||||
|
|
||||||
|
pol_obj, _ = Polarization.objects.get_or_create(name=pol)
|
||||||
|
sat_obj, _ = Satellite.objects.get_or_create(
|
||||||
|
name=row["sat"], defaults={"norad": row["norad_id"]}
|
||||||
|
)
|
||||||
|
mir_1_obj, _ = Mirror.objects.get_or_create(name=row["mir_1"])
|
||||||
|
mir_2_obj, _ = Mirror.objects.get_or_create(name=row["mir_2"])
|
||||||
|
mir_lst = [row["mir_1"], row["mir_2"]]
|
||||||
|
if not pd.isna(row["mir_3"]):
|
||||||
|
mir_3_obj, _ = Mirror.objects.get_or_create(name=row["mir_3"])
|
||||||
|
mir_lst.append(row["mir_3"])
|
||||||
|
|
||||||
|
# Создаем Geo объект (БЕЗ coords_kupsat и coords_valid)
|
||||||
|
geo_obj, _ = Geo.objects.get_or_create(
|
||||||
|
timestamp=row["time"],
|
||||||
|
coords=Point(row["lon"], row["lat"], srid=4326),
|
||||||
|
defaults={
|
||||||
|
"is_average": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
geo_obj.mirrors.set(Mirror.objects.filter(name__in=mir_lst))
|
||||||
|
|
||||||
|
# Проверяем, существует ли уже ObjItem с таким же geo
|
||||||
|
existing_obj_item = ObjItem.objects.filter(geo_obj=geo_obj).first()
|
||||||
|
if existing_obj_item:
|
||||||
|
# Проверяем, существует ли parameter с такими же значениями
|
||||||
|
if (
|
||||||
|
hasattr(existing_obj_item, "parameter_obj")
|
||||||
|
and existing_obj_item.parameter_obj
|
||||||
|
and existing_obj_item.parameter_obj.id_satellite == sat_obj
|
||||||
|
and existing_obj_item.parameter_obj.polarization == pol_obj
|
||||||
|
and existing_obj_item.parameter_obj.frequency == row["freq"]
|
||||||
|
and existing_obj_item.parameter_obj.freq_range == row["f_range"]
|
||||||
|
):
|
||||||
|
# Пропускаем создание дубликата
|
||||||
|
return
|
||||||
|
|
||||||
|
# Создаем новый ObjItem и связываем с Source
|
||||||
|
obj_item = ObjItem.objects.create(
|
||||||
|
name=row["obj"], source=source, created_by=user_to_use
|
||||||
|
)
|
||||||
|
|
||||||
|
# Создаем Parameter
|
||||||
|
Parameter.objects.create(
|
||||||
|
id_satellite=sat_obj,
|
||||||
|
polarization=pol_obj,
|
||||||
|
frequency=row["freq"],
|
||||||
|
freq_range=row["f_range"],
|
||||||
|
objitem=obj_item,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Связываем geo с objitem
|
||||||
|
geo_obj.objitem = obj_item
|
||||||
|
geo_obj.save()
|
||||||
|
|
||||||
|
|
||||||
def get_vch_load_from_html(file, sat: Satellite) -> None:
|
def get_vch_load_from_html(file, sat: Satellite) -> None:
|
||||||
@@ -450,13 +700,13 @@ def get_vch_load_from_html(file, sat: Satellite) -> None:
|
|||||||
def get_frequency_tolerance_percent(freq_range_mhz: float) -> float:
|
def get_frequency_tolerance_percent(freq_range_mhz: float) -> float:
|
||||||
"""
|
"""
|
||||||
Определяет процент погрешности центральной частоты в зависимости от полосы частот.
|
Определяет процент погрешности центральной частоты в зависимости от полосы частот.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
freq_range_mhz (float): Полоса частот в МГц
|
freq_range_mhz (float): Полоса частот в МГц
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
float: Процент погрешности для центральной частоты
|
float: Процент погрешности для центральной частоты
|
||||||
|
|
||||||
Диапазоны:
|
Диапазоны:
|
||||||
- 0 - 0.5 МГц (0 - 500 кГц): 0.1%
|
- 0 - 0.5 МГц (0 - 500 кГц): 0.1%
|
||||||
- 0.5 - 1.5 МГц (500 кГц - 1.5 МГц): 0.5%
|
- 0.5 - 1.5 МГц (500 кГц - 1.5 МГц): 0.5%
|
||||||
@@ -481,62 +731,66 @@ def compare_and_link_vch_load(
|
|||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Привязывает SigmaParameter к Parameter на основе совпадения параметров.
|
Привязывает SigmaParameter к Parameter на основе совпадения параметров.
|
||||||
|
|
||||||
Погрешность центральной частоты определяется автоматически в зависимости от полосы частот:
|
Погрешность центральной частоты определяется автоматически в зависимости от полосы частот:
|
||||||
- 0-500 кГц: 0.1%
|
- 0-500 кГц: 0.1%
|
||||||
- 500 кГц-1.5 МГц: 0.5%
|
- 500 кГц-1.5 МГц: 0.5%
|
||||||
- 1.5-5 МГц: 1%
|
- 1.5-5 МГц: 1%
|
||||||
- 5-10 МГц: 2%
|
- 5-10 МГц: 2%
|
||||||
- >10 МГц: 5%
|
- >10 МГц: 5%
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
sat_id (Satellite): Спутник для фильтрации
|
sat_id (Satellite): Спутник для фильтрации
|
||||||
eps_freq (float): Не используется (оставлен для обратной совместимости)
|
eps_freq (float): Не используется (оставлен для обратной совместимости)
|
||||||
eps_frange (float): Погрешность полосы частот в процентах
|
eps_frange (float): Погрешность полосы частот в процентах
|
||||||
ku_range (float): Не используется (оставлен для обратной совместимости)
|
ku_range (float): Не используется (оставлен для обратной совместимости)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
tuple: (количество объектов, количество привязок)
|
tuple: (количество объектов, количество привязок)
|
||||||
"""
|
"""
|
||||||
# Получаем все ObjItem с Parameter для данного спутника
|
# Получаем все ObjItem с Parameter для данного спутника
|
||||||
item_obj = ObjItem.objects.filter(
|
item_obj = ObjItem.objects.filter(
|
||||||
parameter_obj__id_satellite=sat_id
|
parameter_obj__id_satellite=sat_id
|
||||||
).select_related('parameter_obj', 'parameter_obj__polarization')
|
).select_related("parameter_obj", "parameter_obj__polarization")
|
||||||
|
|
||||||
vch_sigma = SigmaParameter.objects.filter(
|
vch_sigma = SigmaParameter.objects.filter(id_satellite=sat_id).select_related(
|
||||||
id_satellite=sat_id
|
"polarization"
|
||||||
).select_related('polarization')
|
)
|
||||||
|
|
||||||
link_count = 0
|
link_count = 0
|
||||||
obj_count = item_obj.count()
|
obj_count = item_obj.count()
|
||||||
|
|
||||||
for obj in item_obj:
|
for obj in item_obj:
|
||||||
vch_load = obj.parameter_obj
|
vch_load = obj.parameter_obj
|
||||||
|
|
||||||
# Пропускаем объекты с некорректной частотой
|
# Пропускаем объекты с некорректной частотой
|
||||||
if not vch_load or vch_load.frequency == -1.0:
|
if not vch_load or vch_load.frequency == -1.0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Определяем погрешность частоты на основе полосы
|
# Определяем погрешность частоты на основе полосы
|
||||||
freq_tolerance_percent = get_frequency_tolerance_percent(vch_load.freq_range)
|
freq_tolerance_percent = get_frequency_tolerance_percent(vch_load.freq_range)
|
||||||
|
|
||||||
# Вычисляем допустимое отклонение частоты в МГц
|
# Вычисляем допустимое отклонение частоты в МГц
|
||||||
freq_tolerance_mhz = vch_load.frequency * freq_tolerance_percent / 100
|
freq_tolerance_mhz = vch_load.frequency * freq_tolerance_percent / 100
|
||||||
|
|
||||||
# Вычисляем допустимое отклонение полосы в МГц
|
# Вычисляем допустимое отклонение полосы в МГц
|
||||||
frange_tolerance_mhz = vch_load.freq_range * eps_frange / 100
|
frange_tolerance_mhz = vch_load.freq_range * eps_frange / 100
|
||||||
|
|
||||||
for sigma in vch_sigma:
|
for sigma in vch_sigma:
|
||||||
# Проверяем совпадение по всем параметрам
|
# Проверяем совпадение по всем параметрам
|
||||||
freq_match = abs(sigma.transfer_frequency - vch_load.frequency) <= freq_tolerance_mhz
|
freq_match = (
|
||||||
frange_match = abs(sigma.freq_range - vch_load.freq_range) <= frange_tolerance_mhz
|
abs(sigma.transfer_frequency - vch_load.frequency) <= freq_tolerance_mhz
|
||||||
|
)
|
||||||
|
frange_match = (
|
||||||
|
abs(sigma.freq_range - vch_load.freq_range) <= frange_tolerance_mhz
|
||||||
|
)
|
||||||
pol_match = sigma.polarization == vch_load.polarization
|
pol_match = sigma.polarization == vch_load.polarization
|
||||||
|
|
||||||
if freq_match and frange_match and pol_match:
|
if freq_match and frange_match and pol_match:
|
||||||
sigma.parameter = vch_load
|
sigma.parameter = vch_load
|
||||||
sigma.save()
|
sigma.save()
|
||||||
link_count += 1
|
link_count += 1
|
||||||
|
|
||||||
return obj_count, link_count
|
return obj_count, link_count
|
||||||
|
|
||||||
|
|
||||||
@@ -614,6 +868,92 @@ def kub_report(data_in: io.StringIO) -> pd.DataFrame:
|
|||||||
return df
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Утилиты для работы с координатами
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_distance_degrees(coord1: tuple, coord2: tuple) -> float:
|
||||||
|
"""
|
||||||
|
Вычисляет расстояние между двумя координатами в градусах.
|
||||||
|
|
||||||
|
Использует простую евклидову метрику для малых расстояний.
|
||||||
|
Подходит для определения близости точек в радиусе до нескольких градусов.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
coord1 (tuple): Первая координата в формате (longitude, latitude)
|
||||||
|
coord2 (tuple): Вторая координата в формате (longitude, latitude)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: Расстояние в градусах
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> dist = calculate_distance_degrees((37.62, 55.75), (37.63, 55.76))
|
||||||
|
>>> print(f"{dist:.4f}") # ~0.0141 градусов
|
||||||
|
0.0141
|
||||||
|
|
||||||
|
>>> dist = calculate_distance_degrees((37.62, 55.75), (37.62, 55.75))
|
||||||
|
>>> print(dist) # Одинаковые координаты
|
||||||
|
0.0
|
||||||
|
"""
|
||||||
|
lon1, lat1 = coord1
|
||||||
|
lon2, lat2 = coord2
|
||||||
|
|
||||||
|
# Простая евклидова метрика для малых расстояний
|
||||||
|
# Для более точных расчетов на больших расстояниях можно использовать формулу гаверсинуса
|
||||||
|
delta_lon = lon2 - lon1
|
||||||
|
delta_lat = lat2 - lat1
|
||||||
|
|
||||||
|
distance = (delta_lon**2 + delta_lat**2) ** 0.5
|
||||||
|
|
||||||
|
return distance
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_average_coords_incremental(
|
||||||
|
current_average: tuple, new_coord: tuple
|
||||||
|
) -> tuple:
|
||||||
|
"""
|
||||||
|
Вычисляет новое среднее между текущим средним и новой координатой.
|
||||||
|
|
||||||
|
Использует инкрементальное усреднение: каждая новая точка усредняется
|
||||||
|
с текущим средним, а не со всеми точками кластера. Это упрощенный подход,
|
||||||
|
где новое среднее = (текущее_среднее + новая_координата) / 2.
|
||||||
|
|
||||||
|
Важно: Это НЕ среднее арифметическое всех точек кластера, а инкрементальное
|
||||||
|
усреднение между двумя точками (текущим средним и новой точкой).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
current_average (tuple): Текущее среднее в формате (longitude, latitude)
|
||||||
|
new_coord (tuple): Новая координата в формате (longitude, latitude)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: Новое среднее в формате (longitude, latitude)
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> avg1 = (37.62, 55.75) # Первая точка
|
||||||
|
>>> avg2 = calculate_average_coords_incremental(avg1, (37.63, 55.76))
|
||||||
|
>>> print(avg2)
|
||||||
|
(37.625, 55.755)
|
||||||
|
|
||||||
|
>>> avg3 = calculate_average_coords_incremental(avg2, (37.64, 55.77))
|
||||||
|
>>> print(avg3)
|
||||||
|
(37.6325, 55.7625)
|
||||||
|
|
||||||
|
>>> # Проверка: среднее между одинаковыми точками
|
||||||
|
>>> avg = calculate_average_coords_incremental((37.62, 55.75), (37.62, 55.75))
|
||||||
|
>>> print(avg)
|
||||||
|
(37.62, 55.75)
|
||||||
|
"""
|
||||||
|
current_lon, current_lat = current_average
|
||||||
|
new_lon, new_lat = new_coord
|
||||||
|
|
||||||
|
# Инкрементальное усреднение: (current + new) / 2
|
||||||
|
avg_lon = (current_lon + new_lon) / 2
|
||||||
|
avg_lat = (current_lat + new_lat) / 2
|
||||||
|
|
||||||
|
return (avg_lon, avg_lat)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Утилиты для форматирования
|
# Утилиты для форматирования
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
@@ -104,15 +104,6 @@ class ActionsPageView(View):
|
|||||||
return render(request, "mainapp/login_required.html")
|
return render(request, "mainapp/login_required.html")
|
||||||
|
|
||||||
|
|
||||||
class HomePageView(View):
|
|
||||||
def get(self, request):
|
|
||||||
if request.user.is_authenticated:
|
|
||||||
# Redirect to objitem list if authenticated
|
|
||||||
return redirect("mainapp:objitem_list")
|
|
||||||
else:
|
|
||||||
return render(request, "mainapp/login_required.html")
|
|
||||||
|
|
||||||
|
|
||||||
class LoadExcelDataView(LoginRequiredMixin, FormMessageMixin, FormView):
|
class LoadExcelDataView(LoginRequiredMixin, FormMessageMixin, FormView):
|
||||||
template_name = "mainapp/add_data_from_excel.html"
|
template_name = "mainapp/add_data_from_excel.html"
|
||||||
form_class = LoadExcelData
|
form_class = LoadExcelData
|
||||||
@@ -558,6 +549,275 @@ class SigmaParameterDataAPIView(LoginRequiredMixin, View):
|
|||||||
return JsonResponse({'error': str(e)}, status=500)
|
return JsonResponse({'error': str(e)}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
class SourceObjItemsAPIView(LoginRequiredMixin, View):
|
||||||
|
"""API для получения списка ObjItem, связанных с источником"""
|
||||||
|
|
||||||
|
def get(self, request, source_id):
|
||||||
|
from .models import Source
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Загружаем Source с prefetch_related для ObjItem
|
||||||
|
source = Source.objects.prefetch_related(
|
||||||
|
'source_objitems',
|
||||||
|
'source_objitems__parameter_obj',
|
||||||
|
'source_objitems__parameter_obj__id_satellite',
|
||||||
|
'source_objitems__parameter_obj__polarization',
|
||||||
|
'source_objitems__parameter_obj__modulation',
|
||||||
|
'source_objitems__geo_obj'
|
||||||
|
).get(id=source_id)
|
||||||
|
|
||||||
|
# Получаем все связанные ObjItem, отсортированные по created_at
|
||||||
|
objitems = source.source_objitems.all().order_by('created_at')
|
||||||
|
|
||||||
|
objitems_data = []
|
||||||
|
for objitem in objitems:
|
||||||
|
# Получаем данные параметра
|
||||||
|
param = getattr(objitem, 'parameter_obj', None)
|
||||||
|
satellite_name = '-'
|
||||||
|
frequency = '-'
|
||||||
|
freq_range = '-'
|
||||||
|
polarization = '-'
|
||||||
|
bod_velocity = '-'
|
||||||
|
modulation = '-'
|
||||||
|
snr = '-'
|
||||||
|
|
||||||
|
if param:
|
||||||
|
if hasattr(param, 'id_satellite') and param.id_satellite:
|
||||||
|
satellite_name = param.id_satellite.name
|
||||||
|
frequency = f"{param.frequency:.3f}" if param.frequency is not None else '-'
|
||||||
|
freq_range = f"{param.freq_range:.3f}" if param.freq_range is not None else '-'
|
||||||
|
if hasattr(param, 'polarization') and param.polarization:
|
||||||
|
polarization = param.polarization.name
|
||||||
|
bod_velocity = f"{param.bod_velocity:.0f}" if param.bod_velocity is not None else '-'
|
||||||
|
if hasattr(param, 'modulation') and param.modulation:
|
||||||
|
modulation = param.modulation.name
|
||||||
|
snr = f"{param.snr:.0f}" if param.snr is not None else '-'
|
||||||
|
|
||||||
|
# Получаем геоданные
|
||||||
|
geo_timestamp = '-'
|
||||||
|
geo_location = '-'
|
||||||
|
geo_coords = '-'
|
||||||
|
|
||||||
|
if hasattr(objitem, 'geo_obj') and objitem.geo_obj:
|
||||||
|
if objitem.geo_obj.timestamp:
|
||||||
|
local_time = timezone.localtime(objitem.geo_obj.timestamp)
|
||||||
|
geo_timestamp = local_time.strftime("%d.%m.%Y %H:%M")
|
||||||
|
|
||||||
|
geo_location = objitem.geo_obj.location or '-'
|
||||||
|
|
||||||
|
if objitem.geo_obj.coords:
|
||||||
|
longitude = objitem.geo_obj.coords.coords[0]
|
||||||
|
latitude = objitem.geo_obj.coords.coords[1]
|
||||||
|
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||||
|
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||||
|
geo_coords = f"{lat} {lon}"
|
||||||
|
|
||||||
|
objitems_data.append({
|
||||||
|
'id': objitem.id,
|
||||||
|
'name': objitem.name or '-',
|
||||||
|
'satellite_name': satellite_name,
|
||||||
|
'frequency': frequency,
|
||||||
|
'freq_range': freq_range,
|
||||||
|
'polarization': polarization,
|
||||||
|
'bod_velocity': bod_velocity,
|
||||||
|
'modulation': modulation,
|
||||||
|
'snr': snr,
|
||||||
|
'geo_timestamp': geo_timestamp,
|
||||||
|
'geo_location': geo_location,
|
||||||
|
'geo_coords': geo_coords
|
||||||
|
})
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'source_id': source_id,
|
||||||
|
'objitems': objitems_data
|
||||||
|
})
|
||||||
|
except Source.DoesNotExist:
|
||||||
|
return JsonResponse({'error': 'Источник не найден'}, status=404)
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({'error': str(e)}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
class SourceListView(LoginRequiredMixin, View):
|
||||||
|
"""
|
||||||
|
Представление для отображения списка источников (Source).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
from .models import Source
|
||||||
|
from django.db.models import Count
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Получаем параметры пагинации
|
||||||
|
page_number, items_per_page = parse_pagination_params(request)
|
||||||
|
|
||||||
|
# Получаем параметры сортировки
|
||||||
|
sort_param = request.GET.get("sort", "-created_at")
|
||||||
|
|
||||||
|
# Получаем параметры фильтров
|
||||||
|
search_query = request.GET.get("search", "").strip()
|
||||||
|
has_coords_average = request.GET.get("has_coords_average")
|
||||||
|
has_coords_kupsat = request.GET.get("has_coords_kupsat")
|
||||||
|
has_coords_valid = request.GET.get("has_coords_valid")
|
||||||
|
has_coords_reference = request.GET.get("has_coords_reference")
|
||||||
|
objitem_count_min = request.GET.get("objitem_count_min", "").strip()
|
||||||
|
objitem_count_max = request.GET.get("objitem_count_max", "").strip()
|
||||||
|
date_from = request.GET.get("date_from", "").strip()
|
||||||
|
date_to = request.GET.get("date_to", "").strip()
|
||||||
|
|
||||||
|
# Получаем все Source объекты с оптимизацией запросов
|
||||||
|
sources = Source.objects.select_related(
|
||||||
|
'created_by__user',
|
||||||
|
'updated_by__user'
|
||||||
|
).prefetch_related(
|
||||||
|
'source_objitems',
|
||||||
|
'source_objitems__parameter_obj',
|
||||||
|
'source_objitems__geo_obj'
|
||||||
|
).annotate(
|
||||||
|
objitem_count=Count('source_objitems')
|
||||||
|
)
|
||||||
|
|
||||||
|
# Применяем фильтры
|
||||||
|
# Фильтр по наличию coords_average
|
||||||
|
if has_coords_average == "1":
|
||||||
|
sources = sources.filter(coords_average__isnull=False)
|
||||||
|
elif has_coords_average == "0":
|
||||||
|
sources = sources.filter(coords_average__isnull=True)
|
||||||
|
|
||||||
|
# Фильтр по наличию coords_kupsat
|
||||||
|
if has_coords_kupsat == "1":
|
||||||
|
sources = sources.filter(coords_kupsat__isnull=False)
|
||||||
|
elif has_coords_kupsat == "0":
|
||||||
|
sources = sources.filter(coords_kupsat__isnull=True)
|
||||||
|
|
||||||
|
# Фильтр по наличию coords_valid
|
||||||
|
if has_coords_valid == "1":
|
||||||
|
sources = sources.filter(coords_valid__isnull=False)
|
||||||
|
elif has_coords_valid == "0":
|
||||||
|
sources = sources.filter(coords_valid__isnull=True)
|
||||||
|
|
||||||
|
# Фильтр по наличию coords_reference
|
||||||
|
if has_coords_reference == "1":
|
||||||
|
sources = sources.filter(coords_reference__isnull=False)
|
||||||
|
elif has_coords_reference == "0":
|
||||||
|
sources = sources.filter(coords_reference__isnull=True)
|
||||||
|
|
||||||
|
# Фильтр по количеству ObjItem
|
||||||
|
if objitem_count_min:
|
||||||
|
try:
|
||||||
|
min_count = int(objitem_count_min)
|
||||||
|
sources = sources.filter(objitem_count__gte=min_count)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if objitem_count_max:
|
||||||
|
try:
|
||||||
|
max_count = int(objitem_count_max)
|
||||||
|
sources = sources.filter(objitem_count__lte=max_count)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Фильтр по диапазону дат создания
|
||||||
|
if date_from:
|
||||||
|
try:
|
||||||
|
date_from_obj = datetime.strptime(date_from, "%Y-%m-%d")
|
||||||
|
sources = sources.filter(created_at__gte=date_from_obj)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if date_to:
|
||||||
|
try:
|
||||||
|
from datetime import timedelta
|
||||||
|
date_to_obj = datetime.strptime(date_to, "%Y-%m-%d")
|
||||||
|
# Добавляем один день чтобы включить весь конечный день
|
||||||
|
date_to_obj = date_to_obj + timedelta(days=1)
|
||||||
|
sources = sources.filter(created_at__lt=date_to_obj)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Поиск по ID
|
||||||
|
if search_query:
|
||||||
|
try:
|
||||||
|
search_id = int(search_query)
|
||||||
|
sources = sources.filter(id=search_id)
|
||||||
|
except ValueError:
|
||||||
|
# Если не число, игнорируем
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Применяем сортировку
|
||||||
|
valid_sort_fields = {
|
||||||
|
"id": "id",
|
||||||
|
"-id": "-id",
|
||||||
|
"created_at": "created_at",
|
||||||
|
"-created_at": "-created_at",
|
||||||
|
"updated_at": "updated_at",
|
||||||
|
"-updated_at": "-updated_at",
|
||||||
|
"objitem_count": "objitem_count",
|
||||||
|
"-objitem_count": "-objitem_count",
|
||||||
|
}
|
||||||
|
|
||||||
|
if sort_param in valid_sort_fields:
|
||||||
|
sources = sources.order_by(valid_sort_fields[sort_param])
|
||||||
|
|
||||||
|
# Создаем пагинатор
|
||||||
|
paginator = Paginator(sources, items_per_page)
|
||||||
|
page_obj = paginator.get_page(page_number)
|
||||||
|
|
||||||
|
# Подготавливаем данные для отображения
|
||||||
|
processed_sources = []
|
||||||
|
for source in page_obj:
|
||||||
|
# Форматируем координаты
|
||||||
|
def format_coords(point):
|
||||||
|
if point:
|
||||||
|
longitude = point.coords[0]
|
||||||
|
latitude = point.coords[1]
|
||||||
|
lon = f"{longitude}E" if longitude > 0 else f"{abs(longitude)}W"
|
||||||
|
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
|
||||||
|
return f"{lat} {lon}"
|
||||||
|
return "-"
|
||||||
|
|
||||||
|
coords_average_str = format_coords(source.coords_average)
|
||||||
|
coords_kupsat_str = format_coords(source.coords_kupsat)
|
||||||
|
coords_valid_str = format_coords(source.coords_valid)
|
||||||
|
coords_reference_str = format_coords(source.coords_reference)
|
||||||
|
|
||||||
|
# Получаем количество связанных ObjItem
|
||||||
|
objitem_count = source.objitem_count
|
||||||
|
|
||||||
|
processed_sources.append({
|
||||||
|
'id': source.id,
|
||||||
|
'coords_average': coords_average_str,
|
||||||
|
'coords_kupsat': coords_kupsat_str,
|
||||||
|
'coords_valid': coords_valid_str,
|
||||||
|
'coords_reference': coords_reference_str,
|
||||||
|
'objitem_count': objitem_count,
|
||||||
|
'created_at': source.created_at,
|
||||||
|
'updated_at': source.updated_at,
|
||||||
|
'created_by': source.created_by,
|
||||||
|
'updated_by': source.updated_by,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Подготавливаем контекст для шаблона
|
||||||
|
context = {
|
||||||
|
'page_obj': page_obj,
|
||||||
|
'processed_sources': processed_sources,
|
||||||
|
'items_per_page': items_per_page,
|
||||||
|
'available_items_per_page': [50, 100, 500, 1000],
|
||||||
|
'sort': sort_param,
|
||||||
|
'search_query': search_query,
|
||||||
|
'has_coords_average': has_coords_average,
|
||||||
|
'has_coords_kupsat': has_coords_kupsat,
|
||||||
|
'has_coords_valid': has_coords_valid,
|
||||||
|
'has_coords_reference': has_coords_reference,
|
||||||
|
'objitem_count_min': objitem_count_min,
|
||||||
|
'objitem_count_max': objitem_count_max,
|
||||||
|
'date_from': date_from,
|
||||||
|
'date_to': date_to,
|
||||||
|
'full_width_page': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, "mainapp/source_list.html", context)
|
||||||
|
|
||||||
|
|
||||||
class ProcessKubsatView(LoginRequiredMixin, FormMessageMixin, FormView):
|
class ProcessKubsatView(LoginRequiredMixin, FormMessageMixin, FormView):
|
||||||
template_name = "mainapp/process_kubsat.html"
|
template_name = "mainapp/process_kubsat.html"
|
||||||
form_class = NewEventForm
|
form_class = NewEventForm
|
||||||
|
|||||||
Reference in New Issue
Block a user