/
twik
/
hakaton
Обзор
Документация
Войти
/
twik
/
hakaton
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
main.py
778 строк
34 KB
twik
upload files
23 окт 2025, 14:37
23 окт 2025, 14:37
44cb74d
Код
Авторство
О чём код?
import json, html, re from pathlib import Path from statistics import median from collections import OrderedDict, defaultdict import uuid import os import numpy as np import folium import requests from folium.plugins import Fullscreen import warnings import tkinter as tk from tkinter import filedialog, messagebox warnings.filterwarnings("ignore") # ========================= # ФУНКЦИЯ ВЫБОРА ФАЙЛА # ========================= def select_geojson_files(): """Открывает диалог выбора нескольких GeoJSON файлов""" root = tk.Tk() root.withdraw() # Скрываем основное окно file_paths = filedialog.askopenfilenames( title="Выберите GeoJSON файлы", filetypes=[ ("GeoJSON files", "*.geojson"), ("JSON files", "*.json"), ("All files", "*.*") ] ) if file_paths: paths = [Path(file_path) for file_path in file_paths] print(f"Выбрано файлов: {len(paths)}") for path in paths: print(f" - {path}") return paths else: print("Файлы не выбраны") return [] # ========================= # НАСТРОЙКИ GIGACHAT API # ========================= GIGA_AUTH_KEY = 'MDE5YTBjMGQtYWMxMC03OTU3LTgzNWYtNTI3MTVhNzE4OTg0OmE2NTg2YzQ5LTUzOTAtNDY0NC1iOWU1LTgwMTBjZjJiMGQ2ZQ==' GIGA_OAUTH_URL = 'https://ngw.devices.sberbank.ru:9443/api/v2/oauth' GIGA_API_URL = 'https://gigachat.devices.sberbank.ru/api/v1/chat/completions' # ========================= # ФУНКЦИИ GIGACHAT # ========================= def get_access_token(): """Получение Access Token для GigaChat API""" try: url = GIGA_OAUTH_URL rquid = str(uuid.uuid4()) headers = { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json', 'RqUID': rquid, 'Authorization': f'Basic {GIGA_AUTH_KEY}' } data = {'scope': 'GIGACHAT_API_PERS'} response = requests.post(url, headers=headers, data=data, verify=False, timeout=30) response.raise_for_status() token = response.json().get("access_token") if token: print(f"[GigaChat] Токен получен успешно!") return token else: print("[GigaChat] Ошибка: Токен не получен") return None except requests.exceptions.RequestException as e: print(f"[GigaChat ошибка токена] {e}") return None def generate_gigachat_recommendation(access_token, street_data): """Генерация рекомендации от GigaChat на основе расширенных данных улицы""" # Формируем расширенный промт с детальными данными об улице prompt = f""" Ты - эксперт по транспортной инфраструктуре и городскому планированию. Проанализируй данные об улице и дай конкретную, практическую рекомендацию по улучшению транспортной ситуации. КРИТИЧЕСКИЕ ДАННЫЕ ОБ УЛИЦЕ: ОСНОВНЫЕ ХАРАКТЕРИСТИКИ: - Название: {street_data.get('name', 'Не указано')} - Город/населенный пункт: {street_data.get('city', 'Не указано')} - Категория дороги: {street_data.get('road_category', 'Н/Д')} - Функциональный класс: {street_data.get('func_class', 'Н/Д')} - Ширина проезжей части: {street_data.get('width', 'Н/Д')} м ТРАНСПОРТНЫЕ ПОКАЗАТЕЛИ: - Уровень обслуживания (LOS) до: {street_data.get('cur_los', 'Н/Д')} - Уровень обслуживания (LOS) после: {street_data.get('new_los', 'Н/Д')} - Текущая загрузка: {street_data.get('cur_load', 'Н/Д')} - Новая загрузка: {street_data.get('new_load', 'Н/Д')} - Интенсивность движения: {street_data.get('intensity', 'Н/Д')} авто/час - Пропускная способность: {street_data.get('capacity', 'Н/Д')} авто/час СКОРОСТНЫЕ РЕЖИМЫ: - Максимальная разрешенная скорость: {street_data.get('max_speed', 'Н/Д')} км/ч - Средняя скорость: {street_data.get('avg_speed', 'Н/Д')} км/ч - Критическая скорость (текущая): {street_data.get('cur_speed', 'Н/Д')} км/ч - Критическая скорость (новая): {street_data.get('new_speed', 'Н/Д')} км/ч - Скорость в утренние часы: {street_data.get('morning_speed', 'Н/Д')} км/ч ОРГАНИЗАЦИЯ ДВИЖЕНИЯ: - Количество полос (прямое направление): {street_data.get('lanes_forward', 'Н/Д')} - Количество полос (обратное направление): {street_data.get('lanes_backward', 'Н/Д')} - Пешеходные переходы: {street_data.get('ped_cross', 'Н/Д')} - Регулируемые пересечения: {street_data.get('control', 'Н/Д')} - Перегрузка (Ovrld): {street_data.get('overload', 'Н/Д')} ДОПОЛНИТЕЛЬНЫЕ ФАКТОРЫ: - Наличие велодорожки: {street_data.get('bike_lane', 'Н/Д')} - Наличие тротуара: {street_data.get('sidewalk', 'Н/Д')} - Тип покрытия: {street_data.get('surface_type', 'Н/Д')} - Длина участка: {street_data.get('length', 'Н/Д')} м - Перепад высот: {street_data.get('elevation_change', 'Н/Д')} СПЕЦИАЛЬНЫЕ УСЛОВИЯ: - Наличие трамвайных путей: {street_data.get('tram_tracks', 'Н/Д')} - Платная дорога: {street_data.get('toll_road', 'Н/Д')} - Ограничения на разворот: {street_data.get('u_turn_restriction', 'Н/Д')} УРОВНИ ОБСЛУЖИВАНИЯ (LOS): LOS 1-2: Свободное движение | LOS 3-4: Стабильный поток LOS 5-6: Насыщенный поток | LOS 7-8: Неустойчивый поток LOS 9-10: Коллапс ПРОБЛЕМНЫЕ СЦЕНАРИИ ДЛЯ АНАЛИЗА: 1. Если LOS ухудшился с 1-4 до 5+ - предложи меры по восстановлению пропускной способности 2. Если средняя скорость ниже 20 км/ч - рассмотри организационные меры 3. Если загрузка > 0.8 - предложи меры по разгрузке 4. Если интенсивность близка к пропускной способности - предложи альтернативные маршруты 5. Учитывай ширину дороги и возможность расширения/реорганизации Дай ОДНУ самую приоритетную и практическую рекомендацию. Будь максимально конкретным: - Предлагай конкретные инженерные решения (дополнительная полоса, карман, светофор) - Организационные меры (ограничение парковки, выделенные полосы) - Альтернативные маршруты - Временные решения Формат: 1-2 предложения с четким, измеримым предложением. """ headers = { 'Authorization': f'Bearer {access_token}', 'Content-Type': 'application/json', 'Accept': 'application/json', } payload = { 'model': 'GigaChat', 'messages': [{'role': 'user', 'content': prompt}], 'max_tokens': 250, # Увеличил для более развернутых ответов 'temperature': 0.6 # Снизил для более консервативных рекомендаций } try: response = requests.post(GIGA_API_URL, headers=headers, json=payload, verify=False, timeout=30) if response.status_code == 200: recommendation = response.json().get('choices', [{}])[0].get('message', {}).get('content', '') # Очищаем ответ от лишних кавычек и форматирования recommendation = recommendation.strip().strip('"').strip("'") return recommendation if recommendation else "Рекомендация не сгенерирована" else: print(f"[GigaChat] Ошибка API: {response.status_code}") return f"Ошибка получения рекомендации: {response.status_code}" except requests.exceptions.RequestException as e: print(f"[GigaChat ошибка запроса] {e}") return f"Ошибка соединения: {e}" # ========================= # ВЫБОР GEOJSON ФАЙЛОВ # ========================= print("=" * 50) print("ВЫБОР GEOJSON ФАЙЛОВ") print("=" * 50) # Предлагаем пользователю выбрать файлы geo_paths = select_geojson_files() if not geo_paths: # Если файлы не выбраны, используем путь по умолчанию default_path = Path(r"D:\Hakaton\DriveHack\Данные\176002010581_streets_car_extraload_layer.geojson") if default_path.exists(): print(f"Используется файл по умолчанию: {default_path}") geo_paths = [default_path] else: # Создаем простой диалог для ручного ввода пути root = tk.Tk() root.withdraw() messagebox.showwarning("Файл не найден", "Файл по умолчанию не найден. Пожалуйста, выберите GeoJSON файлы вручную.") geo_paths = select_geojson_files() if not geo_paths: print("Файлы не выбраны. Программа завершена.") exit() # Загружаем все файлы и объединяем features all_features = [] all_file_names = [] for geo_path in geo_paths: # Проверяем существование файла if not geo_path.exists(): print(f"⚠️ Файл не найден: {geo_path}") continue print(f"Загружаем файл: {geo_path}") print(f"Размер файла: {geo_path.stat().st_size / 1024 / 1024:.2f} MB") # Загрузка GeoJSON try: with open(geo_path, "r", encoding="utf-8") as f: src = json.load(f) print("✓ Файл успешно загружен") features = src.get("features", []) if not features: print("⚠️ В файле нет features") continue # Добавляем информацию о файле в свойства каждого feature for feature in features: if 'properties' not in feature: feature['properties'] = {} feature['properties']['source_file'] = geo_path.name all_features.extend(features) all_file_names.append(geo_path.name) print(f"✓ Найдено объектов: {len(features)}") except Exception as e: print(f"✗ Ошибка загрузки файла: {e}") messagebox.showerror("Ошибка", f"Не удалось загрузить файл {geo_path}: {e}") if not all_features: print("✗ Не удалось загрузить ни одного файла с features") messagebox.showerror("Ошибка", "Не удалось загрузить ни одного файла с данными") exit() features = all_features print(f"✓ Всего объектов из всех файлов: {len(features)}") print(f"✓ Загружено файлов: {len(all_file_names)}") for file_name in all_file_names: print(f" - {file_name}") # ========================= # CRS: авто-детект и перевод в WGS84 при необходимости # ========================= R = 6378137.0 def mercator_to_wgs84(x, y): lon = (x / R) * (180.0 / np.pi) lat = (2.0 * np.arctan(np.exp(y / R)) - np.pi/2.0) * (180.0 / np.pi) return [lat, lon] def sample_xy(feats, k=100): pts = [] for ft in feats[:k]: g = ft.get("geometry") or {} t, c = g.get("type"), g.get("coordinates") if t == "LineString" and c: pts.append(tuple(c[0])) elif t == "MultiLineString" and c and c[0]: pts.append(tuple(c[0][0])) return [(float(x), float(y)) for (x,y) in pts if isinstance(x,(int,float)) and isinstance(y,(int,float))] def looks_like_degrees(pts): if not pts: return True xs = [abs(x) for x,_ in pts]; ys = [abs(y) for _,y in pts] return max(xs) <= 180 and max(ys) <= 90 samples = sample_xy(features) is_degrees = looks_like_degrees(samples) def to_wgs84(geom): if is_degrees: # уже в градусах return geom t, cs = geom.get("type"), geom.get("coordinates") if t == "LineString": return {"type":"LineString", "coordinates":[[mercator_to_wgs84(x,y)[1], mercator_to_wgs84(x,y)[0]] for x,y in cs]} if t == "MultiLineString": out = [] for line in cs: out.append([[mercator_to_wgs84(x,y)[1], mercator_to_wgs84(x,y)[0]] for x,y in line]) return {"type":"MultiLineString","coordinates": out} return geom # ========================= # УТИЛИТЫ # ========================= def safe_num(x): try: if x in (None, "", "-", "—"): return np.nan return float(x) except: if isinstance(x, str) and x.strip().endswith("%"): try: return float(x.strip().rstrip("%"))/100.0 except: return np.nan return np.nan def both_dir(a, b): vals = [safe_num(a), safe_num(b)] vals = [v for v in vals if not np.isnan(v)] return float(np.mean(vals)) if vals else np.nan def best_los(props, cur=True): if cur: v = safe_num(props.get("CurLos")) return v if not np.isnan(v) else both_dir(props.get("CurLosF"), props.get("CurLosT")) else: v = safe_num(props.get("NewLos")) return v if not np.isnan(v) else both_dir(props.get("NewLosF"), props.get("NewLosT")) def make_street_name(p: dict) -> str: n1 = (p.get("ST_NAME") or "").strip() if n1: return n1 typ = (p.get("ST_TYP_BEF") or "").strip() base = (p.get("ST_NM_BASE") or "").strip() if typ or base: return f"{typ} {base}".strip() for alt in ("CntrlName", "ROAD_CATEG"): v = (p.get(alt) or "").strip() if v: return v eid = p.get("EdgeId") or p.get("OrigEdgeId") or p.get("EdgeIdOld") return f"Без названия ({eid})" if eid is not None else "Без названия" def line_midpoint_lonlat(coords_lonlat): if not coords_lonlat: return None, None mid = coords_lonlat[len(coords_lonlat)//2] try: return float(mid[1]), float(mid[0]) except: return None, None def changed(a, b, tol=1e-6): if a is None or b is None: return False try: return abs(a - b) > tol except: return a != b # ========================= # ПОИСК ПАР «ДО/ПОСЛЕ» И БАЗОВОГО НАЗВАНИЯ АТРИБУТА # ========================= prop_keys = set() for ft in features: prop_keys.update((ft.get("properties") or {}).keys()) # Автопары CurX / NewX pairs = set() for k in prop_keys: if k.startswith("Cur") and k[3:] and ("New"+k[3:]) in prop_keys: pairs.add((k, "New"+k[3:])) # Явные варианты вида XxxCur / XxxNew (например, CrSpdCur / CrSpdNew) for k in prop_keys: if k.endswith("Cur"): base = k[:-3] nn = base + "New" if nn in prop_keys: pairs.add((k, nn)) LOS_PAIR = ("CurLos","NewLos") # отдельная обработка LOS def base_name(cur_key, new_key): if cur_key.startswith("Cur") and new_key.startswith("New") and cur_key[3:]==new_key[3:]: return cur_key[3:] if cur_key.endswith("Cur") and new_key.endswith("New") and cur_key[:-3]==new_key[:-3]: return cur_key[:-3] # запасной вариант — общая часть for cut in ("Cur","New"): cur_key = cur_key.replace(cut,"") new_key = new_key.replace(cut,"") return cur_key if len(cur_key)>=3 else new_key # ========================= # ПОДГОТОВКА И АГРЕГАЦИЯ ПО УЛИЦАМ # ========================= print("Обработка данных...") streets = {} def val_pair(props, cur_key, new_key): if (cur_key, new_key) == LOS_PAIR: return best_los(props, True), best_los(props, False) return safe_num(props.get(cur_key)), safe_num(props.get(new_key)) for i, ft in enumerate(features): if i % 1000 == 0 and i > 0: print(f"Обработано {i}/{len(features)} объектов...") p = ft.get("properties", {}) or {} g = ft.get("geometry", {}) or {} g_wgs = to_wgs84(g) name = make_street_name(p) rep_lat = rep_lon = None if g_wgs and g_wgs.get("type") == "LineString" and g_wgs.get("coordinates"): rep_lat, rep_lon = line_midpoint_lonlat(g_wgs["coordinates"]) elif g_wgs and g_wgs.get("type") == "MultiLineString" and g_wgs.get("coordinates"): rep_lat, rep_lon = line_midpoint_lonlat(g_wgs["coordinates"][0]) rec = streets.setdefault(name, {"lat": [], "lon": [], "properties": [], "pairs": defaultdict(lambda: {"cur": [], "new": []})}) for cur_key, new_key in pairs: cur_val, new_val = val_pair(p, cur_key, new_key) if not np.isnan(cur_val) or not np.isnan(new_val): rec["pairs"][(cur_key,new_key)]["cur"].append(cur_val if not np.isnan(cur_val) else None) rec["pairs"][(cur_key,new_key)]["new"].append(new_val if not np.isnan(new_val) else None) if rep_lat is not None and rep_lon is not None: rec["lat"].append(rep_lat); rec["lon"].append(rep_lon) # Сохраняем свойства для генерации рекомендаций rec["properties"].append(p) print(f"✓ Улиц найдено: {len(streets)}") # Агрегация (медианы) items = [] for name, d in streets.items(): if not d["lat"] or not d["lon"]: continue agg = {} for kpair, vals in d["pairs"].items(): cur_vals = [v for v in vals["cur"] if v is not None] new_vals = [v for v in vals["new"] if v is not None] cur_med = float(median(cur_vals)) if cur_vals else None new_med = float(median(new_vals)) if new_vals else None agg[kpair] = (cur_med, new_med) # Получаем агрегированные свойства для рекомендаций aggregated_props = {} for prop in d["properties"]: for key, value in prop.items(): if value not in (None, "", "-", "—"): if key not in aggregated_props: aggregated_props[key] = [] aggregated_props[key].append(value) # Для числовых значений берем медиану, для текстовых - первое непустое значение final_props = {} for key, values in aggregated_props.items(): if all(isinstance(v, (int, float)) for v in values if v is not None): # Числовое значение - медиана numeric_vals = [v for v in values if v is not None] if numeric_vals: final_props[key] = median(numeric_vals) else: # Текстовое значение - первое непустое for v in values: if v not in (None, "", "-", "—"): final_props[key] = v break items.append({ "name": name, "lat": float(np.mean(d["lat"])), "lon": float(np.mean(d["lon"])), "agg_pairs": agg, "properties": final_props }) # Фильтр: оставляем только те улицы, где есть изменения (LOS сравниваем как целые) def any_change_item(it): for (cur_key, new_key), (v_before, v_after) in it["agg_pairs"].items(): if v_before is None or v_after is None: continue if (cur_key, new_key) == LOS_PAIR: if int(round(v_before)) != int(round(v_after)): return True else: if changed(v_before, v_after): return True return False items_before_filter = len(items) items = [it for it in items if any_change_item(it)] print(f"✓ Улиц с изменениями: {len(items)} (из {items_before_filter})") # ========================= # ГЕНЕРАЦИЯ РЕКОМЕНДАЦИЙ ОТ GIGACHAT (ТОЛЬКО ДЛЯ LOS >= 6) # ========================= print("\n" + "="*50) print("ГЕНЕРАЦИЯ РЕКОМЕНДАЦИЙ GIGACHAT") print("="*50) print("Получение токена GigaChat...") access_token = get_access_token() if access_token: # Генерация рекомендаций только для улиц с LOS >= 6 print("Генерация рекомендаций для проблемных улиц (LOS >= 6)...") problem_items = [] for i, item in enumerate(items): los_after = None if LOS_PAIR in item["agg_pairs"]: _, la = item["agg_pairs"][LOS_PAIR] if la is not None: los_after = int(round(la)) # Генерируем рекомендации только для улиц с LOS >= 6 if los_after is not None and los_after >= 6: problem_items.append((i, item, los_after)) print(f"Найдено проблемных улиц: {len(problem_items)}") if problem_items: for idx, (original_idx, item, los_after) in enumerate(problem_items): print(f"Генерация рекомендации {idx+1}/{len(problem_items)}: {item['name']} (LOS: {los_after})") # Подготавливаем данные для рекомендаций street_data = { 'name': item['name'], 'city': item['properties'].get('ST_NM_CITY'), 'cur_los': item['agg_pairs'].get(LOS_PAIR, (None, None))[0], 'new_los': item['agg_pairs'].get(LOS_PAIR, (None, None))[1], 'cur_load': item['properties'].get('CurLoad'), 'new_load': item['properties'].get('NewLoad'), 'cur_speed': item['properties'].get('CrSpdCur'), 'new_speed': item['properties'].get('CrSpdNew'), 'road_category': item['properties'].get('ROAD_CATEG'), 'func_class': item['properties'].get('FUNC_CLASS'), 'max_speed': item['properties'].get('MaxSpd'), 'avg_speed': item['properties'].get('AvgSpd'), 'morning_speed': item['properties'].get('speed_utro'), 'intensity': item['properties'].get('Intens'), 'capacity': item['properties'].get('Capacity'), 'width': item['properties'].get('Width'), 'lanes_forward': item['properties'].get('RbndStght'), 'lanes_backward': item['properties'].get('RbndBck'), 'ped_cross': item['properties'].get('PedCross'), 'control': item['properties'].get('Control'), 'overload': item['properties'].get('Ovrld'), 'bike_lane': "Да" if item['properties'].get('Bike') == 1 else "Нет", 'sidewalk': "Да" if item['properties'].get('Foot') == 1 else "Нет", 'length': item['properties'].get('length'), 'u_turn_restriction': item['properties'].get('U_TURN'), } # Генерируем рекомендацию recommendation = generate_gigachat_recommendation(access_token, street_data) items[original_idx]['gigachat_recom'] = recommendation # Задержка между запросами import time if idx < len(problem_items) - 1: time.sleep(1) # Для остальных улиц ставим заглушку for i, item in enumerate(items): if 'gigachat_recom' not in item: item['gigachat_recom'] = "Рекомендация не требуется (низкая загруженность)" else: print("Проблемных улиц не найдено") for item in items: item['gigachat_recom'] = "Рекомендация не требуется (низкая загруженность)" else: print("Не удалось получить токен GigaChat. Рекомендации не будут сгенерированы.") for item in items: item['gigachat_recom'] = "Рекомендация не сгенерирована (ошибка API)" # ========================= # КАРТА: ТОЧКИ С РЕКОМЕНДАЦИЯМИ ОТ GIGACHAT # ========================= print("\nСоздание карты...") if items: center_lat = float(np.mean([x["lat"] for x in items])) center_lon = float(np.mean([x["lon"] for x in items])) else: center_lat, center_lon = 55.75, 37.62 m = folium.Map(location=[center_lat, center_lon], zoom_start=12, tiles="CartoDB positron") Fullscreen().add_to(m) # Палитра для бейджей по LOS palette = {1:"#2DC937", 2:"#99C140", 3:"#E7B416", 4:"#E79316", 5:"#DB7B2B", 6:"#CC3232", 7:"#B71C1C", 8:"#8E0000", 9:"#6A0000", 10:"#4A0000"} def color_for_los(val): try: return palette.get(int(round(val)), "#888888") except: return "#888888" # ========================= # ДОБАВЛЯЕМ ОТРЕЗКИ УЛИЦ С ЦВЕТАМИ ПО LOS # ========================= street_layer = folium.FeatureGroup(name="Отрезки улиц по загруженности", show=True) print("Добавление отрезков улиц на карту...") for i, ft in enumerate(features): if i % 1000 == 0 and i > 0: print(f"Добавлено {i}/{len(features)} отрезков...") p = ft.get("properties", {}) or {} g = ft.get("geometry", {}) or {} g_wgs = to_wgs84(g) # Получаем LOS (предпочтительно NewLos, иначе CurLos) los = None if "NewLos" in p and p["NewLos"] not in (None, "", "-", "—"): los = safe_num(p["NewLos"]) elif "CurLos" in p and p["CurLos"] not in (None, "", "-", "—"): los = safe_num(p["CurLos"]) # Пропускаем если нет LOS данных if los is None or np.isnan(los): continue los_int = int(round(los)) color = color_for_los(los_int) # Добавляем линию на карту if g_wgs.get("type") == "LineString" and g_wgs.get("coordinates"): folium.PolyLine( locations=[[coord[1], coord[0]] for coord in g_wgs["coordinates"]], color=color, weight=4, opacity=0.7, popup=f"Уровень загрузки: {los_int}", tooltip=f"LOS: {los_int}" ).add_to(street_layer) elif g_wgs.get("type") == "MultiLineString" and g_wgs.get("coordinates"): for line in g_wgs["coordinates"]: folium.PolyLine( locations=[[coord[1], coord[0]] for coord in line], color=color, weight=4, opacity=0.7, popup=f"Уровень загрузки: {los_int}", tooltip=f"LOS: {los_int}" ).add_to(street_layer) street_layer.add_to(m) # ========================= # ДОБАВЛЯЕМ МАРКЕРЫ С РЕКОМЕНДАЦИЯМИ # ========================= marker_layer = folium.FeatureGroup(name="Улицы с рекомендациями GigaChat", show=True) print("Добавление маркеров с рекомендациями...") for it in items: # Для бейджа используем LOS los_before = los_after = None if LOS_PAIR in it["agg_pairs"]: lb, la = it["agg_pairs"][LOS_PAIR] if lb is not None: los_before = int(round(lb)) if la is not None: los_after = int(round(la)) t_cur = "–" if los_before is None else str(los_before) t_new = "–" if los_after is None else str(los_after) badge_html = f""" <div style="display:inline-flex;align-items:center;gap:6px; background:#fff;border:1px solid #d9d9d9;border-radius:16px; padding:4px 8px; box-shadow:0 1px 3px rgba(0,0,0,0.15); font-family:-apple-system,Segoe UI,Roboto,Arial,sans-serif;"> <span style="display:inline-flex;align-items:center;justify-content:center; min-width:20px;height:20px;border-radius:50%;padding:0 4px; background:{color_for_los(los_before)};color:#fff;font-weight:700;font-size:12px;"> {t_cur} </span> <span style="color:#666;font-weight:700;">></span> <span style="display:inline-flex;align-items:center;justify-content:center; min-width:20px;height:20px;border-radius:50%;padding:0 4px; background:{color_for_los(los_after)};color:#fff;font-weight:700;font-size:12px;"> {t_new} </span> </div> """ # === ПОПАП: данные улицы + рекомендация от GigaChat === lines = [] # 1) LOS - показываем если изменился if LOS_PAIR in it["agg_pairs"]: before, after = it["agg_pairs"][LOS_PAIR] if (before is not None) and (after is not None): bi, ai = int(round(before)), int(round(after)) if bi != ai: lines.append(f"Уровень обслуживания (до): {bi}") lines.append(f"Уровень обслуживания (после): {ai}") lines.append("") # 2) Остальные пары - только изменившиеся for (cur_key, new_key), (v_before, v_after) in it["agg_pairs"].items(): if (cur_key, new_key) == LOS_PAIR: continue if v_before is None or v_after is None: continue if not changed(v_before, v_after): continue base = base_name(cur_key, new_key) def fmt(x): if x is None: return "—" if abs(x - round(x)) < 1e-6: return f"{int(round(x))}" low = base.lower() if ("spd" in low) or ("speed" in low): return f"{x:.1f}" if ("load" in low) or ("vc" in low): return f"{x:.2f}" return f"{x:.2f}" lines.append(f"{base} (до): {fmt(v_before)}") lines.append(f"{base} (после): {fmt(v_after)}") lines.append("") # 3) Рекомендация от GigaChat - только если LOS >= 6 gigachat_recom = it['gigachat_recom'] # Чистим ответ от #, * и других спецсимволов gigachat_recom = re.sub(r'[#*`]', '', gigachat_recom) # Показываем рекомендацию только если LOS >= 6 if los_after is not None and los_after >= 6: lines.append("🤖 РЕКОМЕНДАЦИЯ GIGACHAT:") lines.append(gigachat_recom) while lines and lines[-1] == "": lines.pop() popup_html = f""" <div style="font-family:-apple-system,Segoe UI,Roboto,Arial,sans-serif; font-size:12px; line-height:1.3; max-height:300px; overflow-y:auto;"> <div style="font-weight:700;margin-bottom:4px;font-size:13px;">{html.escape(it['name'])}</div> {"<br/>".join(html.escape(x) for x in lines) if lines else "—"} </div> """ folium.Marker( location=[it["lat"], it["lon"]], icon=folium.DivIcon(html=badge_html, icon_size=(1,1), icon_anchor=(0,0)), popup=folium.Popup(popup_html, max_width=400) ).add_to(marker_layer) marker_layer.add_to(m) folium.LayerControl(collapsed=False).add_to(m) # ========================= # СОХРАНЕНИЕ КАРТЫ # ========================= # Создаем имя файла на основе всех загруженных файлов if len(all_file_names) == 1: output_file = Path(f"transport_analysis_{Path(all_file_names[0]).stem}.html") else: output_file = Path(f"transport_analysis_combined_{len(all_file_names)}_files.html") m.save(output_file) print(f"\n✓ Карта сохранена как: {output_file}") # ========================= # СТАТИСТИКА # ========================= print("\n" + "="*50) print("СТАТИСТИКА АНАЛИЗА") print("="*50) print(f"Файлы: {', '.join(all_file_names)}") print(f"Всего объектов: {len(features)}") print(f"Улиц с изменениями: {len(items)}") print(f"Проблемных улиц (LOS >= 6): {len(problem_items) if 'problem_items' in locals() else 0}") # ПОКАЗ КАРТЫ print(f"\nКарта сгенерирована успешно! Открыт файл: {output_file}") print("Запускаю браузер...") # Автоматическое открытие в браузере import webbrowser webbrowser.open(output_file.absolute().as_uri())