Skip to content

Commit

Permalink
Adding support for osrm paths
Browse files Browse the repository at this point in the history
  • Loading branch information
merschformann committed Oct 15, 2024
1 parent 68560a8 commit a30e89e
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 20 deletions.
1 change: 1 addition & 0 deletions nextplot/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ def entry_point():
weight_points=args.weight_points,
no_points=args.no_points,
start_end_markers=args.start_end_markers,
osrm_host=args.osrm_host,
rk_osm=args.rk_osm,
rk_bin=args.rk_bin,
rk_profile=args.rk_profile,
Expand Down
115 changes: 115 additions & 0 deletions nextplot/osrm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import dataclasses
import sys
import urllib.parse

import polyline
import requests

from nextplot import common, types

TRAVEL_SPEED = 10 # assuming 10m/s travel speed for missing segments and snapping


@dataclasses.dataclass
class OsrmRouteRequest:
positions: list[types.Position]


@dataclasses.dataclass
class OsrRouteResponse:
paths: list[list[types.Position]]
distances: list[float]
durations: list[float]
zero_distance: bool = False


def query_route(
endpoint: str,
route: OsrmRouteRequest,
) -> OsrRouteResponse:
"""
Queries a route from the OSRM server.
"""
# Encode positions as polyline string to better handle large amounts of positions
polyline_str = polyline.encode([(p.lat, p.lon) for p in route.positions])

# Assemble request
url_base = urllib.parse.urljoin(endpoint, "route/v1/driving/")
url = urllib.parse.urljoin(url_base, f"polyline({polyline_str})?overview=full&geometries=polyline&steps=true")

# Query OSRM
try:
response = requests.get(url)
response.raise_for_status()
except requests.exceptions.RequestException as e:
print(f"Error querying OSRM at {url_base}:", e)
sys.exit(1)
result = response.json()
if result["code"] != "Ok":
raise Exception("OSRM returned an error:", result["message"])
if len(result["routes"]) == 0:
raise Exception(f"No route found for {route.positions}")

# Process all legs
all_zero_distances = True
legs, distances, durations = [], [], []
for idx, leg in enumerate(result["routes"][0]["legs"]):
# Combine all steps into a single path
path = []
for step in leg["steps"]:
path.extend(polyline.decode(step["geometry"]))
# Remove subsequent identical points
path = [path[0]] + [p for i, p in enumerate(path[1:], 1) if path[i] != path[i - 1]]
# Convert to Position objects
path = [types.Position(lon=lon, lat=lat, desc=None) for lat, lon in path]
# Add start and end
path = [route.positions[idx]] + path + [route.positions[idx + 1]]
# Extract distance and duration
distance = leg["distance"] / 1000.0 # OSRM return is in meters, convert to km
duration = leg["duration"]
# Make sure we are finding any routes
if distance > 0:
all_zero_distances = False
# Add duration for start and end
start_distance = common.haversine(path[0], route.positions[idx])
end_distance = common.haversine(path[-1], route.positions[idx + 1])
distance += start_distance + end_distance
duration += start_distance / TRAVEL_SPEED + end_distance / TRAVEL_SPEED
# Append to list
legs.append(path)
distances.append(distance)
durations.append(duration)

# Warn if number of legs does not match number of positions
if len(legs) != len(route.positions) - 1:
print(f"Warning: number of legs ({len(legs)}) does not match number of positions ({len(route.positions)} - 1)")

# Extract route
return OsrRouteResponse(paths=legs, distances=distances, durations=durations, zero_distance=all_zero_distances)


def query_routes(
endpoint: str,
routes: list[types.Route],
) -> list[OsrRouteResponse]:
"""
Queries multiple routes from the OSRM server.
param str endpoint: URL of the OSRM server.
param list[OsrmRouteRequest] routes: List of routes to query.
return: List of route results.
"""

# Query all routes
reqs = [OsrmRouteRequest(positions=route.points) for route in routes]
zero_distance_routes = 0
for r, req in enumerate(reqs):
result = query_route(endpoint, req)
routes[r].legs = result.paths
routes[r].leg_distances = result.distances
routes[r].leg_durations = result.durations
if result.zero_distance:
zero_distance_routes += 1
if zero_distance_routes > 0:
print(f"Warning: {zero_distance_routes} / {len(routes)} routes have zero distance according to OSRM")
48 changes: 32 additions & 16 deletions nextplot/route.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import plotly.graph_objects as go
from folium import plugins

from . import common, routingkit, types
from . import common, osrm, routingkit, types

# ==================== This file contains route plotting code (mode: 'route')

