Fixing random turns
Browse files- backend/api/routes.py +38 -16
- backend/routing/routing_engine.py +11 -0
backend/api/routes.py
CHANGED
|
@@ -11,14 +11,40 @@ DISTANCE_BUDGET_FACTOR_POLLUTION = 1.8
|
|
| 11 |
DISTANCE_BUDGET_FACTOR_OVERALL = 1.5
|
| 12 |
|
| 13 |
|
| 14 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
"""
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
"""
|
| 23 |
if len(route) == 0:
|
| 24 |
coordinates = []
|
|
@@ -26,20 +52,16 @@ def route_to_geojson(G, route):
|
|
| 26 |
n = route[0]
|
| 27 |
coordinates = [[G.nodes[n]["x"], G.nodes[n]["y"]]]
|
| 28 |
else:
|
| 29 |
-
coordinates = [
|
| 30 |
for i in range(len(route) - 1):
|
| 31 |
u, v = route[i], route[i + 1]
|
| 32 |
edge = list(G[u][v].values())[0]
|
| 33 |
-
|
| 34 |
-
if
|
| 35 |
-
|
| 36 |
-
# Skip the first point (already added as previous node)
|
| 37 |
-
pts = list(geom.coords)
|
| 38 |
-
for lon, lat in pts[1:]:
|
| 39 |
-
coordinates.append([lon, lat])
|
| 40 |
else:
|
| 41 |
-
#
|
| 42 |
-
coordinates.
|
| 43 |
|
| 44 |
return {
|
| 45 |
"type": "Feature",
|
|
|
|
| 11 |
DISTANCE_BUDGET_FACTOR_OVERALL = 1.5
|
| 12 |
|
| 13 |
|
| 14 |
+
def _edge_geometry_coords(edge, u_node, v_node, G):
|
| 15 |
+
"""
|
| 16 |
+
Return (lon, lat) coordinate list for an edge, using intermediate
|
| 17 |
+
geometry points where available.
|
| 18 |
+
|
| 19 |
+
osmnx stores geometry as either:
|
| 20 |
+
- A Shapely LineString (when built/pickled from fresh download)
|
| 21 |
+
- A WKT string like "LINESTRING(...)" (when loaded from graphml)
|
| 22 |
+
Both are handled here. Falls back to straight nodeβnode if missing.
|
| 23 |
"""
|
| 24 |
+
geom = edge.get("geometry")
|
| 25 |
+
if geom is None:
|
| 26 |
+
return [
|
| 27 |
+
[G.nodes[u_node]["x"], G.nodes[u_node]["y"]],
|
| 28 |
+
[G.nodes[v_node]["x"], G.nodes[v_node]["y"]],
|
| 29 |
+
]
|
| 30 |
+
try:
|
| 31 |
+
# Shapely LineString
|
| 32 |
+
if hasattr(geom, "coords"):
|
| 33 |
+
return [[lon, lat] for lon, lat in geom.coords]
|
| 34 |
+
# WKT string β parse it
|
| 35 |
+
from shapely.wkt import loads as wkt_loads
|
| 36 |
+
return [[lon, lat] for lon, lat in wkt_loads(str(geom)).coords]
|
| 37 |
+
except Exception:
|
| 38 |
+
return [
|
| 39 |
+
[G.nodes[u_node]["x"], G.nodes[u_node]["y"]],
|
| 40 |
+
[G.nodes[v_node]["x"], G.nodes[v_node]["y"]],
|
| 41 |
+
]
|
| 42 |
+
|
| 43 |
|
| 44 |
+
def route_to_geojson(G, route):
|
| 45 |
+
"""
|
| 46 |
+
Build GeoJSON coordinates from the route node list using full edge
|
| 47 |
+
geometry (intermediate curve points), preventing phantom turns.
|
| 48 |
"""
|
| 49 |
if len(route) == 0:
|
| 50 |
coordinates = []
|
|
|
|
| 52 |
n = route[0]
|
| 53 |
coordinates = [[G.nodes[n]["x"], G.nodes[n]["y"]]]
|
| 54 |
else:
|
| 55 |
+
coordinates = []
|
| 56 |
for i in range(len(route) - 1):
|
| 57 |
u, v = route[i], route[i + 1]
|
| 58 |
edge = list(G[u][v].values())[0]
|
| 59 |
+
pts = _edge_geometry_coords(edge, u, v, G)
|
| 60 |
+
if i == 0:
|
| 61 |
+
coordinates.extend(pts)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
else:
|
| 63 |
+
# Skip first point β already added as last point of prev edge
|
| 64 |
+
coordinates.extend(pts[1:])
|
| 65 |
|
| 66 |
return {
|
| 67 |
"type": "Feature",
|
backend/routing/routing_engine.py
CHANGED
|
@@ -231,6 +231,12 @@ def weighted_directional_route(
|
|
| 231 |
|
| 232 |
pq = []
|
| 233 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
def edge_cost(edge, prev_node=None, curr_node=None, next_node=None):
|
| 235 |
# If this edge leads into a signalled junction but the maneuver
|
| 236 |
# is a free left turn, don't apply the signal delay cost
|
|
@@ -266,10 +272,15 @@ def weighted_directional_route(
|
|
| 266 |
|
| 267 |
state = (prev, current)
|
| 268 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
# Skip if we already found a cheaper way to this (prev, curr)
|
| 270 |
if state in visited and visited[state] <= cost:
|
| 271 |
continue
|
| 272 |
visited[state] = cost
|
|
|
|
| 273 |
|
| 274 |
# ββ Reached destination β reconstruct path ββββββββββββββββββββββββ
|
| 275 |
if current == dest:
|
|
|
|
| 231 |
|
| 232 |
pq = []
|
| 233 |
|
| 234 |
+
# ββ Node-level guard: once a node is settled as 'current', never
|
| 235 |
+
# expand it again. This prevents the route from looping back
|
| 236 |
+
# through the same intersection via a different predecessor,
|
| 237 |
+
# which causes T-detours and U-shaped routes. ββββββββββββββββββββββββββ
|
| 238 |
+
settled_nodes = set()
|
| 239 |
+
|
| 240 |
def edge_cost(edge, prev_node=None, curr_node=None, next_node=None):
|
| 241 |
# If this edge leads into a signalled junction but the maneuver
|
| 242 |
# is a free left turn, don't apply the signal delay cost
|
|
|
|
| 272 |
|
| 273 |
state = (prev, current)
|
| 274 |
|
| 275 |
+
# Skip if this node has already been settled (prevents backtracking)
|
| 276 |
+
if current in settled_nodes:
|
| 277 |
+
continue
|
| 278 |
+
|
| 279 |
# Skip if we already found a cheaper way to this (prev, curr)
|
| 280 |
if state in visited and visited[state] <= cost:
|
| 281 |
continue
|
| 282 |
visited[state] = cost
|
| 283 |
+
settled_nodes.add(current)
|
| 284 |
|
| 285 |
# ββ Reached destination β reconstruct path ββββββββββββββββββββββββ
|
| 286 |
if current == dest:
|