Skip to content

Commit

Permalink
Merge pull request #342 from headwaymaps/mkirk/unify-otp
Browse files Browse the repository at this point in the history
travelmux v4, include per-leg transit details
  • Loading branch information
michaelkirk authored Apr 6, 2024
2 parents 89b9145 + ac93f0d commit 87c3fa8
Show file tree
Hide file tree
Showing 16 changed files with 668 additions and 217 deletions.
1 change: 0 additions & 1 deletion services/frontend/www-app/src/models/Itinerary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,6 @@ export class ItineraryLeg {
case OTPMode.Bus:
case OTPMode.Transit:
return '🚍';
case OTPMode.Train:
case OTPMode.Rail:
return '🚆';
case OTPMode.Subway:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export enum OTPMode {
Gondola = 'GONDOLA',
Rail = 'RAIL',
Subway = 'SUBWAY',
Train = 'TRAIN',
Tram = 'TRAM',
Transit = 'TRANSIT',
Walk = 'WALK',
Expand Down
2 changes: 1 addition & 1 deletion services/frontend/www-app/src/services/TravelmuxClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export class TravelmuxClient {

const query = new URLSearchParams(params).toString();

const response = await fetch('/travelmux/v3/plan?' + query);
const response = await fetch('/travelmux/v4/plan?' + query);

if (response.ok) {
const travelmuxResponseJson: TravelmuxPlanResponse =
Expand Down
12 changes: 12 additions & 0 deletions services/travelmux/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions services/travelmux/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ polyline = "0.10.1"
reqwest = { version = "0.11.13", features = ["json", "stream"] }
serde = { version = "1.0.152", features = ["derive"] }
serde_json = "1.0.91"
serde_repr = "0.1.18"
url = "2.4.0"
wkt = "0.10.3"

Expand Down
2 changes: 1 addition & 1 deletion services/travelmux/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ mod app_state;
pub use app_state::AppState;

pub mod health;
pub mod v2;
pub mod v3;
pub mod v4;
1 change: 0 additions & 1 deletion services/travelmux/src/api/v2/mod.rs

This file was deleted.

6 changes: 3 additions & 3 deletions services/travelmux/src/api/v3/plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub struct PlanQuery {
}

use crate::error::ErrorType;
use crate::valhalla::valhalla_api::ManeuverType;
use error::PlanResponseErr;

#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
Expand Down Expand Up @@ -340,7 +341,7 @@ pub struct Maneuver {
pub time: f64,
pub travel_mode: String,
pub travel_type: String,
pub r#type: u64,
pub r#type: ManeuverType,
pub verbal_post_transition_instruction: Option<String>,
pub verbal_pre_transition_instruction: Option<String>,
pub verbal_succinct_transition_instruction: Option<String>,
Expand Down Expand Up @@ -685,15 +686,14 @@ mod tests {
}"#;

let valhalla_maneuver: valhalla_api::Maneuver = serde_json::from_str(json).unwrap();
assert_eq!(valhalla_maneuver.r#type, 2);
assert_eq!(valhalla_maneuver.r#type, ManeuverType::StartRight);
assert_eq!(
valhalla_maneuver.instruction,
"Drive northeast on Fauntleroy Way Southwest."
);

let maneuver = Maneuver::from_valhalla(valhalla_maneuver);
let actual = serde_json::to_string(&maneuver).unwrap();
dbg!(&actual);
// parse the JSON string back into an Object Value
let actual_object: Value = serde_json::from_str(&actual).unwrap();

Expand Down
215 changes: 215 additions & 0 deletions services/travelmux/src/api/v4/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
use crate::otp::otp_api;
use crate::valhalla::valhalla_api;
use crate::{Error, TravelMode};
use actix_web::HttpResponseBuilder;
use serde::{Deserialize, Serialize};

use super::Plan;
use crate::error::ErrorType;
use actix_web::body::BoxBody;
use actix_web::HttpResponse;
use std::fmt;

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UnboxedPlanResponseErr {
pub error: PlanError,
// The raw response from the upstream OTP /plan service
#[serde(rename = "_otp")]
_otp: Option<otp_api::PlanError>,

// The raw response from the upstream Valhalla /route service
#[serde(rename = "_valhalla")]
_valhalla: Option<valhalla_api::RouteResponseError>,
}
pub type PlanResponseErr = Box<UnboxedPlanResponseErr>;

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PlanError {
pub status_code: u16,
pub error_code: u32,
pub message: String,
}

#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PlanResponseOk {
pub(crate) plan: Plan,

// The raw response from the upstream OTP /plan service
#[serde(rename = "_otp")]
_otp: Option<otp_api::PlanResponse>,

// The raw response from the upstream Valhalla /route service
#[serde(rename = "_valhalla")]
_valhalla: Option<valhalla_api::RouteResponse>,
}

impl From<valhalla_api::RouteResponseError> for PlanResponseErr {
fn from(value: valhalla_api::RouteResponseError) -> Self {
Self::new(UnboxedPlanResponseErr {
error: (&value).into(),
_valhalla: Some(value),
_otp: None,
})
}
}

impl From<otp_api::PlanError> for PlanResponseErr {
fn from(value: otp_api::PlanError) -> Self {
Self::new(UnboxedPlanResponseErr {
error: (&value).into(),
_valhalla: None,
_otp: Some(value),
})
}
}

impl From<&valhalla_api::RouteResponseError> for PlanError {
fn from(value: &valhalla_api::RouteResponseError) -> Self {
PlanError {
status_code: value.status_code,
error_code: value.error_code + 2000,
message: value.error.clone(),
}
}
}

impl fmt::Display for PlanResponseErr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"status_code: {}, error_code: {}, message: {}",
self.error.status_code, self.error.error_code, self.error.message
)
}
}

impl std::error::Error for PlanResponseErr {}

impl From<Error> for PlanResponseErr {
fn from(value: Error) -> Self {
Self::new(UnboxedPlanResponseErr {
error: value.into(),
_valhalla: None,
_otp: None,
})
}
}

impl From<Error> for PlanError {
fn from(value: Error) -> Self {
let error_code = value.error_type as u32;
debug_assert!(error_code < 2000);
debug_assert!(error_code > 1000);
match value.error_type {
ErrorType::ThisTransitAreaNotCovered => Self {
status_code: 400,
error_code,
message: value.source.to_string(),
},
ErrorType::User => Self {
status_code: 400,
error_code,
message: value.source.to_string(),
},
ErrorType::Server => Self {
status_code: 500,
error_code,
message: value.source.to_string(),
},
}
}
}

impl From<&otp_api::PlanError> for PlanError {
fn from(value: &otp_api::PlanError) -> Self {
Self {
// This might be overzealous, but anecdotally, I haven't encountered any 500ish
// errors with OTP surfaced in this way yet
status_code: 400,
error_code: value.id,
message: value.msg.clone(),
}
}
}

impl actix_web::ResponseError for PlanResponseErr {
fn status_code(&self) -> actix_web::http::StatusCode {
self.error.status_code.try_into().unwrap_or_else(|e| {
log::error!(
"invalid status code: {}, err: {e:?}",
self.error.status_code
);
actix_web::http::StatusCode::INTERNAL_SERVER_ERROR
})
}

fn error_response(&self) -> HttpResponse<BoxBody> {
HttpResponseBuilder::new(self.status_code())
.content_type("application/json")
.json(self)
}
}

impl PlanResponseOk {
pub fn from_otp(
mode: TravelMode,
mut otp: otp_api::PlanResponse,
) -> Result<PlanResponseOk, PlanResponseErr> {
if let Some(otp_error) = otp.error {
return Err(otp_error.into());
}

otp.plan
.itineraries
.sort_by(|a, b| a.end_time.cmp(&b.end_time));

let itineraries_result: crate::Result<Vec<_>> = otp
.plan
.itineraries
.iter()
.map(|itinerary: &otp_api::Itinerary| {
crate::api::v4::plan::Itinerary::from_otp(itinerary, mode)
})
.collect();

let itineraries = itineraries_result?;

Ok(PlanResponseOk {
plan: Plan { itineraries },
_otp: Some(otp),
_valhalla: None,
})
}

pub fn from_valhalla(
mode: TravelMode,
valhalla: valhalla_api::ValhallaRouteResponseResult,
) -> Result<PlanResponseOk, valhalla_api::RouteResponseError> {
let valhalla = match valhalla {
valhalla_api::ValhallaRouteResponseResult::Ok(valhalla) => valhalla,
valhalla_api::ValhallaRouteResponseResult::Err(err) => return Err(err),
};

let mut itineraries = vec![crate::api::v4::plan::Itinerary::from_valhalla(
&valhalla.trip,
mode,
)];
if let Some(alternates) = &valhalla.alternates {
for alternate in alternates {
itineraries.push(crate::api::v4::plan::Itinerary::from_valhalla(
&alternate.trip,
mode,
));
}
}

Ok(PlanResponseOk {
plan: Plan { itineraries },
_otp: None,
_valhalla: Some(valhalla),
})
}
}
4 changes: 4 additions & 0 deletions services/travelmux/src/api/v4/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
mod error;
pub mod plan;

pub use plan::Plan;
Loading

0 comments on commit 87c3fa8

Please sign in to comment.