Skip to content

Commit

Permalink
[travelmux] when available, return OTP route only
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelkirk committed Aug 8, 2024
1 parent 5bb982a commit 60de35e
Show file tree
Hide file tree
Showing 2 changed files with 185 additions and 55 deletions.
117 changes: 117 additions & 0 deletions services/travelmux/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# travelmux is a routing service

It sits in front of both Valhalla and OpenTripPlanner, providing a uniform response.

OpenTripPlanner (OTP) is a multi-modal transportation planner. It handles
transit, cycling, walking, and driving directions (though we don't currently
use OTP driving directions).

Valhalla is supports driving, walking, and cycling. Relatively cheaply, you can
host a planet-wide instance of Valhalla to get directions anywhere on the
planet.


OTP is necessary for transportation routing, and, anecdotally, it provides
better cycling and walking directions.

However, OTP does not scale as well as Valhalla. It's not feasible to have a
single planet-wide instance, so instead, we have a cluster of smaller
deployments, giving a patchwork of coverage.

Travelmux takes care of forwarding your trip plan request to the appropriate
OTP instance.

A deployment could cover a town or metropolitan area. I've heard of OTP
instances as large as all of Europe, but I expect you will need quite a lot of
RAM. As an example, the Los Angeles OTP instance used for https://maps.earth
uses 3-4GB while mostly idle.

## Services

GET /travelmux/foo/bar
[ headway nginx frontend ]
[ ******************** travelmux ******************** ]
↓ ↓ ↓ ↓...
[ valhalla ] [ OTP Los Angeles ] [ OTP Puget ] [ OTP ...]


## Development Setup

```
# expose valhalla and OTP ports
edit docker-compose-with-transit.yaml
```
diff --git a/docker-compose-with-transit.yaml b/docker-compose-with-transit.yaml
index dffe5bf..f06149a 100644
--- a/docker-compose-with-transit.yaml
+++ b/docker-compose-with-transit.yaml
@@ -47,8 +47,8 @@ services:
condition: service_completed_successfully
networks:
- otp_backend
- # ports:
- # - "9002:8000"
+ ports:
+ - "9002:8000"
travelmux:
image: ghcr.io/headwaymaps/travelmux:latest
restart: always
@@ -88,8 +88,8 @@ services:
depends_on:
valhalla-init:
condition: service_completed_successfully
- # ports:
- # - "9001:8002"
+ ports:
+ - "9001:8002"
```
# consider blowing away any potentially stale containers
docker compose -f docker-compose-with-transit.yaml down --volumes
docker compose -f docker-compose-with-transit.yaml pull
# start services
docker compose -f docker-compose-with-transit.yaml up
```

start travelmux
```
cd services/travelmux
RUST_LOG=debug cargo run http://localhost:9001 http://localhost:9002/otp/routers
# or to rebuild on changes
RUST_LOG=debug cargo watch -- cargo run http://localhost:9001 http://localhost:9002/otp/routers
```

Edit quasar.config so that travelmux points at your local travelmux instance

```
diff --git a/services/frontend/www-app/quasar.config.js b/services/frontend/www-app/quasar.config.js
index 15aad52..42ecacb 100644
--- a/services/frontend/www-app/quasar.config.js
+++ b/services/frontend/www-app/quasar.config.js
@@ -113,10 +114,10 @@ module.exports = configure(function (/* ctx */) {
// rewrite: (path) => path.replace(/^\/pelias/, ''),
},
'/travelmux': {
- target: HEADWAY_HOST,
- changeOrigin: true,
- // target: 'http://0.0.0.0:8000',
- // rewrite: (path) => path.replace(/^\/travelmux/, ''),
+ target: 'http://0.0.0.0:8000',
+ rewrite: (path) => path.replace(/^\/travelmux/, ''),
},
},
},
```

Start the frontend dev server

```
cd services/frontend/www-app && yarn dev
```

