Поправил алгоритм формирования источников
This commit is contained in:
@@ -159,38 +159,98 @@ def remove_str(s: str):
|
|||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def _find_or_create_source_by_name_and_distance(
|
||||||
|
source_name: str, sat: Satellite, coord: tuple, user
|
||||||
|
) -> Source:
|
||||||
|
"""
|
||||||
|
Находит или создает Source на основе имени источника, спутника и расстояния.
|
||||||
|
|
||||||
|
Логика:
|
||||||
|
1. Ищет все существующие Source с ObjItem, у которых:
|
||||||
|
- Совпадает спутник
|
||||||
|
- Совпадает имя (source_name)
|
||||||
|
2. Для каждого найденного Source проверяет расстояние до новой координаты
|
||||||
|
3. Если найден Source в радиусе ≤56 км:
|
||||||
|
- Возвращает его и обновляет coords_average инкрементально
|
||||||
|
4. Если не найден подходящий Source:
|
||||||
|
- Создает новый Source
|
||||||
|
|
||||||
|
Важно: Может существовать несколько Source с одинаковым именем и спутником,
|
||||||
|
но они должны быть географически разделены (>56 км друг от друга).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source_name: имя источника (например, "Turksat 3A 10967,397 [9,348] МГц V")
|
||||||
|
sat: объект Satellite
|
||||||
|
coord: координата в формате (lon, lat)
|
||||||
|
user: пользователь для created_by
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Source: найденный или созданный объект Source
|
||||||
|
"""
|
||||||
|
# Ищем все существующие ObjItem с таким же именем и спутником
|
||||||
|
existing_objitems = ObjItem.objects.filter(
|
||||||
|
name=source_name,
|
||||||
|
parameter_obj__id_satellite=sat,
|
||||||
|
source__isnull=False,
|
||||||
|
source__coords_average__isnull=False
|
||||||
|
).select_related('source', 'parameter_obj')
|
||||||
|
|
||||||
|
# Собираем уникальные Source из найденных ObjItem
|
||||||
|
existing_sources = {}
|
||||||
|
for objitem in existing_objitems:
|
||||||
|
if objitem.source.id not in existing_sources:
|
||||||
|
existing_sources[objitem.source.id] = objitem.source
|
||||||
|
|
||||||
|
# Проверяем расстояние до каждого существующего Source
|
||||||
|
closest_source = None
|
||||||
|
min_distance = float('inf')
|
||||||
|
best_new_avg = None
|
||||||
|
|
||||||
|
for source in existing_sources.values():
|
||||||
|
if source.coords_average:
|
||||||
|
source_coord = (source.coords_average.x, source.coords_average.y)
|
||||||
|
new_avg, distance = calculate_mean_coords(source_coord, coord)
|
||||||
|
|
||||||
|
if distance <= RANGE_DISTANCE and distance < min_distance:
|
||||||
|
min_distance = distance
|
||||||
|
closest_source = source
|
||||||
|
best_new_avg = new_avg
|
||||||
|
|
||||||
|
# Если найден близкий Source (≤56 км)
|
||||||
|
if closest_source:
|
||||||
|
# Обновляем coords_average инкрементально
|
||||||
|
closest_source.coords_average = Point(best_new_avg, srid=4326)
|
||||||
|
closest_source.save()
|
||||||
|
return closest_source
|
||||||
|
|
||||||
|
# Если не найден подходящий Source - создаем новый
|
||||||
|
source = Source.objects.create(
|
||||||
|
coords_average=Point(coord, srid=4326),
|
||||||
|
created_by=user
|
||||||
|
)
|
||||||
|
return source
|
||||||
|
|
||||||
|
|
||||||
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 с группировкой близких координат.
|
Импортирует данные из DataFrame с группировкой по имени источника и расстоянию.
|
||||||
|
|
||||||
Улучшенный алгоритм с учетом существующих Source:
|
Алгоритм:
|
||||||
1. Извлечь все координаты и данные строк из DataFrame
|
1. Для каждой строки DataFrame:
|
||||||
2. Создать список необработанных записей (координата + данные строки)
|
a. Извлечь имя источника (из колонки "Объект наблюдения")
|
||||||
3. Получить все существующие Source из БД
|
b. Найти подходящий Source:
|
||||||
4. Для каждой необработанной записи:
|
- Ищет все Source с таким же именем и спутником
|
||||||
a. Найти ближайший существующий Source (расстояние <= 56 км)
|
- Проверяет расстояние до каждого Source
|
||||||
b. Если найден:
|
- Если найден Source в радиусе ≤56 км - использует его
|
||||||
- Обновить coords_average этого Source (инкрементально)
|
- Иначе создает новый Source
|
||||||
- Создать ObjItem и связать с этим Source
|
c. Обновить coords_average инкрементально
|
||||||
- Удалить запись из списка необработанных
|
d. Создать ObjItem и связать с Source
|
||||||
5. Пока список необработанных записей не пуст:
|
|
||||||
a. Взять первую запись из списка
|
|
||||||
b. Создать новый Source с coords_average = эта координата
|
|
||||||
c. Создать ObjItem для этой записи и связать с Source
|
|
||||||
d. Удалить запись из списка
|
|
||||||
e. Для каждой оставшейся записи в списке:
|
|
||||||
- Вычислить расстояние от её координаты до coords_average
|
|
||||||
- Если расстояние <= 56 км:
|
|
||||||
* Вычислить новое среднее ИНКРЕМЕНТАЛЬНО:
|
|
||||||
new_avg = (coords_average + current_coord) / 2
|
|
||||||
* Обновить coords_average в Source
|
|
||||||
* Создать ObjItem для этой записи и связать с Source
|
|
||||||
* Удалить запись из списка
|
|
||||||
- Иначе: пропустить и проверить следующую запись
|
|
||||||
6. Сохранить все изменения в БД
|
|
||||||
|
|
||||||
Важно: Среднее вычисляется инкрементально - каждая новая точка
|
Важные правила:
|
||||||
усредняется с текущим средним, а не со всеми точками кластера.
|
- Источники разных спутников НЕ объединяются
|
||||||
|
- Может быть несколько Source с одинаковым именем, но разделенных географически
|
||||||
|
- Точка добавляется к Source только если расстояние ≤56 км
|
||||||
|
- Координаты усредняются инкрементально для каждого источника
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
df: DataFrame с данными
|
df: DataFrame с данными
|
||||||
@@ -208,112 +268,70 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None):
|
|||||||
consts = get_all_constants()
|
consts = get_all_constants()
|
||||||
df.fillna(-1, inplace=True)
|
df.fillna(-1, inplace=True)
|
||||||
|
|
||||||
# Шаг 1: Извлечь все координаты и данные строк из DataFrame
|
user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
|
||||||
unprocessed_records = []
|
new_sources_count = 0
|
||||||
|
added_count = 0
|
||||||
|
|
||||||
|
# Словарь для кэширования Source в рамках текущего импорта
|
||||||
|
# Ключ: (имя источника, id Source), Значение: объект Source
|
||||||
|
# Используем id в ключе, т.к. может быть несколько Source с одним именем
|
||||||
|
sources_cache = {}
|
||||||
|
|
||||||
for idx, row in df.iterrows():
|
for idx, row in df.iterrows():
|
||||||
try:
|
try:
|
||||||
# Извлекаем координату
|
# Извлекаем координату
|
||||||
coord_tuple = coords_transform(row["Координаты"])
|
coord_tuple = coords_transform(row["Координаты"])
|
||||||
|
|
||||||
# Сохраняем запись с координатой и данными строки
|
# Извлекаем имя источника
|
||||||
unprocessed_records.append({"coord": coord_tuple, "row": row, "index": idx})
|
source_name = row["Объект наблюдения"]
|
||||||
|
|
||||||
|
# Проверяем кэш: ищем подходящий Source среди закэшированных
|
||||||
|
found_in_cache = False
|
||||||
|
for cache_key, cached_source in sources_cache.items():
|
||||||
|
cached_name, cached_id = cache_key
|
||||||
|
|
||||||
|
# Проверяем имя
|
||||||
|
if cached_name != source_name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Проверяем расстояние
|
||||||
|
if cached_source.coords_average:
|
||||||
|
source_coord = (cached_source.coords_average.x, cached_source.coords_average.y)
|
||||||
|
new_avg, distance = calculate_mean_coords(source_coord, coord_tuple)
|
||||||
|
|
||||||
|
if distance <= RANGE_DISTANCE:
|
||||||
|
# Нашли подходящий Source в кэше
|
||||||
|
cached_source.coords_average = Point(new_avg, srid=4326)
|
||||||
|
cached_source.save()
|
||||||
|
source = cached_source
|
||||||
|
found_in_cache = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not found_in_cache:
|
||||||
|
# Ищем в БД или создаем новый Source
|
||||||
|
source = _find_or_create_source_by_name_and_distance(
|
||||||
|
source_name, sat, coord_tuple, user_to_use
|
||||||
|
)
|
||||||
|
|
||||||
|
# Проверяем, был ли создан новый Source
|
||||||
|
if source.created_at.timestamp() > (datetime.now().timestamp() - 1):
|
||||||
|
new_sources_count += 1
|
||||||
|
|
||||||
|
# Добавляем в кэш
|
||||||
|
sources_cache[(source_name, source.id)] = source
|
||||||
|
|
||||||
|
# Создаем ObjItem и связываем с Source
|
||||||
|
_create_objitem_from_row(row, sat, source, user_to_use, consts)
|
||||||
|
added_count += 1
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Ошибка при обработке строки {idx}: {e}")
|
print(f"Ошибка при обработке строки {idx}: {e}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
|
print(f"Импорт завершен: создано {new_sources_count} новых источников, "
|
||||||
source_count = 0
|
f"добавлено {added_count} точек")
|
||||||
added_to_existing_count = 0
|
|
||||||
|
|
||||||
# Шаг 3: Получить все существующие Source из БД
|
return new_sources_count
|
||||||
existing_sources = list(Source.objects.filter(coords_average__isnull=False))
|
|
||||||
|
|
||||||
# Шаг 4: Попытка добавить записи к существующим Source
|
|
||||||
records_to_remove = []
|
|
||||||
|
|
||||||
for i, record in enumerate(unprocessed_records):
|
|
||||||
current_coord = record["coord"]
|
|
||||||
|
|
||||||
# Найти ближайший существующий Source
|
|
||||||
closest_source = None
|
|
||||||
min_distance = float('inf')
|
|
||||||
best_new_avg = None
|
|
||||||
|
|
||||||
for source in existing_sources:
|
|
||||||
source_coord = (source.coords_average.x, source.coords_average.y)
|
|
||||||
new_avg, distance = calculate_mean_coords(source_coord, current_coord)
|
|
||||||
|
|
||||||
if distance < min_distance:
|
|
||||||
min_distance = distance
|
|
||||||
closest_source = source
|
|
||||||
best_new_avg = new_avg
|
|
||||||
|
|
||||||
# Если найден близкий Source (расстояние <= 56 км)
|
|
||||||
if closest_source and min_distance <= RANGE_DISTANCE:
|
|
||||||
# Обновить coords_average инкрементально
|
|
||||||
closest_source.coords_average = Point(best_new_avg, srid=4326)
|
|
||||||
closest_source.save()
|
|
||||||
|
|
||||||
# Создать ObjItem и связать с существующим Source
|
|
||||||
_create_objitem_from_row(
|
|
||||||
record["row"], sat, closest_source, user_to_use, consts
|
|
||||||
)
|
|
||||||
added_to_existing_count += 1
|
|
||||||
|
|
||||||
# Пометить запись для удаления
|
|
||||||
records_to_remove.append(i)
|
|
||||||
|
|
||||||
# Удалить обработанные записи из списка (в обратном порядке, чтобы не сбить индексы)
|
|
||||||
for i in reversed(records_to_remove):
|
|
||||||
unprocessed_records.pop(i)
|
|
||||||
|
|
||||||
# Шаг 5: Цикл обработки оставшихся записей - создание новых Source
|
|
||||||
while unprocessed_records:
|
|
||||||
# Шаг 5a: Взять первую запись из списка
|
|
||||||
first_record = unprocessed_records.pop(0)
|
|
||||||
first_coord = first_record["coord"]
|
|
||||||
|
|
||||||
# Шаг 5b: Создать новый Source с coords_average = эта координата
|
|
||||||
source = Source.objects.create(
|
|
||||||
coords_average=Point(first_coord, srid=4326), created_by=user_to_use
|
|
||||||
)
|
|
||||||
source_count += 1
|
|
||||||
|
|
||||||
# Шаг 5c: Создать ObjItem для этой записи и связать с Source
|
|
||||||
_create_objitem_from_row(first_record["row"], sat, source, user_to_use, consts)
|
|
||||||
|
|
||||||
# Шаг 5e: Для каждой оставшейся записи в списке
|
|
||||||
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)
|
|
||||||
new_avg, distance = calculate_mean_coords(current_avg, current_coord)
|
|
||||||
|
|
||||||
if distance <= RANGE_DISTANCE:
|
|
||||||
# Обновить coords_average в Source
|
|
||||||
source.coords_average = Point(new_avg, srid=4326)
|
|
||||||
source.save()
|
|
||||||
|
|
||||||
# Создать ObjItem для этой записи и связать с Source
|
|
||||||
_create_objitem_from_row(
|
|
||||||
record["row"], sat, source, user_to_use, consts
|
|
||||||
)
|
|
||||||
|
|
||||||
# Пометить запись для удаления
|
|
||||||
records_to_remove.append(i)
|
|
||||||
|
|
||||||
# Удалить обработанные записи из списка (в обратном порядке, чтобы не сбить индексы)
|
|
||||||
for i in reversed(records_to_remove):
|
|
||||||
unprocessed_records.pop(i)
|
|
||||||
|
|
||||||
print(f"Импорт завершен: создано {source_count} новых источников, "
|
|
||||||
f"добавлено {added_to_existing_count} точек к существующим источникам")
|
|
||||||
|
|
||||||
return source_count
|
|
||||||
|
|
||||||
|
|
||||||
def _create_objitem_from_row(row, sat, source, user_to_use, consts):
|
def _create_objitem_from_row(row, sat, source, user_to_use, consts):
|
||||||
@@ -509,24 +527,25 @@ 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 с группировкой близких координат.
|
Импортирует данные из CSV с группировкой по имени источника и расстоянию.
|
||||||
|
|
||||||
Улучшенный алгоритм с учетом существующих Source:
|
Алгоритм:
|
||||||
1. Извлечь все координаты и данные строк из DataFrame
|
1. Для каждой строки CSV:
|
||||||
2. Создать список необработанных записей (координата + данные строки)
|
a. Извлечь имя источника (из колонки "obj") и спутник
|
||||||
3. Получить все существующие Source из БД
|
b. Проверить дубликаты (координаты + частота)
|
||||||
4. Для каждой записи:
|
c. Найти подходящий Source:
|
||||||
a. Проверить, существует ли дубликат (координаты + частота)
|
- Ищет все Source с таким же именем и спутником
|
||||||
b. Если дубликат найден, пропустить запись
|
- Проверяет расстояние до каждого Source
|
||||||
c. Найти ближайший существующий Source (расстояние <= 56 км)
|
- Если найден Source в радиусе ≤56 км - использует его
|
||||||
d. Если найден:
|
- Иначе создает новый Source
|
||||||
- Обновить coords_average этого Source (инкрементально)
|
d. Обновить coords_average инкрементально
|
||||||
- Создать ObjItem и связать с этим Source
|
e. Создать ObjItem и связать с Source
|
||||||
e. Если не найден:
|
|
||||||
- Создать новый Source
|
Важные правила:
|
||||||
- Создать ObjItem и связать с новым Source
|
- Источники разных спутников НЕ объединяются
|
||||||
- Добавить новый Source в список существующих
|
- Может быть несколько Source с одинаковым именем, но разделенных географически
|
||||||
5. Сохранить все изменения в БД
|
- Точка добавляется к Source только если расстояние ≤56 км
|
||||||
|
- Координаты усредняются инкрементально для каждого источника
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
file_content: содержимое CSV файла
|
file_content: содержимое CSV файла
|
||||||
@@ -563,75 +582,76 @@ def get_points_from_csv(file_content, current_user=None):
|
|||||||
)
|
)
|
||||||
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")
|
||||||
|
|
||||||
# Шаг 1: Извлечь все координаты и данные строк из DataFrame
|
user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
|
||||||
records = []
|
new_sources_count = 0
|
||||||
|
added_count = 0
|
||||||
|
skipped_count = 0
|
||||||
|
|
||||||
|
# Словарь для кэширования Source в рамках текущего импорта
|
||||||
|
# Ключ: (имя источника, имя спутника, id Source), Значение: объект Source
|
||||||
|
sources_cache = {}
|
||||||
|
|
||||||
for idx, row in df.iterrows():
|
for idx, row in df.iterrows():
|
||||||
try:
|
try:
|
||||||
# Извлекаем координату из колонок lat и lon
|
# Извлекаем координату из колонок lat и lon
|
||||||
coord_tuple = (row["lon"], row["lat"])
|
coord_tuple = (row["lon"], row["lat"])
|
||||||
|
|
||||||
# Сохраняем запись с координатой и данными строки
|
# Извлекаем имя источника и спутника
|
||||||
records.append({"coord": coord_tuple, "row": row, "index": idx})
|
source_name = row["obj"]
|
||||||
except Exception as e:
|
sat_name = row["sat"]
|
||||||
print(f"Ошибка при обработке строки {idx}: {e}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
user_to_use = current_user if current_user else CustomUser.objects.get(id=1)
|
# Проверяем дубликаты
|
||||||
|
if _is_duplicate_objitem(coord_tuple, row["freq"], row["f_range"]):
|
||||||
# Шаг 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"]
|
|
||||||
|
|
||||||
# Шаг 4a: Проверить, существует ли дубликат (координаты + частота)
|
|
||||||
if _is_duplicate_objitem(current_coord, row["freq"], row["f_range"]):
|
|
||||||
skipped_count += 1
|
skipped_count += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Шаг 4c: Найти ближайший существующий Source
|
# Получаем или создаем объект спутника
|
||||||
closest_source = None
|
sat_obj, _ = Satellite.objects.get_or_create(
|
||||||
min_distance = float('inf')
|
name=sat_name, defaults={"norad": row["norad_id"]}
|
||||||
best_new_avg = None
|
|
||||||
|
|
||||||
for source in existing_sources:
|
|
||||||
source_coord = (source.coords_average.x, source.coords_average.y)
|
|
||||||
new_avg, distance = calculate_mean_coords(source_coord, current_coord)
|
|
||||||
|
|
||||||
if distance < min_distance:
|
|
||||||
min_distance = distance
|
|
||||||
closest_source = source
|
|
||||||
best_new_avg = new_avg
|
|
||||||
|
|
||||||
# Шаг 4d: Если найден близкий Source (расстояние <= 56 км)
|
|
||||||
if closest_source and min_distance <= RANGE_DISTANCE:
|
|
||||||
# Обновить coords_average инкрементально
|
|
||||||
closest_source.coords_average = Point(best_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
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Проверяем кэш: ищем подходящий Source среди закэшированных
|
||||||
|
found_in_cache = False
|
||||||
|
for cache_key, cached_source in sources_cache.items():
|
||||||
|
cached_name, cached_sat, cached_id = cache_key
|
||||||
|
|
||||||
|
# Проверяем имя и спутник
|
||||||
|
if cached_name != source_name or cached_sat != sat_name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Проверяем расстояние
|
||||||
|
if cached_source.coords_average:
|
||||||
|
source_coord = (cached_source.coords_average.x, cached_source.coords_average.y)
|
||||||
|
new_avg, distance = calculate_mean_coords(source_coord, coord_tuple)
|
||||||
|
|
||||||
|
if distance <= RANGE_DISTANCE:
|
||||||
|
# Нашли подходящий Source в кэше
|
||||||
|
cached_source.coords_average = Point(new_avg, srid=4326)
|
||||||
|
cached_source.save()
|
||||||
|
source = cached_source
|
||||||
|
found_in_cache = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not found_in_cache:
|
||||||
|
# Ищем в БД или создаем новый Source
|
||||||
|
source = _find_or_create_source_by_name_and_distance(
|
||||||
|
source_name, sat_obj, coord_tuple, user_to_use
|
||||||
|
)
|
||||||
|
|
||||||
|
# Проверяем, был ли создан новый Source
|
||||||
|
if source.created_at.timestamp() > (datetime.now().timestamp() - 1):
|
||||||
new_sources_count += 1
|
new_sources_count += 1
|
||||||
|
|
||||||
# Создать ObjItem и связать с новым Source
|
# Добавляем в кэш
|
||||||
_create_objitem_from_csv_row(row, new_source, user_to_use)
|
sources_cache[(source_name, sat_name, source.id)] = source
|
||||||
|
|
||||||
|
# Создаем ObjItem и связываем с Source
|
||||||
|
_create_objitem_from_csv_row(row, source, user_to_use)
|
||||||
added_count += 1
|
added_count += 1
|
||||||
|
|
||||||
# Добавить новый Source в список существующих
|
except Exception as e:
|
||||||
existing_sources.append(new_source)
|
print(f"Ошибка при обработке строки {idx}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
print(f"Импорт завершен: создано {new_sources_count} новых источников, "
|
print(f"Импорт завершен: создано {new_sources_count} новых источников, "
|
||||||
f"добавлено {added_count} точек, пропущено {skipped_count} дубликатов")
|
f"добавлено {added_count} точек, пропущено {skipped_count} дубликатов")
|
||||||
|
|||||||
Reference in New Issue
Block a user