auto-sync: 2026-05-06 18:50:01

This commit is contained in:
Stream
2026-05-06 18:50:01 +03:00
parent ec10299532
commit 1e8811a5d2
2 changed files with 1217 additions and 0 deletions

View File

@@ -12,6 +12,7 @@ import math
import struct
import sqlite3
import json
import itertools
from pathlib import Path
from shapely.geometry import LineString
from typing import List
@@ -886,6 +887,152 @@ async def post_scenic(req: ScenicRequest):
raise HTTPException(500, f"Ошибка: {e}")
async def _build_segmented_route(req: RouteRequest) -> dict:
"""Строит маршрут через промежуточные точки с альтернативами по сегментам."""
waypoints = req.waypoints
segments_count = len(waypoints) - 1
segment_alternatives = [] # список списков маршрутов OSRM
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 → 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":
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"):
raise HTTPException(404, f"Маршрут не найден на сегменте {i + 1}")
segment_alternatives.append(data["routes"])
# Для каждого сегмента берём до 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"]
if all_coords:
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", [])],
})
# Дедупликация: если дистанции отличаются менее чем на 2% — считать дублем
deduped: list = []
for route in combined_routes:
is_dup = False
for existing in deduped:
if abs(route["distance"] - existing["distance"]) / max(existing["distance"], 1) < 0.02:
is_dup = True
break
if not is_dup:
deduped.append(route)
# Топ-5
deduped = deduped[:5]
# Считаем статистику через существующую calc_route_stats
try:
conn = get_db()
except Exception:
conn = None
routes_out = []
for idx, route in enumerate(deduped):
stats = None
if conn is not None:
try:
stats = calc_route_stats(route["geometry"], conn)
except Exception:
stats = None
routes_out.append({
"index": idx,
"distance_m": round(route["distance"]),
"duration_s": round(route["duration"]),
"geometry": route["geometry"],
"stats": stats,
})
if conn is not None:
try:
conn.close()
except Exception:
pass
if not routes_out:
raise HTTPException(404, "Маршрут не найден")
# Сортировать по 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
# Waypoints для ответа
waypoints_out = []
for i, wp in enumerate(req.waypoints):
label = "Старт" if i == 0 else ("Финиш" if i == len(req.waypoints) - 1 else f"Точка {i}")
waypoints_out.append({"lon": wp.lon, "lat": wp.lat, "label": label})
return {"routes": routes_out, "waypoints": waypoints_out}
@app.post("/api/route")
async def post_route(req: RouteRequest):
"""
@@ -895,6 +1042,10 @@ async def post_route(req: RouteRequest):
if len(req.waypoints) < 2:
raise HTTPException(400, "Нужно минимум 2 точки")
# При промежуточных точках — сегментный подход
if len(req.waypoints) > 2:
return await _build_segmented_route(req)
# Строим строку координат для OSRM
coords_str = ";".join(f"{wp.lon},{wp.lat}" for wp in req.waypoints)
alternatives = max(1, min(5, req.alternatives))

File diff suppressed because it is too large Load Diff