auto-sync: 2026-05-06 21:00:01
This commit is contained in:
@@ -887,85 +887,85 @@ async def post_scenic(req: ScenicRequest):
|
|||||||
raise HTTPException(500, f"Ошибка: {e}")
|
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:
|
async def _build_segmented_route(req: RouteRequest) -> dict:
|
||||||
"""Строит маршрут через промежуточные точки с альтернативами по сегментам."""
|
"""Строит маршрут через промежуточные точки с альтернативами по сегментам."""
|
||||||
waypoints = req.waypoints
|
waypoints = req.waypoints
|
||||||
segments_count = len(waypoints) - 1
|
segments_count = len(waypoints) - 1
|
||||||
|
|
||||||
segment_alternatives = [] # список списков маршрутов OSRM
|
# Получить альтернативы для каждого сегмента
|
||||||
|
segment_alternatives = []
|
||||||
for i in range(segments_count):
|
for i in range(segments_count):
|
||||||
wp_a = waypoints[i]
|
wp_a = waypoints[i]
|
||||||
wp_b = waypoints[i + 1]
|
wp_b = waypoints[i + 1]
|
||||||
coords_str = f"{wp_a.lon},{wp_a.lat};{wp_b.lon},{wp_b.lat}"
|
alts = await _osrm_segment_alternatives(wp_a.lon, wp_a.lat, wp_b.lon, wp_b.lat)
|
||||||
radiuses_str = "5000;5000"
|
if not alts:
|
||||||
|
|
||||||
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"):
|
|
||||||
raise HTTPException(404, f"Маршрут не найден на сегменте {i + 1}")
|
raise HTTPException(404, f"Маршрут не найден на сегменте {i + 1}")
|
||||||
|
segment_alternatives.append(alts)
|
||||||
|
|
||||||
segment_alternatives.append(data["routes"])
|
# Скомбинировать сегменты (до 3 вариантов на сегмент чтобы не взорвать комбинаторику)
|
||||||
|
max_per_segment = 3
|
||||||
# Для каждого сегмента берём до 5 вариантов чтобы не взорвать комбинаторику
|
|
||||||
max_per_segment = 5
|
|
||||||
trimmed = [alts[:max_per_segment] for alts in segment_alternatives]
|
trimmed = [alts[:max_per_segment] for alts in segment_alternatives]
|
||||||
|
|
||||||
all_combos = list(itertools.product(*trimmed))
|
all_combos = list(itertools.product(*trimmed))
|
||||||
|
|
||||||
|
# Склеить геометрию для каждой комбинации
|
||||||
combined_routes = []
|
combined_routes = []
|
||||||
for combo in all_combos:
|
for combo in all_combos:
|
||||||
total_distance = sum(r["distance"] for r in combo)
|
total_distance = sum(r["distance"] for r in combo)
|
||||||
total_duration = sum(r["duration"] for r in combo)
|
total_duration = sum(r["duration"] for r in combo)
|
||||||
|
|
||||||
# Склеить геометрию, убирая дублирующую точку стыка
|
|
||||||
all_coords: list = []
|
all_coords: list = []
|
||||||
for r in combo:
|
for r in combo:
|
||||||
coords = r["geometry"]["coordinates"]
|
coords = r["geometry"]["coordinates"]
|
||||||
@@ -973,37 +973,33 @@ async def _build_segmented_route(req: RouteRequest) -> dict:
|
|||||||
all_coords.extend(coords[1:])
|
all_coords.extend(coords[1:])
|
||||||
else:
|
else:
|
||||||
all_coords.extend(coords)
|
all_coords.extend(coords)
|
||||||
|
|
||||||
combined_routes.append({
|
combined_routes.append({
|
||||||
"distance": total_distance,
|
"distance": total_distance,
|
||||||
"duration": total_duration,
|
"duration": total_duration,
|
||||||
"geometry": {"type": "LineString", "coordinates": all_coords},
|
"geometry": {"type": "LineString", "coordinates": all_coords},
|
||||||
"legs": [leg for r in combo for leg in r.get("legs", [])],
|
|
||||||
})
|
})
|
||||||
|
|
||||||
# Дедупликация по геометрии (не по дистанции)
|
# Дедупликация по геометрии (5 контрольных точек)
|
||||||
def _route_signature(coords: list) -> tuple:
|
def route_sig(coords):
|
||||||
"""Сигнатура маршрута: несколько контрольных точек по геометрии."""
|
|
||||||
n = len(coords)
|
n = len(coords)
|
||||||
if n == 0:
|
if n == 0:
|
||||||
return ()
|
return ()
|
||||||
indices = [0, n//4, n//2, 3*n//4, n-1]
|
idxs = [0, n//4, n//2, 3*n//4, n-1]
|
||||||
return tuple(
|
return tuple((round(coords[i][0], 3), round(coords[i][1], 3)) for i in idxs if i < n)
|
||||||
(round(coords[i][0], 3), round(coords[i][1], 3))
|
|
||||||
for i in indices if i < n
|
|
||||||
)
|
|
||||||
|
|
||||||
seen_sigs = set()
|
seen = set()
|
||||||
deduped: list = []
|
deduped = []
|
||||||
for route in combined_routes:
|
for route in combined_routes:
|
||||||
sig = _route_signature(route["geometry"]["coordinates"])
|
sig = route_sig(route["geometry"]["coordinates"])
|
||||||
if sig not in seen_sigs:
|
if sig not in seen:
|
||||||
seen_sigs.add(sig)
|
seen.add(sig)
|
||||||
deduped.append(route)
|
deduped.append(route)
|
||||||
|
|
||||||
# Топ-5
|
|
||||||
deduped = deduped[:5]
|
deduped = deduped[:5]
|
||||||
|
|
||||||
|
if not deduped:
|
||||||
|
raise HTTPException(404, "Маршрут не найден")
|
||||||
|
|
||||||
# Считаем статистику через существующую calc_route_stats
|
# Считаем статистику через существующую calc_route_stats
|
||||||
try:
|
try:
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
@@ -1018,7 +1014,6 @@ async def _build_segmented_route(req: RouteRequest) -> dict:
|
|||||||
stats = calc_route_stats(route["geometry"], conn)
|
stats = calc_route_stats(route["geometry"], conn)
|
||||||
except Exception:
|
except Exception:
|
||||||
stats = None
|
stats = None
|
||||||
|
|
||||||
routes_out.append({
|
routes_out.append({
|
||||||
"index": idx,
|
"index": idx,
|
||||||
"distance_m": round(route["distance"]),
|
"distance_m": round(route["distance"]),
|
||||||
@@ -1036,7 +1031,7 @@ async def _build_segmented_route(req: RouteRequest) -> dict:
|
|||||||
if not routes_out:
|
if not routes_out:
|
||||||
raise HTTPException(404, "Маршрут не найден")
|
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)
|
routes_out.sort(key=lambda r: (r["stats"] or {}).get("dirt_total_pct", 0), reverse=True)
|
||||||
for idx, r in enumerate(routes_out):
|
for idx, r in enumerate(routes_out):
|
||||||
r["index"] = idx
|
r["index"] = idx
|
||||||
|
|||||||
Reference in New Issue
Block a user