From bfa2f26599ccec7fdb680198fa5a812fda58009d Mon Sep 17 00:00:00 2001 From: Stream Date: Wed, 6 May 2026 21:00:01 +0300 Subject: [PATCH] auto-sync: 2026-05-06 21:00:01 --- tasks/enduro-trails/prototype/app.py | 155 +++++++++++++-------------- 1 file changed, 75 insertions(+), 80 deletions(-) diff --git a/tasks/enduro-trails/prototype/app.py b/tasks/enduro-trails/prototype/app.py index 41432c3..2e4c03b 100644 --- a/tasks/enduro-trails/prototype/app.py +++ b/tasks/enduro-trails/prototype/app.py @@ -887,85 +887,85 @@ async def post_scenic(req: ScenicRequest): raise HTTPException(500, f"Ошибка: {e}") +async def _osrm_segment_alternatives(lon_a: float, lat_a: float, lon_b: float, lat_b: float, depth: int = 0) -> list: + """Запросить альтернативы для одного сегмента. При TooBig — разбить пополам.""" + coords_str = f"{lon_a},{lat_a};{lon_b},{lat_b}" + url = ( + f"{OSRM_URL}/route/v1/driving/{coords_str}" + f"?alternatives=true&overview=full&geometries=geojson&annotations=false&radiuses=5000;5000" + ) + try: + async with httpx.AsyncClient(timeout=30) as client: + resp = await client.get(url) + data = resp.json() + except Exception: + return [] + + if data.get("code") == "TooBig" and depth < 2: + # Разбить пополам — найти середину через среднее координат + mid_lon = (lon_a + lon_b) / 2 + mid_lat = (lat_a + lat_b) / 2 + # Рекурсивно получить альтернативы для каждой половины + alts_a = await _osrm_segment_alternatives(lon_a, lat_a, mid_lon, mid_lat, depth + 1) + alts_b = await _osrm_segment_alternatives(mid_lon, mid_lat, lon_b, lat_b, depth + 1) + if not alts_a or not alts_b: + return [] + # Скомбинировать: каждый вариант первой половины × каждый вариант второй + combined = [] + for r_a, r_b in itertools.product(alts_a[:3], alts_b[:3]): + coords_a = r_a["geometry"]["coordinates"] + coords_b = r_b["geometry"]["coordinates"] + merged_coords = coords_a + coords_b[1:] # убрать дублирующую точку стыка + combined.append({ + "distance": r_a["distance"] + r_b["distance"], + "duration": r_a["duration"] + r_b["duration"], + "geometry": {"type": "LineString", "coordinates": merged_coords}, + }) + return combined + + if data.get("code") != "Ok" or not data.get("routes"): + # Попробовать без alternatives + url_single = ( + f"{OSRM_URL}/route/v1/driving/{coords_str}" + f"?overview=full&geometries=geojson&annotations=false&radiuses=5000;5000" + ) + try: + async with httpx.AsyncClient(timeout=30) as client: + resp = await client.get(url_single) + data = resp.json() + except Exception: + return [] + if data.get("code") != "Ok": + return [] + + return data.get("routes", []) + + async def _build_segmented_route(req: RouteRequest) -> dict: """Строит маршрут через промежуточные точки с альтернативами по сегментам.""" waypoints = req.waypoints segments_count = len(waypoints) - 1 - segment_alternatives = [] # список списков маршрутов OSRM - + # Получить альтернативы для каждого сегмента + segment_alternatives = [] for i in range(segments_count): wp_a = waypoints[i] wp_b = waypoints[i + 1] - coords_str = f"{wp_a.lon},{wp_a.lat};{wp_b.lon},{wp_b.lat}" - radiuses_str = "5000;5000" - - url = ( - f"{OSRM_URL}/route/v1/driving/{coords_str}" - f"?alternatives=5&overview=full&geometries=geojson&annotations=false" - f"&radiuses={radiuses_str}" - ) - - try: - async with httpx.AsyncClient(timeout=30) as client: - resp = await client.get(url) - data = resp.json() - except Exception as e: - raise HTTPException(503, f"OSRM недоступен: {e}") - - # Retry TooBig → 3 → 2 → 1 - if data.get("code") == "TooBig": - url2 = url.replace("alternatives=5", "alternatives=3") - try: - async with httpx.AsyncClient(timeout=60) as client: - resp = await client.get(url2) - data = resp.json() - except Exception as e: - raise HTTPException(503, f"OSRM недоступен: {e}") - if data.get("code") == "TooBig": - url2b = url.replace("alternatives=5", "alternatives=2") - try: - async with httpx.AsyncClient(timeout=60) as client: - resp = await client.get(url2b) - data = resp.json() - except Exception as e: - raise HTTPException(503, f"OSRM недоступен: {e}") - if data.get("code") == "TooBig": - url1 = url.replace("alternatives=5", "alternatives=false") - try: - async with httpx.AsyncClient(timeout=60) as client: - resp = await client.get(url1) - data = resp.json() - except Exception as e: - raise HTTPException(503, f"OSRM недоступен: {e}") - - # Retry NoSegment с radius 10km - if data.get("code") == "NoSegment": - url_wide = url.replace("5000;5000", "10000;10000") - try: - async with httpx.AsyncClient(timeout=60) as client: - resp = await client.get(url_wide) - data = resp.json() - except Exception as e: - raise HTTPException(503, f"OSRM недоступен: {e}") - - if data.get("code") != "Ok" or not data.get("routes"): + alts = await _osrm_segment_alternatives(wp_a.lon, wp_a.lat, wp_b.lon, wp_b.lat) + if not alts: raise HTTPException(404, f"Маршрут не найден на сегменте {i + 1}") + segment_alternatives.append(alts) - segment_alternatives.append(data["routes"]) - - # Для каждого сегмента берём до 5 вариантов чтобы не взорвать комбинаторику - max_per_segment = 5 + # Скомбинировать сегменты (до 3 вариантов на сегмент чтобы не взорвать комбинаторику) + max_per_segment = 3 trimmed = [alts[:max_per_segment] for alts in segment_alternatives] - all_combos = list(itertools.product(*trimmed)) + # Склеить геометрию для каждой комбинации combined_routes = [] for combo in all_combos: total_distance = sum(r["distance"] for r in combo) total_duration = sum(r["duration"] for r in combo) - - # Склеить геометрию, убирая дублирующую точку стыка all_coords: list = [] for r in combo: coords = r["geometry"]["coordinates"] @@ -973,37 +973,33 @@ async def _build_segmented_route(req: RouteRequest) -> dict: all_coords.extend(coords[1:]) else: all_coords.extend(coords) - combined_routes.append({ "distance": total_distance, "duration": total_duration, "geometry": {"type": "LineString", "coordinates": all_coords}, - "legs": [leg for r in combo for leg in r.get("legs", [])], }) - # Дедупликация по геометрии (не по дистанции) - def _route_signature(coords: list) -> tuple: - """Сигнатура маршрута: несколько контрольных точек по геометрии.""" + # Дедупликация по геометрии (5 контрольных точек) + def route_sig(coords): n = len(coords) if n == 0: return () - indices = [0, n//4, n//2, 3*n//4, n-1] - return tuple( - (round(coords[i][0], 3), round(coords[i][1], 3)) - for i in indices if i < n - ) + idxs = [0, n//4, n//2, 3*n//4, n-1] + return tuple((round(coords[i][0], 3), round(coords[i][1], 3)) for i in idxs if i < n) - seen_sigs = set() - deduped: list = [] + seen = set() + deduped = [] for route in combined_routes: - sig = _route_signature(route["geometry"]["coordinates"]) - if sig not in seen_sigs: - seen_sigs.add(sig) + sig = route_sig(route["geometry"]["coordinates"]) + if sig not in seen: + seen.add(sig) deduped.append(route) - # Топ-5 deduped = deduped[:5] + if not deduped: + raise HTTPException(404, "Маршрут не найден") + # Считаем статистику через существующую calc_route_stats try: conn = get_db() @@ -1018,7 +1014,6 @@ async def _build_segmented_route(req: RouteRequest) -> dict: stats = calc_route_stats(route["geometry"], conn) except Exception: stats = None - routes_out.append({ "index": idx, "distance_m": round(route["distance"]), @@ -1036,7 +1031,7 @@ async def _build_segmented_route(req: RouteRequest) -> dict: if not routes_out: raise HTTPException(404, "Маршрут не найден") - # Сортировать по dirt_total_pct убывающий (больше грунта = лучше) + # Сортировать по dirt_total_pct убывающий routes_out.sort(key=lambda r: (r["stats"] or {}).get("dirt_total_pct", 0), reverse=True) for idx, r in enumerate(routes_out): r["index"] = idx