At this point, you should be ready to go. Visit localhost:9000
123 changes: 68 additions & 55 deletions services/travelmux/src/api/v6/plan.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use actix_web::web::{Data, Query};
use actix_web::{get, web, HttpRequest, HttpResponseBuilder};
use geo::algorithm::BoundingRect;
use geo::geometry::{LineString, Point, Rect};
Expand Down Expand Up @@ -609,65 +610,22 @@ pub async fn _get_plan(
match primary_mode {
TravelMode::Transit => otp_plan(&query, req, &app_state, primary_mode).await,
other => {
debug_assert!(query.mode.len() == 1, "valhalla only supports one mode");

let mode = match other {
TravelMode::Transit => unreachable!("handled above"),
TravelMode::Bicycle => valhalla_api::ModeCosting::Bicycle,
TravelMode::Car => valhalla_api::ModeCosting::Auto,
TravelMode::Walk => valhalla_api::ModeCosting::Pedestrian,
};

// route?json={%22locations%22:[{%22lat%22:47.575837,%22lon%22:-122.339414},{%22lat%22:47.651048,%22lon%22:-122.347234}],%22costing%22:%22auto%22,%22alternates%22:3,%22units%22:%22miles%22}
let router_url = app_state.valhalla_router().plan_url(
query.from_place,
query.to_place,
mode,
query.num_itineraries,
distance_units,
)?;
let valhalla_response: reqwest::Response =
reqwest::get(router_url).await.map_err(|e| {
log::error!("error while fetching from valhalla service: {e}");
PlanResponseErr::from(Error::server(e))
})?;
if !valhalla_response.status().is_success() {
log::warn!(
"upstream HTTP Error from valhalla service: {}",
valhalla_response.status()
)
}

let mut response = HttpResponseBuilder::new(valhalla_response.status());
debug_assert_eq!(
valhalla_response
.headers()
.get(HeaderName::from_static("content-type")),
Some(&HeaderValue::from_str("application/json;charset=utf-8").unwrap())
);
response.content_type("application/json;charset=utf-8");

let valhalla_route_response: valhalla_api::ValhallaRouteResponseResult =
valhalla_response.json().await.map_err(|e| {
log::error!("error while parsing valhalla response: {e}");
PlanResponseErr::from(Error::server(e))
})?;

let mut plan_response =
PlanResponseOk::from_valhalla(*primary_mode, valhalla_route_response)?;

if primary_mode == &TravelMode::Bicycle || primary_mode == &TravelMode::Walk {
match otp_plan(&query, req, &app_state, primary_mode).await {
Ok(mut otp_response) => {
Ok(otp_response) => {
debug_assert_eq!(
1,
otp_response.plan.itineraries.len(),
"expected exactly one itinerary from OTP"
);
if let Some(otp_itinerary) = otp_response.plan.itineraries.pop() {
log::debug!("adding OTP itinerary to valhalla response");
plan_response.plan.itineraries.insert(0, otp_itinerary);
}
// Prefer OTP response when available - anecdotally, it tends to be higher quality than Valhalla routes for
// walking and cycling.
//
// We could combine the results and return them all, but I seemingly never want the valhalla directions when OTP are available.
//
// Plus, when re-routing, the navigation SDK tries to do route-matching so that the "most similar" route
// will be applied. The end result is that you sometimes end up on the valhalla route, which IME is typically worse.
return Ok(otp_response);
}
Err(e) => {
// match error_code to raw value of ErrorType enum
Expand All @@ -677,20 +635,75 @@ pub async fn _get_plan(
}
other => {
debug_assert!(other.is_ok(), "unexpected error code: {e:?}");
// We're mixing with results from Valhalla anyway, so dont' surface this error
// We're mixing with results from Valhalla anyway, so don't surface this error
// to the user. Likely we just don't support this area.
log::error!("OTP failed to plan {primary_mode:?} route: {e}");
}
}
}
}
}

Ok(plan_response)
Ok(valhalla_plan(&query, &app_state, primary_mode, distance_units, other).await?)
}
}
}

async fn valhalla_plan(
query: &Query<PlanQuery>,
app_state: &Data<AppState>,
primary_mode: &TravelMode,
distance_units: DistanceUnit,
other: &TravelMode,
) -> Result<PlanResponseOk, PlanResponseErr> {
debug_assert!(query.mode.len() == 1, "valhalla only supports one mode");

let mode = match other {
TravelMode::Transit => unreachable!("handled above"),
TravelMode::Bicycle => valhalla_api::ModeCosting::Bicycle,
TravelMode::Car => valhalla_api::ModeCosting::Auto,
TravelMode::Walk => valhalla_api::ModeCosting::Pedestrian,
};

// route?json={%22locations%22:[{%22lat%22:47.575837,%22lon%22:-122.339414},{%22lat%22:47.651048,%22lon%22:-122.347234}],%22costing%22:%22auto%22,%22alternates%22:3,%22units%22:%22miles%22}
let router_url = app_state.valhalla_router().plan_url(
query.from_place,
query.to_place,
mode,
query.num_itineraries,
distance_units,
)?;
let valhalla_response: reqwest::Response = reqwest::get(router_url).await.map_err(|e| {
log::error!("error while fetching from valhalla service: {e}");
PlanResponseErr::from(Error::server(e))
})?;
if !valhalla_response.status().is_success() {
log::warn!(
"upstream HTTP Error from valhalla service: {}",
valhalla_response.status()
)
}

let mut response = HttpResponseBuilder::new(valhalla_response.status());
debug_assert_eq!(
valhalla_response
.headers()
.get(HeaderName::from_static("content-type")),
Some(&HeaderValue::from_str("application/json;charset=utf-8").unwrap())
);
response.content_type("application/json;charset=utf-8");

let valhalla_route_response: valhalla_api::ValhallaRouteResponseResult =
valhalla_response.json().await.map_err(|e| {
log::error!("error while parsing valhalla response: {e}");
PlanResponseErr::from(Error::server(e))
})?;

Ok(PlanResponseOk::from_valhalla(
*primary_mode,
valhalla_route_response,
)?)
}

async fn otp_plan(
query: &web::Query<PlanQuery>,
req: HttpRequest,
Expand Down

0 comments on commit 60de35e

Please sign in to comment.