Expand Down Expand Up @@ -175,6 +175,13 @@ def arguments(parser):
action="store_true",
help="indicates whether to add start and end markers",
)
parser.add_argument(
"--osrm_host",
type=str,
nargs="?",
default=None,
help="host and port of the OSRM server (e.g. 'http://localhost:5000')",
)
parser.add_argument(
"--rk_bin",
type=str,
Expand Down Expand Up @@ -334,7 +341,6 @@ def create_map(
route_animation_color: str,
start_end_markers: bool,
custom_map_tile: list[str],
rk_distance: bool,
) -> folium.Map:
"""
Plots the given routes on a folium map.
Expand Down Expand Up @@ -369,8 +375,6 @@ def create_map(
omit_end,
route_direction,
route_animation_color,
1.0 / 1000.0 if rk_distance else 1.0 / 1000.0,
"km" if rk_distance else "s",
)

# Plot points
Expand Down Expand Up @@ -514,6 +518,7 @@ def plot(
weight_points: float,
no_points: bool,
start_end_markers: bool,
osrm_host: str,
rk_osm: str,
rk_bin: str,
rk_profile: routingkit.RoutingKitProfile,
Expand Down Expand Up @@ -600,8 +605,10 @@ def plot(
route.points[i].distance = length
route.length = length

# Determine route shapes (if routingkit is available)
if rk_osm:
# Determine route shapes (if osrm or routingkit are available)
if osrm_host:
osrm.query_routes(osrm_host, routes)
elif rk_osm:
routingkit.query_routes(rk_bin, rk_osm, routes, rk_profile, rk_distance)

# Dump some stats
Expand Down Expand Up @@ -670,7 +677,6 @@ def plot(
route_animation_color,
start_end_markers,
custom_map_tile,
rk_distance,
)

# Save map
Expand Down Expand Up @@ -737,15 +743,15 @@ def plot_map_route(
omit_end: bool,
direction: types.RouteDirectionIndicator = types.RouteDirectionIndicator.none,
animation_bg_color: str = "FFFFFF",
rk_factor: float = None,
rk_unit: str = None,
):
"""
Plots a route on the given map.
"""
rk_text = ""
if route.legs is not None:
rk_text = f"Route cost (routingkit): {sum(route.leg_costs) * rk_factor:.2f} {rk_unit}</br>"
if route.leg_distances is not None:
rk_text += f"Route cost (rk/osrm): {sum(route.leg_distances):.2f} km</br>"
if route.leg_durations is not None:
rk_text += f"Route duration (rk/osrm): {sum(route.leg_durations):.2f} s</br>"
popup_text = folium.Html(
"<p>"
+ f"Route: {route_idx+1} / {route_count}</br>"
Expand Down Expand Up @@ -838,13 +844,23 @@ def statistics(
types.Stat("nunassigned", "Unassigned stops", sum([len(g) for g in unassigned])),
]

if all((r.legs is not None) for r in routes):
costs = [sum(r.leg_costs) for r in routes]
if all((r.leg_distances is not None) for r in routes):
costs = [sum(r.leg_distances) for r in routes]
stats.extend(
[
types.Stat("distances_max", "RK/OSRM distances (max)", max(costs)),
types.Stat("distances_min", "RK/OSRM distances (min)", min(costs)),
types.Stat("distances_avg", "RK/OSRM distances (avg)", sum(costs) / float(len(routes))),
]
)

if all((r.leg_durations is not None) for r in routes):
durations = [sum(r.leg_durations) for r in routes]
stats.extend(
[
types.Stat("costs_max", "RK costs (max)", max(costs)),
types.Stat("costs_min", "RK costs (min)", min(costs)),
types.Stat("costs_avg", "RK costs (avg)", sum(costs) / float(len(routes))),
types.Stat("durations_max", "RK/OSRM durations (max)", max(durations)),
types.Stat("durations_min", "RK/OSRM durations (min)", min(durations)),
types.Stat("durations_avg", "RK/OSRM durations (avg)", sum(durations) / float(len(routes))),
]
)

Expand Down
16 changes: 13 additions & 3 deletions nextplot/routingkit.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ def query_routes(
# Clear any previously existing information
for route in routes:
route.legs = None
route.leg_costs = None
route.leg_distances = None
route.leg_durations = None

# Add results to routes
for i, path in enumerate(paths):
Expand Down Expand Up @@ -115,13 +116,22 @@ def query_routes(
end_cost /= travel_speed
cost += start_cost + end_cost

# RK uses milliseconds and meters, convert to seconds and kilometers (same factor)
cost /= 1000.0

# Add leg to route
if route.legs is None:
route.legs = [leg]
route.leg_costs = [cost]
if distance:
route.leg_distances = [cost]
else:
route.leg_durations = [cost]
else:
route.legs.append(leg)
route.leg_costs.append(cost)
if distance:
route.leg_distances.append(cost)
else:
route.leg_durations.append(cost)


def query(
Expand Down
3 changes: 2 additions & 1 deletion nextplot/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ class Route:
def __init__(self, points: list[Position]):
self.points = points
self.legs = None
self.leg_costs = 0
self.leg_distances = None
self.leg_durations = None

def to_points(self, omit_start: bool, omit_end: bool) -> list[Position]:
"""
Expand Down

0 comments on commit a30e89e

Please sign in to comment.