From 9a816e62c26a754540620bbd03d21e320dced2a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=BE=D1=88=D0=BA=D0=B8=D0=BD=20=D0=A1=D0=B5=D1=80?= =?UTF-8?q?=D0=B3=D0=B5=D0=B9?= Date: Sat, 15 Nov 2025 21:54:13 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=BE=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=B0=D0=BB=D0=B3=D0=BE=D1=80=D0=B8=D1=82=D0=BC=20?= =?UTF-8?q?=D1=84=D0=BE=D1=80=D0=BC=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B8=D1=81=D1=82=D0=BE=D1=87=D0=BD=D0=B8=D0=BA?= =?UTF-8?q?=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dbapp/mainapp/utils.py | 426 +++++++++++++++++++++-------------------- 1 file changed, 223 insertions(+), 203 deletions(-) diff --git a/dbapp/mainapp/utils.py b/dbapp/mainapp/utils.py index 8dd9b53..d71025c 100644 --- a/dbapp/mainapp/utils.py +++ b/dbapp/mainapp/utils.py @@ -159,38 +159,98 @@ def remove_str(s: str): 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): """ - Импортирует данные из DataFrame с группировкой близких координат. + Импортирует данные из DataFrame с группировкой по имени источника и расстоянию. - Улучшенный алгоритм с учетом существующих Source: - 1. Извлечь все координаты и данные строк из DataFrame - 2. Создать список необработанных записей (координата + данные строки) - 3. Получить все существующие Source из БД - 4. Для каждой необработанной записи: - a. Найти ближайший существующий Source (расстояние <= 56 км) - b. Если найден: - - Обновить coords_average этого Source (инкрементально) - - Создать 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. Сохранить все изменения в БД - - Важно: Среднее вычисляется инкрементально - каждая новая точка - усредняется с текущим средним, а не со всеми точками кластера. + Алгоритм: + 1. Для каждой строки DataFrame: + a. Извлечь имя источника (из колонки "Объект наблюдения") + b. Найти подходящий Source: + - Ищет все Source с таким же именем и спутником + - Проверяет расстояние до каждого Source + - Если найден Source в радиусе ≤56 км - использует его + - Иначе создает новый Source + c. Обновить coords_average инкрементально + d. Создать ObjItem и связать с Source + + Важные правила: + - Источники разных спутников НЕ объединяются + - Может быть несколько Source с одинаковым именем, но разделенных географически + - Точка добавляется к Source только если расстояние ≤56 км + - Координаты усредняются инкрементально для каждого источника Args: df: DataFrame с данными @@ -208,112 +268,70 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite, current_user=None): consts = get_all_constants() df.fillna(-1, inplace=True) - # Шаг 1: Извлечь все координаты и данные строк из DataFrame - unprocessed_records = [] + user_to_use = current_user if current_user else CustomUser.objects.get(id=1) + new_sources_count = 0 + added_count = 0 + + # Словарь для кэширования Source в рамках текущего импорта + # Ключ: (имя источника, id Source), Значение: объект Source + # Используем id в ключе, т.к. может быть несколько Source с одним именем + sources_cache = {} for idx, row in df.iterrows(): try: # Извлекаем координату 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: print(f"Ошибка при обработке строки {idx}: {e}") continue - user_to_use = current_user if current_user else CustomUser.objects.get(id=1) - source_count = 0 - added_to_existing_count = 0 + print(f"Импорт завершен: создано {new_sources_count} новых источников, " + f"добавлено {added_count} точек") - # Шаг 3: Получить все существующие Source из БД - 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 + return new_sources_count 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): """ - Импортирует данные из CSV с группировкой близких координат. + Импортирует данные из CSV с группировкой по имени источника и расстоянию. - Улучшенный алгоритм с учетом существующих Source: - 1. Извлечь все координаты и данные строк из DataFrame - 2. Создать список необработанных записей (координата + данные строки) - 3. Получить все существующие Source из БД - 4. Для каждой записи: - a. Проверить, существует ли дубликат (координаты + частота) - b. Если дубликат найден, пропустить запись - c. Найти ближайший существующий Source (расстояние <= 56 км) - d. Если найден: - - Обновить coords_average этого Source (инкрементально) - - Создать ObjItem и связать с этим Source - e. Если не найден: - - Создать новый Source - - Создать ObjItem и связать с новым Source - - Добавить новый Source в список существующих - 5. Сохранить все изменения в БД + Алгоритм: + 1. Для каждой строки CSV: + a. Извлечь имя источника (из колонки "obj") и спутник + b. Проверить дубликаты (координаты + частота) + c. Найти подходящий Source: + - Ищет все Source с таким же именем и спутником + - Проверяет расстояние до каждого Source + - Если найден Source в радиусе ≤56 км - использует его + - Иначе создает новый Source + d. Обновить coords_average инкрементально + e. Создать ObjItem и связать с Source + + Важные правила: + - Источники разных спутников НЕ объединяются + - Может быть несколько Source с одинаковым именем, но разделенных географически + - Точка добавляется к Source только если расстояние ≤56 км + - Координаты усредняются инкрементально для каждого источника Args: 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") - # Шаг 1: Извлечь все координаты и данные строк из DataFrame - records = [] + user_to_use = current_user if current_user else CustomUser.objects.get(id=1) + new_sources_count = 0 + added_count = 0 + skipped_count = 0 + + # Словарь для кэширования Source в рамках текущего импорта + # Ключ: (имя источника, имя спутника, id Source), Значение: объект Source + sources_cache = {} for idx, row in df.iterrows(): try: # Извлекаем координату из колонок lat и lon coord_tuple = (row["lon"], row["lat"]) - - # Сохраняем запись с координатой и данными строки - records.append({"coord": coord_tuple, "row": row, "index": idx}) + + # Извлекаем имя источника и спутника + source_name = row["obj"] + sat_name = row["sat"] + + # Проверяем дубликаты + if _is_duplicate_objitem(coord_tuple, row["freq"], row["f_range"]): + skipped_count += 1 + continue + + # Получаем или создаем объект спутника + sat_obj, _ = Satellite.objects.get_or_create( + name=sat_name, defaults={"norad": row["norad_id"]} + ) + + # Проверяем кэш: ищем подходящий 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 + + # Добавляем в кэш + sources_cache[(source_name, sat_name, source.id)] = source + + # Создаем ObjItem и связываем с Source + _create_objitem_from_csv_row(row, source, user_to_use) + added_count += 1 + except Exception as e: print(f"Ошибка при обработке строки {idx}: {e}") continue - - user_to_use = current_user if current_user else CustomUser.objects.get(id=1) - - # Шаг 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 - continue - - # Шаг 4c: Найти ближайший существующий 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 - - # Шаг 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 - ) - 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} дубликатов")