From 92f5e1fb01e365bc906b1e7ef4fac91b967d117f Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Fri, 10 May 2024 14:59:07 -0700 Subject: [PATCH 01/24] wip on OSRM formatted responses Route will render on iOS. -[ ] text instructions -[ ] arrow instructions -[ ] lane configuration -[ ] camera orientation faces forward --- services/travelmux/src/api/v5/error.rs | 4 +- services/travelmux/src/api/v5/mod.rs | 2 + services/travelmux/src/api/v5/plan.rs | 447 +++++++++++++++--- services/travelmux/src/api/v5/travel_modes.rs | 49 ++ .../src/bin/travelmux-server/main.rs | 1 + services/travelmux/src/util/mod.rs | 23 +- .../travelmux/src/valhalla/valhalla_api.rs | 9 +- 7 files changed, 466 insertions(+), 69 deletions(-) create mode 100644 services/travelmux/src/api/v5/travel_modes.rs diff --git a/services/travelmux/src/api/v5/error.rs b/services/travelmux/src/api/v5/error.rs index d300f1af4..8e33623e8 100644 --- a/services/travelmux/src/api/v5/error.rs +++ b/services/travelmux/src/api/v5/error.rs @@ -2,7 +2,7 @@ use crate::otp::otp_api; use crate::valhalla::valhalla_api; use crate::{DistanceUnit, Error, TravelMode}; use actix_web::HttpResponseBuilder; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use super::{Itinerary, Plan}; use crate::error::ErrorType; @@ -32,7 +32,7 @@ pub struct PlanError { pub message: String, } -#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +#[derive(Debug, Serialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub struct PlanResponseOk { pub(crate) plan: Plan, diff --git a/services/travelmux/src/api/v5/mod.rs b/services/travelmux/src/api/v5/mod.rs index 20c8b18b4..44166667c 100644 --- a/services/travelmux/src/api/v5/mod.rs +++ b/services/travelmux/src/api/v5/mod.rs @@ -1,4 +1,6 @@ mod error; pub mod plan; +mod travel_modes; +pub use travel_modes::TravelModes; pub use plan::{Itinerary, Plan}; diff --git a/services/travelmux/src/api/v5/plan.rs b/services/travelmux/src/api/v5/plan.rs index 4ec9a56fe..c02ddf935 100644 --- a/services/travelmux/src/api/v5/plan.rs +++ b/services/travelmux/src/api/v5/plan.rs @@ -3,26 +3,27 @@ use geo::algorithm::BoundingRect; use geo::geometry::{LineString, Point, Rect}; use polyline::decode_polyline; use reqwest::header::{HeaderName, HeaderValue}; -use serde::{de, de::IntoDeserializer, de::Visitor, Deserialize, Deserializer, Serialize}; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use std::fmt; use std::time::{Duration, SystemTime}; use super::error::{PlanResponseErr, PlanResponseOk}; +use super::TravelModes; + use crate::api::AppState; use crate::error::ErrorType; use crate::otp::otp_api; use crate::otp::otp_api::{AbsoluteDirection, RelativeDirection}; use crate::util::format::format_meters; use crate::util::{ - deserialize_point_from_lat_lon, extend_bounds, serialize_rect_to_lng_lat, - serialize_system_time_as_millis, system_time_from_millis, + deserialize_point_from_lat_lon, extend_bounds, serialize_line_string_as_polyline6, + serialize_rect_to_lng_lat, serialize_system_time_as_millis, system_time_from_millis, }; use crate::valhalla::valhalla_api; use crate::valhalla::valhalla_api::{LonLat, ManeuverType}; use crate::{DistanceUnit, Error, TravelMode}; -#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +#[derive(Debug, Deserialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub struct PlanQuery { #[serde(deserialize_with = "deserialize_point_from_lat_lon")] @@ -40,13 +41,13 @@ pub struct PlanQuery { preferred_distance_units: Option, } -#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +#[derive(Debug, Serialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub struct Plan { pub(crate) itineraries: Vec, } -#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +#[derive(Debug, Serialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub struct Itinerary { mode: TravelMode, @@ -58,7 +59,11 @@ pub struct Itinerary { /// unix millis, UTC #[serde(serialize_with = "serialize_system_time_as_millis")] end_time: SystemTime, + /// Units are in `distance_units` distance: f64, + /// FIXME: I think we're returning meters even though distance unit is "Kilometers" + /// Probably we should rename DistanceUnit::Kilometers to DistanceUnit::Meters + /// This is passed as a parameter though, so it'd be a breaking change. distance_units: DistanceUnit, #[serde(serialize_with = "serialize_rect_to_lng_lat")] bounds: Rect, @@ -66,6 +71,21 @@ pub struct Itinerary { } impl Itinerary { + pub fn distance_meters(&self) -> f64 { + match self.distance_units { + DistanceUnit::Kilometers => self.distance, + DistanceUnit::Miles => self.distance * 1609.34, + } + } + + pub fn combined_geometry(&self) -> LineString { + let mut combined_geometry = LineString::new(vec![]); + for leg in &self.legs { + combined_geometry.0.extend(&leg.geometry.0); + } + combined_geometry + } + pub fn from_valhalla(valhalla: &valhalla_api::Trip, mode: TravelMode) -> Self { let bounds = Rect::new( geo::coord!(x: valhalla.summary.min_lon, y: valhalla.summary.min_lat), @@ -136,11 +156,11 @@ impl Itinerary { let Some(first_leg) = legs_iter.next() else { return Err(Error::server("itinerary had no legs")); }; - let Ok(Some(mut itinerary_bounds)) = first_leg.bounding_rect() else { + let Some(mut itinerary_bounds) = first_leg.bounding_rect() else { return Err(Error::server("first leg has no bounding_rect")); }; for leg in legs_iter { - let Ok(Some(leg_bounds)) = leg.bounding_rect() else { + let Some(leg_bounds) = leg.bounding_rect() else { return Err(Error::server("leg has no bounding_rect")); }; extend_bounds(&mut itinerary_bounds, &leg_bounds); @@ -185,11 +205,12 @@ impl From for Place { } } -#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +#[derive(Debug, Serialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] struct Leg { /// encoded polyline. 1e-6 scale, (lat, lon) - geometry: String, + #[serde(serialize_with = "serialize_line_string_as_polyline6")] + geometry: LineString, /// Which mode is this leg of the journey? mode: TravelMode, @@ -214,12 +235,18 @@ struct Leg { /// Start time of the leg #[serde(serialize_with = "serialize_system_time_as_millis")] end_time: SystemTime, + + /// Length of this leg + distance_meters: f64, + + /// Duration of this leg + duration_seconds: f64, } // Should we just pass the entire OTP leg? type TransitLeg = otp_api::Leg; -#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +#[derive(Debug, Serialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] enum ModeLeg { // REVIEW: rename? There is a boolean field for OTP called TransitLeg @@ -230,7 +257,7 @@ enum ModeLeg { NonTransit(Box), } -#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +#[derive(Debug, Serialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] struct NonTransitLeg { maneuvers: Vec, @@ -282,18 +309,21 @@ impl NonTransitLeg { // Eventually we might want to coalesce this into something not valhalla specific // but for now we only use it for valhalla trips -#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +#[derive(Debug, Serialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub struct Maneuver { pub instruction: Option, // pub cost: f64, // pub begin_shape_index: u64, // pub end_shape_index: u64, + #[serde(skip_serializing)] + pub geometry: LineString, // pub highway: Option, /// In units of the `distance_unit` of the trip leg pub distance: f64, pub street_names: Option>, - // pub time: f64, + #[serde(skip_serializing)] + pub duration_seconds: f64, // pub travel_mode: String, // pub travel_type: String, pub r#type: ManeuverType, @@ -305,13 +335,19 @@ pub struct Maneuver { impl Maneuver { fn from_valhalla(valhalla: valhalla_api::Maneuver, leg_geometry: &LineString) -> Self { + let coords = leg_geometry.0 + [valhalla.begin_shape_index as usize..=valhalla.end_shape_index as usize] + .to_owned(); + let geometry = LineString::from(coords); Self { instruction: Some(valhalla.instruction), street_names: valhalla.street_names, + duration_seconds: valhalla.time, r#type: valhalla.r#type, start_point: Point(leg_geometry[valhalla.begin_shape_index as usize]).into(), verbal_post_transition_instruction: valhalla.verbal_post_transition_instruction, distance: valhalla.length, + geometry, } } @@ -340,13 +376,25 @@ impl Maneuver { // round to the nearest ten-thousandth DistanceUnit::Miles => (otp.distance * 0.621371 * 10_000.0).round() / 10_000.0, }; + + log::error!("TODO: synthesize geometry for OTP steps"); + let geometry = LineString::new(vec![]); Self { instruction, r#type: otp.relative_direction.into(), street_names, verbal_post_transition_instruction, distance: localized_distance, + duration_seconds: 666.0, // TODO: OTP doesn't provide this at a granular level - only at the Leg level start_point: Point::new(otp.lon, otp.lat).into(), + geometry, + } + } + + fn distance_meters(&self, distance_unit: DistanceUnit) -> f64 { + match distance_unit { + DistanceUnit::Kilometers => self.distance, + DistanceUnit::Miles => self.distance * 1609.34, } } } @@ -422,13 +470,8 @@ impl Leg { const VALHALLA_GEOMETRY_PRECISION: u32 = 6; const OTP_GEOMETRY_PRECISION: u32 = 5; - fn decoded_geometry(&self) -> std::result::Result { - decode_polyline(&self.geometry, Self::GEOMETRY_PRECISION) - } - - fn bounding_rect(&self) -> std::result::Result, String> { - let line_string = self.decoded_geometry()?; - Ok(line_string.bounding_rect()) + fn bounding_rect(&self) -> Option { + self.geometry.bounding_rect() } fn from_otp( @@ -437,8 +480,7 @@ impl Leg { distance_unit: DistanceUnit, ) -> std::result::Result { debug_assert_ne!(Self::OTP_GEOMETRY_PRECISION, Self::GEOMETRY_PRECISION); - let line = decode_polyline(&otp.leg_geometry.points, Self::OTP_GEOMETRY_PRECISION)?; - let geometry = polyline::encode_coordinates(line, Self::GEOMETRY_PRECISION)?; + let geometry = decode_polyline(&otp.leg_geometry.points, Self::OTP_GEOMETRY_PRECISION)?; let from_place: Place = (&otp.from).into(); let to_place: Place = (&otp.to).into(); @@ -459,9 +501,11 @@ impl Leg { instruction: Some("Arrive at your destination.".to_string()), distance: 0.0, street_names: None, + duration_seconds: 0.0, r#type: ManeuverType::Destination, verbal_post_transition_instruction: None, start_point: to_place.location, + geometry: LineString::new(vec![to_place.location.into()]), }; maneuvers.push(maneuver); } @@ -481,6 +525,8 @@ impl Leg { end_time: system_time_from_millis(otp.end_time), geometry, mode: otp.mode.into(), + distance_meters: otp.distance, + duration_seconds: (otp.end_time - otp.start_time) as f64 / 1000.0, mode_leg, }) } @@ -494,7 +540,8 @@ impl Leg { to_place: LonLat, ) -> Self { let geometry = - polyline::decode_polyline(&valhalla.shape, Self::VALHALLA_GEOMETRY_PRECISION).unwrap(); + polyline::decode_polyline(&valhalla.shape, Self::VALHALLA_GEOMETRY_PRECISION) + .expect("valid polyline from valhalla"); let maneuvers = valhalla .maneuvers .iter() @@ -502,60 +549,273 @@ impl Leg { .map(|valhalla_maneuver| Maneuver::from_valhalla(valhalla_maneuver, &geometry)) .collect(); let leg = NonTransitLeg::new(maneuvers); - debug_assert_eq!(Self::VALHALLA_GEOMETRY_PRECISION, Self::GEOMETRY_PRECISION); Self { start_time, end_time, from_place: from_place.into(), to_place: to_place.into(), - geometry: valhalla.shape.clone(), + geometry, mode: travel_mode, mode_leg: ModeLeg::NonTransit(Box::new(leg)), + // TODO: verify units here - might be in miles + distance_meters: valhalla.summary.length, + duration_seconds: valhalla.summary.time, } } } -// Comma separated list of travel modes -#[derive(Debug, Serialize, PartialEq, Eq, Clone)] -struct TravelModes(Vec); +impl actix_web::Responder for PlanResponseOk { + type Body = actix_web::body::BoxBody; + + fn respond_to(self, _req: &HttpRequest) -> actix_web::HttpResponse { + let mut response = HttpResponseBuilder::new(actix_web::http::StatusCode::OK); + response.content_type("application/json"); + response.json(self) + } +} + +#[derive(Debug, Serialize, Clone, PartialEq)] +struct DirectionsResponseOk { + routes: Vec, +} + +impl actix_web::Responder for DirectionsResponseOk { + type Body = actix_web::body::BoxBody; + + fn respond_to(self, _req: &HttpRequest) -> actix_web::HttpResponse { + let mut response = HttpResponseBuilder::new(actix_web::http::StatusCode::OK); + response.content_type("application/json"); + response.json(self) + } +} + +#[get("/v5/directions")] +pub async fn get_directions( + query: web::Query, + req: HttpRequest, + app_state: web::Data, +) -> std::result::Result { + let plan_response_ok = _get_plan(query, req, app_state).await?; + Ok(plan_response_ok.into()) +} + +pub mod osrm_api { + use super::{Itinerary, Leg, Maneuver, ModeLeg}; + use crate::util::{serialize_point_as_lon_lat_pair, serialize_line_string_as_polyline6}; + use crate::{DistanceUnit, TravelMode}; + use geo::{LineString, Point}; + use serde::Serialize; -impl<'de> Deserialize<'de> for TravelModes { - fn deserialize(deserializer: D) -> std::result::Result - where - D: Deserializer<'de>, - { - struct CommaSeparatedVecVisitor; + #[derive(Debug, Serialize, PartialEq, Clone)] + pub struct Route { + /// The distance traveled by the route, in float meters. + pub distance: f64, - impl<'de> Visitor<'de> for CommaSeparatedVecVisitor { - type Value = TravelModes; + /// The estimated travel time, in float number of seconds. + pub duration: f64, - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a comma-separated string") + // todo: simplify? + /// The entire geometry of the route + #[serde(serialize_with="serialize_line_string_as_polyline6")] + pub geometry: LineString, + + /// The legs between the given waypoints + pub legs: Vec, + } + + impl From for Route { + fn from(itinerary: Itinerary) -> Self { + Route { + distance: itinerary.distance_meters(), + duration: itinerary.duration, + geometry: itinerary.combined_geometry(), + legs: itinerary + .legs + .into_iter() + .map(|leg| RouteLeg::from_leg(leg, itinerary.distance_units)) + .collect(), } + } + } - fn visit_str(self, value: &str) -> std::result::Result - where - E: de::Error, - { - let modes = value - .split(',') - .map(|s| TravelMode::deserialize(s.into_deserializer())) - .collect::>()?; - Ok(TravelModes(modes)) + #[derive(Debug, Serialize, PartialEq, Clone)] + pub struct RouteLeg { + /// The distance traveled by this leg, in float meters. + pub distance: f64, + + /// The estimated travel time, in float number of seconds. + pub duration: f64, + + /// A short human-readable summary of the route leg + pub summary: String, + + /// Objects describing the turn-by-turn instructions of the route leg + pub steps: Vec, + // /// Additional details about each coordinate along the route geometry + // annotation: Annotation + } + + impl RouteLeg { + fn from_leg(value: Leg, distance_unit: DistanceUnit) -> Self { + let (summary, steps) = match value.mode_leg { + ModeLeg::Transit(_) => { + debug_assert!( + false, + "didn't expect to generate navigation for transit leg" + ); + ("".to_string(), vec![]) + } + ModeLeg::NonTransit(non_transit_leg) => { + let summary = non_transit_leg.substantial_street_names.join(", "); + let steps = non_transit_leg + .maneuvers + .into_iter() + .map(|maneuver| { + RouteStep::from_maneuver(maneuver, value.mode, distance_unit) + }) + .collect(); + (summary, steps) + } + }; + Self { + distance: value.distance_meters, + duration: value.duration_seconds, + summary, + steps, } } + } + + #[derive(Debug, Serialize, PartialEq, Clone)] + pub struct RouteStep { + /// The distance traveled by this step, in float meters. + pub distance: f64, + + /// The estimated travel time, in float number of seconds. + pub duration: f64, + + /// The unsimplified geometry of the route segment. + #[serde(serialize_with="serialize_line_string_as_polyline6")] + pub geometry: LineString, - deserializer.deserialize_str(CommaSeparatedVecVisitor) + /// The name of the way along which travel proceeds. + pub name: String, + + /// A reference number or code for the way. Optionally included, if ref data is available for the given way. + pub r#ref: Option, + + /// The pronunciation hint of the way name. Will be undefined if there is no pronunciation hit. + pub pronunciation: Option, + + /// The destinations of the way. Will be undefined if there are no destinations. + pub destinations: Option>, + + /// A string signifying the mode of transportation. + pub mode: TravelMode, + + /// A `StepManeuver` object representing the maneuver. + pub maneuver: StepManeuver, + + /// A list of `Intersection` objects that are passed along the segment, the very first belonging to the `StepManeuver` + pub intersections: Option>, } -} -impl actix_web::Responder for PlanResponseOk { - type Body = actix_web::body::BoxBody; + impl RouteStep { + fn from_maneuver( + value: Maneuver, + mode: TravelMode, + from_distance_unit: DistanceUnit, + ) -> Self { + RouteStep { + distance: value.distance_meters(from_distance_unit), + duration: value.duration_seconds, + geometry: value.geometry, + name: value + .street_names + .unwrap_or(vec!["".to_string()]) + .join(", "), + r#ref: None, + pronunciation: None, + destinations: None, + mode, + maneuver: StepManeuver { + location: value.start_point.into(), + }, + intersections: None, //vec![], + } + } + } + + #[derive(Debug, Serialize, PartialEq, Clone)] + pub struct StepManeuver { + /// The location of the maneuver + #[serde(serialize_with = "serialize_point_as_lon_lat_pair")] + pub location: Point, + // /// The type of maneuver. new identifiers might be introduced without changing the API, so best practice is to gracefully handle any new values + // r#type: String, + // /// A human-readable instruction of how to execute the returned maneuver + // instruction: String, + // /// The bearing before the turn, in degrees + // bearing_before: f64, + // /// The bearing after the turn, in degrees + // bearing_after: f64, + // /// The exit number or name of the way. This field is to indicate the next way or roundabout exit to take + // exit: String, + // /// The modifier of the maneuver. new identifiers might be introduced without changing the API, so best practice is to gracefully handle any new values + // modifier: String, + // /// The type of the modifier of the maneuver. new identifiers might be introduced without changing the API, so best practice is to gracefully handle any new values + // modifier_type: String, + } + + #[derive(Debug, Serialize, PartialEq, Clone)] + pub struct Intersection { + /// A [longitude, latitude] pair describing the location of the turn. + #[serde(serialize_with = "serialize_point_as_lon_lat_pair")] + pub location: Point, + + /// A list of bearing values that are available at the intersection. The bearings describe all available roads at the intersection. + pub bearings: Vec, + + // /// An array of strings signifying the classes of the road exiting the intersection. + // pub classes: Vec + + /// A list of entry flags, corresponding in a 1:1 relationship to the bearings. A value of true indicates that the respective road could be entered on a valid route. + pub entry: Vec, + + /// The zero-based index into the geometry, relative to the start of the leg it's on. This value can be used to apply the duration annotation that corresponds with the intersection. + pub geometry_index: Option, + + /// The index in the bearings and entry arrays. Used to calculate the bearing before the turn. Namely, the clockwise angle from true north to the direction of travel before the maneuver/passing the intersection. To get the bearing in the direction of driving, the bearing has to be rotated by a value of 180. The value is not supplied for departure maneuvers. + pub r#in: Option, + + /// The index in the bearings and entry arrays. Used to extract the bearing after the turn. Namely, the clockwise angle from true north to the direction of travel after the maneuver/passing the intersection. The value is not supplied for arrival maneuvers. + pub out: Option, + + /// An array of lane objects that represent the available turn lanes at the intersection. If no lane information is available for an intersection, the lanes property will not be present. + pub lanes: Vec, + + /// The time required, in seconds, to traverse the intersection. Only available on the driving profile. + pub duration: Option, + + // TODO: lots more fields in OSRM + } + + #[derive(Debug, Serialize, PartialEq, Clone)] + pub struct Lane { + // TODO: lots more fields in OSRM - fn respond_to(self, _req: &HttpRequest) -> actix_web::HttpResponse { - let mut response = HttpResponseBuilder::new(actix_web::http::StatusCode::OK); - response.content_type("application/json"); - response.json(self) + } +} + +impl From for DirectionsResponseOk { + fn from(value: PlanResponseOk) -> Self { + let routes = value + .plan + .itineraries + .into_iter() + .map(osrm_api::Route::from) + .collect(); + Self { routes } } } @@ -565,7 +825,15 @@ pub async fn get_plan( req: HttpRequest, app_state: web::Data, ) -> std::result::Result { - let Some(primary_mode) = query.mode.0.first() else { + _get_plan(query, req, app_state).await +} + +pub async fn _get_plan( + query: web::Query, + req: HttpRequest, + app_state: web::Data, +) -> std::result::Result { + let Some(primary_mode) = query.mode.first() else { return Err(PlanResponseErr::from(Error::user("mode is required"))); }; @@ -578,7 +846,7 @@ pub async fn get_plan( match primary_mode { TravelMode::Transit => otp_plan(&query, req, &app_state, primary_mode).await, other => { - debug_assert!(query.mode.0.len() == 1, "valhalla only supports one mode"); + debug_assert!(query.mode.len() == 1, "valhalla only supports one mode"); let mode = match other { TravelMode::Transit => unreachable!("handled above"), @@ -753,9 +1021,8 @@ mod tests { // legs assert_eq!(first_itinerary.legs.len(), 1); let first_leg = &first_itinerary.legs[0]; - let geometry = decode_polyline(&first_leg.geometry, 6).unwrap(); assert_relative_eq!( - geometry.0[0], + first_leg.geometry.0[0], geo::coord!(x: -122.33922, y: 47.57583), epsilon = 1e-4 ); @@ -799,9 +1066,8 @@ mod tests { // legs assert_eq!(first_itinerary.legs.len(), 7); let first_leg = &first_itinerary.legs[0]; - let geometry = polyline::decode_polyline(&first_leg.geometry, 6).unwrap(); assert_relative_eq!( - geometry.0[0], + first_leg.geometry.0[0], geo::coord!(x: -122.33922, y: 47.57583), epsilon = 1e-4 ); @@ -1009,7 +1275,7 @@ mod tests { { "begin_shape_index": 0, "cost": 246.056, - "end_shape_index": 69, + "end_shape_index": 1, "highway": true, "instruction": "Drive northeast on Fauntleroy Way Southwest.", "length": 2.218, @@ -1069,4 +1335,57 @@ mod tests { assert_eq!(plan_error.error.status_code, 400); assert_eq!(plan_error.error.error_code, 2154); } + + #[test] + fn navigation_response_from_valhalla() { + let stubbed_response = + File::open("tests/fixtures/requests/valhalla_route_walk.json").unwrap(); + let valhalla: valhalla_api::RouteResponse = + serde_json::from_reader(BufReader::new(stubbed_response)).unwrap(); + + let valhalla_response_result = valhalla_api::ValhallaRouteResponseResult::Ok(valhalla); + let plan_response = + PlanResponseOk::from_valhalla(TravelMode::Walk, valhalla_response_result).unwrap(); + + let directions_response = DirectionsResponseOk::from(plan_response); + assert_eq!(directions_response.routes.len(), 3); + + let first_route = &directions_response.routes[0]; + assert_relative_eq!(first_route.distance, 9.148); + assert_relative_eq!(first_route.duration, 6488.443); + assert_relative_eq!( + first_route.geometry.0[0], + geo::coord!(x: -122.339216, y: 47.575836) + ); + assert_relative_eq!( + first_route.geometry.0.last().unwrap(), + &geo::coord!(x: -122.347199, y: 47.651048) + ); + + let legs = &first_route.legs; + assert_eq!(legs.len(), 1); + let first_leg = &legs[0]; + + assert_eq!(first_leg.distance, 9.148); + assert_eq!(first_leg.duration, 6488.443); + assert_eq!( + first_leg.summary, + "Dexter Avenue, East Marginal Way South, Alaskan Way South" + ); + assert_eq!(first_leg.steps.len(), 21); + + let first_step = &first_leg.steps[0]; + assert_eq!(first_step.distance, 0.019); + assert_eq!(first_step.duration, 13.567); + assert_eq!(first_step.name, "East Marginal Way South"); + assert_eq!(first_step.mode, TravelMode::Walk); + + let step_maneuver = &first_step.maneuver; + assert_eq!( + step_maneuver.location, + geo::point!(x: -122.339216, y: 47.575836) + ); + // TODO: step_maneuver stuff + // etc... + } } diff --git a/services/travelmux/src/api/v5/travel_modes.rs b/services/travelmux/src/api/v5/travel_modes.rs new file mode 100644 index 000000000..6c3cee415 --- /dev/null +++ b/services/travelmux/src/api/v5/travel_modes.rs @@ -0,0 +1,49 @@ +use crate::TravelMode; +use serde::de::Visitor; +use serde::{de, de::IntoDeserializer, Deserialize, Deserializer}; +use std::fmt; + +// Comma separated list of travel modes +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct TravelModes(Vec); +impl TravelModes { + pub fn len(&self) -> usize { + self.0.len() + } + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + pub fn first(&self) -> Option<&TravelMode> { + self.0.first() + } +} + +impl<'de> Deserialize<'de> for TravelModes { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct CommaSeparatedVecVisitor; + + impl<'de> Visitor<'de> for CommaSeparatedVecVisitor { + type Value = TravelModes; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a comma-separated string") + } + + fn visit_str(self, value: &str) -> std::result::Result + where + E: de::Error, + { + let modes = value + .split(',') + .map(|s| TravelMode::deserialize(s.into_deserializer())) + .collect::>()?; + Ok(TravelModes(modes)) + } + } + + deserializer.deserialize_str(CommaSeparatedVecVisitor) + } +} diff --git a/services/travelmux/src/bin/travelmux-server/main.rs b/services/travelmux/src/bin/travelmux-server/main.rs index b205c56bb..1d9c3df00 100644 --- a/services/travelmux/src/bin/travelmux-server/main.rs +++ b/services/travelmux/src/bin/travelmux-server/main.rs @@ -50,6 +50,7 @@ async fn main() -> Result<()> { .app_data(web::Data::new(app_state.clone())) .service(api::v4::plan::get_plan) .service(api::v5::plan::get_plan) + .service(api::v5::plan::get_directions) .service(api::health::get_ready) .service(api::health::get_alive) }) diff --git a/services/travelmux/src/util/mod.rs b/services/travelmux/src/util/mod.rs index 39a76696a..5b69b2b89 100644 --- a/services/travelmux/src/util/mod.rs +++ b/services/travelmux/src/util/mod.rs @@ -2,7 +2,7 @@ pub mod format; use geo::{Point, Rect}; use serde::{ - ser::{Error, SerializeStruct}, + ser::{Error, SerializeStruct, SerializeTuple}, Deserialize, Deserializer, Serializer, }; use std::time::{Duration, SystemTime}; @@ -35,6 +35,27 @@ where Ok(Point::new(lon, lat)) } +pub fn serialize_point_as_lon_lat_pair(point: &Point, serializer: S) -> Result +where + S: Serializer, +{ + let mut tuple_serializer = serializer.serialize_tuple(2)?; + tuple_serializer.serialize_element(&point.x())?; + tuple_serializer.serialize_element(&point.y())?; + tuple_serializer.end() +} + +pub fn serialize_line_string_as_polyline6( + line_string: &geo::LineString, + serializer: S, +) -> Result +where + S: Serializer, +{ + let string = polyline::encode_coordinates(line_string.0.iter().copied(), 6) + .map_err(S::Error::custom)?; + serializer.serialize_str(&string) +} pub fn deserialize_duration_from_seconds<'de, D>(deserializer: D) -> Result where diff --git a/services/travelmux/src/valhalla/valhalla_api.rs b/services/travelmux/src/valhalla/valhalla_api.rs index 245696b33..93db511ad 100644 --- a/services/travelmux/src/valhalla/valhalla_api.rs +++ b/services/travelmux/src/valhalla/valhalla_api.rs @@ -1,6 +1,6 @@ use crate::otp::otp_api; use crate::DistanceUnit; -use geo::Point; +use geo::{Coord, Point}; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; use std::collections::HashMap; @@ -86,7 +86,6 @@ pub struct Leg { pub summary: Summary, pub maneuvers: Vec, pub shape: String, - // pub length: f64, // pub steps: Vec, #[serde(flatten)] pub extra: HashMap, @@ -190,6 +189,12 @@ impl From for Point { } } +impl From for Coord { + fn from(value: LonLat) -> Self { + geo::coord!(x: value.lon, y: value.lat) + } +} + impl From for LonLat { fn from(value: otp_api::LonLat) -> Self { Self { From 7f5b382d821acfc726e88b76f0ae2c18c1c0da65 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Thu, 16 May 2024 09:50:10 -0700 Subject: [PATCH 02/24] WIP: banner format --- services/travelmux/src/api/v5/plan.rs | 240 ++++++++++++++++++++++++-- services/travelmux/src/util/mod.rs | 4 +- 2 files changed, 230 insertions(+), 14 deletions(-) diff --git a/services/travelmux/src/api/v5/plan.rs b/services/travelmux/src/api/v5/plan.rs index c02ddf935..10a8b036a 100644 --- a/services/travelmux/src/api/v5/plan.rs +++ b/services/travelmux/src/api/v5/plan.rs @@ -355,6 +355,7 @@ impl Maneuver { otp: otp_api::Step, mode: otp_api::TransitMode, distance_unit: DistanceUnit, + // leg_geometry: &LineString, ) -> Self { let instruction = build_instruction( mode, @@ -575,6 +576,7 @@ impl actix_web::Responder for PlanResponseOk { } #[derive(Debug, Serialize, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] struct DirectionsResponseOk { routes: Vec, } @@ -601,12 +603,15 @@ pub async fn get_directions( pub mod osrm_api { use super::{Itinerary, Leg, Maneuver, ModeLeg}; - use crate::util::{serialize_point_as_lon_lat_pair, serialize_line_string_as_polyline6}; + use crate::otp::otp_api::RelativeDirection::Continue; + use crate::util::{serialize_line_string_as_polyline6, serialize_point_as_lon_lat_pair}; + use crate::valhalla::valhalla_api::ManeuverType; use crate::{DistanceUnit, TravelMode}; use geo::{LineString, Point}; use serde::Serialize; #[derive(Debug, Serialize, PartialEq, Clone)] + #[serde(rename_all = "camelCase")] pub struct Route { /// The distance traveled by the route, in float meters. pub distance: f64, @@ -616,7 +621,7 @@ pub mod osrm_api { // todo: simplify? /// The entire geometry of the route - #[serde(serialize_with="serialize_line_string_as_polyline6")] + #[serde(serialize_with = "serialize_line_string_as_polyline6")] pub geometry: LineString, /// The legs between the given waypoints @@ -639,6 +644,7 @@ pub mod osrm_api { } #[derive(Debug, Serialize, PartialEq, Clone)] + #[serde(rename_all = "camelCase")] pub struct RouteLeg { /// The distance traveled by this leg, in float meters. pub distance: f64, @@ -687,6 +693,7 @@ pub mod osrm_api { } #[derive(Debug, Serialize, PartialEq, Clone)] + #[serde(rename_all = "camelCase")] pub struct RouteStep { /// The distance traveled by this step, in float meters. pub distance: f64, @@ -695,7 +702,7 @@ pub mod osrm_api { pub duration: f64, /// The unsimplified geometry of the route segment. - #[serde(serialize_with="serialize_line_string_as_polyline6")] + #[serde(serialize_with = "serialize_line_string_as_polyline6")] pub geometry: LineString, /// The name of the way along which travel proceeds. @@ -716,6 +723,9 @@ pub mod osrm_api { /// A `StepManeuver` object representing the maneuver. pub maneuver: StepManeuver, + /// A list of `BannerInstruction` objects that represent all signs on the step. + pub banner_instructions: Option>, + /// A list of `Intersection` objects that are passed along the segment, the very first belonging to the `StepManeuver` pub intersections: Option>, } @@ -726,6 +736,7 @@ pub mod osrm_api { mode: TravelMode, from_distance_unit: DistanceUnit, ) -> Self { + let banner_instructions = BannerInstruction::from_maneuver(&value); RouteStep { distance: value.distance_meters(from_distance_unit), duration: value.duration_seconds, @@ -742,32 +753,239 @@ pub mod osrm_api { location: value.start_point.into(), }, intersections: None, //vec![], + banner_instructions, } } } #[derive(Debug, Serialize, PartialEq, Clone)] + #[serde(rename_all = "camelCase")] + pub struct BannerInstruction { + distance_along_geometry: f64, + primary: BannerInstructionContent, + // secondary: Option, + // sub: Option, + } + + impl BannerInstruction { + fn from_maneuver(value: &Maneuver) -> Option> { + let text = value.street_names.as_ref().map(|names| names.join(", ")); + + let banner_maneuver = (|| { + use BannerManeuverModifier::*; + use BannerManeuverType::*; + let (banner_type, modifier) = match value.r#type { + ManeuverType::None => return None, + ManeuverType::Start => (Depart, None), + ManeuverType::StartRight => (Depart, Some(Right)), + ManeuverType::StartLeft => (Depart, Some(Left)), + ManeuverType::Destination => (Arrive, None), + ManeuverType::DestinationRight => (Arrive, Some(Right)), + ManeuverType::DestinationLeft => (Arrive, Some(Left)), + /* + ManeuverType::Becomes => {} + */ + ManeuverType::Continue => (Fork, None), // Or maybe just return None? + ManeuverType::SlightRight => (Turn, Some(SlightRight)), + ManeuverType::Right => (Turn, Some(Right)), + ManeuverType::SharpRight => (Turn, Some(SharpRight)), + /* + ManeuverType::UturnRight => {} + ManeuverType::UturnLeft => {} + */ + ManeuverType::SharpLeft => (Turn, Some(SharpLeft)), + ManeuverType::Left => (Turn, Some(Left)), + ManeuverType::SlightLeft => (Turn, Some(SlightLeft)), + // REVIEW: Is OffRamp correct here? + ManeuverType::RampStraight => (OffRamp, Some(Straight)), + ManeuverType::RampRight => (OffRamp, Some(Right)), + ManeuverType::RampLeft => (OffRamp, Some(Left)), + ManeuverType::ExitRight => (OffRamp, Some(Right)), + ManeuverType::ExitLeft => (OffRamp, Some(Left)), + ManeuverType::StayStraight => (Fork, None), // Or maybe just return None? + ManeuverType::StayRight => (Fork, Some(Right)), + ManeuverType::StayLeft => (Fork, Some(Left)), + /* + ManeuverType::Merge => {} + ManeuverType::RoundaboutEnter => {} + ManeuverType::RoundaboutExit => {} + ManeuverType::FerryEnter => {} + ManeuverType::FerryExit => {} + ManeuverType::Transit => {} + ManeuverType::TransitTransfer => {} + ManeuverType::TransitRemainOn => {} + ManeuverType::TransitConnectionStart => {} + ManeuverType::TransitConnectionTransfer => {} + ManeuverType::TransitConnectionDestination => {} + ManeuverType::PostTransitConnectionDestination => {} + ManeuverType::MergeRight => {} + ManeuverType::MergeLeft => {} + ManeuverType::ElevatorEnter => {} + ManeuverType::StepsEnter => {} + ManeuverType::EscalatorEnter => {} + ManeuverType::BuildingEnter => {} + ManeuverType::BuildingExit => {} + */ + _ => todo!("implement manuever type: {:?}", value.r#type), + }; + Some(BannerManeuver { + r#type: banner_type, + modifier, + }) + })(); + + let text_component = BannerComponent::Text(VisualInstructionComponent { text }); + // if let Some(banner_maneuver) = banner_maneuver { + // BannerComponent::Text(VisualInstructionComponent { + // text, + // }) + // } else { + // panic!("no banner_maneuver") + // } + // }; + + let primary = BannerInstructionContent { + text: value.instruction.clone()?, + components: vec![text_component], // TODO + banner_maneuver, + degrees: None, + driving_side: None, + }; + let instruction = BannerInstruction { + distance_along_geometry: 0.0, + primary, + }; + Some(vec![instruction]) + } + } + + // REVIEW: Rename to VisualInstructionBanner? + // How do audible instructions fit into this? + #[derive(Debug, Serialize, PartialEq, Clone)] + #[serde(rename_all = "camelCase")] + struct BannerInstructionContent { + text: String, + // components: Vec, + #[serde(flatten)] + banner_maneuver: Option, + degrees: Option, + driving_side: Option, + components: Vec, + } + + #[derive(Debug, Serialize, PartialEq, Clone)] + #[serde(rename_all = "lowercase")] + struct BannerManeuver { + r#type: BannerManeuverType, + modifier: Option, + } + + // This is for `banner.primary(et. al).type` + // There is a lot of overlap between this and `step_maneuver.type`, + // but the docs imply they are different. + #[derive(Debug, Serialize, PartialEq, Clone)] + #[serde(rename_all = "lowercase")] + enum BannerManeuverType { + Turn, + Merge, + Depart, + Arrive, + Fork, + #[serde(rename = "off ramp")] + OffRamp, + RoundAbout, + } + + #[derive(Debug, Serialize, PartialEq, Clone)] + #[serde(rename_all = "lowercase")] + enum BannerManeuverModifier { + Uturn, + #[serde(rename = "sharp right")] + SharpRight, + Right, + #[serde(rename = "slight right")] + SlightRight, + Straight, + #[serde(rename = "slight left")] + SlightLeft, + Left, + #[serde(rename = "sharp left")] + SharpLeft, + } + + // REVIEW: Rename to VisualInstruction? + // REVIEW: convert to inner enum of Lane or VisualInstructionComponent + #[derive(Debug, Serialize, PartialEq, Clone)] + #[serde(rename_all = "camelCase", tag = "type")] + #[non_exhaustive] + enum BannerComponent { + Text(VisualInstructionComponent), + // Icon(VisualInstructionComponent), + // Delimiter(VisualInstructionComponent), + // #[serde(rename="exit-number")] + // ExitNumber(VisualInstructionComponent), + // Exit(VisualInstructionComponent), + Lane(LaneInstructionComponent), + } + + #[derive(Debug, Serialize, PartialEq, Clone)] + #[serde(rename_all = "camelCase")] + struct LaneInstructionComponent {} + + // Maybe we won't use this? Because it'll need to be implicit in the containing BannerComponent enum variant of + #[derive(Debug, Serialize, PartialEq, Clone)] + #[serde(rename_all = "lowercase")] + enum VisualInstructionComponentType { + /// The component separates two other destination components. + /// + /// If the two adjacent components are both displayed as images, you can hide this delimiter component. + Delimiter, + + /// The component bears the name of a place or street. + Text, + + /// Component contains an image that should be rendered. + Image, + + /// The component contains the localized word for "exit". + /// + /// This component may appear before or after an `.ExitCode` component, depending on the language. + Exit, + + /// A component contains an exit number. + #[serde(rename = "exit-number")] + ExitCode, + } + + #[derive(Debug, Serialize, PartialEq, Clone)] + #[serde(rename_all = "camelCase")] + struct VisualInstructionComponent { + text: Option, + } + + #[derive(Debug, Serialize, PartialEq, Clone)] + #[serde(rename_all = "camelCase")] pub struct StepManeuver { /// The location of the maneuver #[serde(serialize_with = "serialize_point_as_lon_lat_pair")] pub location: Point, // /// The type of maneuver. new identifiers might be introduced without changing the API, so best practice is to gracefully handle any new values // r#type: String, + // /// The modifier of the maneuver. new identifiers might be introduced without changing the API, so best practice is to gracefully handle any new values + // modifier: String, // /// A human-readable instruction of how to execute the returned maneuver // instruction: String, // /// The bearing before the turn, in degrees + // OSRM expects `bearing_before` to be snake cased. + // #[serde(rename_all = "snake_case")] // bearing_before: f64, + // OSRM expects `bearing_after` to be snake cased. // /// The bearing after the turn, in degrees // bearing_after: f64, - // /// The exit number or name of the way. This field is to indicate the next way or roundabout exit to take - // exit: String, - // /// The modifier of the maneuver. new identifiers might be introduced without changing the API, so best practice is to gracefully handle any new values - // modifier: String, - // /// The type of the modifier of the maneuver. new identifiers might be introduced without changing the API, so best practice is to gracefully handle any new values - // modifier_type: String, } #[derive(Debug, Serialize, PartialEq, Clone)] + #[serde(rename_all = "camelCase")] pub struct Intersection { /// A [longitude, latitude] pair describing the location of the turn. #[serde(serialize_with = "serialize_point_as_lon_lat_pair")] @@ -778,7 +996,6 @@ pub mod osrm_api { // /// An array of strings signifying the classes of the road exiting the intersection. // pub classes: Vec - /// A list of entry flags, corresponding in a 1:1 relationship to the bearings. A value of true indicates that the respective road could be entered on a valid route. pub entry: Vec, @@ -796,14 +1013,13 @@ pub mod osrm_api { /// The time required, in seconds, to traverse the intersection. Only available on the driving profile. pub duration: Option, - // TODO: lots more fields in OSRM } #[derive(Debug, Serialize, PartialEq, Clone)] + #[serde(rename_all = "camelCase")] pub struct Lane { // TODO: lots more fields in OSRM - } } diff --git a/services/travelmux/src/util/mod.rs b/services/travelmux/src/util/mod.rs index 5b69b2b89..53d2a6536 100644 --- a/services/travelmux/src/util/mod.rs +++ b/services/travelmux/src/util/mod.rs @@ -52,8 +52,8 @@ pub fn serialize_line_string_as_polyline6( where S: Serializer, { - let string = polyline::encode_coordinates(line_string.0.iter().copied(), 6) - .map_err(S::Error::custom)?; + let string = + polyline::encode_coordinates(line_string.0.iter().copied(), 6).map_err(S::Error::custom)?; serializer.serialize_str(&string) } From caf9179ceb0896a5cc0431d0eb853c3826c1237d Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Thu, 16 May 2024 12:17:19 -0700 Subject: [PATCH 03/24] get my metric ducks in a metric row --- services/travelmux/src/api/v5/plan.rs | 67 ++++++++++++++++----------- 1 file changed, 41 insertions(+), 26 deletions(-) diff --git a/services/travelmux/src/api/v5/plan.rs b/services/travelmux/src/api/v5/plan.rs index 10a8b036a..392e499f3 100644 --- a/services/travelmux/src/api/v5/plan.rs +++ b/services/travelmux/src/api/v5/plan.rs @@ -23,6 +23,22 @@ use crate::valhalla::valhalla_api; use crate::valhalla::valhalla_api::{LonLat, ManeuverType}; use crate::{DistanceUnit, Error, TravelMode}; +const METERS_PER_MILE: f64 = 1609.34; + +fn convert_from_meters(meters: f64, output_units: DistanceUnit) -> f64 { + match output_units { + DistanceUnit::Kilometers => meters / 1000.0, + DistanceUnit::Miles => meters / METERS_PER_MILE, + } +} + +fn convert_to_meters(distance: f64, input_units: DistanceUnit) -> f64 { + match input_units { + DistanceUnit::Kilometers => distance * 1000.0, + DistanceUnit::Miles => distance * METERS_PER_MILE, + } +} + #[derive(Debug, Deserialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub struct PlanQuery { @@ -73,8 +89,8 @@ pub struct Itinerary { impl Itinerary { pub fn distance_meters(&self) -> f64 { match self.distance_units { - DistanceUnit::Kilometers => self.distance, - DistanceUnit::Miles => self.distance * 1609.34, + DistanceUnit::Kilometers => self.distance * 1000.0, + DistanceUnit::Miles => self.distance * METERS_PER_MILE, } } @@ -116,6 +132,7 @@ impl Itinerary { leg_end_time, locations[0], locations[1], + valhalla.units, ) }) .collect(); @@ -171,8 +188,8 @@ impl Itinerary { start_time: system_time_from_millis(itinerary.start_time), end_time: system_time_from_millis(itinerary.end_time), mode, - distance: distance_meters / 1000.0, - distance_units: DistanceUnit::Kilometers, + distance: convert_from_meters(distance_meters, distance_unit), + distance_units: distance_unit, bounds: itinerary_bounds, legs, }) @@ -236,8 +253,8 @@ struct Leg { #[serde(serialize_with = "serialize_system_time_as_millis")] end_time: SystemTime, - /// Length of this leg - distance_meters: f64, + /// Length of this leg, in units of the `distance_unit` of the Itinerary + distance: f64, /// Duration of this leg duration_seconds: f64, @@ -372,11 +389,6 @@ impl Maneuver { } else { Some(vec![otp.street_name]) }; - let localized_distance = match distance_unit { - DistanceUnit::Kilometers => otp.distance, - // round to the nearest ten-thousandth - DistanceUnit::Miles => (otp.distance * 0.621371 * 10_000.0).round() / 10_000.0, - }; log::error!("TODO: synthesize geometry for OTP steps"); let geometry = LineString::new(vec![]); @@ -385,7 +397,7 @@ impl Maneuver { r#type: otp.relative_direction.into(), street_names, verbal_post_transition_instruction, - distance: localized_distance, + distance: convert_from_meters(otp.distance, distance_unit), duration_seconds: 666.0, // TODO: OTP doesn't provide this at a granular level - only at the Leg level start_point: Point::new(otp.lon, otp.lat).into(), geometry, @@ -393,10 +405,7 @@ impl Maneuver { } fn distance_meters(&self, distance_unit: DistanceUnit) -> f64 { - match distance_unit { - DistanceUnit::Kilometers => self.distance, - DistanceUnit::Miles => self.distance * 1609.34, - } + convert_to_meters(self.distance, distance_unit) } } @@ -475,6 +484,10 @@ impl Leg { self.geometry.bounding_rect() } + fn distance_meters(&self, itinerary_units: DistanceUnit) -> f64 { + convert_to_meters(self.distance, itinerary_units) + } + fn from_otp( otp: &otp_api::Leg, is_destination_leg: bool, @@ -526,7 +539,7 @@ impl Leg { end_time: system_time_from_millis(otp.end_time), geometry, mode: otp.mode.into(), - distance_meters: otp.distance, + distance: convert_from_meters(otp.distance, distance_unit), duration_seconds: (otp.end_time - otp.start_time) as f64 / 1000.0, mode_leg, }) @@ -539,6 +552,7 @@ impl Leg { end_time: SystemTime, from_place: LonLat, to_place: LonLat, + distance_unit: DistanceUnit, ) -> Self { let geometry = polyline::decode_polyline(&valhalla.shape, Self::VALHALLA_GEOMETRY_PRECISION) @@ -558,8 +572,7 @@ impl Leg { geometry, mode: travel_mode, mode_leg: ModeLeg::NonTransit(Box::new(leg)), - // TODO: verify units here - might be in miles - distance_meters: valhalla.summary.length, + distance: valhalla.summary.length, duration_seconds: valhalla.summary.time, } } @@ -663,6 +676,7 @@ pub mod osrm_api { impl RouteLeg { fn from_leg(value: Leg, distance_unit: DistanceUnit) -> Self { + let distance_meters = value.distance_meters(distance_unit); let (summary, steps) = match value.mode_leg { ModeLeg::Transit(_) => { debug_assert!( @@ -684,7 +698,7 @@ pub mod osrm_api { } }; Self { - distance: value.distance_meters, + distance: distance_meters, duration: value.duration_seconds, summary, steps, @@ -1276,7 +1290,7 @@ mod tests { // itineraries let first_itinerary = &itineraries[0]; assert_eq!(first_itinerary.mode, TravelMode::Transit); - assert_relative_eq!(first_itinerary.distance, 10.69944); + assert_relative_eq!(first_itinerary.distance, 6.648340313420409); assert_relative_eq!(first_itinerary.duration, 3273.0); // legs @@ -1380,7 +1394,7 @@ mod tests { .unwrap(); let first_maneuver = maneuvers.get(0).unwrap(); let expected_maneuver = json!({ - "distance": 11.893, + "distance": 0.011893074179477305, // TODO: truncate precision in serializer "instruction": "Walk south on East Marginal Way South.", "startPoint": { "lat": 47.5758355, @@ -1553,7 +1567,7 @@ mod tests { } #[test] - fn navigation_response_from_valhalla() { + fn osrm_style_navigation_response_from_valhalla() { let stubbed_response = File::open("tests/fixtures/requests/valhalla_route_walk.json").unwrap(); let valhalla: valhalla_api::RouteResponse = @@ -1567,7 +1581,8 @@ mod tests { assert_eq!(directions_response.routes.len(), 3); let first_route = &directions_response.routes[0]; - assert_relative_eq!(first_route.distance, 9.148); + // distance is always in meters for OSRM responses + assert_relative_eq!(first_route.distance, 9148.0); assert_relative_eq!(first_route.duration, 6488.443); assert_relative_eq!( first_route.geometry.0[0], @@ -1582,7 +1597,7 @@ mod tests { assert_eq!(legs.len(), 1); let first_leg = &legs[0]; - assert_eq!(first_leg.distance, 9.148); + assert_eq!(first_leg.distance, 9148.0); assert_eq!(first_leg.duration, 6488.443); assert_eq!( first_leg.summary, @@ -1591,7 +1606,7 @@ mod tests { assert_eq!(first_leg.steps.len(), 21); let first_step = &first_leg.steps[0]; - assert_eq!(first_step.distance, 0.019); + assert_eq!(first_step.distance, 19.0); assert_eq!(first_step.duration, 13.567); assert_eq!(first_step.name, "East Marginal Way South"); assert_eq!(first_step.mode, TravelMode::Walk); From a98a3db8a7f54c4726afdbda4b22c49836dce0cb Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Thu, 16 May 2024 12:53:11 -0700 Subject: [PATCH 04/24] distance along --- services/travelmux/src/api/v5/plan.rs | 37 ++++++++++++++++----------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/services/travelmux/src/api/v5/plan.rs b/services/travelmux/src/api/v5/plan.rs index 392e499f3..d6c01fca4 100644 --- a/services/travelmux/src/api/v5/plan.rs +++ b/services/travelmux/src/api/v5/plan.rs @@ -132,7 +132,6 @@ impl Itinerary { leg_end_time, locations[0], locations[1], - valhalla.units, ) }) .collect(); @@ -552,7 +551,6 @@ impl Leg { end_time: SystemTime, from_place: LonLat, to_place: LonLat, - distance_unit: DistanceUnit, ) -> Self { let geometry = polyline::decode_polyline(&valhalla.shape, Self::VALHALLA_GEOMETRY_PRECISION) @@ -616,7 +614,6 @@ pub async fn get_directions( pub mod osrm_api { use super::{Itinerary, Leg, Maneuver, ModeLeg}; - use crate::otp::otp_api::RelativeDirection::Continue; use crate::util::{serialize_line_string_as_polyline6, serialize_point_as_lon_lat_pair}; use crate::valhalla::valhalla_api::ManeuverType; use crate::{DistanceUnit, TravelMode}; @@ -750,7 +747,7 @@ pub mod osrm_api { mode: TravelMode, from_distance_unit: DistanceUnit, ) -> Self { - let banner_instructions = BannerInstruction::from_maneuver(&value); + let banner_instructions = BannerInstruction::from_maneuver(&value, from_distance_unit); RouteStep { distance: value.distance_meters(from_distance_unit), duration: value.duration_seconds, @@ -775,20 +772,23 @@ pub mod osrm_api { #[derive(Debug, Serialize, PartialEq, Clone)] #[serde(rename_all = "camelCase")] pub struct BannerInstruction { - distance_along_geometry: f64, - primary: BannerInstructionContent, + pub distance_along_geometry: f64, + pub primary: BannerInstructionContent, // secondary: Option, // sub: Option, } impl BannerInstruction { - fn from_maneuver(value: &Maneuver) -> Option> { - let text = value.street_names.as_ref().map(|names| names.join(", ")); + fn from_maneuver( + maneuver: &Maneuver, + from_distance_unit: DistanceUnit, + ) -> Option> { + let text = maneuver.street_names.as_ref().map(|names| names.join(", ")); let banner_maneuver = (|| { use BannerManeuverModifier::*; use BannerManeuverType::*; - let (banner_type, modifier) = match value.r#type { + let (banner_type, modifier) = match maneuver.r#type { ManeuverType::None => return None, ManeuverType::Start => (Depart, None), ManeuverType::StartRight => (Depart, Some(Right)), @@ -840,7 +840,7 @@ pub mod osrm_api { ManeuverType::BuildingEnter => {} ManeuverType::BuildingExit => {} */ - _ => todo!("implement manuever type: {:?}", value.r#type), + _ => todo!("implement manuever type: {:?}", maneuver.r#type), }; Some(BannerManeuver { r#type: banner_type, @@ -859,14 +859,14 @@ pub mod osrm_api { // }; let primary = BannerInstructionContent { - text: value.instruction.clone()?, + text: maneuver.instruction.clone()?, components: vec![text_component], // TODO banner_maneuver, degrees: None, driving_side: None, }; let instruction = BannerInstruction { - distance_along_geometry: 0.0, + distance_along_geometry: maneuver.distance_meters(from_distance_unit), primary, }; Some(vec![instruction]) @@ -877,7 +877,7 @@ pub mod osrm_api { // How do audible instructions fit into this? #[derive(Debug, Serialize, PartialEq, Clone)] #[serde(rename_all = "camelCase")] - struct BannerInstructionContent { + pub struct BannerInstructionContent { text: String, // components: Vec, #[serde(flatten)] @@ -889,7 +889,7 @@ pub mod osrm_api { #[derive(Debug, Serialize, PartialEq, Clone)] #[serde(rename_all = "lowercase")] - struct BannerManeuver { + pub struct BannerManeuver { r#type: BannerManeuverType, modifier: Option, } @@ -899,7 +899,7 @@ pub mod osrm_api { // but the docs imply they are different. #[derive(Debug, Serialize, PartialEq, Clone)] #[serde(rename_all = "lowercase")] - enum BannerManeuverType { + pub enum BannerManeuverType { Turn, Merge, Depart, @@ -1611,6 +1611,13 @@ mod tests { assert_eq!(first_step.name, "East Marginal Way South"); assert_eq!(first_step.mode, TravelMode::Walk); + let banner_instructions = first_step.banner_instructions.as_ref().unwrap(); + assert_eq!(banner_instructions.len(), 1); + let banner_instruction = &banner_instructions[0]; + assert_relative_eq!(banner_instruction.distance_along_geometry, 19.0); + let primary = &banner_instruction.primary; + // TODO: banner content + let step_maneuver = &first_step.maneuver; assert_eq!( step_maneuver.location, From 6da1e36bad3b70f62c487dc4cac873981cff645c Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Thu, 16 May 2024 13:19:18 -0700 Subject: [PATCH 05/24] banner text should be name of *next* street --- services/travelmux/src/api/v5/plan.rs | 75 ++++++++++++++++++--------- 1 file changed, 51 insertions(+), 24 deletions(-) diff --git a/services/travelmux/src/api/v5/plan.rs b/services/travelmux/src/api/v5/plan.rs index d6c01fca4..80f3ce087 100644 --- a/services/travelmux/src/api/v5/plan.rs +++ b/services/travelmux/src/api/v5/plan.rs @@ -684,13 +684,29 @@ pub mod osrm_api { } ModeLeg::NonTransit(non_transit_leg) => { let summary = non_transit_leg.substantial_street_names.join(", "); - let steps = non_transit_leg + let mut steps: Vec<_> = non_transit_leg .maneuvers - .into_iter() - .map(|maneuver| { - RouteStep::from_maneuver(maneuver, value.mode, distance_unit) + .windows(2) + .map(|this_and_next| { + let maneuver = this_and_next[0].clone(); + let next_maneuver = this_and_next.get(1); + RouteStep::from_maneuver( + maneuver.clone(), + next_maneuver.cloned(), + value.mode, + distance_unit, + ) }) .collect(); + if let Some(final_maneuver) = non_transit_leg.maneuvers.last() { + let final_step = RouteStep::from_maneuver( + final_maneuver.clone(), + None, + value.mode, + distance_unit, + ); + steps.push(final_step); + } (summary, steps) } }; @@ -743,16 +759,21 @@ pub mod osrm_api { impl RouteStep { fn from_maneuver( - value: Maneuver, + maneuver: Maneuver, + next_maneuver: Option, mode: TravelMode, from_distance_unit: DistanceUnit, ) -> Self { - let banner_instructions = BannerInstruction::from_maneuver(&value, from_distance_unit); + let banner_instructions = BannerInstruction::from_maneuver( + &maneuver, + next_maneuver.as_ref(), + from_distance_unit, + ); RouteStep { - distance: value.distance_meters(from_distance_unit), - duration: value.duration_seconds, - geometry: value.geometry, - name: value + distance: maneuver.distance_meters(from_distance_unit), + duration: maneuver.duration_seconds, + geometry: maneuver.geometry, + name: maneuver .street_names .unwrap_or(vec!["".to_string()]) .join(", "), @@ -761,7 +782,7 @@ pub mod osrm_api { destinations: None, mode, maneuver: StepManeuver { - location: value.start_point.into(), + location: maneuver.start_point.into(), }, intersections: None, //vec![], banner_instructions, @@ -781,9 +802,14 @@ pub mod osrm_api { impl BannerInstruction { fn from_maneuver( maneuver: &Maneuver, + next_maneuver: Option<&Maneuver>, from_distance_unit: DistanceUnit, ) -> Option> { - let text = maneuver.street_names.as_ref().map(|names| names.join(", ")); + let text = next_maneuver + .unwrap_or(maneuver) + .street_names + .as_ref() + .map(|names| names.join(", ")); let banner_maneuver = (|| { use BannerManeuverModifier::*; @@ -878,20 +904,20 @@ pub mod osrm_api { #[derive(Debug, Serialize, PartialEq, Clone)] #[serde(rename_all = "camelCase")] pub struct BannerInstructionContent { - text: String, + pub text: String, // components: Vec, #[serde(flatten)] - banner_maneuver: Option, - degrees: Option, - driving_side: Option, - components: Vec, + pub banner_maneuver: Option, + pub degrees: Option, + pub driving_side: Option, + pub components: Vec, } #[derive(Debug, Serialize, PartialEq, Clone)] #[serde(rename_all = "lowercase")] pub struct BannerManeuver { - r#type: BannerManeuverType, - modifier: Option, + pub r#type: BannerManeuverType, + pub modifier: Option, } // This is for `banner.primary(et. al).type` @@ -912,7 +938,7 @@ pub mod osrm_api { #[derive(Debug, Serialize, PartialEq, Clone)] #[serde(rename_all = "lowercase")] - enum BannerManeuverModifier { + pub enum BannerManeuverModifier { Uturn, #[serde(rename = "sharp right")] SharpRight, @@ -932,7 +958,7 @@ pub mod osrm_api { #[derive(Debug, Serialize, PartialEq, Clone)] #[serde(rename_all = "camelCase", tag = "type")] #[non_exhaustive] - enum BannerComponent { + pub enum BannerComponent { Text(VisualInstructionComponent), // Icon(VisualInstructionComponent), // Delimiter(VisualInstructionComponent), @@ -944,12 +970,12 @@ pub mod osrm_api { #[derive(Debug, Serialize, PartialEq, Clone)] #[serde(rename_all = "camelCase")] - struct LaneInstructionComponent {} + pub struct LaneInstructionComponent {} // Maybe we won't use this? Because it'll need to be implicit in the containing BannerComponent enum variant of #[derive(Debug, Serialize, PartialEq, Clone)] #[serde(rename_all = "lowercase")] - enum VisualInstructionComponentType { + pub enum VisualInstructionComponentType { /// The component separates two other destination components. /// /// If the two adjacent components are both displayed as images, you can hide this delimiter component. @@ -1616,7 +1642,8 @@ mod tests { let banner_instruction = &banner_instructions[0]; assert_relative_eq!(banner_instruction.distance_along_geometry, 19.0); let primary = &banner_instruction.primary; - // TODO: banner content + assert_eq!(primary.text, "Walk south on East Marginal Way South."); + // TODO: more banner content let step_maneuver = &first_step.maneuver; assert_eq!( From 746511514914f91f039a578ea5d98e6a0efc74a2 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Fri, 17 May 2024 11:18:22 -0700 Subject: [PATCH 06/24] tests --- services/travelmux/src/api/v5/plan.rs | 44 +++++++++++++++++---------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/services/travelmux/src/api/v5/plan.rs b/services/travelmux/src/api/v5/plan.rs index 80f3ce087..bc814e9d5 100644 --- a/services/travelmux/src/api/v5/plan.rs +++ b/services/travelmux/src/api/v5/plan.rs @@ -805,16 +805,20 @@ pub mod osrm_api { next_maneuver: Option<&Maneuver>, from_distance_unit: DistanceUnit, ) -> Option> { - let text = next_maneuver - .unwrap_or(maneuver) - .street_names - .as_ref() - .map(|names| names.join(", ")); + let text = if let Some(next_maneuver) = next_maneuver { + next_maneuver.street_names + .as_ref() + .map(|names| names.join(", ")) + .or(next_maneuver.instruction.clone()) + } else { + assert!(matches!(maneuver.r#type, ManeuverType::Destination | ManeuverType::DestinationRight | ManeuverType::DestinationLeft)); + maneuver.instruction.to_owned() + }; let banner_maneuver = (|| { use BannerManeuverModifier::*; use BannerManeuverType::*; - let (banner_type, modifier) = match maneuver.r#type { + let (banner_type, modifier) = match next_maneuver.unwrap_or(maneuver).r#type { ManeuverType::None => return None, ManeuverType::Start => (Depart, None), ManeuverType::StartRight => (Depart, Some(Right)), @@ -836,10 +840,9 @@ pub mod osrm_api { ManeuverType::SharpLeft => (Turn, Some(SharpLeft)), ManeuverType::Left => (Turn, Some(Left)), ManeuverType::SlightLeft => (Turn, Some(SlightLeft)), - // REVIEW: Is OffRamp correct here? - ManeuverType::RampStraight => (OffRamp, Some(Straight)), - ManeuverType::RampRight => (OffRamp, Some(Right)), - ManeuverType::RampLeft => (OffRamp, Some(Left)), + ManeuverType::RampStraight => (OnRamp, Some(Straight)), + ManeuverType::RampRight => (OnRamp, Some(Right)), + ManeuverType::RampLeft => (OnRamp, Some(Left)), ManeuverType::ExitRight => (OffRamp, Some(Right)), ManeuverType::ExitLeft => (OffRamp, Some(Left)), ManeuverType::StayStraight => (Fork, None), // Or maybe just return None? @@ -874,7 +877,7 @@ pub mod osrm_api { }) })(); - let text_component = BannerComponent::Text(VisualInstructionComponent { text }); + let text_component = BannerComponent::Text(VisualInstructionComponent { text: text.clone() }); // if let Some(banner_maneuver) = banner_maneuver { // BannerComponent::Text(VisualInstructionComponent { // text, @@ -885,7 +888,7 @@ pub mod osrm_api { // }; let primary = BannerInstructionContent { - text: maneuver.instruction.clone()?, + text: text.unwrap_or_default(), components: vec![text_component], // TODO banner_maneuver, degrees: None, @@ -931,6 +934,8 @@ pub mod osrm_api { Depart, Arrive, Fork, + #[serde(rename = "on ramp")] + OnRamp, #[serde(rename = "off ramp")] OffRamp, RoundAbout, @@ -999,8 +1004,8 @@ pub mod osrm_api { #[derive(Debug, Serialize, PartialEq, Clone)] #[serde(rename_all = "camelCase")] - struct VisualInstructionComponent { - text: Option, + pub struct VisualInstructionComponent { + pub(crate) text: Option, } #[derive(Debug, Serialize, PartialEq, Clone)] @@ -1248,6 +1253,7 @@ mod tests { use serde_json::{json, Value}; use std::fs::File; use std::io::BufReader; + use crate::api::v5::plan::osrm_api::BannerComponent; #[test] fn parse_from_valhalla() { @@ -1642,14 +1648,20 @@ mod tests { let banner_instruction = &banner_instructions[0]; assert_relative_eq!(banner_instruction.distance_along_geometry, 19.0); let primary = &banner_instruction.primary; - assert_eq!(primary.text, "Walk south on East Marginal Way South."); - // TODO: more banner content + assert_eq!(primary.text, "Turn right onto the walkway."); + + let Some(BannerComponent::Text(first_component)) = &primary.components.first() else { + panic!("unexpected banner component: {:?}", primary.components.first()) + }; + assert_eq!(first_component.text, Some("Turn right onto the walkway.".to_string())); let step_maneuver = &first_step.maneuver; assert_eq!( step_maneuver.location, geo::point!(x: -122.339216, y: 47.575836) ); + // assert_eq!(step_maneuver.r#type, "my type"); + // assert_eq!(step_maneuver.modifier, "my modifier"); // TODO: step_maneuver stuff // etc... } From 52e748cfa34011b3e19332d949dc7b4f3c006442 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Fri, 17 May 2024 11:23:29 -0700 Subject: [PATCH 07/24] move osrm_api to separate module --- services/travelmux/src/api/v5/mod.rs | 2 + services/travelmux/src/api/v5/osrm_api.rs | 457 ++++++++++++++++++++ services/travelmux/src/api/v5/plan.rs | 497 ++-------------------- 3 files changed, 483 insertions(+), 473 deletions(-) create mode 100644 services/travelmux/src/api/v5/osrm_api.rs diff --git a/services/travelmux/src/api/v5/mod.rs b/services/travelmux/src/api/v5/mod.rs index 44166667c..e557dec8c 100644 --- a/services/travelmux/src/api/v5/mod.rs +++ b/services/travelmux/src/api/v5/mod.rs @@ -1,6 +1,8 @@ mod error; +mod osrm_api; pub mod plan; mod travel_modes; + pub use travel_modes::TravelModes; pub use plan::{Itinerary, Plan}; diff --git a/services/travelmux/src/api/v5/osrm_api.rs b/services/travelmux/src/api/v5/osrm_api.rs new file mode 100644 index 000000000..9c545a89f --- /dev/null +++ b/services/travelmux/src/api/v5/osrm_api.rs @@ -0,0 +1,457 @@ +use super::plan::{Itinerary, Leg, Maneuver, ModeLeg}; +use crate::util::{serialize_line_string_as_polyline6, serialize_point_as_lon_lat_pair}; +use crate::valhalla::valhalla_api::ManeuverType; +use crate::{DistanceUnit, TravelMode}; +use geo::{LineString, Point}; +use serde::Serialize; + +#[derive(Debug, Serialize, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Route { + /// The distance traveled by the route, in float meters. + pub distance: f64, + + /// The estimated travel time, in float number of seconds. + pub duration: f64, + + // todo: simplify? + /// The entire geometry of the route + #[serde(serialize_with = "serialize_line_string_as_polyline6")] + pub geometry: LineString, + + /// The legs between the given waypoints + pub legs: Vec, +} + +impl From for Route { + fn from(itinerary: Itinerary) -> Self { + Route { + distance: itinerary.distance_meters(), + duration: itinerary.duration, + geometry: itinerary.combined_geometry(), + legs: itinerary + .legs + .into_iter() + .map(|leg| RouteLeg::from_leg(leg, itinerary.distance_units)) + .collect(), + } + } +} + +#[derive(Debug, Serialize, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct RouteLeg { + /// The distance traveled by this leg, in float meters. + pub distance: f64, + + /// The estimated travel time, in float number of seconds. + pub duration: f64, + + /// A short human-readable summary of the route leg + pub summary: String, + + /// Objects describing the turn-by-turn instructions of the route leg + pub steps: Vec, + // /// Additional details about each coordinate along the route geometry + // annotation: Annotation +} + +impl RouteLeg { + fn from_leg(value: Leg, distance_unit: DistanceUnit) -> Self { + let distance_meters = value.distance_meters(distance_unit); + let (summary, steps) = match value.mode_leg { + ModeLeg::Transit(_) => { + debug_assert!( + false, + "didn't expect to generate navigation for transit leg" + ); + ("".to_string(), vec![]) + } + ModeLeg::NonTransit(non_transit_leg) => { + let summary = non_transit_leg.substantial_street_names.join(", "); + let mut steps: Vec<_> = non_transit_leg + .maneuvers + .windows(2) + .map(|this_and_next| { + let maneuver = this_and_next[0].clone(); + let next_maneuver = this_and_next.get(1); + RouteStep::from_maneuver( + maneuver.clone(), + next_maneuver.cloned(), + value.mode, + distance_unit, + ) + }) + .collect(); + if let Some(final_maneuver) = non_transit_leg.maneuvers.last() { + let final_step = RouteStep::from_maneuver( + final_maneuver.clone(), + None, + value.mode, + distance_unit, + ); + steps.push(final_step); + } + (summary, steps) + } + }; + Self { + distance: distance_meters, + duration: value.duration_seconds, + summary, + steps, + } + } +} + +#[derive(Debug, Serialize, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct RouteStep { + /// The distance traveled by this step, in float meters. + pub distance: f64, + + /// The estimated travel time, in float number of seconds. + pub duration: f64, + + /// The unsimplified geometry of the route segment. + #[serde(serialize_with = "serialize_line_string_as_polyline6")] + pub geometry: LineString, + + /// The name of the way along which travel proceeds. + pub name: String, + + /// A reference number or code for the way. Optionally included, if ref data is available for the given way. + pub r#ref: Option, + + /// The pronunciation hint of the way name. Will be undefined if there is no pronunciation hit. + pub pronunciation: Option, + + /// The destinations of the way. Will be undefined if there are no destinations. + pub destinations: Option>, + + /// A string signifying the mode of transportation. + pub mode: TravelMode, + + /// A `StepManeuver` object representing the maneuver. + pub maneuver: StepManeuver, + + /// A list of `BannerInstruction` objects that represent all signs on the step. + pub banner_instructions: Option>, + + /// A list of `Intersection` objects that are passed along the segment, the very first belonging to the `StepManeuver` + pub intersections: Option>, +} + +impl RouteStep { + fn from_maneuver( + maneuver: Maneuver, + next_maneuver: Option, + mode: TravelMode, + from_distance_unit: DistanceUnit, + ) -> Self { + let banner_instructions = + BannerInstruction::from_maneuver(&maneuver, next_maneuver.as_ref(), from_distance_unit); + RouteStep { + distance: maneuver.distance_meters(from_distance_unit), + duration: maneuver.duration_seconds, + geometry: maneuver.geometry, + name: maneuver + .street_names + .unwrap_or(vec!["".to_string()]) + .join(", "), + r#ref: None, + pronunciation: None, + destinations: None, + mode, + maneuver: StepManeuver { + location: maneuver.start_point.into(), + }, + intersections: None, //vec![], + banner_instructions, + } + } +} + +#[derive(Debug, Serialize, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct BannerInstruction { + pub distance_along_geometry: f64, + pub primary: BannerInstructionContent, + // secondary: Option, + // sub: Option, +} + +impl BannerInstruction { + fn from_maneuver( + maneuver: &Maneuver, + next_maneuver: Option<&Maneuver>, + from_distance_unit: DistanceUnit, + ) -> Option> { + let text = if let Some(next_maneuver) = next_maneuver { + next_maneuver + .street_names + .as_ref() + .map(|names| names.join(", ")) + .or(next_maneuver.instruction.clone()) + } else { + assert!(matches!( + maneuver.r#type, + ManeuverType::Destination + | ManeuverType::DestinationRight + | ManeuverType::DestinationLeft + )); + maneuver.instruction.to_owned() + }; + + let banner_maneuver = (|| { + use BannerManeuverModifier::*; + use BannerManeuverType::*; + let (banner_type, modifier) = match next_maneuver.unwrap_or(maneuver).r#type { + ManeuverType::None => return None, + ManeuverType::Start => (Depart, None), + ManeuverType::StartRight => (Depart, Some(Right)), + ManeuverType::StartLeft => (Depart, Some(Left)), + ManeuverType::Destination => (Arrive, None), + ManeuverType::DestinationRight => (Arrive, Some(Right)), + ManeuverType::DestinationLeft => (Arrive, Some(Left)), + /* + ManeuverType::Becomes => {} + */ + ManeuverType::Continue => (Fork, None), // Or maybe just return None? + ManeuverType::SlightRight => (Turn, Some(SlightRight)), + ManeuverType::Right => (Turn, Some(Right)), + ManeuverType::SharpRight => (Turn, Some(SharpRight)), + /* + ManeuverType::UturnRight => {} + ManeuverType::UturnLeft => {} + */ + ManeuverType::SharpLeft => (Turn, Some(SharpLeft)), + ManeuverType::Left => (Turn, Some(Left)), + ManeuverType::SlightLeft => (Turn, Some(SlightLeft)), + ManeuverType::RampStraight => (OnRamp, Some(Straight)), + ManeuverType::RampRight => (OnRamp, Some(Right)), + ManeuverType::RampLeft => (OnRamp, Some(Left)), + ManeuverType::ExitRight => (OffRamp, Some(Right)), + ManeuverType::ExitLeft => (OffRamp, Some(Left)), + ManeuverType::StayStraight => (Fork, None), // Or maybe just return None? + ManeuverType::StayRight => (Fork, Some(Right)), + ManeuverType::StayLeft => (Fork, Some(Left)), + /* + ManeuverType::Merge => {} + ManeuverType::RoundaboutEnter => {} + ManeuverType::RoundaboutExit => {} + ManeuverType::FerryEnter => {} + ManeuverType::FerryExit => {} + ManeuverType::Transit => {} + ManeuverType::TransitTransfer => {} + ManeuverType::TransitRemainOn => {} + ManeuverType::TransitConnectionStart => {} + ManeuverType::TransitConnectionTransfer => {} + ManeuverType::TransitConnectionDestination => {} + ManeuverType::PostTransitConnectionDestination => {} + ManeuverType::MergeRight => {} + ManeuverType::MergeLeft => {} + ManeuverType::ElevatorEnter => {} + ManeuverType::StepsEnter => {} + ManeuverType::EscalatorEnter => {} + ManeuverType::BuildingEnter => {} + ManeuverType::BuildingExit => {} + */ + _ => todo!("implement manuever type: {:?}", maneuver.r#type), + }; + Some(BannerManeuver { + r#type: banner_type, + modifier, + }) + })(); + + let text_component = + BannerComponent::Text(VisualInstructionComponent { text: text.clone() }); + // if let Some(banner_maneuver) = banner_maneuver { + // BannerComponent::Text(VisualInstructionComponent { + // text, + // }) + // } else { + // panic!("no banner_maneuver") + // } + // }; + + let primary = BannerInstructionContent { + text: text.unwrap_or_default(), + components: vec![text_component], // TODO + banner_maneuver, + degrees: None, + driving_side: None, + }; + let instruction = BannerInstruction { + distance_along_geometry: maneuver.distance_meters(from_distance_unit), + primary, + }; + Some(vec![instruction]) + } +} + +// REVIEW: Rename to VisualInstructionBanner? +// How do audible instructions fit into this? +#[derive(Debug, Serialize, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct BannerInstructionContent { + pub text: String, + // components: Vec, + #[serde(flatten)] + pub banner_maneuver: Option, + pub degrees: Option, + pub driving_side: Option, + pub components: Vec, +} + +#[derive(Debug, Serialize, PartialEq, Clone)] +#[serde(rename_all = "lowercase")] +pub struct BannerManeuver { + pub r#type: BannerManeuverType, + pub modifier: Option, +} + +// This is for `banner.primary(et. al).type` +// There is a lot of overlap between this and `step_maneuver.type`, +// but the docs imply they are different. +#[derive(Debug, Serialize, PartialEq, Clone)] +#[serde(rename_all = "lowercase")] +pub enum BannerManeuverType { + Turn, + Merge, + Depart, + Arrive, + Fork, + #[serde(rename = "on ramp")] + OnRamp, + #[serde(rename = "off ramp")] + OffRamp, + RoundAbout, +} + +#[derive(Debug, Serialize, PartialEq, Clone)] +#[serde(rename_all = "lowercase")] +pub enum BannerManeuverModifier { + Uturn, + #[serde(rename = "sharp right")] + SharpRight, + Right, + #[serde(rename = "slight right")] + SlightRight, + Straight, + #[serde(rename = "slight left")] + SlightLeft, + Left, + #[serde(rename = "sharp left")] + SharpLeft, +} + +// REVIEW: Rename to VisualInstruction? +// REVIEW: convert to inner enum of Lane or VisualInstructionComponent +#[derive(Debug, Serialize, PartialEq, Clone)] +#[serde(rename_all = "camelCase", tag = "type")] +#[non_exhaustive] +pub enum BannerComponent { + Text(VisualInstructionComponent), + // Icon(VisualInstructionComponent), + // Delimiter(VisualInstructionComponent), + // #[serde(rename="exit-number")] + // ExitNumber(VisualInstructionComponent), + // Exit(VisualInstructionComponent), + Lane(LaneInstructionComponent), +} + +#[derive(Debug, Serialize, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct LaneInstructionComponent {} + +// Maybe we won't use this? Because it'll need to be implicit in the containing BannerComponent enum variant of +#[derive(Debug, Serialize, PartialEq, Clone)] +#[serde(rename_all = "lowercase")] +pub enum VisualInstructionComponentType { + /// The component separates two other destination components. + /// + /// If the two adjacent components are both displayed as images, you can hide this delimiter component. + Delimiter, + + /// The component bears the name of a place or street. + Text, + + /// Component contains an image that should be rendered. + Image, + + /// The component contains the localized word for "exit". + /// + /// This component may appear before or after an `.ExitCode` component, depending on the language. + Exit, + + /// A component contains an exit number. + #[serde(rename = "exit-number")] + ExitCode, +} + +#[derive(Debug, Serialize, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct VisualInstructionComponent { + pub(crate) text: Option, +} + +#[derive(Debug, Serialize, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct StepManeuver { + /// The location of the maneuver + #[serde(serialize_with = "serialize_point_as_lon_lat_pair")] + pub location: Point, + // /// The type of maneuver. new identifiers might be introduced without changing the API, so best practice is to gracefully handle any new values + // r#type: String, + // /// The modifier of the maneuver. new identifiers might be introduced without changing the API, so best practice is to gracefully handle any new values + // modifier: String, + // /// A human-readable instruction of how to execute the returned maneuver + // instruction: String, + // /// The bearing before the turn, in degrees + // OSRM expects `bearing_before` to be snake cased. + // #[serde(rename_all = "snake_case")] + // bearing_before: f64, + // OSRM expects `bearing_after` to be snake cased. + // /// The bearing after the turn, in degrees + // bearing_after: f64, +} + +#[derive(Debug, Serialize, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Intersection { + /// A [longitude, latitude] pair describing the location of the turn. + #[serde(serialize_with = "serialize_point_as_lon_lat_pair")] + pub location: Point, + + /// A list of bearing values that are available at the intersection. The bearings describe all available roads at the intersection. + pub bearings: Vec, + + // /// An array of strings signifying the classes of the road exiting the intersection. + // pub classes: Vec + /// A list of entry flags, corresponding in a 1:1 relationship to the bearings. A value of true indicates that the respective road could be entered on a valid route. + pub entry: Vec, + + /// The zero-based index into the geometry, relative to the start of the leg it's on. This value can be used to apply the duration annotation that corresponds with the intersection. + pub geometry_index: Option, + + /// The index in the bearings and entry arrays. Used to calculate the bearing before the turn. Namely, the clockwise angle from true north to the direction of travel before the maneuver/passing the intersection. To get the bearing in the direction of driving, the bearing has to be rotated by a value of 180. The value is not supplied for departure maneuvers. + pub r#in: Option, + + /// The index in the bearings and entry arrays. Used to extract the bearing after the turn. Namely, the clockwise angle from true north to the direction of travel after the maneuver/passing the intersection. The value is not supplied for arrival maneuvers. + pub out: Option, + + /// An array of lane objects that represent the available turn lanes at the intersection. If no lane information is available for an intersection, the lanes property will not be present. + pub lanes: Vec, + + /// The time required, in seconds, to traverse the intersection. Only available on the driving profile. + pub duration: Option, + // TODO: lots more fields in OSRM +} + +#[derive(Debug, Serialize, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Lane { + // TODO: lots more fields in OSRM +} diff --git a/services/travelmux/src/api/v5/plan.rs b/services/travelmux/src/api/v5/plan.rs index bc814e9d5..7c6b26c73 100644 --- a/services/travelmux/src/api/v5/plan.rs +++ b/services/travelmux/src/api/v5/plan.rs @@ -8,6 +8,7 @@ use std::collections::HashMap; use std::time::{Duration, SystemTime}; use super::error::{PlanResponseErr, PlanResponseOk}; +use super::osrm_api; use super::TravelModes; use crate::api::AppState; @@ -68,7 +69,7 @@ pub struct Plan { pub struct Itinerary { mode: TravelMode, /// seconds - duration: f64, + pub(crate) duration: f64, /// unix millis, UTC #[serde(serialize_with = "serialize_system_time_as_millis")] start_time: SystemTime, @@ -80,10 +81,10 @@ pub struct Itinerary { /// FIXME: I think we're returning meters even though distance unit is "Kilometers" /// Probably we should rename DistanceUnit::Kilometers to DistanceUnit::Meters /// This is passed as a parameter though, so it'd be a breaking change. - distance_units: DistanceUnit, + pub(crate) distance_units: DistanceUnit, #[serde(serialize_with = "serialize_rect_to_lng_lat")] bounds: Rect, - legs: Vec, + pub(crate) legs: Vec, } impl Itinerary { @@ -223,16 +224,16 @@ impl From for Place { #[derive(Debug, Serialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] -struct Leg { +pub(crate) struct Leg { /// encoded polyline. 1e-6 scale, (lat, lon) #[serde(serialize_with = "serialize_line_string_as_polyline6")] geometry: LineString, /// Which mode is this leg of the journey? - mode: TravelMode, + pub(crate) mode: TravelMode, #[serde(flatten)] - mode_leg: ModeLeg, + pub(crate) mode_leg: ModeLeg, /// Beginning of the leg from_place: Place, @@ -256,7 +257,7 @@ struct Leg { distance: f64, /// Duration of this leg - duration_seconds: f64, + pub(crate) duration_seconds: f64, } // Should we just pass the entire OTP leg? @@ -264,7 +265,7 @@ type TransitLeg = otp_api::Leg; #[derive(Debug, Serialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] -enum ModeLeg { +pub(crate) enum ModeLeg { // REVIEW: rename? There is a boolean field for OTP called TransitLeg #[serde(rename = "transitLeg")] Transit(Box), @@ -275,11 +276,11 @@ enum ModeLeg { #[derive(Debug, Serialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] -struct NonTransitLeg { - maneuvers: Vec, +pub struct NonTransitLeg { + pub(crate) maneuvers: Vec, /// The substantial road names along the route - substantial_street_names: Vec, + pub(crate) substantial_street_names: Vec, } impl NonTransitLeg { @@ -403,7 +404,7 @@ impl Maneuver { } } - fn distance_meters(&self, distance_unit: DistanceUnit) -> f64 { + pub(crate) fn distance_meters(&self, distance_unit: DistanceUnit) -> f64 { convert_to_meters(self.distance, distance_unit) } } @@ -483,7 +484,7 @@ impl Leg { self.geometry.bounding_rect() } - fn distance_meters(&self, itinerary_units: DistanceUnit) -> f64 { + pub(crate) fn distance_meters(&self, itinerary_units: DistanceUnit) -> f64 { convert_to_meters(self.distance, itinerary_units) } @@ -612,462 +613,6 @@ pub async fn get_directions( Ok(plan_response_ok.into()) } -pub mod osrm_api { - use super::{Itinerary, Leg, Maneuver, ModeLeg}; - use crate::util::{serialize_line_string_as_polyline6, serialize_point_as_lon_lat_pair}; - use crate::valhalla::valhalla_api::ManeuverType; - use crate::{DistanceUnit, TravelMode}; - use geo::{LineString, Point}; - use serde::Serialize; - - #[derive(Debug, Serialize, PartialEq, Clone)] - #[serde(rename_all = "camelCase")] - pub struct Route { - /// The distance traveled by the route, in float meters. - pub distance: f64, - - /// The estimated travel time, in float number of seconds. - pub duration: f64, - - // todo: simplify? - /// The entire geometry of the route - #[serde(serialize_with = "serialize_line_string_as_polyline6")] - pub geometry: LineString, - - /// The legs between the given waypoints - pub legs: Vec, - } - - impl From for Route { - fn from(itinerary: Itinerary) -> Self { - Route { - distance: itinerary.distance_meters(), - duration: itinerary.duration, - geometry: itinerary.combined_geometry(), - legs: itinerary - .legs - .into_iter() - .map(|leg| RouteLeg::from_leg(leg, itinerary.distance_units)) - .collect(), - } - } - } - - #[derive(Debug, Serialize, PartialEq, Clone)] - #[serde(rename_all = "camelCase")] - pub struct RouteLeg { - /// The distance traveled by this leg, in float meters. - pub distance: f64, - - /// The estimated travel time, in float number of seconds. - pub duration: f64, - - /// A short human-readable summary of the route leg - pub summary: String, - - /// Objects describing the turn-by-turn instructions of the route leg - pub steps: Vec, - // /// Additional details about each coordinate along the route geometry - // annotation: Annotation - } - - impl RouteLeg { - fn from_leg(value: Leg, distance_unit: DistanceUnit) -> Self { - let distance_meters = value.distance_meters(distance_unit); - let (summary, steps) = match value.mode_leg { - ModeLeg::Transit(_) => { - debug_assert!( - false, - "didn't expect to generate navigation for transit leg" - ); - ("".to_string(), vec![]) - } - ModeLeg::NonTransit(non_transit_leg) => { - let summary = non_transit_leg.substantial_street_names.join(", "); - let mut steps: Vec<_> = non_transit_leg - .maneuvers - .windows(2) - .map(|this_and_next| { - let maneuver = this_and_next[0].clone(); - let next_maneuver = this_and_next.get(1); - RouteStep::from_maneuver( - maneuver.clone(), - next_maneuver.cloned(), - value.mode, - distance_unit, - ) - }) - .collect(); - if let Some(final_maneuver) = non_transit_leg.maneuvers.last() { - let final_step = RouteStep::from_maneuver( - final_maneuver.clone(), - None, - value.mode, - distance_unit, - ); - steps.push(final_step); - } - (summary, steps) - } - }; - Self { - distance: distance_meters, - duration: value.duration_seconds, - summary, - steps, - } - } - } - - #[derive(Debug, Serialize, PartialEq, Clone)] - #[serde(rename_all = "camelCase")] - pub struct RouteStep { - /// The distance traveled by this step, in float meters. - pub distance: f64, - - /// The estimated travel time, in float number of seconds. - pub duration: f64, - - /// The unsimplified geometry of the route segment. - #[serde(serialize_with = "serialize_line_string_as_polyline6")] - pub geometry: LineString, - - /// The name of the way along which travel proceeds. - pub name: String, - - /// A reference number or code for the way. Optionally included, if ref data is available for the given way. - pub r#ref: Option, - - /// The pronunciation hint of the way name. Will be undefined if there is no pronunciation hit. - pub pronunciation: Option, - - /// The destinations of the way. Will be undefined if there are no destinations. - pub destinations: Option>, - - /// A string signifying the mode of transportation. - pub mode: TravelMode, - - /// A `StepManeuver` object representing the maneuver. - pub maneuver: StepManeuver, - - /// A list of `BannerInstruction` objects that represent all signs on the step. - pub banner_instructions: Option>, - - /// A list of `Intersection` objects that are passed along the segment, the very first belonging to the `StepManeuver` - pub intersections: Option>, - } - - impl RouteStep { - fn from_maneuver( - maneuver: Maneuver, - next_maneuver: Option, - mode: TravelMode, - from_distance_unit: DistanceUnit, - ) -> Self { - let banner_instructions = BannerInstruction::from_maneuver( - &maneuver, - next_maneuver.as_ref(), - from_distance_unit, - ); - RouteStep { - distance: maneuver.distance_meters(from_distance_unit), - duration: maneuver.duration_seconds, - geometry: maneuver.geometry, - name: maneuver - .street_names - .unwrap_or(vec!["".to_string()]) - .join(", "), - r#ref: None, - pronunciation: None, - destinations: None, - mode, - maneuver: StepManeuver { - location: maneuver.start_point.into(), - }, - intersections: None, //vec![], - banner_instructions, - } - } - } - - #[derive(Debug, Serialize, PartialEq, Clone)] - #[serde(rename_all = "camelCase")] - pub struct BannerInstruction { - pub distance_along_geometry: f64, - pub primary: BannerInstructionContent, - // secondary: Option, - // sub: Option, - } - - impl BannerInstruction { - fn from_maneuver( - maneuver: &Maneuver, - next_maneuver: Option<&Maneuver>, - from_distance_unit: DistanceUnit, - ) -> Option> { - let text = if let Some(next_maneuver) = next_maneuver { - next_maneuver.street_names - .as_ref() - .map(|names| names.join(", ")) - .or(next_maneuver.instruction.clone()) - } else { - assert!(matches!(maneuver.r#type, ManeuverType::Destination | ManeuverType::DestinationRight | ManeuverType::DestinationLeft)); - maneuver.instruction.to_owned() - }; - - let banner_maneuver = (|| { - use BannerManeuverModifier::*; - use BannerManeuverType::*; - let (banner_type, modifier) = match next_maneuver.unwrap_or(maneuver).r#type { - ManeuverType::None => return None, - ManeuverType::Start => (Depart, None), - ManeuverType::StartRight => (Depart, Some(Right)), - ManeuverType::StartLeft => (Depart, Some(Left)), - ManeuverType::Destination => (Arrive, None), - ManeuverType::DestinationRight => (Arrive, Some(Right)), - ManeuverType::DestinationLeft => (Arrive, Some(Left)), - /* - ManeuverType::Becomes => {} - */ - ManeuverType::Continue => (Fork, None), // Or maybe just return None? - ManeuverType::SlightRight => (Turn, Some(SlightRight)), - ManeuverType::Right => (Turn, Some(Right)), - ManeuverType::SharpRight => (Turn, Some(SharpRight)), - /* - ManeuverType::UturnRight => {} - ManeuverType::UturnLeft => {} - */ - ManeuverType::SharpLeft => (Turn, Some(SharpLeft)), - ManeuverType::Left => (Turn, Some(Left)), - ManeuverType::SlightLeft => (Turn, Some(SlightLeft)), - ManeuverType::RampStraight => (OnRamp, Some(Straight)), - ManeuverType::RampRight => (OnRamp, Some(Right)), - ManeuverType::RampLeft => (OnRamp, Some(Left)), - ManeuverType::ExitRight => (OffRamp, Some(Right)), - ManeuverType::ExitLeft => (OffRamp, Some(Left)), - ManeuverType::StayStraight => (Fork, None), // Or maybe just return None? - ManeuverType::StayRight => (Fork, Some(Right)), - ManeuverType::StayLeft => (Fork, Some(Left)), - /* - ManeuverType::Merge => {} - ManeuverType::RoundaboutEnter => {} - ManeuverType::RoundaboutExit => {} - ManeuverType::FerryEnter => {} - ManeuverType::FerryExit => {} - ManeuverType::Transit => {} - ManeuverType::TransitTransfer => {} - ManeuverType::TransitRemainOn => {} - ManeuverType::TransitConnectionStart => {} - ManeuverType::TransitConnectionTransfer => {} - ManeuverType::TransitConnectionDestination => {} - ManeuverType::PostTransitConnectionDestination => {} - ManeuverType::MergeRight => {} - ManeuverType::MergeLeft => {} - ManeuverType::ElevatorEnter => {} - ManeuverType::StepsEnter => {} - ManeuverType::EscalatorEnter => {} - ManeuverType::BuildingEnter => {} - ManeuverType::BuildingExit => {} - */ - _ => todo!("implement manuever type: {:?}", maneuver.r#type), - }; - Some(BannerManeuver { - r#type: banner_type, - modifier, - }) - })(); - - let text_component = BannerComponent::Text(VisualInstructionComponent { text: text.clone() }); - // if let Some(banner_maneuver) = banner_maneuver { - // BannerComponent::Text(VisualInstructionComponent { - // text, - // }) - // } else { - // panic!("no banner_maneuver") - // } - // }; - - let primary = BannerInstructionContent { - text: text.unwrap_or_default(), - components: vec![text_component], // TODO - banner_maneuver, - degrees: None, - driving_side: None, - }; - let instruction = BannerInstruction { - distance_along_geometry: maneuver.distance_meters(from_distance_unit), - primary, - }; - Some(vec![instruction]) - } - } - - // REVIEW: Rename to VisualInstructionBanner? - // How do audible instructions fit into this? - #[derive(Debug, Serialize, PartialEq, Clone)] - #[serde(rename_all = "camelCase")] - pub struct BannerInstructionContent { - pub text: String, - // components: Vec, - #[serde(flatten)] - pub banner_maneuver: Option, - pub degrees: Option, - pub driving_side: Option, - pub components: Vec, - } - - #[derive(Debug, Serialize, PartialEq, Clone)] - #[serde(rename_all = "lowercase")] - pub struct BannerManeuver { - pub r#type: BannerManeuverType, - pub modifier: Option, - } - - // This is for `banner.primary(et. al).type` - // There is a lot of overlap between this and `step_maneuver.type`, - // but the docs imply they are different. - #[derive(Debug, Serialize, PartialEq, Clone)] - #[serde(rename_all = "lowercase")] - pub enum BannerManeuverType { - Turn, - Merge, - Depart, - Arrive, - Fork, - #[serde(rename = "on ramp")] - OnRamp, - #[serde(rename = "off ramp")] - OffRamp, - RoundAbout, - } - - #[derive(Debug, Serialize, PartialEq, Clone)] - #[serde(rename_all = "lowercase")] - pub enum BannerManeuverModifier { - Uturn, - #[serde(rename = "sharp right")] - SharpRight, - Right, - #[serde(rename = "slight right")] - SlightRight, - Straight, - #[serde(rename = "slight left")] - SlightLeft, - Left, - #[serde(rename = "sharp left")] - SharpLeft, - } - - // REVIEW: Rename to VisualInstruction? - // REVIEW: convert to inner enum of Lane or VisualInstructionComponent - #[derive(Debug, Serialize, PartialEq, Clone)] - #[serde(rename_all = "camelCase", tag = "type")] - #[non_exhaustive] - pub enum BannerComponent { - Text(VisualInstructionComponent), - // Icon(VisualInstructionComponent), - // Delimiter(VisualInstructionComponent), - // #[serde(rename="exit-number")] - // ExitNumber(VisualInstructionComponent), - // Exit(VisualInstructionComponent), - Lane(LaneInstructionComponent), - } - - #[derive(Debug, Serialize, PartialEq, Clone)] - #[serde(rename_all = "camelCase")] - pub struct LaneInstructionComponent {} - - // Maybe we won't use this? Because it'll need to be implicit in the containing BannerComponent enum variant of - #[derive(Debug, Serialize, PartialEq, Clone)] - #[serde(rename_all = "lowercase")] - pub enum VisualInstructionComponentType { - /// The component separates two other destination components. - /// - /// If the two adjacent components are both displayed as images, you can hide this delimiter component. - Delimiter, - - /// The component bears the name of a place or street. - Text, - - /// Component contains an image that should be rendered. - Image, - - /// The component contains the localized word for "exit". - /// - /// This component may appear before or after an `.ExitCode` component, depending on the language. - Exit, - - /// A component contains an exit number. - #[serde(rename = "exit-number")] - ExitCode, - } - - #[derive(Debug, Serialize, PartialEq, Clone)] - #[serde(rename_all = "camelCase")] - pub struct VisualInstructionComponent { - pub(crate) text: Option, - } - - #[derive(Debug, Serialize, PartialEq, Clone)] - #[serde(rename_all = "camelCase")] - pub struct StepManeuver { - /// The location of the maneuver - #[serde(serialize_with = "serialize_point_as_lon_lat_pair")] - pub location: Point, - // /// The type of maneuver. new identifiers might be introduced without changing the API, so best practice is to gracefully handle any new values - // r#type: String, - // /// The modifier of the maneuver. new identifiers might be introduced without changing the API, so best practice is to gracefully handle any new values - // modifier: String, - // /// A human-readable instruction of how to execute the returned maneuver - // instruction: String, - // /// The bearing before the turn, in degrees - // OSRM expects `bearing_before` to be snake cased. - // #[serde(rename_all = "snake_case")] - // bearing_before: f64, - // OSRM expects `bearing_after` to be snake cased. - // /// The bearing after the turn, in degrees - // bearing_after: f64, - } - - #[derive(Debug, Serialize, PartialEq, Clone)] - #[serde(rename_all = "camelCase")] - pub struct Intersection { - /// A [longitude, latitude] pair describing the location of the turn. - #[serde(serialize_with = "serialize_point_as_lon_lat_pair")] - pub location: Point, - - /// A list of bearing values that are available at the intersection. The bearings describe all available roads at the intersection. - pub bearings: Vec, - - // /// An array of strings signifying the classes of the road exiting the intersection. - // pub classes: Vec - /// A list of entry flags, corresponding in a 1:1 relationship to the bearings. A value of true indicates that the respective road could be entered on a valid route. - pub entry: Vec, - - /// The zero-based index into the geometry, relative to the start of the leg it's on. This value can be used to apply the duration annotation that corresponds with the intersection. - pub geometry_index: Option, - - /// The index in the bearings and entry arrays. Used to calculate the bearing before the turn. Namely, the clockwise angle from true north to the direction of travel before the maneuver/passing the intersection. To get the bearing in the direction of driving, the bearing has to be rotated by a value of 180. The value is not supplied for departure maneuvers. - pub r#in: Option, - - /// The index in the bearings and entry arrays. Used to extract the bearing after the turn. Namely, the clockwise angle from true north to the direction of travel after the maneuver/passing the intersection. The value is not supplied for arrival maneuvers. - pub out: Option, - - /// An array of lane objects that represent the available turn lanes at the intersection. If no lane information is available for an intersection, the lanes property will not be present. - pub lanes: Vec, - - /// The time required, in seconds, to traverse the intersection. Only available on the driving profile. - pub duration: Option, - // TODO: lots more fields in OSRM - } - - #[derive(Debug, Serialize, PartialEq, Clone)] - #[serde(rename_all = "camelCase")] - pub struct Lane { - // TODO: lots more fields in OSRM - } -} - impl From for DirectionsResponseOk { fn from(value: PlanResponseOk) -> Self { let routes = value @@ -1253,7 +798,6 @@ mod tests { use serde_json::{json, Value}; use std::fs::File; use std::io::BufReader; - use crate::api::v5::plan::osrm_api::BannerComponent; #[test] fn parse_from_valhalla() { @@ -1650,10 +1194,17 @@ mod tests { let primary = &banner_instruction.primary; assert_eq!(primary.text, "Turn right onto the walkway."); - let Some(BannerComponent::Text(first_component)) = &primary.components.first() else { - panic!("unexpected banner component: {:?}", primary.components.first()) + let Some(osrm_api::BannerComponent::Text(first_component)) = &primary.components.first() + else { + panic!( + "unexpected banner component: {:?}", + primary.components.first() + ) }; - assert_eq!(first_component.text, Some("Turn right onto the walkway.".to_string())); + assert_eq!( + first_component.text, + Some("Turn right onto the walkway.".to_string()) + ); let step_maneuver = &first_step.maneuver; assert_eq!( From 4da9c8eb10c0ba7a25fa6a0fa94eb77f390242ed Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Fri, 17 May 2024 11:51:10 -0700 Subject: [PATCH 08/24] further mod breakup and some clippy --- services/travelmux/src/api/v4/plan.rs | 12 +- services/travelmux/src/api/v5/directions.rs | 131 ++++++++++++++++++ services/travelmux/src/api/v5/mod.rs | 1 + services/travelmux/src/api/v5/plan.rs | 126 +---------------- .../src/bin/travelmux-server/main.rs | 2 +- 5 files changed, 145 insertions(+), 127 deletions(-) create mode 100644 services/travelmux/src/api/v5/directions.rs diff --git a/services/travelmux/src/api/v4/plan.rs b/services/travelmux/src/api/v4/plan.rs index 19839ff1d..3c250f35e 100644 --- a/services/travelmux/src/api/v4/plan.rs +++ b/services/travelmux/src/api/v4/plan.rs @@ -621,12 +621,12 @@ mod tests { .unwrap() .as_array() .unwrap() - .get(0) + .first() .unwrap(); let legs = first_itinerary.get("legs").unwrap().as_array().unwrap(); // Verify walking leg - let first_leg = legs.get(0).unwrap().as_object().unwrap(); + let first_leg = legs.first().unwrap().as_object().unwrap(); let mode = first_leg.get("mode").unwrap().as_str().unwrap(); assert_eq!(mode, "WALK"); @@ -646,7 +646,7 @@ mod tests { assert!(first_leg.get("transitLeg").is_none()); let maneuvers = first_leg.get("maneuvers").unwrap().as_array().unwrap(); - let first_maneuver = maneuvers.get(0).unwrap(); + let first_maneuver = maneuvers.first().unwrap(); let expected_maneuver = json!({ "type": 1, "instruction": null, @@ -701,17 +701,17 @@ mod tests { .unwrap() .as_array() .unwrap() - .get(0) + .first() .unwrap(); let legs = first_itinerary.get("legs").unwrap().as_array().unwrap(); // Verify walking leg - let first_leg = legs.get(0).unwrap().as_object().unwrap(); + let first_leg = legs.first().unwrap().as_object().unwrap(); let mode = first_leg.get("mode").unwrap().as_str().unwrap(); assert_eq!(mode, "WALK"); assert!(first_leg.get("transitLeg").is_none()); let maneuvers = first_leg.get("maneuvers").unwrap().as_array().unwrap(); - let first_maneuver = maneuvers.get(0).unwrap(); + let first_maneuver = maneuvers.first().unwrap(); let expected_maneuver = json!({ "type": 2, "instruction": "Walk south on East Marginal Way South.", diff --git a/services/travelmux/src/api/v5/directions.rs b/services/travelmux/src/api/v5/directions.rs new file mode 100644 index 000000000..15652a9e7 --- /dev/null +++ b/services/travelmux/src/api/v5/directions.rs @@ -0,0 +1,131 @@ +use super::error::{PlanResponseErr, PlanResponseOk}; +use super::osrm_api; +use super::plan::{PlanQuery, _get_plan}; +use crate::api::AppState; +use actix_web::{get, web, HttpRequest, HttpResponseBuilder}; +use serde::Serialize; + +#[get("/v5/directions")] +pub async fn get_directions( + query: web::Query, + req: HttpRequest, + app_state: web::Data, +) -> Result { + let plan_response_ok = _get_plan(query, req, app_state).await?; + Ok(plan_response_ok.into()) +} + +#[derive(Debug, Serialize, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +struct DirectionsResponseOk { + routes: Vec, +} + +impl actix_web::Responder for DirectionsResponseOk { + type Body = actix_web::body::BoxBody; + + fn respond_to(self, _req: &HttpRequest) -> actix_web::HttpResponse { + let mut response = HttpResponseBuilder::new(actix_web::http::StatusCode::OK); + response.content_type("application/json"); + response.json(self) + } +} + +impl From for DirectionsResponseOk { + fn from(value: PlanResponseOk) -> Self { + let routes = value + .plan + .itineraries + .into_iter() + .map(osrm_api::Route::from) + .collect(); + Self { routes } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::api::v5::error::PlanResponseOk; + use crate::api::v5::osrm_api; + use crate::valhalla::valhalla_api; + use crate::TravelMode; + use approx::assert_relative_eq; + use std::fs::File; + use std::io::BufReader; + + #[test] + fn osrm_style_navigation_response_from_valhalla() { + let stubbed_response = + File::open("tests/fixtures/requests/valhalla_route_walk.json").unwrap(); + let valhalla: valhalla_api::RouteResponse = + serde_json::from_reader(BufReader::new(stubbed_response)).unwrap(); + + let valhalla_response_result = valhalla_api::ValhallaRouteResponseResult::Ok(valhalla); + let plan_response = + PlanResponseOk::from_valhalla(TravelMode::Walk, valhalla_response_result).unwrap(); + + let directions_response = DirectionsResponseOk::from(plan_response); + assert_eq!(directions_response.routes.len(), 3); + + let first_route = &directions_response.routes[0]; + // distance is always in meters for OSRM responses + assert_relative_eq!(first_route.distance, 9148.0); + assert_relative_eq!(first_route.duration, 6488.443); + assert_relative_eq!( + first_route.geometry.0[0], + geo::coord!(x: -122.339216, y: 47.575836) + ); + assert_relative_eq!( + first_route.geometry.0.last().unwrap(), + &geo::coord!(x: -122.347199, y: 47.651048) + ); + + let legs = &first_route.legs; + assert_eq!(legs.len(), 1); + let first_leg = &legs[0]; + + assert_eq!(first_leg.distance, 9148.0); + assert_eq!(first_leg.duration, 6488.443); + assert_eq!( + first_leg.summary, + "Dexter Avenue, East Marginal Way South, Alaskan Way South" + ); + assert_eq!(first_leg.steps.len(), 21); + + let first_step = &first_leg.steps[0]; + assert_eq!(first_step.distance, 19.0); + assert_eq!(first_step.duration, 13.567); + assert_eq!(first_step.name, "East Marginal Way South"); + assert_eq!(first_step.mode, TravelMode::Walk); + + let banner_instructions = first_step.banner_instructions.as_ref().unwrap(); + assert_eq!(banner_instructions.len(), 1); + let banner_instruction = &banner_instructions[0]; + assert_relative_eq!(banner_instruction.distance_along_geometry, 19.0); + let primary = &banner_instruction.primary; + assert_eq!(primary.text, "Turn right onto the walkway."); + + let Some(osrm_api::BannerComponent::Text(first_component)) = &primary.components.first() + else { + panic!( + "unexpected banner component: {:?}", + primary.components.first() + ) + }; + assert_eq!( + first_component.text, + Some("Turn right onto the walkway.".to_string()) + ); + + let step_maneuver = &first_step.maneuver; + assert_eq!( + step_maneuver.location, + geo::point!(x: -122.339216, y: 47.575836) + ); + // assert_eq!(step_maneuver.r#type, "my type"); + // assert_eq!(step_maneuver.modifier, "my modifier"); + // TODO: step_maneuver stuff + // etc... + } +} diff --git a/services/travelmux/src/api/v5/mod.rs b/services/travelmux/src/api/v5/mod.rs index e557dec8c..45cd1f61b 100644 --- a/services/travelmux/src/api/v5/mod.rs +++ b/services/travelmux/src/api/v5/mod.rs @@ -1,3 +1,4 @@ +pub mod directions; mod error; mod osrm_api; pub mod plan; diff --git a/services/travelmux/src/api/v5/plan.rs b/services/travelmux/src/api/v5/plan.rs index 7c6b26c73..01e9c4f3b 100644 --- a/services/travelmux/src/api/v5/plan.rs +++ b/services/travelmux/src/api/v5/plan.rs @@ -8,7 +8,6 @@ use std::collections::HashMap; use std::time::{Duration, SystemTime}; use super::error::{PlanResponseErr, PlanResponseOk}; -use super::osrm_api; use super::TravelModes; use crate::api::AppState; @@ -587,44 +586,6 @@ impl actix_web::Responder for PlanResponseOk { } } -#[derive(Debug, Serialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -struct DirectionsResponseOk { - routes: Vec, -} - -impl actix_web::Responder for DirectionsResponseOk { - type Body = actix_web::body::BoxBody; - - fn respond_to(self, _req: &HttpRequest) -> actix_web::HttpResponse { - let mut response = HttpResponseBuilder::new(actix_web::http::StatusCode::OK); - response.content_type("application/json"); - response.json(self) - } -} - -#[get("/v5/directions")] -pub async fn get_directions( - query: web::Query, - req: HttpRequest, - app_state: web::Data, -) -> std::result::Result { - let plan_response_ok = _get_plan(query, req, app_state).await?; - Ok(plan_response_ok.into()) -} - -impl From for DirectionsResponseOk { - fn from(value: PlanResponseOk) -> Self { - let routes = value - .plan - .itineraries - .into_iter() - .map(osrm_api::Route::from) - .collect(); - Self { routes } - } -} - #[get("/v5/plan")] pub async fn get_plan( query: web::Query, @@ -929,12 +890,12 @@ mod tests { .unwrap() .as_array() .unwrap() - .get(0) + .first() .unwrap(); let legs = first_itinerary.get("legs").unwrap().as_array().unwrap(); // Verify walking leg - let first_leg = legs.get(0).unwrap().as_object().unwrap(); + let first_leg = legs.first().unwrap().as_object().unwrap(); let mode = first_leg.get("mode").unwrap().as_str().unwrap(); assert_eq!(mode, "WALK"); @@ -968,7 +929,7 @@ mod tests { .unwrap() .as_array() .unwrap(); - let first_maneuver = maneuvers.get(0).unwrap(); + let first_maneuver = maneuvers.first().unwrap(); let expected_maneuver = json!({ "distance": 0.011893074179477305, // TODO: truncate precision in serializer "instruction": "Walk south on East Marginal Way South.", @@ -1029,12 +990,12 @@ mod tests { .unwrap() .as_array() .unwrap() - .get(0) + .first() .unwrap(); let legs = first_itinerary.get("legs").unwrap().as_array().unwrap(); // Verify walking leg - let first_leg = legs.get(0).unwrap().as_object().unwrap(); + let first_leg = legs.first().unwrap().as_object().unwrap(); let mode = first_leg.get("mode").unwrap().as_str().unwrap(); assert_eq!(mode, "WALK"); assert!(first_leg.get("transitLeg").is_none()); @@ -1059,7 +1020,7 @@ mod tests { .unwrap() .as_array() .unwrap(); - let first_maneuver = maneuvers.get(0).unwrap(); + let first_maneuver = maneuvers.first().unwrap(); let expected_maneuver = json!({ "type": 2, "instruction": "Walk south on East Marginal Way South.", @@ -1141,79 +1102,4 @@ mod tests { assert_eq!(plan_error.error.status_code, 400); assert_eq!(plan_error.error.error_code, 2154); } - - #[test] - fn osrm_style_navigation_response_from_valhalla() { - let stubbed_response = - File::open("tests/fixtures/requests/valhalla_route_walk.json").unwrap(); - let valhalla: valhalla_api::RouteResponse = - serde_json::from_reader(BufReader::new(stubbed_response)).unwrap(); - - let valhalla_response_result = valhalla_api::ValhallaRouteResponseResult::Ok(valhalla); - let plan_response = - PlanResponseOk::from_valhalla(TravelMode::Walk, valhalla_response_result).unwrap(); - - let directions_response = DirectionsResponseOk::from(plan_response); - assert_eq!(directions_response.routes.len(), 3); - - let first_route = &directions_response.routes[0]; - // distance is always in meters for OSRM responses - assert_relative_eq!(first_route.distance, 9148.0); - assert_relative_eq!(first_route.duration, 6488.443); - assert_relative_eq!( - first_route.geometry.0[0], - geo::coord!(x: -122.339216, y: 47.575836) - ); - assert_relative_eq!( - first_route.geometry.0.last().unwrap(), - &geo::coord!(x: -122.347199, y: 47.651048) - ); - - let legs = &first_route.legs; - assert_eq!(legs.len(), 1); - let first_leg = &legs[0]; - - assert_eq!(first_leg.distance, 9148.0); - assert_eq!(first_leg.duration, 6488.443); - assert_eq!( - first_leg.summary, - "Dexter Avenue, East Marginal Way South, Alaskan Way South" - ); - assert_eq!(first_leg.steps.len(), 21); - - let first_step = &first_leg.steps[0]; - assert_eq!(first_step.distance, 19.0); - assert_eq!(first_step.duration, 13.567); - assert_eq!(first_step.name, "East Marginal Way South"); - assert_eq!(first_step.mode, TravelMode::Walk); - - let banner_instructions = first_step.banner_instructions.as_ref().unwrap(); - assert_eq!(banner_instructions.len(), 1); - let banner_instruction = &banner_instructions[0]; - assert_relative_eq!(banner_instruction.distance_along_geometry, 19.0); - let primary = &banner_instruction.primary; - assert_eq!(primary.text, "Turn right onto the walkway."); - - let Some(osrm_api::BannerComponent::Text(first_component)) = &primary.components.first() - else { - panic!( - "unexpected banner component: {:?}", - primary.components.first() - ) - }; - assert_eq!( - first_component.text, - Some("Turn right onto the walkway.".to_string()) - ); - - let step_maneuver = &first_step.maneuver; - assert_eq!( - step_maneuver.location, - geo::point!(x: -122.339216, y: 47.575836) - ); - // assert_eq!(step_maneuver.r#type, "my type"); - // assert_eq!(step_maneuver.modifier, "my modifier"); - // TODO: step_maneuver stuff - // etc... - } } diff --git a/services/travelmux/src/bin/travelmux-server/main.rs b/services/travelmux/src/bin/travelmux-server/main.rs index 1d9c3df00..44d50bb69 100644 --- a/services/travelmux/src/bin/travelmux-server/main.rs +++ b/services/travelmux/src/bin/travelmux-server/main.rs @@ -50,7 +50,7 @@ async fn main() -> Result<()> { .app_data(web::Data::new(app_state.clone())) .service(api::v4::plan::get_plan) .service(api::v5::plan::get_plan) - .service(api::v5::plan::get_directions) + .service(api::v5::directions::get_directions) .service(api::health::get_ready) .service(api::health::get_alive) }) From 414a34985890a58606ff233f018d5e7bfc82f82e Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Fri, 17 May 2024 12:30:17 -0700 Subject: [PATCH 09/24] regen test fixtures, include more --- services/travelmux/src/api/v4/plan.rs | 49 +- services/travelmux/src/api/v5/directions.rs | 90 +- services/travelmux/src/api/v5/plan.rs | 57 +- .../opentripplanner_bicycle_plan.json | 549 +++ .../opentripplanner_plan_transit.json | 2493 ---------- ...pentripplanner_plan_transit_with_bike.json | 2666 ----------- .../opentripplanner_transit_plan.json | 2271 +++++++++ ...tripplanner_transit_with_bicycle_plan.json | 4169 +++++++++++++++++ .../requests/opentripplanner_walk_plan.json | 388 ++ .../tests/fixtures/requests/refresh.sh | 45 + .../requests/valhalla_auto_route.json | 107 + .../requests/valhalla_bicycle_route.json | 332 ++ ...lk.json => valhalla_pedestrian_route.json} | 2380 +++++----- 13 files changed, 9184 insertions(+), 6412 deletions(-) create mode 100644 services/travelmux/tests/fixtures/requests/opentripplanner_bicycle_plan.json delete mode 100644 services/travelmux/tests/fixtures/requests/opentripplanner_plan_transit.json delete mode 100644 services/travelmux/tests/fixtures/requests/opentripplanner_plan_transit_with_bike.json create mode 100644 services/travelmux/tests/fixtures/requests/opentripplanner_transit_plan.json create mode 100644 services/travelmux/tests/fixtures/requests/opentripplanner_transit_with_bicycle_plan.json create mode 100644 services/travelmux/tests/fixtures/requests/opentripplanner_walk_plan.json create mode 100755 services/travelmux/tests/fixtures/requests/refresh.sh create mode 100644 services/travelmux/tests/fixtures/requests/valhalla_auto_route.json create mode 100644 services/travelmux/tests/fixtures/requests/valhalla_bicycle_route.json rename services/travelmux/tests/fixtures/requests/{valhalla_route_walk.json => valhalla_pedestrian_route.json} (85%) diff --git a/services/travelmux/src/api/v4/plan.rs b/services/travelmux/src/api/v4/plan.rs index 3c250f35e..465bdae2d 100644 --- a/services/travelmux/src/api/v4/plan.rs +++ b/services/travelmux/src/api/v4/plan.rs @@ -495,7 +495,7 @@ mod tests { #[test] fn parse_from_valhalla() { let stubbed_response = - File::open("tests/fixtures/requests/valhalla_route_walk.json").unwrap(); + File::open("tests/fixtures/requests/valhalla_pedestrian_route.json").unwrap(); let valhalla: valhalla_api::RouteResponse = serde_json::from_reader(BufReader::new(stubbed_response)).unwrap(); @@ -507,7 +507,7 @@ mod tests { // itineraries let first_itinerary = &plan_response.plan.itineraries[0]; assert_eq!(first_itinerary.mode, TravelMode::Walk); - assert_relative_eq!(first_itinerary.distance, 9.148); + assert_relative_eq!(first_itinerary.distance, 5.684); assert_relative_eq!(first_itinerary.duration, 6488.443); assert_relative_eq!( first_itinerary.bounds, @@ -548,22 +548,22 @@ mod tests { #[test] fn parse_from_otp() { let stubbed_response = - File::open("tests/fixtures/requests/opentripplanner_plan_transit.json").unwrap(); + File::open("tests/fixtures/requests/opentripplanner_transit_plan.json").unwrap(); let otp: otp_api::PlanResponse = serde_json::from_reader(BufReader::new(stubbed_response)).unwrap(); let plan_response = PlanResponseOk::from_otp(TravelMode::Transit, otp).unwrap(); let itineraries = plan_response.plan.itineraries; - assert_eq!(itineraries.len(), 5); + assert_eq!(itineraries.len(), 6); // itineraries let first_itinerary = &itineraries[0]; assert_eq!(first_itinerary.mode, TravelMode::Transit); - assert_relative_eq!(first_itinerary.distance, 10.69944); - assert_relative_eq!(first_itinerary.duration, 3273.0); + assert_relative_eq!(first_itinerary.distance, 10.15766); + assert_relative_eq!(first_itinerary.duration, 2347.0); // legs - assert_eq!(first_itinerary.legs.len(), 7); + assert_eq!(first_itinerary.legs.len(), 4); let first_leg = &first_itinerary.legs[0]; let geometry = polyline::decode_polyline(&first_leg.geometry, 6).unwrap(); assert_relative_eq!( @@ -589,22 +589,22 @@ mod tests { let ModeLeg::NonTransit(maneuvers) = &first_leg.mode_leg else { panic!("expected non-transit leg") }; - assert_eq!(maneuvers.len(), 4); + assert_eq!(maneuvers.len(), 2); assert_eq!(maneuvers[0].r#type, ManeuverType::Start); assert_eq!(maneuvers[1].r#type, ManeuverType::Left); - let fourth_leg = &first_itinerary.legs[3]; - assert_eq!(fourth_leg.mode, TravelMode::Transit); - let ModeLeg::Transit(transit_leg) = &fourth_leg.mode_leg else { + let third_leg = &first_itinerary.legs[2]; + assert_eq!(third_leg.mode, TravelMode::Transit); + let ModeLeg::Transit(transit_leg) = &third_leg.mode_leg else { panic!("expected transit leg") }; - assert_eq!(transit_leg.route_color, Some("28813F".to_string())); + assert_eq!(transit_leg.route_color, None); } #[test] fn serialize_response_from_otp() { let stubbed_response = - File::open("tests/fixtures/requests/opentripplanner_plan_transit.json").unwrap(); + File::open("tests/fixtures/requests/opentripplanner_transit_plan.json").unwrap(); let otp: otp_api::PlanResponse = serde_json::from_reader(BufReader::new(stubbed_response)).unwrap(); let plan_response = PlanResponseOk::from_otp(TravelMode::Transit, otp).unwrap(); @@ -635,14 +635,14 @@ mod tests { .expect("field missing") .as_u64() .expect("unexpected type. expected u64"); - assert_eq!(mode, 1708728373000); + assert_eq!(mode, 1715974501000); let mode = first_leg .get("endTime") .expect("field missing") .as_u64() .expect("unexpected type. expected u64"); - assert_eq!(mode, 1708728745000); + assert_eq!(mode, 1715974870000); assert!(first_leg.get("transitLeg").is_none()); let maneuvers = first_leg.get("maneuvers").unwrap().as_array().unwrap(); @@ -655,11 +655,11 @@ mod tests { assert_eq!(first_maneuver, &expected_maneuver); // Verify Transit leg - let fourth_leg = legs.get(3).unwrap().as_object().unwrap(); - let mode = fourth_leg.get("mode").unwrap().as_str().unwrap(); + let transit_leg = legs.get(1).unwrap().as_object().unwrap(); + let mode = transit_leg.get("mode").unwrap().as_str().unwrap(); assert_eq!(mode, "TRANSIT"); - assert!(fourth_leg.get("maneuvers").is_none()); - let transit_leg = fourth_leg + assert!(transit_leg.get("maneuvers").is_none()); + let transit_leg = transit_leg .get("transitLeg") .unwrap() .as_object() @@ -668,19 +668,16 @@ mod tests { // Brittle: If the fixtures are updated, these values might change due to time of day or whatever. assert_eq!( transit_leg.get("agencyName").unwrap().as_str().unwrap(), - "Sound Transit" + "Metro Transit" ); - assert_eq!( - transit_leg.get("route").unwrap().as_str().unwrap(), - "Northgate - Angle Lake" - ); + assert!(transit_leg.get("route").is_none()); } #[test] fn serialize_response_from_valhalla() { let stubbed_response = - File::open("tests/fixtures/requests/valhalla_route_walk.json").unwrap(); + File::open("tests/fixtures/requests/valhalla_pedestrian_route.json").unwrap(); let valhalla: valhalla_api::RouteResponse = serde_json::from_reader(BufReader::new(stubbed_response)).unwrap(); @@ -715,7 +712,7 @@ mod tests { let expected_maneuver = json!({ "type": 2, "instruction": "Walk south on East Marginal Way South.", - "verbalPostTransitionInstruction": "Continue for 20 meters." + "verbalPostTransitionInstruction": "Continue for 60 feet." }); assert_eq!(first_maneuver, &expected_maneuver); } diff --git a/services/travelmux/src/api/v5/directions.rs b/services/travelmux/src/api/v5/directions.rs index 15652a9e7..277f72ce4 100644 --- a/services/travelmux/src/api/v5/directions.rs +++ b/services/travelmux/src/api/v5/directions.rs @@ -48,25 +48,26 @@ mod tests { use super::*; use crate::api::v5::error::PlanResponseOk; use crate::api::v5::osrm_api; + use crate::otp::otp_api; use crate::valhalla::valhalla_api; - use crate::TravelMode; + use crate::{DistanceUnit, TravelMode}; use approx::assert_relative_eq; use std::fs::File; use std::io::BufReader; #[test] - fn osrm_style_navigation_response_from_valhalla() { + fn directions_from_otp() { let stubbed_response = - File::open("tests/fixtures/requests/valhalla_route_walk.json").unwrap(); - let valhalla: valhalla_api::RouteResponse = + File::open("tests/fixtures/requests/opentripplanner_walk_plan.json").unwrap(); + + let otp: otp_api::PlanResponse = serde_json::from_reader(BufReader::new(stubbed_response)).unwrap(); - let valhalla_response_result = valhalla_api::ValhallaRouteResponseResult::Ok(valhalla); let plan_response = - PlanResponseOk::from_valhalla(TravelMode::Walk, valhalla_response_result).unwrap(); + PlanResponseOk::from_otp(TravelMode::Walk, otp, DistanceUnit::Miles).unwrap(); let directions_response = DirectionsResponseOk::from(plan_response); - assert_eq!(directions_response.routes.len(), 3); + assert_eq!(directions_response.routes.len(), 1); let first_route = &directions_response.routes[0]; // distance is always in meters for OSRM responses @@ -128,4 +129,79 @@ mod tests { // TODO: step_maneuver stuff // etc... } + + #[test] + fn directions_from_valhalla() { + let stubbed_response = + File::open("tests/fixtures/requests/valhalla_pedestrian_route.json").unwrap(); + let valhalla: valhalla_api::RouteResponse = + serde_json::from_reader(BufReader::new(stubbed_response)).unwrap(); + + let valhalla_response_result = valhalla_api::ValhallaRouteResponseResult::Ok(valhalla); + let plan_response = + PlanResponseOk::from_valhalla(TravelMode::Walk, valhalla_response_result).unwrap(); + + let directions_response = DirectionsResponseOk::from(plan_response); + assert_eq!(directions_response.routes.len(), 3); + + let first_route = &directions_response.routes[0]; + // distance is always in meters for OSRM responses + assert_relative_eq!(first_route.distance, 9147.48856); + assert_relative_eq!(first_route.duration, 6488.443); + assert_relative_eq!( + first_route.geometry.0[0], + geo::coord!(x: -122.339216, y: 47.575836) + ); + assert_relative_eq!( + first_route.geometry.0.last().unwrap(), + &geo::coord!(x: -122.347199, y: 47.651048) + ); + + let legs = &first_route.legs; + assert_eq!(legs.len(), 1); + let first_leg = &legs[0]; + + assert_eq!(first_leg.distance, 9147.48856); + assert_eq!(first_leg.duration, 6488.443); + assert_eq!( + first_leg.summary, + "Dexter Avenue, East Marginal Way South, Alaskan Way South" + ); + assert_eq!(first_leg.steps.len(), 21); + + let first_step = &first_leg.steps[0]; + assert_eq!(first_step.distance, 17.70274); + assert_eq!(first_step.duration, 13.567); + assert_eq!(first_step.name, "East Marginal Way South"); + assert_eq!(first_step.mode, TravelMode::Walk); + + let banner_instructions = first_step.banner_instructions.as_ref().unwrap(); + assert_eq!(banner_instructions.len(), 1); + let banner_instruction = &banner_instructions[0]; + assert_relative_eq!(banner_instruction.distance_along_geometry, 17.70274); + let primary = &banner_instruction.primary; + assert_eq!(primary.text, "Turn right onto the walkway."); + + let Some(osrm_api::BannerComponent::Text(first_component)) = &primary.components.first() + else { + panic!( + "unexpected banner component: {:?}", + primary.components.first() + ) + }; + assert_eq!( + first_component.text, + Some("Turn right onto the walkway.".to_string()) + ); + + let step_maneuver = &first_step.maneuver; + assert_eq!( + step_maneuver.location, + geo::point!(x: -122.339216, y: 47.575836) + ); + // assert_eq!(step_maneuver.r#type, "my type"); + // assert_eq!(step_maneuver.modifier, "my modifier"); + // TODO: step_maneuver stuff + // etc... + } } diff --git a/services/travelmux/src/api/v5/plan.rs b/services/travelmux/src/api/v5/plan.rs index 01e9c4f3b..c969da157 100644 --- a/services/travelmux/src/api/v5/plan.rs +++ b/services/travelmux/src/api/v5/plan.rs @@ -763,7 +763,7 @@ mod tests { #[test] fn parse_from_valhalla() { let stubbed_response = - File::open("tests/fixtures/requests/valhalla_route_walk.json").unwrap(); + File::open("tests/fixtures/requests/valhalla_pedestrian_route.json").unwrap(); let valhalla: valhalla_api::RouteResponse = serde_json::from_reader(BufReader::new(stubbed_response)).unwrap(); @@ -775,7 +775,7 @@ mod tests { // itineraries let first_itinerary = &plan_response.plan.itineraries[0]; assert_eq!(first_itinerary.mode, TravelMode::Walk); - assert_relative_eq!(first_itinerary.distance, 9.148); + assert_relative_eq!(first_itinerary.distance, 5.684); assert_relative_eq!(first_itinerary.duration, 6488.443); assert_relative_eq!( first_itinerary.bounds, @@ -815,23 +815,23 @@ mod tests { #[test] fn parse_from_otp() { let stubbed_response = - File::open("tests/fixtures/requests/opentripplanner_plan_transit.json").unwrap(); + File::open("tests/fixtures/requests/opentripplanner_transit_plan.json").unwrap(); let otp: otp_api::PlanResponse = serde_json::from_reader(BufReader::new(stubbed_response)).unwrap(); let plan_response = PlanResponseOk::from_otp(TravelMode::Transit, otp, DistanceUnit::Miles).unwrap(); let itineraries = plan_response.plan.itineraries; - assert_eq!(itineraries.len(), 5); + assert_eq!(itineraries.len(), 6); // itineraries let first_itinerary = &itineraries[0]; assert_eq!(first_itinerary.mode, TravelMode::Transit); - assert_relative_eq!(first_itinerary.distance, 6.648340313420409); - assert_relative_eq!(first_itinerary.duration, 3273.0); + assert_relative_eq!(first_itinerary.distance, 6.311692992158277); + assert_relative_eq!(first_itinerary.duration, 2347.0); // legs - assert_eq!(first_itinerary.legs.len(), 7); + assert_eq!(first_itinerary.legs.len(), 4); let first_leg = &first_itinerary.legs[0]; assert_relative_eq!( first_leg.geometry.0[0], @@ -857,22 +857,22 @@ mod tests { panic!("expected non-transit leg") }; let maneuvers = &non_transit_leg.maneuvers; - assert_eq!(maneuvers.len(), 4); + assert_eq!(maneuvers.len(), 2); assert_eq!(maneuvers[0].r#type, ManeuverType::Start); assert_eq!(maneuvers[1].r#type, ManeuverType::Left); - let fourth_leg = &first_itinerary.legs[3]; - assert_eq!(fourth_leg.mode, TravelMode::Transit); - let ModeLeg::Transit(transit_leg) = &fourth_leg.mode_leg else { + let transit_leg = &first_itinerary.legs[2]; + assert_eq!(transit_leg.mode, TravelMode::Transit); + let ModeLeg::Transit(transit_leg) = &transit_leg.mode_leg else { panic!("expected transit leg") }; - assert_eq!(transit_leg.route_color, Some("28813F".to_string())); + assert!(transit_leg.route_color.is_none()); } #[test] fn serialize_response_from_otp() { let stubbed_response = - File::open("tests/fixtures/requests/opentripplanner_plan_transit.json").unwrap(); + File::open("tests/fixtures/requests/opentripplanner_transit_plan.json").unwrap(); let otp: otp_api::PlanResponse = serde_json::from_reader(BufReader::new(stubbed_response)).unwrap(); let plan_response = @@ -904,14 +904,14 @@ mod tests { .expect("field missing") .as_u64() .expect("unexpected type. expected u64"); - assert_eq!(mode, 1708728373000); + assert_eq!(mode, 1715974501000); let mode = first_leg .get("endTime") .expect("field missing") .as_u64() .expect("unexpected type. expected u64"); - assert_eq!(mode, 1708728745000); + assert_eq!(mode, 1715974870000); assert!(first_leg.get("transitLeg").is_none()); let non_transit_leg = first_leg.get("nonTransitLeg").unwrap().as_object().unwrap(); @@ -931,11 +931,11 @@ mod tests { .unwrap(); let first_maneuver = maneuvers.first().unwrap(); let expected_maneuver = json!({ - "distance": 0.011893074179477305, // TODO: truncate precision in serializer + "distance": 0.0118992879068438, // TODO: truncate precision in serializer "instruction": "Walk south on East Marginal Way South.", "startPoint": { - "lat": 47.5758355, - "lon": -122.3392164 + "lat": 47.5758346, + "lon": -122.3392181 }, "streetNames": ["East Marginal Way South"], "type": 1, @@ -944,11 +944,11 @@ mod tests { assert_eq!(first_maneuver, &expected_maneuver); // Verify Transit leg - let fourth_leg = legs.get(3).unwrap().as_object().unwrap(); - let mode = fourth_leg.get("mode").unwrap().as_str().unwrap(); + let transit_leg = legs.get(2).unwrap().as_object().unwrap(); + let mode = transit_leg.get("mode").unwrap().as_str().unwrap(); assert_eq!(mode, "TRANSIT"); - assert!(fourth_leg.get("maneuvers").is_none()); - let transit_leg = fourth_leg + assert!(transit_leg.get("maneuvers").is_none()); + let transit_leg = transit_leg .get("transitLeg") .unwrap() .as_object() @@ -957,19 +957,16 @@ mod tests { // Brittle: If the fixtures are updated, these values might change due to time of day or whatever. assert_eq!( transit_leg.get("agencyName").unwrap().as_str().unwrap(), - "Sound Transit" + "Metro Transit" ); - assert_eq!( - transit_leg.get("route").unwrap().as_str().unwrap(), - "Northgate - Angle Lake" - ); + assert!(transit_leg.get("route").is_none()); } #[test] fn serialize_response_from_valhalla() { let stubbed_response = - File::open("tests/fixtures/requests/valhalla_route_walk.json").unwrap(); + File::open("tests/fixtures/requests/valhalla_pedestrian_route.json").unwrap(); let valhalla: valhalla_api::RouteResponse = serde_json::from_reader(BufReader::new(stubbed_response)).unwrap(); @@ -1024,8 +1021,8 @@ mod tests { let expected_maneuver = json!({ "type": 2, "instruction": "Walk south on East Marginal Way South.", - "verbalPostTransitionInstruction": "Continue for 20 meters.", - "distance": 0.019, + "verbalPostTransitionInstruction": "Continue for 60 feet.", + "distance": 0.011, "startPoint": { "lat": 47.575836, "lon": -122.339216 diff --git a/services/travelmux/tests/fixtures/requests/opentripplanner_bicycle_plan.json b/services/travelmux/tests/fixtures/requests/opentripplanner_bicycle_plan.json new file mode 100644 index 000000000..48629d282 --- /dev/null +++ b/services/travelmux/tests/fixtures/requests/opentripplanner_bicycle_plan.json @@ -0,0 +1,549 @@ +{ + "debugOutput": { + "directStreetRouterTime": 283901529, + "filteringTime": 18391574, + "precalculationTime": 1493433, + "renderingTime": 16039298, + "totalTime": 344494559, + "transitRouterTime": 24196720, + "transitRouterTimes": { + "accessEgressTime": 0, + "itineraryCreationTime": 0, + "raptorSearchTime": 0, + "tripPatternFilterTime": 0 + } + }, + "elevationMetadata": { + "ellipsoidToGeoidDifference": -19.263942171473552, + "geoidElevation": false + }, + "plan": { + "date": 1715974434475, + "from": { + "lat": 47.575837, + "lon": -122.339414, + "name": "Origin", + "vertexType": "NORMAL" + }, + "itineraries": [ + { + "arrivedAtDestinationWithRentedBicycle": false, + "duration": 3039, + "elevationGained": 0.0, + "elevationLost": 0.0, + "endTime": 1715977473000, + "fare": { + "details": {}, + "fare": {} + }, + "generalizedCost": 6840, + "legs": [ + { + "agencyTimeZoneOffset": -25200000, + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 10392.34, + "duration": 3039.0, + "endTime": 1715977473000, + "from": { + "departure": 1715974434000, + "lat": 47.575837, + "lon": -122.339414, + "name": "Origin", + "vertexType": "NORMAL" + }, + "generalizedCost": 6840, + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 491, + "points": "}ckaHbkuiV`@@J??L?bA?p@?F?Pa@?}PCcRFq@@}W?{BCe@EYGUM[Um@y@a@g@KQEGq@w@uAiAa@UYQa@Q{EgBk@Q_@MOEsA_@DI]GGE?GSA]Eg@IyAc@YIkAWe@SeA_@y@QKC_AO[ASAcAGaACaCAqA@mEGgA?_@?iEBGAAC?K?QG?I?I?a@@GD_@?_ABm@?G?K@_EAGCIC_@?G@Y?U@I@M@OBG@UDWJ[NKFEHURQNABCBMJA@AgA?s@?sAHB?Q?iC?kB?Q?S?K?}A?K?Q?qA?KE??m@Ac@?]?]?C@m@?M?E?cB?e@?qA?O?IAi@?MAQCKCICGCOE[KYO[YYIEG@c@^WR_Av@m@j@SPMLuBjBGFSNML[XgA`AKHKJuBjBIHQJqBfBMJMLsBhBKJSPgB~AQNONoBdBMJQPeDvCMLSNgDzCMLQN}CpCSPSPcDvCKJWf@IR_CzEA@IPKTw@|Ai@fAYj@EHKNINCBEHERcBhDKHEHIPMVADKVqB`EINGIQWq@aAa@i@MSCCG??AMQIMEEyAuBCEEEMIMKACGKk@w@m@{@EEIOEEEGEGIKqAiBKOOUGIEGaAqAEEIGQICCg@GuC?E?OMS?eECM?Q?Y?oDAO?O?gEAO?Q?sB?y@Au@?W?_@AQ?eBAW?U?mB?c@?]?_@?E?Qk@SAG?CCIBC?C?Y@M@MBKBIBC@C@ACKGEACAaBfASLEBC@C?EBGBEBMH[RMLCDGDIFA?KD[LWLc@JSBYDY@CBG@E@GBE@K?G?C?G?E?GECAEEC?cB?M@I@I?o@?QAuC?o@AK@E?ODG@GAC?GAECGCKKECIIGCCAgCo@i@OGAOEICaBOI?]?kB?UBu@@mD@qA@G?cB@C@MHG@[Dc@FOBMDOD]LYJSJKDc@RG@M@G@uCvAIBGDOL_@PMRCBOFCBOBG@ULiAl@[Ry@b@EDCFCFEDIDmAn@E@C@OAC@EB]PWLIFKFGBG@y@d@[NEDq@h@eA|@aAt@u@l@}@t@STSXOVGNO`@Sj@a@hACF@BHF@FBFBP?FADgCfIeAjDIVENGPQb@Yb@i@r@UXIFMHMDGBG@M@W@i@@k@@iEAICuEAEACAGG?CCEA@G@AQ?W?kA@{@B]^qEBe@G?I@iA@}A?oA?cA?A?K?@I@IMAA?OAAe@MQSCm@\\mB}@EEC??DFHt@l@d@V\\JNBNBB?L?\\?" + }, + "mode": "BICYCLE", + "pathway": false, + "realTime": false, + "rentedBike": false, + "route": "", + "startTime": 1715974434000, + "steps": [ + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": false, + "distance": 26.0, + "elevation": "", + "lat": 47.5758346, + "lon": -122.3392181, + "relativeDirection": "DEPART", + "stayOn": false, + "streetName": "East Marginal Way South", + "walkingBike": false + }, + { + "absoluteDirection": "WEST", + "area": false, + "bogusName": false, + "distance": 59.45, + "elevation": "", + "lat": 47.5756008, + "lon": -122.3392243, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "South Hanford Street", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 1218.07, + "elevation": "", + "lat": 47.5756059, + "lon": -122.3400167, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "East Marginal Way South", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 515.99, + "elevation": "", + "lat": 47.58656, + "lon": -122.3400253, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "Alaskan Way South", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTHEAST", + "area": false, + "bogusName": false, + "distance": 4.97, + "elevation": "", + "lat": 47.5907595, + "lon": -122.337345, + "relativeDirection": "HARD_RIGHT", + "stayOn": false, + "streetName": "South Atlantic Street", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": true, + "distance": 186.23, + "elevation": "", + "lat": 47.5907275, + "lon": -122.3372987, + "relativeDirection": "HARD_LEFT", + "stayOn": false, + "streetName": "Marginal Way Cycle Route", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 659.69, + "elevation": "", + "lat": 47.592328, + "lon": -122.3367458, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "Portside Trail", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 129.47, + "elevation": "", + "lat": 47.5981133, + "lon": -122.3360192, + "relativeDirection": "LEFT", + "stayOn": true, + "streetName": "Portside Trail", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": true, + "distance": 111.29, + "elevation": "", + "lat": 47.5992728, + "lon": -122.3360808, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "Marginal Way Cycle Route", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": true, + "distance": 176.79, + "elevation": "", + "lat": 47.6002712, + "lon": -122.3360595, + "relativeDirection": "SLIGHTLY_RIGHT", + "stayOn": true, + "streetName": "Marginal Way Cycle Route", + "walkingBike": false + }, + { + "absoluteDirection": "NORTHWEST", + "area": false, + "bogusName": false, + "distance": 1.96, + "elevation": "", + "lat": 47.6017628, + "lon": -122.3366416, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "Elliott Bay Trail", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": true, + "distance": 77.8, + "elevation": "", + "lat": 47.6017781, + "lon": -122.3366546, + "relativeDirection": "HARD_RIGHT", + "stayOn": false, + "streetName": "bike path", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": true, + "distance": 5.04, + "elevation": "", + "lat": 47.6017831, + "lon": -122.3356171, + "relativeDirection": "RIGHT", + "stayOn": true, + "streetName": "path", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": false, + "distance": 199.61, + "elevation": "", + "lat": 47.6017396, + "lon": -122.335636, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "Yesler Way", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": true, + "distance": 4.21, + "elevation": "", + "lat": 47.601731, + "lon": -122.3329736, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "service road", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": false, + "distance": 219.26, + "elevation": "", + "lat": 47.6017688, + "lon": -122.3329762, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "Yesler Way Cycletrack", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": false, + "distance": 1895.86, + "elevation": "", + "lat": 47.6018657, + "lon": -122.3300857, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "4th Avenue Cycletrack", + "walkingBike": false + }, + { + "absoluteDirection": "NORTHEAST", + "area": false, + "bogusName": false, + "distance": 94.94, + "elevation": "", + "lat": 47.6153211, + "lon": -122.3439964, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "Bell Street", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 375.08, + "elevation": "", + "lat": 47.6159653, + "lon": -122.343165, + "relativeDirection": "SLIGHTLY_LEFT", + "stayOn": false, + "streetName": "Bell St Cycletrack", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 132.43, + "elevation": "", + "lat": 47.6185462, + "lon": -122.3399723, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "9th Ave Cycletrack", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 768.81, + "elevation": "", + "lat": 47.6197152, + "lon": -122.339792, + "relativeDirection": "SLIGHTLY_LEFT", + "stayOn": false, + "streetName": "9th Avenue North", + "walkingBike": false + }, + { + "absoluteDirection": "NORTHEAST", + "area": false, + "bogusName": true, + "distance": 19.74, + "elevation": "", + "lat": 47.6266292, + "lon": -122.3397288, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "bike path", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": true, + "distance": 27.65, + "elevation": "", + "lat": 47.6267195, + "lon": -122.339502, + "relativeDirection": "LEFT", + "stayOn": true, + "streetName": "bike path", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 47.87, + "elevation": "", + "lat": 47.6269619, + "lon": -122.3394925, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "Cheshiahud Lake Union Loop", + "walkingBike": false + }, + { + "absoluteDirection": "NORTHEAST", + "area": false, + "bogusName": false, + "distance": 88.27, + "elevation": "", + "lat": 47.6273847, + "lon": -122.3395952, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "Westlake Protected Bike Lane", + "walkingBike": false + }, + { + "absoluteDirection": "NORTHWEST", + "area": false, + "bogusName": false, + "distance": 208.81, + "elevation": "", + "lat": 47.6280979, + "lon": -122.3399451, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "Westlake Cycle Track", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 1768.5, + "elevation": "", + "lat": 47.6298708, + "lon": -122.3407137, + "relativeDirection": "SLIGHTLY_RIGHT", + "stayOn": false, + "streetName": "Westlake Protected Bike Lane", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": false, + "distance": 781.14, + "elevation": "", + "lat": 47.6446397, + "lon": -122.3456348, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "Cheshiahud Lake Union Loop", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": false, + "distance": 106.11, + "elevation": "", + "lat": 47.6494012, + "lon": -122.3486434, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "North 34th Street", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 193.79, + "elevation": "", + "lat": 47.6492019, + "lon": -122.347258, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "Troll Avenue North", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": false, + "distance": 7.88, + "elevation": "", + "lat": 47.6509446, + "lon": -122.347277, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "North 36th Street", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": true, + "distance": 17.1, + "elevation": "", + "lat": 47.6509293, + "lon": -122.3471743, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "path", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "alerts": [ + { + "alertHeaderText": "Unpaved surface" + } + ], + "area": false, + "bogusName": true, + "distance": 137.31, + "elevation": "", + "lat": 47.6510827, + "lon": -122.3471583, + "relativeDirection": "RIGHT", + "stayOn": true, + "streetName": "path", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": true, + "distance": 125.23, + "elevation": "", + "lat": 47.6520984, + "lon": -122.3466956, + "relativeDirection": "HARD_LEFT", + "stayOn": false, + "streetName": "sidewalk", + "walkingBike": true + } + ], + "to": { + "arrival": 1715977473000, + "lat": 47.651048, + "lon": -122.347234, + "name": "Destination", + "vertexType": "NORMAL" + }, + "transitLeg": false, + "walkingBike": false + } + ], + "startTime": 1715974434000, + "tooSloped": false, + "transfers": 0, + "transitTime": 0, + "waitingTime": 0, + "walkDistance": 10392.34, + "walkLimitExceeded": false, + "walkTime": 3039 + } + ], + "to": { + "lat": 47.651048, + "lon": -122.347234, + "name": "Destination", + "vertexType": "NORMAL" + } + }, + "requestParameters": { + "fromPlace": "47.575837,-122.339414", + "mode": "BICYCLE", + "toPlace": "47.651048,-122.347234" + } +} diff --git a/services/travelmux/tests/fixtures/requests/opentripplanner_plan_transit.json b/services/travelmux/tests/fixtures/requests/opentripplanner_plan_transit.json deleted file mode 100644 index 354284ccb..000000000 --- a/services/travelmux/tests/fixtures/requests/opentripplanner_plan_transit.json +++ /dev/null @@ -1,2493 +0,0 @@ -{ - "requestParameters": { - "mode": "TRANSIT", - "fromPlace": "47.575837,-122.339414", - "toPlace": "47.651048,-122.347234", - "numItineraries": "5" - }, - "plan": { - "date": 1708728124228, - "from": { - "name": "Origin", - "lon": -122.339414, - "lat": 47.575837, - "vertexType": "NORMAL" - }, - "to": { - "name": "Destination", - "lon": -122.347234, - "lat": 47.651048, - "vertexType": "NORMAL" - }, - "itineraries": [ - { - "duration": 3273, - "startTime": 1708728373000, - "endTime": 1708731646000, - "walkTime": 1011, - "transitTime": 1685, - "waitingTime": 577, - "walkDistance": 1137.67, - "walkLimitExceeded": false, - "generalizedCost": 5912, - "elevationLost": 0.0, - "elevationGained": 0.0, - "transfers": 2, - "fare": { - "fare": { - "regular": { - "cents": 825, - "currency": { - "currency": "USD", - "defaultFractionDigits": 2, - "currencyCode": "USD", - "symbol": "$" - } - } - }, - "details": { - "regular": [ - { - "fareId": "headway-1080:31", - "price": { - "cents": 275, - "currency": { - "currency": "USD", - "defaultFractionDigits": 2, - "currencyCode": "USD", - "symbol": "$" - } - }, - "routes": [ - "headway-1080:100230" - ] - }, - { - "fareId": "headway-1080:31", - "price": { - "cents": 275, - "currency": { - "currency": "USD", - "defaultFractionDigits": 2, - "currencyCode": "USD", - "symbol": "$" - } - }, - "routes": [ - "headway-1080:100479" - ] - }, - { - "fareId": "headway-1080:31", - "price": { - "cents": 275, - "currency": { - "currency": "USD", - "defaultFractionDigits": 2, - "currencyCode": "USD", - "symbol": "$" - } - }, - "routes": [ - "headway-1080:102574" - ] - } - ] - } - }, - "legs": [ - { - "startTime": 1708728373000, - "endTime": 1708728745000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 441.92, - "generalizedCost": 683, - "pathway": false, - "mode": "WALK", - "transitLeg": false, - "route": "", - "agencyTimeZoneOffset": -28800000, - "interlineWithPreviousLeg": false, - "from": { - "name": "Origin", - "lon": -122.339414, - "lat": 47.575837, - "departure": 1708728373000, - "vertexType": "NORMAL" - }, - "to": { - "name": "1st Ave S & S Hanford St", - "stopId": "headway-1080:15205", - "stopCode": "15205", - "lon": -122.334106, - "lat": 47.575924, - "arrival": 1708728745000, - "departure": 1708728745000, - "zoneId": "1", - "vertexType": "TRANSIT" - }, - "legGeometry": { - "points": "}ckaHbkuiV`@??[?qDE??C?G?A?C?aB?_@?G?M?I?w@D??IAW?K@SAG?I@C?c@?y@?_A?gA?a@?C?U?K?_CCE@c@?]q@??H", - "length": 37 - }, - "steps": [ - { - "distance": 19.14, - "relativeDirection": "DEPART", - "streetName": "East Marginal Way South", - "absoluteDirection": "SOUTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3392164, - "lat": 47.5758355, - "elevation": "", - "walkingBike": false - }, - { - "distance": 170.43, - "relativeDirection": "LEFT", - "streetName": "path", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3392192, - "lat": 47.5756634, - "elevation": "", - "walkingBike": false - }, - { - "distance": 225.05, - "relativeDirection": "RIGHT", - "streetName": "path", - "absoluteDirection": "SOUTH", - "stayOn": true, - "area": false, - "bogusName": true, - "lon": -122.3370064, - "lat": 47.5756971, - "elevation": "", - "walkingBike": false - }, - { - "distance": 27.28, - "relativeDirection": "LEFT", - "streetName": "sidewalk", - "absoluteDirection": "NORTH", - "stayOn": true, - "area": false, - "bogusName": true, - "lon": -122.3340553, - "lat": 47.5756785, - "elevation": "", - "walkingBike": false - } - ], - "rentedBike": false, - "walkingBike": false, - "duration": 372.0 - }, - { - "startTime": 1708728745000, - "endTime": 1708728900000, - "departureDelay": 240, - "arrivalDelay": 240, - "realTime": true, - "distance": 1021.6, - "generalizedCost": 755, - "pathway": false, - "mode": "BUS", - "transitLeg": true, - "agencyName": "Metro Transit", - "agencyUrl": "https://kingcounty.gov/en/dept/metro", - "agencyTimeZoneOffset": -28800000, - "routeType": 3, - "routeId": "headway-1080:100230", - "interlineWithPreviousLeg": false, - "tripBlockId": "7024945", - "agencyId": "headway-1080:1", - "tripId": "headway-1080:635467465", - "serviceDate": "2024-02-23", - "from": { - "name": "1st Ave S & S Hanford St", - "stopId": "headway-1080:15205", - "stopCode": "15205", - "lon": -122.334106, - "lat": 47.575924, - "arrival": 1708728745000, - "departure": 1708728745000, - "zoneId": "1", - "stopIndex": 28, - "stopSequence": 205, - "vertexType": "TRANSIT" - }, - "to": { - "name": "SODO Busway & S Lander St", - "stopId": "headway-1080:99263", - "stopCode": "99263", - "lon": -122.327614, - "lat": 47.578999, - "arrival": 1708728900000, - "departure": 1708728900000, - "zoneId": "1", - "stopIndex": 30, - "stopSequence": 214, - "vertexType": "TRANSIT" - }, - "legGeometry": { - "points": "odkaHvktiVgJ?gK???W??wDAaG@mK?yD?sH~C?", - "length": 11 - }, - "steps": [], - "routeShortName": "50", - "duration": 155.0 - }, - { - "startTime": 1708728900000, - "endTime": 1708729085000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 208.1, - "generalizedCost": 328, - "pathway": false, - "mode": "WALK", - "transitLeg": false, - "route": "", - "agencyTimeZoneOffset": -28800000, - "interlineWithPreviousLeg": false, - "from": { - "name": "SODO Busway & S Lander St", - "stopId": "headway-1080:99263", - "stopCode": "99263", - "lon": -122.327614, - "lat": 47.578999, - "arrival": 1708728900000, - "departure": 1708728900000, - "zoneId": "1", - "vertexType": "TRANSIT" - }, - "to": { - "name": "SODO", - "stopId": "headway-1080:99256", - "lon": -122.327263, - "lat": 47.580589, - "arrival": 1708729085000, - "departure": 1708729440000, - "zoneId": "C15", - "vertexType": "TRANSIT" - }, - "legGeometry": { - "points": "uwkaHrbsiVOBkB?C@C??D?D?DG?E?O?S?E?AE?EAICM?CAG?AE?mA?E?C???_@??E?C?K?C?EAKF?", - "length": 33 - }, - "steps": [ - { - "distance": 120.15, - "relativeDirection": "DEPART", - "streetName": "sidewalk", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3276323, - "lat": 47.5790782, - "elevation": "", - "walkingBike": false - }, - { - "distance": 71.66, - "relativeDirection": "LEFT", - "streetName": "sidewalk", - "absoluteDirection": "NORTH", - "stayOn": true, - "area": false, - "bogusName": true, - "lon": -122.327499, - "lat": 47.5799821, - "elevation": "", - "walkingBike": false - }, - { - "distance": 16.31, - "relativeDirection": "RIGHT", - "streetName": "path", - "absoluteDirection": "EAST", - "stayOn": true, - "area": false, - "bogusName": true, - "lon": -122.3274855, - "lat": 47.5806181, - "elevation": "", - "walkingBike": false - } - ], - "rentedBike": false, - "walkingBike": false, - "duration": 185.0 - }, - { - "startTime": 1708729440000, - "endTime": 1708729830000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 3281.57, - "generalizedCost": 1345, - "pathway": false, - "mode": "TRAM", - "transitLeg": true, - "route": "Northgate - Angle Lake", - "agencyName": "Sound Transit", - "agencyUrl": "https://www.soundtransit.org", - "agencyTimeZoneOffset": -28800000, - "routeColor": "28813F", - "routeType": 0, - "routeId": "headway-1080:100479", - "routeTextColor": "FFFFFF", - "interlineWithPreviousLeg": false, - "headsign": "Northgate", - "agencyId": "headway-1080:40", - "tripId": "headway-1080:LLR_2024-02-05_Dec13_Winter2023-2024_Rev_Weekday_100479_1071", - "serviceDate": "2024-02-23", - "from": { - "name": "SODO", - "stopId": "headway-1080:99256", - "lon": -122.327263, - "lat": 47.580589, - "arrival": 1708729085000, - "departure": 1708729440000, - "zoneId": "C15", - "stopIndex": 8, - "stopSequence": 9, - "vertexType": "TRANSIT" - }, - "to": { - "name": "University St", - "stopId": "headway-1080:40-565", - "lon": -122.336166, - "lat": 47.608246, - "arrival": 1708729830000, - "departure": 1708729830000, - "zoneId": "DSTT", - "stopIndex": 12, - "stopSequence": 13, - "vertexType": "TRANSIT" - }, - "legGeometry": { - "points": "salaH`asiVkj@AaA?m@?OCM?K?OCSE[Cw@Qg@EQCMCM?YA[?mC?kK???q@?g@?S@OBOBOBUF]J{JtCe@J_@DYDY@U?WA]A[EYCS?kCDKEMKIKMGqD?????cB?s@@iBBY@UDa@P]TUZy@lAq@nAeC|Dk@`AWXa@b@gCzBgA`AA@??g@b@]ZoC~BmC`CoC`CmC`CoC`CaDtC{@t@A@", - "length": 75 - }, - "steps": [], - "routeShortName": "1 Line", - "routeLongName": "Northgate - Angle Lake", - "duration": 390.0 - }, - { - "startTime": 1708729830000, - "endTime": 1708729878000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 62.77, - "generalizedCost": 96, - "pathway": false, - "mode": "WALK", - "transitLeg": false, - "route": "", - "agencyTimeZoneOffset": -28800000, - "interlineWithPreviousLeg": false, - "from": { - "name": "University St", - "stopId": "headway-1080:40-565", - "lon": -122.336166, - "lat": 47.608246, - "arrival": 1708729830000, - "departure": 1708729830000, - "zoneId": "DSTT", - "vertexType": "TRANSIT" - }, - "to": { - "name": "3rd Ave & Union St", - "stopId": "headway-1080:1-570", - "stopCode": "570", - "lon": -122.3367, - "lat": 47.608688, - "arrival": 1708729878000, - "departure": 1708730100000, - "zoneId": "21", - "vertexType": "TRANSIT" - }, - "legGeometry": { - "points": "onqaH`xtiVDLaBtABD", - "length": 4 - }, - "steps": [ - { - "distance": 62.77, - "relativeDirection": "DEPART", - "streetName": "sidewalk", - "absoluteDirection": "NORTHWEST", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.336233, - "lat": 47.6082187, - "elevation": "", - "walkingBike": false - } - ], - "rentedBike": false, - "walkingBike": false, - "duration": 48.0 - }, - { - "startTime": 1708730100000, - "endTime": 1708731240000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 5258.6, - "generalizedCost": 1962, - "pathway": false, - "mode": "BUS", - "transitLeg": true, - "agencyName": "Metro Transit", - "agencyUrl": "https://kingcounty.gov/en/dept/metro", - "agencyTimeZoneOffset": -28800000, - "routeType": 3, - "routeId": "headway-1080:102574", - "interlineWithPreviousLeg": false, - "tripBlockId": "7026221", - "agencyId": "headway-1080:1", - "tripId": "headway-1080:628199155", - "serviceDate": "2024-02-23", - "from": { - "name": "3rd Ave & Union St", - "stopId": "headway-1080:1-570", - "stopCode": "570", - "lon": -122.3367, - "lat": 47.608688, - "arrival": 1708729878000, - "departure": 1708730100000, - "zoneId": "21", - "stopIndex": 3, - "stopSequence": 27, - "vertexType": "TRANSIT" - }, - "to": { - "name": "Fremont Ave N & N 34th St", - "stopId": "headway-1080:26860", - "stopCode": "26860", - "lon": -122.349678, - "lat": 47.64986, - "arrival": 1708731240000, - "departure": 1708731240000, - "zoneId": "1", - "stopIndex": 16, - "stopSequence": 146, - "vertexType": "TRANSIT" - }, - "legGeometry": { - "points": "aqqaH~{tiVSRgErDwChCm@j@gA`A??cA`AIFGFGHIPMTOVKRO`@aB`DkBvD??k@jAyCdGaAwAaAsAcAwAw@iAKM_AsA{@kA??EIcAuAaAuAcAuAc@m@]e@aAuAKIM[GSAYa@B}@J??w@HiFAiFAaE???g@?gFCmGA]?mB?S@UDC@??SHWJmAv@mBrAIHEJEJCLAXcA?M?QDQJwAhAwBvASLu@b@g@Pm@Le@@cA?g@???mIAcA?e@AYCOEOAQCk@Q??cBe@_@GYKg@KeAGcGD??cD@_C?y@@iARq@Ps@\\YL??{DfBk@VuAr@{DtBgB`A_FlCo@d@i@b@cBxAsAbAa@\\W`@Sf@A@??eAnCuEvO??Qh@eBnEUDcC?aD?}@Cq@?eF?e@Bw@B", - "length": 132 - }, - "steps": [], - "alerts": [ - { - "alertHeaderText": "Northgate Park & Ride D Partial Closure for TOD Construction – Dec. 22, 2023", - "alertDescriptionText": "The northern section of Northgate Park & Ride D is scheduled to permanently close in December as part of a transit-oriented development (TOD) project that will build affordable housing next to transit service. In addition to housing, the development will also include retail space serving the Northgate community. \r\n\nMore information about the Northgate TOD project and how you can access alternate parking can be found at Metro’s website: Northgate Transit-Oriented Development Project - King County, Washington.\r\n\nTo prepare the site for construction, transit customers will no longer be able to park in the northern section (north of entrance off Third Avenue Northeast) of Northgate Park & Ride D after December 22. Any vehicle parked in the construction area after December 22th is subject to being towed at the owner’s expense.\r\n\nMake sure to move your vehicle by that date to avoid a tow. The remaining southern section of Northgate Park & Ride D is accessible through the south entrance off Northeast 100th Street.\r\n", - "alertUrl": "https://kingcounty.gov/en/dept/metro/projects/transit-oriented-communities/northgate#toc-alternative-travel-options", - "effectiveStartDate": 1702587060000, - "effectiveEndDate": 1711969140000 - } - ], - "routeShortName": "40", - "duration": 1140.0 - }, - { - "startTime": 1708731240000, - "endTime": 1708731646000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 424.88, - "generalizedCost": 741, - "pathway": false, - "mode": "WALK", - "transitLeg": false, - "route": "", - "agencyTimeZoneOffset": -28800000, - "interlineWithPreviousLeg": false, - "from": { - "name": "Fremont Ave N & N 34th St", - "stopId": "headway-1080:26860", - "stopCode": "26860", - "lon": -122.349678, - "lat": 47.64986, - "arrival": 1708731240000, - "departure": 1708731240000, - "zoneId": "1", - "vertexType": "TRANSIT" - }, - "to": { - "name": "Destination", - "lon": -122.347234, - "lat": 47.651048, - "arrival": 1708731646000, - "vertexType": "NORMAL" - }, - "legGeometry": { - "points": "sryaHnlwiV?E_@B_ADM@C?M@RgCHu@B_@JwAR}B@QDa@oA?qA?@I@QKAA?QDc@@?DL?\\?", - "length": 25 - }, - "steps": [ - { - "distance": 71.5, - "relativeDirection": "DEPART", - "streetName": "sidewalk", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3496494, - "lat": 47.6498612, - "elevation": "", - "walkingBike": false - }, - { - "distance": 187.68, - "relativeDirection": "RIGHT", - "streetName": "North 35th Street", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3497184, - "lat": 47.6505025, - "elevation": "", - "walkingBike": false - }, - { - "distance": 89.88, - "relativeDirection": "LEFT", - "streetName": "Troll Avenue North", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3472726, - "lat": 47.6501363, - "elevation": "", - "walkingBike": false - }, - { - "distance": 11.21, - "relativeDirection": "RIGHT", - "streetName": "North 36th Street", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.347277, - "lat": 47.6509446, - "elevation": "", - "walkingBike": false - }, - { - "distance": 38.2, - "relativeDirection": "LEFT", - "streetName": "path", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3471309, - "lat": 47.6509228, - "elevation": "", - "walkingBike": false - }, - { - "distance": 2.36, - "relativeDirection": "LEFT", - "streetName": "path", - "absoluteDirection": "WEST", - "stayOn": true, - "area": false, - "bogusName": true, - "lon": -122.3471612, - "lat": 47.6512641, - "elevation": "", - "walkingBike": false - }, - { - "distance": 24.05, - "relativeDirection": "LEFT", - "streetName": "sidewalk", - "absoluteDirection": "SOUTH", - "stayOn": true, - "area": false, - "bogusName": true, - "lon": -122.3471927, - "lat": 47.6512642, - "elevation": "", - "walkingBike": false - } - ], - "rentedBike": false, - "walkingBike": false, - "duration": 406.0 - } - ], - "tooSloped": false, - "arrivedAtDestinationWithRentedBicycle": false - }, - { - "duration": 3409, - "startTime": 1708728237000, - "endTime": 1708731646000, - "walkTime": 1657, - "transitTime": 1530, - "waitingTime": 222, - "walkDistance": 1957.54, - "walkLimitExceeded": false, - "generalizedCost": 6039, - "elevationLost": 0.0, - "elevationGained": 0.0, - "transfers": 1, - "fare": { - "fare": { - "regular": { - "cents": 550, - "currency": { - "currency": "USD", - "defaultFractionDigits": 2, - "currencyCode": "USD", - "symbol": "$" - } - } - }, - "details": { - "regular": [ - { - "fareId": "headway-1080:31", - "price": { - "cents": 275, - "currency": { - "currency": "USD", - "defaultFractionDigits": 2, - "currencyCode": "USD", - "symbol": "$" - } - }, - "routes": [ - "headway-1080:100479" - ] - }, - { - "fareId": "headway-1080:31", - "price": { - "cents": 275, - "currency": { - "currency": "USD", - "defaultFractionDigits": 2, - "currencyCode": "USD", - "symbol": "$" - } - }, - "routes": [ - "headway-1080:102574" - ] - } - ] - } - }, - "legs": [ - { - "startTime": 1708728237000, - "endTime": 1708729440000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 1469.89, - "generalizedCost": 2249, - "pathway": false, - "mode": "WALK", - "transitLeg": false, - "route": "", - "agencyTimeZoneOffset": -28800000, - "interlineWithPreviousLeg": false, - "from": { - "name": "Origin", - "lon": -122.339414, - "lat": 47.575837, - "departure": 1708728237000, - "vertexType": "NORMAL" - }, - "to": { - "name": "SODO", - "stopId": "headway-1080:99256", - "lon": -122.327263, - "lat": 47.580589, - "arrival": 1708729440000, - "departure": 1708729440000, - "zoneId": "C15", - "vertexType": "TRANSIT" - }, - "legGeometry": { - "points": "}ckaHbkuiV`@??[?qDE??C?G?A?C?aB?_@?G?M?I?w@D??IAW?K@SAG?I@C?c@?y@?_A?gA?a@?C?U?K?_CCE@c@?]q@?K?{BAaA@_B?m@AQCMAABA@y@?_@?a@?a@?_@?aB?qA?AAAAA?W@Q?CA@C?EBeA?sT?CACCA?E?Q?Q?C?K?Q?sAAG?C?e@?a@AC@C?A@C?C?oB?eBAE?EAICM?CAG?AE?mA?E?C???_@??E?C?K?C?EAKF?", - "length": 103 - }, - "steps": [ - { - "distance": 19.14, - "relativeDirection": "DEPART", - "streetName": "East Marginal Way South", - "absoluteDirection": "SOUTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3392164, - "lat": 47.5758355, - "elevation": "", - "walkingBike": false - }, - { - "distance": 170.43, - "relativeDirection": "LEFT", - "streetName": "path", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3392192, - "lat": 47.5756634, - "elevation": "", - "walkingBike": false - }, - { - "distance": 225.05, - "relativeDirection": "RIGHT", - "streetName": "path", - "absoluteDirection": "SOUTH", - "stayOn": true, - "area": false, - "bogusName": true, - "lon": -122.3370064, - "lat": 47.5756971, - "elevation": "", - "walkingBike": false - }, - { - "distance": 472.93, - "relativeDirection": "LEFT", - "streetName": "sidewalk", - "absoluteDirection": "NORTH", - "stayOn": true, - "area": false, - "bogusName": true, - "lon": -122.3340553, - "lat": 47.5756785, - "elevation": "", - "walkingBike": false - }, - { - "distance": 494.41, - "relativeDirection": "RIGHT", - "streetName": "sidewalk", - "absoluteDirection": "NORTHEAST", - "stayOn": true, - "area": false, - "bogusName": true, - "lon": -122.3340378, - "lat": 47.5799193, - "elevation": "", - "walkingBike": false - }, - { - "distance": 71.66, - "relativeDirection": "LEFT", - "streetName": "sidewalk", - "absoluteDirection": "NORTH", - "stayOn": true, - "area": false, - "bogusName": true, - "lon": -122.327499, - "lat": 47.5799821, - "elevation": "", - "walkingBike": false - }, - { - "distance": 16.31, - "relativeDirection": "RIGHT", - "streetName": "path", - "absoluteDirection": "EAST", - "stayOn": true, - "area": false, - "bogusName": true, - "lon": -122.3274855, - "lat": 47.5806181, - "elevation": "", - "walkingBike": false - } - ], - "rentedBike": false, - "walkingBike": false, - "duration": 1203.0 - }, - { - "startTime": 1708729440000, - "endTime": 1708729830000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 3281.57, - "generalizedCost": 990, - "pathway": false, - "mode": "TRAM", - "transitLeg": true, - "route": "Northgate - Angle Lake", - "agencyName": "Sound Transit", - "agencyUrl": "https://www.soundtransit.org", - "agencyTimeZoneOffset": -28800000, - "routeColor": "28813F", - "routeType": 0, - "routeId": "headway-1080:100479", - "routeTextColor": "FFFFFF", - "interlineWithPreviousLeg": false, - "headsign": "Northgate", - "agencyId": "headway-1080:40", - "tripId": "headway-1080:LLR_2024-02-05_Dec13_Winter2023-2024_Rev_Weekday_100479_1071", - "serviceDate": "2024-02-23", - "from": { - "name": "SODO", - "stopId": "headway-1080:99256", - "lon": -122.327263, - "lat": 47.580589, - "arrival": 1708729440000, - "departure": 1708729440000, - "zoneId": "C15", - "stopIndex": 8, - "stopSequence": 9, - "vertexType": "TRANSIT" - }, - "to": { - "name": "University St", - "stopId": "headway-1080:40-565", - "lon": -122.336166, - "lat": 47.608246, - "arrival": 1708729830000, - "departure": 1708729830000, - "zoneId": "DSTT", - "stopIndex": 12, - "stopSequence": 13, - "vertexType": "TRANSIT" - }, - "legGeometry": { - "points": "salaH`asiVkj@AaA?m@?OCM?K?OCSE[Cw@Qg@EQCMCM?YA[?mC?kK???q@?g@?S@OBOBOBUF]J{JtCe@J_@DYDY@U?WA]A[EYCS?kCDKEMKIKMGqD?????cB?s@@iBBY@UDa@P]TUZy@lAq@nAeC|Dk@`AWXa@b@gCzBgA`AA@??g@b@]ZoC~BmC`CoC`CmC`CoC`CaDtC{@t@A@", - "length": 75 - }, - "steps": [], - "routeShortName": "1 Line", - "routeLongName": "Northgate - Angle Lake", - "duration": 390.0 - }, - { - "startTime": 1708729830000, - "endTime": 1708729878000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 62.77, - "generalizedCost": 96, - "pathway": false, - "mode": "WALK", - "transitLeg": false, - "route": "", - "agencyTimeZoneOffset": -28800000, - "interlineWithPreviousLeg": false, - "from": { - "name": "University St", - "stopId": "headway-1080:40-565", - "lon": -122.336166, - "lat": 47.608246, - "arrival": 1708729830000, - "departure": 1708729830000, - "zoneId": "DSTT", - "vertexType": "TRANSIT" - }, - "to": { - "name": "3rd Ave & Union St", - "stopId": "headway-1080:1-570", - "stopCode": "570", - "lon": -122.3367, - "lat": 47.608688, - "arrival": 1708729878000, - "departure": 1708730100000, - "zoneId": "21", - "vertexType": "TRANSIT" - }, - "legGeometry": { - "points": "onqaH`xtiVDLaBtABD", - "length": 4 - }, - "steps": [ - { - "distance": 62.77, - "relativeDirection": "DEPART", - "streetName": "sidewalk", - "absoluteDirection": "NORTHWEST", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.336233, - "lat": 47.6082187, - "elevation": "", - "walkingBike": false - } - ], - "rentedBike": false, - "walkingBike": false, - "duration": 48.0 - }, - { - "startTime": 1708730100000, - "endTime": 1708731240000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 5258.6, - "generalizedCost": 1962, - "pathway": false, - "mode": "BUS", - "transitLeg": true, - "agencyName": "Metro Transit", - "agencyUrl": "https://kingcounty.gov/en/dept/metro", - "agencyTimeZoneOffset": -28800000, - "routeType": 3, - "routeId": "headway-1080:102574", - "interlineWithPreviousLeg": false, - "tripBlockId": "7026221", - "agencyId": "headway-1080:1", - "tripId": "headway-1080:628199155", - "serviceDate": "2024-02-23", - "from": { - "name": "3rd Ave & Union St", - "stopId": "headway-1080:1-570", - "stopCode": "570", - "lon": -122.3367, - "lat": 47.608688, - "arrival": 1708729878000, - "departure": 1708730100000, - "zoneId": "21", - "stopIndex": 3, - "stopSequence": 27, - "vertexType": "TRANSIT" - }, - "to": { - "name": "Fremont Ave N & N 34th St", - "stopId": "headway-1080:26860", - "stopCode": "26860", - "lon": -122.349678, - "lat": 47.64986, - "arrival": 1708731240000, - "departure": 1708731240000, - "zoneId": "1", - "stopIndex": 16, - "stopSequence": 146, - "vertexType": "TRANSIT" - }, - "legGeometry": { - "points": "aqqaH~{tiVSRgErDwChCm@j@gA`A??cA`AIFGFGHIPMTOVKRO`@aB`DkBvD??k@jAyCdGaAwAaAsAcAwAw@iAKM_AsA{@kA??EIcAuAaAuAcAuAc@m@]e@aAuAKIM[GSAYa@B}@J??w@HiFAiFAaE???g@?gFCmGA]?mB?S@UDC@??SHWJmAv@mBrAIHEJEJCLAXcA?M?QDQJwAhAwBvASLu@b@g@Pm@Le@@cA?g@???mIAcA?e@AYCOEOAQCk@Q??cBe@_@GYKg@KeAGcGD??cD@_C?y@@iARq@Ps@\\YL??{DfBk@VuAr@{DtBgB`A_FlCo@d@i@b@cBxAsAbAa@\\W`@Sf@A@??eAnCuEvO??Qh@eBnEUDcC?aD?}@Cq@?eF?e@Bw@B", - "length": 132 - }, - "steps": [], - "alerts": [ - { - "alertHeaderText": "Northgate Park & Ride D Partial Closure for TOD Construction – Dec. 22, 2023", - "alertDescriptionText": "The northern section of Northgate Park & Ride D is scheduled to permanently close in December as part of a transit-oriented development (TOD) project that will build affordable housing next to transit service. In addition to housing, the development will also include retail space serving the Northgate community. \r\n\nMore information about the Northgate TOD project and how you can access alternate parking can be found at Metro’s website: Northgate Transit-Oriented Development Project - King County, Washington.\r\n\nTo prepare the site for construction, transit customers will no longer be able to park in the northern section (north of entrance off Third Avenue Northeast) of Northgate Park & Ride D after December 22. Any vehicle parked in the construction area after December 22th is subject to being towed at the owner’s expense.\r\n\nMake sure to move your vehicle by that date to avoid a tow. The remaining southern section of Northgate Park & Ride D is accessible through the south entrance off Northeast 100th Street.\r\n", - "alertUrl": "https://kingcounty.gov/en/dept/metro/projects/transit-oriented-communities/northgate#toc-alternative-travel-options", - "effectiveStartDate": 1702587060000, - "effectiveEndDate": 1711969140000 - } - ], - "routeShortName": "40", - "duration": 1140.0 - }, - { - "startTime": 1708731240000, - "endTime": 1708731646000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 424.88, - "generalizedCost": 741, - "pathway": false, - "mode": "WALK", - "transitLeg": false, - "route": "", - "agencyTimeZoneOffset": -28800000, - "interlineWithPreviousLeg": false, - "from": { - "name": "Fremont Ave N & N 34th St", - "stopId": "headway-1080:26860", - "stopCode": "26860", - "lon": -122.349678, - "lat": 47.64986, - "arrival": 1708731240000, - "departure": 1708731240000, - "zoneId": "1", - "vertexType": "TRANSIT" - }, - "to": { - "name": "Destination", - "lon": -122.347234, - "lat": 47.651048, - "arrival": 1708731646000, - "vertexType": "NORMAL" - }, - "legGeometry": { - "points": "sryaHnlwiV?E_@B_ADM@C?M@RgCHu@B_@JwAR}B@QDa@oA?qA?@I@QKAA?QDc@@?DL?\\?", - "length": 25 - }, - "steps": [ - { - "distance": 71.5, - "relativeDirection": "DEPART", - "streetName": "sidewalk", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3496494, - "lat": 47.6498612, - "elevation": "", - "walkingBike": false - }, - { - "distance": 187.68, - "relativeDirection": "RIGHT", - "streetName": "North 35th Street", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3497184, - "lat": 47.6505025, - "elevation": "", - "walkingBike": false - }, - { - "distance": 89.88, - "relativeDirection": "LEFT", - "streetName": "Troll Avenue North", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3472726, - "lat": 47.6501363, - "elevation": "", - "walkingBike": false - }, - { - "distance": 11.21, - "relativeDirection": "RIGHT", - "streetName": "North 36th Street", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.347277, - "lat": 47.6509446, - "elevation": "", - "walkingBike": false - }, - { - "distance": 38.2, - "relativeDirection": "LEFT", - "streetName": "path", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3471309, - "lat": 47.6509228, - "elevation": "", - "walkingBike": false - }, - { - "distance": 2.36, - "relativeDirection": "LEFT", - "streetName": "path", - "absoluteDirection": "WEST", - "stayOn": true, - "area": false, - "bogusName": true, - "lon": -122.3471612, - "lat": 47.6512641, - "elevation": "", - "walkingBike": false - }, - { - "distance": 24.05, - "relativeDirection": "LEFT", - "streetName": "sidewalk", - "absoluteDirection": "SOUTH", - "stayOn": true, - "area": false, - "bogusName": true, - "lon": -122.3471927, - "lat": 47.6512642, - "elevation": "", - "walkingBike": false - } - ], - "rentedBike": false, - "walkingBike": false, - "duration": 406.0 - } - ], - "tooSloped": false, - "arrivedAtDestinationWithRentedBicycle": false - }, - { - "duration": 2530, - "startTime": 1708729318000, - "endTime": 1708731848000, - "walkTime": 560, - "transitTime": 1970, - "waitingTime": 0, - "walkDistance": 646.21, - "walkLimitExceeded": false, - "generalizedCost": 3590, - "elevationLost": 0.0, - "elevationGained": 0.0, - "transfers": 0, - "fare": { - "fare": { - "regular": { - "cents": 550, - "currency": { - "currency": "USD", - "defaultFractionDigits": 2, - "currencyCode": "USD", - "symbol": "$" - } - } - }, - "details": { - "regular": [ - { - "fareId": "headway-1080:31", - "price": { - "cents": 275, - "currency": { - "currency": "USD", - "defaultFractionDigits": 2, - "currencyCode": "USD", - "symbol": "$" - } - }, - "routes": [ - "headway-1080:100101" - ] - }, - { - "fareId": "headway-1080:31", - "price": { - "cents": 275, - "currency": { - "currency": "USD", - "defaultFractionDigits": 2, - "currencyCode": "USD", - "symbol": "$" - } - }, - "routes": [ - "headway-1080:100229" - ] - } - ] - } - }, - "legs": [ - { - "startTime": 1708729318000, - "endTime": 1708729690000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 441.92, - "generalizedCost": 683, - "pathway": false, - "mode": "WALK", - "transitLeg": false, - "route": "", - "agencyTimeZoneOffset": -28800000, - "interlineWithPreviousLeg": false, - "from": { - "name": "Origin", - "lon": -122.339414, - "lat": 47.575837, - "departure": 1708729318000, - "vertexType": "NORMAL" - }, - "to": { - "name": "1st Ave S & S Hanford St", - "stopId": "headway-1080:15205", - "stopCode": "15205", - "lon": -122.334106, - "lat": 47.575924, - "arrival": 1708729690000, - "departure": 1708729690000, - "zoneId": "1", - "vertexType": "TRANSIT" - }, - "legGeometry": { - "points": "}ckaHbkuiV`@??[?qDE??C?G?A?C?aB?_@?G?M?I?w@D??IAW?K@SAG?I@C?c@?y@?_A?gA?a@?C?U?K?_CCE@c@?]q@??H", - "length": 37 - }, - "steps": [ - { - "distance": 19.14, - "relativeDirection": "DEPART", - "streetName": "East Marginal Way South", - "absoluteDirection": "SOUTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3392164, - "lat": 47.5758355, - "elevation": "", - "walkingBike": false - }, - { - "distance": 170.43, - "relativeDirection": "LEFT", - "streetName": "path", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3392192, - "lat": 47.5756634, - "elevation": "", - "walkingBike": false - }, - { - "distance": 225.05, - "relativeDirection": "RIGHT", - "streetName": "path", - "absoluteDirection": "SOUTH", - "stayOn": true, - "area": false, - "bogusName": true, - "lon": -122.3370064, - "lat": 47.5756971, - "elevation": "", - "walkingBike": false - }, - { - "distance": 27.28, - "relativeDirection": "LEFT", - "streetName": "sidewalk", - "absoluteDirection": "NORTH", - "stayOn": true, - "area": false, - "bogusName": true, - "lon": -122.3340553, - "lat": 47.5756785, - "elevation": "", - "walkingBike": false - } - ], - "rentedBike": false, - "walkingBike": false, - "duration": 372.0 - }, - { - "startTime": 1708729690000, - "endTime": 1708730100000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 2322.8, - "generalizedCost": 1010, - "pathway": false, - "mode": "BUS", - "transitLeg": true, - "agencyName": "Metro Transit", - "agencyUrl": "https://kingcounty.gov/en/dept/metro", - "agencyTimeZoneOffset": -28800000, - "routeType": 3, - "routeId": "headway-1080:100101", - "interlineWithPreviousLeg": false, - "tripBlockId": "7026124", - "headsign": "Downtown Seattle Via 35th Ave SW", - "agencyId": "headway-1080:1", - "tripId": "headway-1080:605082385", - "serviceDate": "2024-02-23", - "from": { - "name": "1st Ave S & S Hanford St", - "stopId": "headway-1080:15205", - "stopCode": "15205", - "lon": -122.334106, - "lat": 47.575924, - "arrival": 1708729690000, - "departure": 1708729690000, - "zoneId": "1", - "stopIndex": 26, - "stopSequence": 183, - "vertexType": "TRANSIT" - }, - "to": { - "name": "4th Ave S & S Royal Brougham Way", - "stopId": "headway-1080:30635", - "stopCode": "30635", - "lon": -122.328957, - "lat": 47.593342, - "arrival": 1708730100000, - "departure": 1708730100000, - "zoneId": "1", - "stopIndex": 32, - "stopSequence": 218, - "vertexType": "TRANSIT" - }, - "legGeometry": { - "points": "odkaHvktiVgJ?gK???W?aL?cA???}I?s@???qJ?sA???uI?yK?@oC???g@?_AEyB?gB?cA?mC?cCAgBCiBEMACCCCAECI?{FAc@D[RQ?Y?W?G?sD?", - "length": 41 - }, - "steps": [], - "routeShortName": "21", - "duration": 410.0 - }, - { - "startTime": 1708730100000, - "endTime": 1708731660000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 7187.77, - "generalizedCost": 1560, - "pathway": false, - "mode": "BUS", - "transitLeg": true, - "agencyName": "Metro Transit", - "agencyUrl": "https://kingcounty.gov/en/dept/metro", - "agencyTimeZoneOffset": -28800000, - "routeType": 3, - "routeId": "headway-1080:100229", - "interlineWithPreviousLeg": true, - "tripBlockId": "7026124", - "headsign": "Shoreline Greenwood", - "agencyId": "headway-1080:1", - "tripId": "headway-1080:571900135", - "serviceDate": "2024-02-23", - "from": { - "name": "4th Ave S & S Royal Brougham Way", - "stopId": "headway-1080:30635", - "stopCode": "30635", - "lon": -122.328957, - "lat": 47.593342, - "arrival": 1708730100000, - "departure": 1708730100000, - "zoneId": "1", - "stopIndex": 0, - "stopSequence": 1, - "vertexType": "TRANSIT" - }, - "to": { - "name": "Aurora Ave N & N 38th St", - "stopId": "headway-1080:6340", - "stopCode": "6340", - "lon": -122.346619, - "lat": 47.652473, - "arrival": 1708731660000, - "departure": 1708731660000, - "zoneId": "1", - "stopIndex": 15, - "stopSequence": 181, - "vertexType": "TRANSIT" - }, - "legGeometry": { - "points": "kqnaHpksiVmD?e@?_L?}ECoDCqCAYLY@c@B[CIA??QCUMeDBuAtAILKJEJGLo@rBM^GPIHGLa@\\gB~A??_@\\oC`C]ZoBbBoCbCuBlB??WTmC`CoC~BmCbCqDbD??SRgErDwChCm@j@gA`A??cA`AIFGFGHIPMTOVKRO`@aB`DkBvD??k@jAyCdGyCbGiAtB??oAjCaAsAaAwAcAuAy@kAIIaCkDa@?wDZkB@??}BD]IsA?uB?_@?{@C_@As@CY???E?O?K?MBODM@KDMBOFOJIFKDODOFO@O@M?OAUEWG]M]IQEQEWC]E_@E]A_@GcC@[?o@?}CGcAEiAAoBEaBCsCA{@???mGAeJAi@???qDCcFAgCC]?Q?QBO?O@QBOBQDMBMBOFODQJOFMHSHiCbB??IBwCjBsAx@]RMFCB??YN[No@\\m@Va@R[Nc@Ri@Pi@Rs@Te@Nw@T_Cp@SD??}@ReATYHUDYDQDY@_AFo@Dg@Bq@?q@?eNB}Y@i@IYIYKQKOIi@c@US{@?", - "length": 195 - }, - "steps": [], - "routeShortName": "5", - "duration": 1560.0 - }, - { - "startTime": 1708731660000, - "endTime": 1708731848000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 204.29, - "generalizedCost": 335, - "pathway": false, - "mode": "WALK", - "transitLeg": false, - "route": "", - "agencyTimeZoneOffset": -28800000, - "interlineWithPreviousLeg": false, - "from": { - "name": "Aurora Ave N & N 38th St", - "stopId": "headway-1080:6340", - "stopCode": "6340", - "lon": -122.346619, - "lat": 47.652473, - "arrival": 1708731660000, - "departure": 1708731660000, - "zoneId": "1", - "vertexType": "TRANSIT" - }, - "to": { - "name": "Destination", - "lon": -122.347234, - "lat": 47.651048, - "arrival": 1708731848000, - "vertexType": "NORMAL" - }, - "legGeometry": { - "points": "}bzaHjyviVG??A@q@FILTFNNRLNHEJHFHt@l@d@V\\JNBNBB?L?\\?", - "length": 20 - }, - "steps": [ - { - "distance": 25.71, - "relativeDirection": "DEPART", - "streetName": "sidewalk", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3466171, - "lat": 47.6525198, - "elevation": "", - "walkingBike": false - }, - { - "distance": 40.3, - "relativeDirection": "HARD_RIGHT", - "streetName": "Bridge Way North", - "absoluteDirection": "SOUTHWEST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3463029, - "lat": 47.6524649, - "elevation": "", - "walkingBike": false - }, - { - "distance": 6.19, - "relativeDirection": "LEFT", - "streetName": "Winslow Place North", - "absoluteDirection": "SOUTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3466732, - "lat": 47.6522039, - "elevation": "", - "walkingBike": false - }, - { - "distance": 132.1, - "relativeDirection": "RIGHT", - "streetName": "path", - "absoluteDirection": "SOUTHWEST", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3466476, - "lat": 47.652151, - "elevation": "", - "walkingBike": false - } - ], - "rentedBike": false, - "walkingBike": false, - "duration": 188.0 - } - ], - "tooSloped": false, - "arrivedAtDestinationWithRentedBicycle": false - }, - { - "duration": 2694, - "startTime": 1708729394000, - "endTime": 1708732088000, - "walkTime": 793, - "transitTime": 1445, - "waitingTime": 456, - "walkDistance": 917.08, - "walkLimitExceeded": false, - "generalizedCost": 5146, - "elevationLost": 0.0, - "elevationGained": 0.0, - "transfers": 2, - "fare": { - "fare": { - "regular": { - "cents": 825, - "currency": { - "currency": "USD", - "defaultFractionDigits": 2, - "currencyCode": "USD", - "symbol": "$" - } - } - }, - "details": { - "regular": [ - { - "fareId": "headway-1080:31", - "price": { - "cents": 275, - "currency": { - "currency": "USD", - "defaultFractionDigits": 2, - "currencyCode": "USD", - "symbol": "$" - } - }, - "routes": [ - "headway-1080:100230" - ] - }, - { - "fareId": "headway-1080:31", - "price": { - "cents": 275, - "currency": { - "currency": "USD", - "defaultFractionDigits": 2, - "currencyCode": "USD", - "symbol": "$" - } - }, - "routes": [ - "headway-1080:100479" - ] - }, - { - "fareId": "headway-1080:31", - "price": { - "cents": 275, - "currency": { - "currency": "USD", - "defaultFractionDigits": 2, - "currencyCode": "USD", - "symbol": "$" - } - }, - "routes": [ - "headway-1080:100169" - ] - } - ] - } - }, - "legs": [ - { - "startTime": 1708729394000, - "endTime": 1708729766000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 441.92, - "generalizedCost": 683, - "pathway": false, - "mode": "WALK", - "transitLeg": false, - "route": "", - "agencyTimeZoneOffset": -28800000, - "interlineWithPreviousLeg": false, - "from": { - "name": "Origin", - "lon": -122.339414, - "lat": 47.575837, - "departure": 1708729394000, - "vertexType": "NORMAL" - }, - "to": { - "name": "1st Ave S & S Hanford St", - "stopId": "headway-1080:15205", - "stopCode": "15205", - "lon": -122.334106, - "lat": 47.575924, - "arrival": 1708729766000, - "departure": 1708729766000, - "zoneId": "1", - "vertexType": "TRANSIT" - }, - "legGeometry": { - "points": "}ckaHbkuiV`@??[?qDE??C?G?A?C?aB?_@?G?M?I?w@D??IAW?K@SAG?I@C?c@?y@?_A?gA?a@?C?U?K?_CCE@c@?]q@??H", - "length": 37 - }, - "steps": [ - { - "distance": 19.14, - "relativeDirection": "DEPART", - "streetName": "East Marginal Way South", - "absoluteDirection": "SOUTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3392164, - "lat": 47.5758355, - "elevation": "", - "walkingBike": false - }, - { - "distance": 170.43, - "relativeDirection": "LEFT", - "streetName": "path", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3392192, - "lat": 47.5756634, - "elevation": "", - "walkingBike": false - }, - { - "distance": 225.05, - "relativeDirection": "RIGHT", - "streetName": "path", - "absoluteDirection": "SOUTH", - "stayOn": true, - "area": false, - "bogusName": true, - "lon": -122.3370064, - "lat": 47.5756971, - "elevation": "", - "walkingBike": false - }, - { - "distance": 27.28, - "relativeDirection": "LEFT", - "streetName": "sidewalk", - "absoluteDirection": "NORTH", - "stayOn": true, - "area": false, - "bogusName": true, - "lon": -122.3340553, - "lat": 47.5756785, - "elevation": "", - "walkingBike": false - } - ], - "rentedBike": false, - "walkingBike": false, - "duration": 372.0 - }, - { - "startTime": 1708729766000, - "endTime": 1708729921000, - "departureDelay": 61, - "arrivalDelay": 61, - "realTime": true, - "distance": 1021.6, - "generalizedCost": 755, - "pathway": false, - "mode": "BUS", - "transitLeg": true, - "agencyName": "Metro Transit", - "agencyUrl": "https://kingcounty.gov/en/dept/metro", - "agencyTimeZoneOffset": -28800000, - "routeType": 3, - "routeId": "headway-1080:100230", - "interlineWithPreviousLeg": false, - "tripBlockId": "7024947", - "agencyId": "headway-1080:1", - "tripId": "headway-1080:635467745", - "serviceDate": "2024-02-23", - "from": { - "name": "1st Ave S & S Hanford St", - "stopId": "headway-1080:15205", - "stopCode": "15205", - "lon": -122.334106, - "lat": 47.575924, - "arrival": 1708729766000, - "departure": 1708729766000, - "zoneId": "1", - "stopIndex": 28, - "stopSequence": 205, - "vertexType": "TRANSIT" - }, - "to": { - "name": "SODO Busway & S Lander St", - "stopId": "headway-1080:99263", - "stopCode": "99263", - "lon": -122.327614, - "lat": 47.578999, - "arrival": 1708729921000, - "departure": 1708729921000, - "zoneId": "1", - "stopIndex": 30, - "stopSequence": 214, - "vertexType": "TRANSIT" - }, - "legGeometry": { - "points": "odkaHvktiVgJ?gK???W??wDAaG@mK?yD?sH~C?", - "length": 11 - }, - "steps": [], - "routeShortName": "50", - "duration": 155.0 - }, - { - "startTime": 1708729921000, - "endTime": 1708730106000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 208.1, - "generalizedCost": 328, - "pathway": false, - "mode": "WALK", - "transitLeg": false, - "route": "", - "agencyTimeZoneOffset": -28800000, - "interlineWithPreviousLeg": false, - "from": { - "name": "SODO Busway & S Lander St", - "stopId": "headway-1080:99263", - "stopCode": "99263", - "lon": -122.327614, - "lat": 47.578999, - "arrival": 1708729921000, - "departure": 1708729921000, - "zoneId": "1", - "vertexType": "TRANSIT" - }, - "to": { - "name": "SODO", - "stopId": "headway-1080:99256", - "lon": -122.327263, - "lat": 47.580589, - "arrival": 1708730106000, - "departure": 1708730400000, - "zoneId": "C15", - "vertexType": "TRANSIT" - }, - "legGeometry": { - "points": "uwkaHrbsiVOBkB?C@C??D?D?DG?E?O?S?E?AE?EAICM?CAG?AE?mA?E?C???_@??E?C?K?C?EAKF?", - "length": 33 - }, - "steps": [ - { - "distance": 120.15, - "relativeDirection": "DEPART", - "streetName": "sidewalk", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3276323, - "lat": 47.5790782, - "elevation": "", - "walkingBike": false - }, - { - "distance": 71.66, - "relativeDirection": "LEFT", - "streetName": "sidewalk", - "absoluteDirection": "NORTH", - "stayOn": true, - "area": false, - "bogusName": true, - "lon": -122.327499, - "lat": 47.5799821, - "elevation": "", - "walkingBike": false - }, - { - "distance": 16.31, - "relativeDirection": "RIGHT", - "streetName": "path", - "absoluteDirection": "EAST", - "stayOn": true, - "area": false, - "bogusName": true, - "lon": -122.3274855, - "lat": 47.5806181, - "elevation": "", - "walkingBike": false - } - ], - "rentedBike": false, - "walkingBike": false, - "duration": 185.0 - }, - { - "startTime": 1708730400000, - "endTime": 1708730790000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 3281.57, - "generalizedCost": 1284, - "pathway": false, - "mode": "TRAM", - "transitLeg": true, - "route": "Northgate - Angle Lake", - "agencyName": "Sound Transit", - "agencyUrl": "https://www.soundtransit.org", - "agencyTimeZoneOffset": -28800000, - "routeColor": "28813F", - "routeType": 0, - "routeId": "headway-1080:100479", - "routeTextColor": "FFFFFF", - "interlineWithPreviousLeg": false, - "headsign": "Northgate", - "agencyId": "headway-1080:40", - "tripId": "headway-1080:LLR_2024-02-05_Dec13_Winter2023-2024_Rev_Weekday_100479_1074", - "serviceDate": "2024-02-23", - "from": { - "name": "SODO", - "stopId": "headway-1080:99256", - "lon": -122.327263, - "lat": 47.580589, - "arrival": 1708730106000, - "departure": 1708730400000, - "zoneId": "C15", - "stopIndex": 8, - "stopSequence": 9, - "vertexType": "TRANSIT" - }, - "to": { - "name": "University St", - "stopId": "headway-1080:40-565", - "lon": -122.336166, - "lat": 47.608246, - "arrival": 1708730790000, - "departure": 1708730790000, - "zoneId": "DSTT", - "stopIndex": 12, - "stopSequence": 13, - "vertexType": "TRANSIT" - }, - "legGeometry": { - "points": "salaH`asiVkj@AaA?m@?OCM?K?OCSE[Cw@Qg@EQCMCM?YA[?mC?kK???q@?g@?S@OBOBOBUF]J{JtCe@J_@DYDY@U?WA]A[EYCS?kCDKEMKIKMGqD?????cB?s@@iBBY@UDa@P]TUZy@lAq@nAeC|Dk@`AWXa@b@gCzBgA`AA@??g@b@]ZoC~BmC`CoC`CmC`CoC`CaDtC{@t@A@", - "length": 75 - }, - "steps": [], - "routeShortName": "1 Line", - "routeLongName": "Northgate - Angle Lake", - "duration": 390.0 - }, - { - "startTime": 1708730790000, - "endTime": 1708730838000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 62.77, - "generalizedCost": 96, - "pathway": false, - "mode": "WALK", - "transitLeg": false, - "route": "", - "agencyTimeZoneOffset": -28800000, - "interlineWithPreviousLeg": false, - "from": { - "name": "University St", - "stopId": "headway-1080:40-565", - "lon": -122.336166, - "lat": 47.608246, - "arrival": 1708730790000, - "departure": 1708730790000, - "zoneId": "DSTT", - "vertexType": "TRANSIT" - }, - "to": { - "name": "3rd Ave & Union St", - "stopId": "headway-1080:1-570", - "stopCode": "570", - "lon": -122.3367, - "lat": 47.608688, - "arrival": 1708730838000, - "departure": 1708731000000, - "zoneId": "21", - "vertexType": "TRANSIT" - }, - "legGeometry": { - "points": "onqaH`xtiVDLaBtABD", - "length": 4 - }, - "steps": [ - { - "distance": 62.77, - "relativeDirection": "DEPART", - "streetName": "sidewalk", - "absoluteDirection": "NORTHWEST", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.336233, - "lat": 47.6082187, - "elevation": "", - "walkingBike": false - } - ], - "rentedBike": false, - "walkingBike": false, - "duration": 48.0 - }, - { - "startTime": 1708731000000, - "endTime": 1708731900000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 5296.18, - "generalizedCost": 1662, - "pathway": false, - "mode": "BUS", - "transitLeg": true, - "agencyName": "Metro Transit", - "agencyUrl": "https://kingcounty.gov/en/dept/metro", - "agencyTimeZoneOffset": -28800000, - "routeType": 3, - "routeId": "headway-1080:100169", - "interlineWithPreviousLeg": false, - "tripShortName": "EXPRESS", - "tripBlockId": "7028751", - "headsign": "Carkeek Park", - "agencyId": "headway-1080:1", - "tripId": "headway-1080:473772225", - "serviceDate": "2024-02-23", - "from": { - "name": "3rd Ave & Union St", - "stopId": "headway-1080:1-570", - "stopCode": "570", - "lon": -122.3367, - "lat": 47.608688, - "arrival": 1708730838000, - "departure": 1708731000000, - "zoneId": "21", - "stopIndex": 4, - "stopSequence": 38, - "vertexType": "TRANSIT" - }, - "to": { - "name": "Aurora Ave N & N 38th St", - "stopId": "headway-1080:6340", - "stopCode": "6340", - "lon": -122.346619, - "lat": 47.652473, - "arrival": 1708731900000, - "departure": 1708731900000, - "zoneId": "1", - "stopIndex": 12, - "stopSequence": 178, - "vertexType": "TRANSIT" - }, - "legGeometry": { - "points": "aqqaH~{tiVSRgErDwChCm@j@gA`A??cA`AIFGFGHIPMTOVKRO`@aB`DkBvD??k@jAyCdGyCbGiAtB??oAjCaAsAaAwAcAuAy@kAIIaCkDa@?wDZkB@??}BD]IsA?uB?_@?{@C_@As@CY???E?O?K?MBODM@KDMBOFOJIFKDODOFO@O@M?OAUEWG]M]IQEQEWC]E_@E]A_@GcC@[?o@?}CGcAEiAAoBEaBCsCAiIAeJAi@???qDCcFAgCC]?Q?QBO?O@QBOBQDMBMBOFODQJOFMHSHsCfBwCjBsAx@]RMFCB??YN[No@\\m@Va@R[Nc@Ri@Pi@Rs@Te@Nw@T_Cp@qAXeATYHUDYDQDY@_AFo@Dg@Bq@?q@?eNB}Y@i@IYIYKQKOIi@c@US{@?", - "length": 148 - }, - "steps": [], - "routeShortName": "28", - "duration": 900.0 - }, - { - "startTime": 1708731900000, - "endTime": 1708732088000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 204.29, - "generalizedCost": 335, - "pathway": false, - "mode": "WALK", - "transitLeg": false, - "route": "", - "agencyTimeZoneOffset": -28800000, - "interlineWithPreviousLeg": false, - "from": { - "name": "Aurora Ave N & N 38th St", - "stopId": "headway-1080:6340", - "stopCode": "6340", - "lon": -122.346619, - "lat": 47.652473, - "arrival": 1708731900000, - "departure": 1708731900000, - "zoneId": "1", - "vertexType": "TRANSIT" - }, - "to": { - "name": "Destination", - "lon": -122.347234, - "lat": 47.651048, - "arrival": 1708732088000, - "vertexType": "NORMAL" - }, - "legGeometry": { - "points": "}bzaHjyviVG??A@q@FILTFNNRLNHEJHFHt@l@d@V\\JNBNBB?L?\\?", - "length": 20 - }, - "steps": [ - { - "distance": 25.71, - "relativeDirection": "DEPART", - "streetName": "sidewalk", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3466171, - "lat": 47.6525198, - "elevation": "", - "walkingBike": false - }, - { - "distance": 40.3, - "relativeDirection": "HARD_RIGHT", - "streetName": "Bridge Way North", - "absoluteDirection": "SOUTHWEST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3463029, - "lat": 47.6524649, - "elevation": "", - "walkingBike": false - }, - { - "distance": 6.19, - "relativeDirection": "LEFT", - "streetName": "Winslow Place North", - "absoluteDirection": "SOUTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3466732, - "lat": 47.6522039, - "elevation": "", - "walkingBike": false - }, - { - "distance": 132.1, - "relativeDirection": "RIGHT", - "streetName": "path", - "absoluteDirection": "SOUTHWEST", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3466476, - "lat": 47.652151, - "elevation": "", - "walkingBike": false - } - ], - "rentedBike": false, - "walkingBike": false, - "duration": 188.0 - } - ], - "tooSloped": false, - "arrivedAtDestinationWithRentedBicycle": false - }, - { - "duration": 2530, - "startTime": 1708730218000, - "endTime": 1708732748000, - "walkTime": 560, - "transitTime": 1970, - "waitingTime": 0, - "walkDistance": 646.21, - "walkLimitExceeded": false, - "generalizedCost": 3590, - "elevationLost": 0.0, - "elevationGained": 0.0, - "transfers": 0, - "fare": { - "fare": { - "regular": { - "cents": 550, - "currency": { - "currency": "USD", - "defaultFractionDigits": 2, - "currencyCode": "USD", - "symbol": "$" - } - } - }, - "details": { - "regular": [ - { - "fareId": "headway-1080:31", - "price": { - "cents": 275, - "currency": { - "currency": "USD", - "defaultFractionDigits": 2, - "currencyCode": "USD", - "symbol": "$" - } - }, - "routes": [ - "headway-1080:100101" - ] - }, - { - "fareId": "headway-1080:31", - "price": { - "cents": 275, - "currency": { - "currency": "USD", - "defaultFractionDigits": 2, - "currencyCode": "USD", - "symbol": "$" - } - }, - "routes": [ - "headway-1080:100229" - ] - } - ] - } - }, - "legs": [ - { - "startTime": 1708730218000, - "endTime": 1708730590000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 441.92, - "generalizedCost": 683, - "pathway": false, - "mode": "WALK", - "transitLeg": false, - "route": "", - "agencyTimeZoneOffset": -28800000, - "interlineWithPreviousLeg": false, - "from": { - "name": "Origin", - "lon": -122.339414, - "lat": 47.575837, - "departure": 1708730218000, - "vertexType": "NORMAL" - }, - "to": { - "name": "1st Ave S & S Hanford St", - "stopId": "headway-1080:15205", - "stopCode": "15205", - "lon": -122.334106, - "lat": 47.575924, - "arrival": 1708730590000, - "departure": 1708730590000, - "zoneId": "1", - "vertexType": "TRANSIT" - }, - "legGeometry": { - "points": "}ckaHbkuiV`@??[?qDE??C?G?A?C?aB?_@?G?M?I?w@D??IAW?K@SAG?I@C?c@?y@?_A?gA?a@?C?U?K?_CCE@c@?]q@??H", - "length": 37 - }, - "steps": [ - { - "distance": 19.14, - "relativeDirection": "DEPART", - "streetName": "East Marginal Way South", - "absoluteDirection": "SOUTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3392164, - "lat": 47.5758355, - "elevation": "", - "walkingBike": false - }, - { - "distance": 170.43, - "relativeDirection": "LEFT", - "streetName": "path", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3392192, - "lat": 47.5756634, - "elevation": "", - "walkingBike": false - }, - { - "distance": 225.05, - "relativeDirection": "RIGHT", - "streetName": "path", - "absoluteDirection": "SOUTH", - "stayOn": true, - "area": false, - "bogusName": true, - "lon": -122.3370064, - "lat": 47.5756971, - "elevation": "", - "walkingBike": false - }, - { - "distance": 27.28, - "relativeDirection": "LEFT", - "streetName": "sidewalk", - "absoluteDirection": "NORTH", - "stayOn": true, - "area": false, - "bogusName": true, - "lon": -122.3340553, - "lat": 47.5756785, - "elevation": "", - "walkingBike": false - } - ], - "rentedBike": false, - "walkingBike": false, - "duration": 372.0 - }, - { - "startTime": 1708730590000, - "endTime": 1708731000000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 2322.8, - "generalizedCost": 1010, - "pathway": false, - "mode": "BUS", - "transitLeg": true, - "agencyName": "Metro Transit", - "agencyUrl": "https://kingcounty.gov/en/dept/metro", - "agencyTimeZoneOffset": -28800000, - "routeType": 3, - "routeId": "headway-1080:100101", - "interlineWithPreviousLeg": false, - "tripBlockId": "7026129", - "headsign": "Downtown Seattle Via 35th Ave SW", - "agencyId": "headway-1080:1", - "tripId": "headway-1080:605084275", - "serviceDate": "2024-02-23", - "from": { - "name": "1st Ave S & S Hanford St", - "stopId": "headway-1080:15205", - "stopCode": "15205", - "lon": -122.334106, - "lat": 47.575924, - "arrival": 1708730590000, - "departure": 1708730590000, - "zoneId": "1", - "stopIndex": 28, - "stopSequence": 194, - "vertexType": "TRANSIT" - }, - "to": { - "name": "4th Ave S & S Royal Brougham Way", - "stopId": "headway-1080:30635", - "stopCode": "30635", - "lon": -122.328957, - "lat": 47.593342, - "arrival": 1708731000000, - "departure": 1708731000000, - "zoneId": "1", - "stopIndex": 34, - "stopSequence": 229, - "vertexType": "TRANSIT" - }, - "legGeometry": { - "points": "odkaHvktiVgJ?gK???W?aL?cA???}I?s@???qJ?sA???uI?yK?@oC???g@?_AEyB?gB?cA?mC?cCAgBCiBEMACCCCAECI?{FAc@D[RQ?Y?W?G?sD?", - "length": 41 - }, - "steps": [], - "routeShortName": "21", - "duration": 410.0 - }, - { - "startTime": 1708731000000, - "endTime": 1708732560000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 7187.77, - "generalizedCost": 1560, - "pathway": false, - "mode": "BUS", - "transitLeg": true, - "agencyName": "Metro Transit", - "agencyUrl": "https://kingcounty.gov/en/dept/metro", - "agencyTimeZoneOffset": -28800000, - "routeType": 3, - "routeId": "headway-1080:100229", - "interlineWithPreviousLeg": true, - "tripBlockId": "7026129", - "headsign": "Shoreline Greenwood", - "agencyId": "headway-1080:1", - "tripId": "headway-1080:571899765", - "serviceDate": "2024-02-23", - "from": { - "name": "4th Ave S & S Royal Brougham Way", - "stopId": "headway-1080:30635", - "stopCode": "30635", - "lon": -122.328957, - "lat": 47.593342, - "arrival": 1708731000000, - "departure": 1708731000000, - "zoneId": "1", - "stopIndex": 0, - "stopSequence": 1, - "vertexType": "TRANSIT" - }, - "to": { - "name": "Aurora Ave N & N 38th St", - "stopId": "headway-1080:6340", - "stopCode": "6340", - "lon": -122.346619, - "lat": 47.652473, - "arrival": 1708732560000, - "departure": 1708732560000, - "zoneId": "1", - "stopIndex": 15, - "stopSequence": 181, - "vertexType": "TRANSIT" - }, - "legGeometry": { - "points": "kqnaHpksiVmD?e@?_L?}ECoDCqCAYLY@c@B[CIA??QCUMeDBuAtAILKJEJGLo@rBM^GPIHGLa@\\gB~A??_@\\oC`C]ZoBbBoCbCuBlB??WTmC`CoC~BmCbCqDbD??SRgErDwChCm@j@gA`A??cA`AIFGFGHIPMTOVKRO`@aB`DkBvD??k@jAyCdGyCbGiAtB??oAjCaAsAaAwAcAuAy@kAIIaCkDa@?wDZkB@??}BD]IsA?uB?_@?{@C_@As@CY???E?O?K?MBODM@KDMBOFOJIFKDODOFO@O@M?OAUEWG]M]IQEQEWC]E_@E]A_@GcC@[?o@?}CGcAEiAAoBEaBCsCA{@???mGAeJAi@???qDCcFAgCC]?Q?QBO?O@QBOBQDMBMBOFODQJOFMHSHiCbB??IBwCjBsAx@]RMFCB??YN[No@\\m@Va@R[Nc@Ri@Pi@Rs@Te@Nw@T_Cp@SD??}@ReATYHUDYDQDY@_AFo@Dg@Bq@?q@?eNB}Y@i@IYIYKQKOIi@c@US{@?", - "length": 195 - }, - "steps": [], - "routeShortName": "5", - "duration": 1560.0 - }, - { - "startTime": 1708732560000, - "endTime": 1708732748000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 204.29, - "generalizedCost": 335, - "pathway": false, - "mode": "WALK", - "transitLeg": false, - "route": "", - "agencyTimeZoneOffset": -28800000, - "interlineWithPreviousLeg": false, - "from": { - "name": "Aurora Ave N & N 38th St", - "stopId": "headway-1080:6340", - "stopCode": "6340", - "lon": -122.346619, - "lat": 47.652473, - "arrival": 1708732560000, - "departure": 1708732560000, - "zoneId": "1", - "vertexType": "TRANSIT" - }, - "to": { - "name": "Destination", - "lon": -122.347234, - "lat": 47.651048, - "arrival": 1708732748000, - "vertexType": "NORMAL" - }, - "legGeometry": { - "points": "}bzaHjyviVG??A@q@FILTFNNRLNHEJHFHt@l@d@V\\JNBNBB?L?\\?", - "length": 20 - }, - "steps": [ - { - "distance": 25.71, - "relativeDirection": "DEPART", - "streetName": "sidewalk", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3466171, - "lat": 47.6525198, - "elevation": "", - "walkingBike": false - }, - { - "distance": 40.3, - "relativeDirection": "HARD_RIGHT", - "streetName": "Bridge Way North", - "absoluteDirection": "SOUTHWEST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3463029, - "lat": 47.6524649, - "elevation": "", - "walkingBike": false - }, - { - "distance": 6.19, - "relativeDirection": "LEFT", - "streetName": "Winslow Place North", - "absoluteDirection": "SOUTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3466732, - "lat": 47.6522039, - "elevation": "", - "walkingBike": false - }, - { - "distance": 132.1, - "relativeDirection": "RIGHT", - "streetName": "path", - "absoluteDirection": "SOUTHWEST", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3466476, - "lat": 47.652151, - "elevation": "", - "walkingBike": false - } - ], - "rentedBike": false, - "walkingBike": false, - "duration": 188.0 - } - ], - "tooSloped": false, - "arrivedAtDestinationWithRentedBicycle": false - } - ] - }, - "metadata": { - "searchWindowUsed": 3000, - "nextDateTime": 1708731060000, - "prevDateTime": 1708725124228 - }, - "previousPageCursor": "rO0ABXc1AQANUFJFVklPVVNfUEFHRQfNMoSAAAAAAAALuAAXU1RSRUVUX0FORF9BUlJJVkFMX1RJTUU=", - "nextPageCursor": "rO0ABXcxAQAJTkVYVF9QQUdFB81JtIAAAAAAAAu4ABdTVFJFRVRfQU5EX0FSUklWQUxfVElNRQ==", - "debugOutput": { - "precalculationTime": 62518, - "directStreetRouterTime": 60983128, - "transitRouterTime": 84317964, - "filteringTime": 1332211, - "renderingTime": 1315008, - "totalTime": 148039092, - "transitRouterTimes": { - "tripPatternFilterTime": 8395242, - "accessEgressTime": 35250562, - "raptorSearchTime": 38373709, - "itineraryCreationTime": 2247816 - } - }, - "elevationMetadata": { - "ellipsoidToGeoidDifference": -19.32009447665611, - "geoidElevation": false - } -} diff --git a/services/travelmux/tests/fixtures/requests/opentripplanner_plan_transit_with_bike.json b/services/travelmux/tests/fixtures/requests/opentripplanner_plan_transit_with_bike.json deleted file mode 100644 index 0f7e4eac4..000000000 --- a/services/travelmux/tests/fixtures/requests/opentripplanner_plan_transit_with_bike.json +++ /dev/null @@ -1,2666 +0,0 @@ -{ - "requestParameters": { - "mode": "TRANSIT,BICYCLE", - "fromPlace": "47.575837,-122.339414", - "toPlace": "47.651048,-122.347234", - "numItineraries": "5" - }, - "plan": { - "date": 1708728099191, - "from": { - "name": "Origin", - "lon": -122.339414, - "lat": 47.575837, - "vertexType": "NORMAL" - }, - "to": { - "name": "Destination", - "lon": -122.347234, - "lat": 47.651048, - "vertexType": "NORMAL" - }, - "itineraries": [ - { - "duration": 3047, - "startTime": 1708728099000, - "endTime": 1708731146000, - "walkTime": 3047, - "transitTime": 0, - "waitingTime": 0, - "walkDistance": 10402.04, - "walkLimitExceeded": false, - "generalizedCost": 6865, - "elevationLost": 0.0, - "elevationGained": 0.0, - "transfers": 0, - "fare": { - "fare": {}, - "details": {} - }, - "legs": [ - { - "startTime": 1708728099000, - "endTime": 1708731146000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 10402.04, - "generalizedCost": 6865, - "pathway": false, - "mode": "BICYCLE", - "transitLeg": false, - "route": "", - "agencyTimeZoneOffset": -28800000, - "interlineWithPreviousLeg": false, - "from": { - "name": "Origin", - "lon": -122.339414, - "lat": 47.575837, - "departure": 1708728099000, - "vertexType": "NORMAL" - }, - "to": { - "name": "Destination", - "lon": -122.347234, - "lat": 47.651048, - "arrival": 1708731146000, - "vertexType": "NORMAL" - }, - "legGeometry": { - "points": "}ckaHbkuiV`@?J@?L?bA?n@?H?Pa@?}PCcRFq@@}W?{BCe@EYGUM[Um@y@a@g@KQEGq@w@uAiAa@UYQa@Q{EgBk@Q_@MOEsA_@DI]GGE?GSA]Eg@IyAc@YIkAWe@SeA_@y@QKC_AO[ASAcAGaACaCAqA@mEGgA?_@?iEBGAAC?K?QG?I?I?a@@GD_@?_ABm@?G?K@_EAGCIC_@?G@Y?U@I@M@OBG@UDWJ[NKFEHURQNABCBMJA@AgA?s@?sAHB?Q?iC?kB?Q?S?K?}A?K?Q?qA?KE??m@Ac@?]?]?C@m@?M?E?cB?e@@qA?O?Q?c@?OAs@?CAEa@w@OWW]MEk@d@WR_Av@m@j@SPMLuBjBGFSNML[XgA`AKHKJuBjBIHQJqBfBMJMLsBhBKJSPgB~AQNONoBdBMJQPeDvCMLSNgDzCMLQN}CpCSPSPcDvCKJWf@IR_CzEA@IPKTw@|Ai@fAYj@EHKNINCBEHERcBhDKHEHIPMVADKVqB`EINGIQWq@aAa@i@MSCCG??AMQIMEEyAuBCEEEMIMKACGKk@w@m@{@EEIOEEEGEGIKqAiBKOOUGIEGaAsAAAMIMIGCg@GuC?E?OMS?eECM?Q?Y?oDAO?O?gEAO?Q?sB?y@Au@?W?_@AQ?eBAW?U?mB?c@?]?_@?E?Qk@SAG?CCIBC?C?Y@M@MBKBIBC@C@ACKGEACAaBfASLEBC@C?EBGBEBMH[RMLCDGDIFA?KD[LWLc@JSBYDY@CBG@E@GBE@K?G?C?G?E?GECAEEC?cB?M@I@I?o@?QAuC?o@AK@E?ODG@GAC?GAECGCKKECIIGCCAgCo@i@OGAOEICaBOI?]?kB?UBu@@mD@qA@G?cB@C@MHG@[Dc@FOBMDOD]LYJSJKDc@RG@M@G@uCvAIBGDOL_@PMRCBOFCBOBG@ULiAl@[Ry@b@EDCFCFEDIDmAn@E@C@OAC@EB]PWLIFKFGBG@y@d@[NEDq@h@eA|@aAt@u@l@}@t@STSXOVGNO`@Sj@a@hACF@BHF@FBFBP?FADgCfIeAjDIVENGPQb@Yb@i@r@UXIFMHMDGBG@M@W@i@@k@@iEAICuEAEACAGG?CCEA@G@AQ?W?kA@{@B]^qEBe@G?I@iA@}A?oA?qA?@I@QKAA?QDAe@MQSCm@\\mB}@EEC??DFHt@l@d@V\\JNBNBB?L?\\?", - "length": 485 - }, - "steps": [ - { - "distance": 26.11, - "relativeDirection": "DEPART", - "streetName": "East Marginal Way South", - "absoluteDirection": "SOUTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3392164, - "lat": 47.5758355, - "elevation": "", - "walkingBike": false - }, - { - "distance": 59.45, - "relativeDirection": "RIGHT", - "streetName": "South Hanford Street", - "absoluteDirection": "WEST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3392243, - "lat": 47.5756008, - "elevation": "", - "walkingBike": false - }, - { - "distance": 1218.07, - "relativeDirection": "RIGHT", - "streetName": "East Marginal Way South", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3400167, - "lat": 47.5756059, - "elevation": "", - "walkingBike": false - }, - { - "distance": 515.99, - "relativeDirection": "CONTINUE", - "streetName": "Alaskan Way South", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3400253, - "lat": 47.58656, - "elevation": "", - "walkingBike": false - }, - { - "distance": 4.97, - "relativeDirection": "HARD_RIGHT", - "streetName": "South Atlantic Street", - "absoluteDirection": "SOUTHEAST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.337345, - "lat": 47.5907595, - "elevation": "", - "walkingBike": false - }, - { - "distance": 186.23, - "relativeDirection": "HARD_LEFT", - "streetName": "Marginal Way Cycle Route", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3372987, - "lat": 47.5907275, - "elevation": "", - "walkingBike": false - }, - { - "distance": 659.69, - "relativeDirection": "CONTINUE", - "streetName": "Portside Trail", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3367458, - "lat": 47.592328, - "elevation": "", - "walkingBike": false - }, - { - "distance": 129.47, - "relativeDirection": "LEFT", - "streetName": "Portside Trail", - "absoluteDirection": "NORTH", - "stayOn": true, - "area": false, - "bogusName": false, - "lon": -122.3360192, - "lat": 47.5981133, - "elevation": "", - "walkingBike": false - }, - { - "distance": 111.29, - "relativeDirection": "CONTINUE", - "streetName": "Marginal Way Cycle Route", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3360808, - "lat": 47.5992728, - "elevation": "", - "walkingBike": false - }, - { - "distance": 176.79, - "relativeDirection": "SLIGHTLY_RIGHT", - "streetName": "Marginal Way Cycle Route", - "absoluteDirection": "NORTH", - "stayOn": true, - "area": false, - "bogusName": true, - "lon": -122.3360595, - "lat": 47.6002712, - "elevation": "", - "walkingBike": false - }, - { - "distance": 1.96, - "relativeDirection": "CONTINUE", - "streetName": "Elliott Bay Trail", - "absoluteDirection": "NORTHWEST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3366416, - "lat": 47.6017628, - "elevation": "", - "walkingBike": false - }, - { - "distance": 77.8, - "relativeDirection": "HARD_RIGHT", - "streetName": "bike path", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3366546, - "lat": 47.6017781, - "elevation": "", - "walkingBike": false - }, - { - "distance": 5.04, - "relativeDirection": "RIGHT", - "streetName": "path", - "absoluteDirection": "SOUTH", - "stayOn": true, - "area": false, - "bogusName": true, - "lon": -122.3356171, - "lat": 47.6017831, - "elevation": "", - "walkingBike": false - }, - { - "distance": 199.61, - "relativeDirection": "LEFT", - "streetName": "Yesler Way", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.335636, - "lat": 47.6017396, - "elevation": "", - "walkingBike": false - }, - { - "distance": 4.21, - "relativeDirection": "LEFT", - "streetName": "service road", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3329736, - "lat": 47.601731, - "elevation": "", - "walkingBike": false - }, - { - "distance": 213.75, - "relativeDirection": "RIGHT", - "streetName": "Yesler Way Cycletrack", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3329762, - "lat": 47.6017688, - "elevation": "", - "walkingBike": false - }, - { - "distance": 70.44, - "relativeDirection": "SLIGHTLY_LEFT", - "streetName": "4th Avenue Cycletrack", - "absoluteDirection": "NORTHEAST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.330126, - "lat": 47.6017627, - "elevation": "", - "walkingBike": false - }, - { - "distance": 1836.22, - "relativeDirection": "LEFT", - "streetName": "4th Avenue Cycletrack", - "absoluteDirection": "NORTHWEST", - "stayOn": true, - "area": false, - "bogusName": false, - "lon": -122.3294953, - "lat": 47.6022169, - "elevation": "", - "walkingBike": false - }, - { - "distance": 94.94, - "relativeDirection": "RIGHT", - "streetName": "Bell Street", - "absoluteDirection": "NORTHEAST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3439964, - "lat": 47.6153211, - "elevation": "", - "walkingBike": false - }, - { - "distance": 375.15, - "relativeDirection": "SLIGHTLY_LEFT", - "streetName": "Bell St Cycletrack", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.343165, - "lat": 47.6159653, - "elevation": "", - "walkingBike": false - }, - { - "distance": 132.44, - "relativeDirection": "CONTINUE", - "streetName": "9th Ave Cycletrack", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3399723, - "lat": 47.6185462, - "elevation": "", - "walkingBike": false - }, - { - "distance": 768.81, - "relativeDirection": "SLIGHTLY_LEFT", - "streetName": "9th Avenue North", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.339792, - "lat": 47.6197152, - "elevation": "", - "walkingBike": false - }, - { - "distance": 19.74, - "relativeDirection": "RIGHT", - "streetName": "bike path", - "absoluteDirection": "NORTHEAST", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3397288, - "lat": 47.6266292, - "elevation": "", - "walkingBike": false - }, - { - "distance": 27.65, - "relativeDirection": "LEFT", - "streetName": "bike path", - "absoluteDirection": "NORTH", - "stayOn": true, - "area": false, - "bogusName": true, - "lon": -122.339502, - "lat": 47.6267195, - "elevation": "", - "walkingBike": false - }, - { - "distance": 47.87, - "relativeDirection": "CONTINUE", - "streetName": "Cheshiahud Lake Union Loop", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3394925, - "lat": 47.6269619, - "elevation": "", - "walkingBike": false - }, - { - "distance": 88.27, - "relativeDirection": "RIGHT", - "streetName": "Westlake Protected Bike Lane", - "absoluteDirection": "NORTHEAST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3395952, - "lat": 47.6273847, - "elevation": "", - "walkingBike": false - }, - { - "distance": 208.81, - "relativeDirection": "CONTINUE", - "streetName": "Westlake Cycle Track", - "absoluteDirection": "NORTHWEST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3399451, - "lat": 47.6280979, - "elevation": "", - "walkingBike": false - }, - { - "distance": 1768.5, - "relativeDirection": "SLIGHTLY_RIGHT", - "streetName": "Westlake Protected Bike Lane", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3407137, - "lat": 47.6298708, - "elevation": "", - "walkingBike": false - }, - { - "distance": 781.14, - "relativeDirection": "LEFT", - "streetName": "Cheshiahud Lake Union Loop", - "absoluteDirection": "SOUTHWEST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3456348, - "lat": 47.6446397, - "elevation": "", - "walkingBike": false - }, - { - "distance": 106.11, - "relativeDirection": "CONTINUE", - "streetName": "North 34th Street", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3486434, - "lat": 47.6494012, - "elevation": "", - "walkingBike": false - }, - { - "distance": 193.79, - "relativeDirection": "LEFT", - "streetName": "Troll Avenue North", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.347258, - "lat": 47.6492019, - "elevation": "", - "walkingBike": false - }, - { - "distance": 11.21, - "relativeDirection": "RIGHT", - "streetName": "North 36th Street", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.347277, - "lat": 47.6509446, - "elevation": "", - "walkingBike": false - }, - { - "distance": 18.03, - "relativeDirection": "LEFT", - "streetName": "path", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3471309, - "lat": 47.6509228, - "elevation": "", - "walkingBike": false - }, - { - "distance": 137.31, - "relativeDirection": "RIGHT", - "streetName": "path", - "absoluteDirection": "EAST", - "stayOn": true, - "area": false, - "bogusName": true, - "lon": -122.3471583, - "lat": 47.6510827, - "elevation": "", - "walkingBike": false, - "alerts": [ - { - "alertHeaderText": "Unpaved surface" - } - ] - }, - { - "distance": 125.23, - "relativeDirection": "HARD_LEFT", - "streetName": "sidewalk", - "absoluteDirection": "SOUTHWEST", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3466956, - "lat": 47.6520984, - "elevation": "", - "walkingBike": true - } - ], - "alerts": [ - { - "alertHeaderText": "Unpaved surface" - } - ], - "rentedBike": false, - "walkingBike": false, - "duration": 3047.0 - } - ], - "tooSloped": false, - "arrivedAtDestinationWithRentedBicycle": false - }, - { - "duration": 2447, - "startTime": 1708728133000, - "endTime": 1708730580000, - "walkTime": 1263, - "transitTime": 1184, - "waitingTime": 0, - "walkDistance": 3858.53, - "walkLimitExceeded": false, - "generalizedCost": 5415, - "elevationLost": 0.0, - "elevationGained": 0.0, - "transfers": 0, - "fare": { - "fare": { - "regular": { - "cents": 275, - "currency": { - "currency": "USD", - "defaultFractionDigits": 2, - "currencyCode": "USD", - "symbol": "$" - } - } - }, - "details": { - "regular": [ - { - "fareId": "headway-1080:31", - "price": { - "cents": 275, - "currency": { - "currency": "USD", - "defaultFractionDigits": 2, - "currencyCode": "USD", - "symbol": "$" - } - }, - "routes": [ - "headway-1080:100169" - ] - } - ] - } - }, - "legs": [ - { - "startTime": 1708728133000, - "endTime": 1708729216000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 3653.15, - "generalizedCost": 2851, - "pathway": false, - "mode": "BICYCLE", - "transitLeg": false, - "route": "", - "agencyTimeZoneOffset": -28800000, - "interlineWithPreviousLeg": false, - "from": { - "name": "Origin", - "lon": -122.339414, - "lat": 47.575837, - "departure": 1708728133000, - "vertexType": "NORMAL" - }, - "to": { - "name": "3rd Ave & James St", - "stopId": "headway-1080:1-531", - "stopCode": "531", - "lon": -122.331184, - "lat": 47.602642, - "arrival": 1708729216000, - "departure": 1708729216000, - "zoneId": "21", - "vertexType": "TRANSIT" - }, - "legGeometry": { - "points": "}ckaHbkuiV`@?J@?L?bA?n@?H?Pa@?}PCcRFq@@}W?{BCe@EYGUM[Um@y@a@g@KQEGq@w@uAiAa@UYQa@Q{EgBk@Q_@MOEsA_@DI]GGE?GSA]Eg@IyAc@YIkAWe@SeA_@y@QKC_AO[ASAcAGaACaCAqA@mEGgA?_@?iEBGAAC?K?QG?I?I?a@@GD_@?_ABm@?G?K@_EAGCIC_@?G@Y?U@I@M@OBG@UDWJ[NKFEHURQNABCBMJA@AgA?s@?sAHB?Q?iC?kB?Q?S?K?}A?K?Q?qA?KE??m@Ac@?]?]?C@m@?M?EABAB}ApAEDKHAEe@yAGQ[gAIQGSJI?CRS@D", - "length": 132 - }, - "steps": [ - { - "distance": 26.11, - "relativeDirection": "DEPART", - "streetName": "East Marginal Way South", - "absoluteDirection": "SOUTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3392164, - "lat": 47.5758355, - "elevation": "", - "walkingBike": false - }, - { - "distance": 59.45, - "relativeDirection": "RIGHT", - "streetName": "South Hanford Street", - "absoluteDirection": "WEST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3392243, - "lat": 47.5756008, - "elevation": "", - "walkingBike": false - }, - { - "distance": 1218.07, - "relativeDirection": "RIGHT", - "streetName": "East Marginal Way South", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3400167, - "lat": 47.5756059, - "elevation": "", - "walkingBike": false - }, - { - "distance": 515.99, - "relativeDirection": "CONTINUE", - "streetName": "Alaskan Way South", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3400253, - "lat": 47.58656, - "elevation": "", - "walkingBike": false - }, - { - "distance": 4.97, - "relativeDirection": "HARD_RIGHT", - "streetName": "South Atlantic Street", - "absoluteDirection": "SOUTHEAST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.337345, - "lat": 47.5907595, - "elevation": "", - "walkingBike": false - }, - { - "distance": 186.23, - "relativeDirection": "HARD_LEFT", - "streetName": "Marginal Way Cycle Route", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3372987, - "lat": 47.5907275, - "elevation": "", - "walkingBike": false - }, - { - "distance": 659.69, - "relativeDirection": "CONTINUE", - "streetName": "Portside Trail", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3367458, - "lat": 47.592328, - "elevation": "", - "walkingBike": false - }, - { - "distance": 129.47, - "relativeDirection": "LEFT", - "streetName": "Portside Trail", - "absoluteDirection": "NORTH", - "stayOn": true, - "area": false, - "bogusName": false, - "lon": -122.3360192, - "lat": 47.5981133, - "elevation": "", - "walkingBike": false - }, - { - "distance": 111.29, - "relativeDirection": "CONTINUE", - "streetName": "Marginal Way Cycle Route", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3360808, - "lat": 47.5992728, - "elevation": "", - "walkingBike": false - }, - { - "distance": 176.79, - "relativeDirection": "SLIGHTLY_RIGHT", - "streetName": "Marginal Way Cycle Route", - "absoluteDirection": "NORTH", - "stayOn": true, - "area": false, - "bogusName": true, - "lon": -122.3360595, - "lat": 47.6002712, - "elevation": "", - "walkingBike": false - }, - { - "distance": 1.96, - "relativeDirection": "CONTINUE", - "streetName": "Elliott Bay Trail", - "absoluteDirection": "NORTHWEST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3366416, - "lat": 47.6017628, - "elevation": "", - "walkingBike": false - }, - { - "distance": 77.8, - "relativeDirection": "HARD_RIGHT", - "streetName": "bike path", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3366546, - "lat": 47.6017781, - "elevation": "", - "walkingBike": false - }, - { - "distance": 5.04, - "relativeDirection": "RIGHT", - "streetName": "path", - "absoluteDirection": "SOUTH", - "stayOn": true, - "area": false, - "bogusName": true, - "lon": -122.3356171, - "lat": 47.6017831, - "elevation": "", - "walkingBike": false - }, - { - "distance": 199.61, - "relativeDirection": "LEFT", - "streetName": "Yesler Way", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.335636, - "lat": 47.6017396, - "elevation": "", - "walkingBike": false - }, - { - "distance": 4.21, - "relativeDirection": "LEFT", - "streetName": "service road", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3329736, - "lat": 47.601731, - "elevation": "", - "walkingBike": false - }, - { - "distance": 79.44, - "relativeDirection": "RIGHT", - "streetName": "Yesler Way Cycletrack", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3329762, - "lat": 47.6017688, - "elevation": "", - "walkingBike": false - }, - { - "distance": 3.75, - "relativeDirection": "HARD_LEFT", - "streetName": "path", - "absoluteDirection": "NORTHWEST", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3319168, - "lat": 47.6017686, - "elevation": "", - "walkingBike": false - }, - { - "distance": 72.14, - "relativeDirection": "RIGHT", - "streetName": "2nd Avenue Cycletrack", - "absoluteDirection": "NORTHWEST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3319597, - "lat": 47.6017834, - "elevation": "", - "walkingBike": false - }, - { - "distance": 99.25, - "relativeDirection": "RIGHT", - "streetName": "James Street", - "absoluteDirection": "NORTHEAST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3324476, - "lat": 47.6023425, - "elevation": "", - "walkingBike": false - }, - { - "distance": 7.09, - "relativeDirection": "RIGHT", - "streetName": "path", - "absoluteDirection": "SOUTHEAST", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3313219, - "lat": 47.6028122, - "elevation": "", - "walkingBike": false - }, - { - "distance": 14.85, - "relativeDirection": "LEFT", - "streetName": "sidewalk", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3312735, - "lat": 47.6027574, - "elevation": "", - "walkingBike": true - } - ], - "rentedBike": false, - "walkingBike": false, - "duration": 1083.0 - }, - { - "startTime": 1708729216000, - "endTime": 1708730400000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 6086.08, - "generalizedCost": 1784, - "pathway": false, - "mode": "BUS", - "transitLeg": true, - "agencyName": "Metro Transit", - "agencyUrl": "https://kingcounty.gov/en/dept/metro", - "agencyTimeZoneOffset": -28800000, - "routeType": 3, - "routeId": "headway-1080:100169", - "interlineWithPreviousLeg": false, - "tripShortName": "EXPRESS", - "tripBlockId": "7028762", - "headsign": "Carkeek Park", - "agencyId": "headway-1080:1", - "tripId": "headway-1080:473771465", - "serviceDate": "2024-02-23", - "from": { - "name": "3rd Ave & James St", - "stopId": "headway-1080:1-531", - "stopCode": "531", - "lon": -122.331184, - "lat": 47.602642, - "arrival": 1708729216000, - "departure": 1708729216000, - "zoneId": "21", - "stopIndex": 2, - "stopSequence": 27, - "vertexType": "TRANSIT" - }, - "to": { - "name": "Aurora Ave N & N 38th St", - "stopId": "headway-1080:6340", - "stopCode": "6340", - "lon": -122.346619, - "lat": 47.652473, - "arrival": 1708730400000, - "departure": 1708730400000, - "zoneId": "1", - "stopIndex": 12, - "stopSequence": 178, - "vertexType": "TRANSIT" - }, - "legGeometry": { - "points": "gkpaHlysiV_@\\oC`C]ZoBbBoCbCuBlB??WTmC`CoC~BmCbCqDbD??SRgErDwChCm@j@gA`A??cA`AIFGFGHIPMTOVKRO`@aB`DkBvD??k@jAyCdGyCbGiAtB??oAjCaAsAaAwAcAuAy@kAIIaCkDa@?wDZkB@??}BD]IsA?uB?_@?{@C_@As@CY???E?O?K?MBODM@KDMBOFOJIFKDODOFO@O@M?OAUEWG]M]IQEQEWC]E_@E]A_@GcC@[?o@?}CGcAEiAAoBEaBCsCAiIAeJAi@???qDCcFAgCC]?Q?QBO?O@QBOBQDMBMBOFODQJOFMHSHsCfBwCjBsAx@]RMFCB??YN[No@\\m@Va@R[Nc@Ri@Pi@Rs@Te@Nw@T_Cp@qAXeATYHUDYDQDY@_AFo@Dg@Bq@?q@?eNB}Y@i@IYIYKQKOIi@c@US{@?", - "length": 161 - }, - "steps": [], - "routeShortName": "28", - "duration": 1184.0 - }, - { - "startTime": 1708730400000, - "endTime": 1708730580000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 205.38, - "generalizedCost": 779, - "pathway": false, - "mode": "BICYCLE", - "transitLeg": false, - "route": "", - "agencyTimeZoneOffset": -28800000, - "interlineWithPreviousLeg": false, - "from": { - "name": "Aurora Ave N & N 38th St", - "stopId": "headway-1080:6340", - "stopCode": "6340", - "lon": -122.346619, - "lat": 47.652473, - "arrival": 1708730400000, - "departure": 1708730400000, - "zoneId": "1", - "vertexType": "TRANSIT" - }, - "to": { - "name": "Destination", - "lon": -122.347234, - "lat": 47.651048, - "arrival": 1708730580000, - "vertexType": "NORMAL" - }, - "legGeometry": { - "points": "}bzaHjyviVG??A@q@FIDGNXPZLNFDJHFHt@l@d@V\\JNBNBB?L?\\?", - "length": 20 - }, - "steps": [ - { - "distance": 30.56, - "relativeDirection": "DEPART", - "streetName": "sidewalk", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3466171, - "lat": 47.6525198, - "elevation": "", - "walkingBike": true - }, - { - "distance": 37.35, - "relativeDirection": "RIGHT", - "streetName": "sidewalk", - "absoluteDirection": "SOUTHWEST", - "stayOn": true, - "area": false, - "bogusName": true, - "lon": -122.3462617, - "lat": 47.6524313, - "elevation": "", - "walkingBike": true - }, - { - "distance": 12.25, - "relativeDirection": "CONTINUE", - "streetName": "path", - "absoluteDirection": "SOUTHWEST", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.34661, - "lat": 47.6521922, - "elevation": "", - "walkingBike": false - }, - { - "distance": 125.23, - "relativeDirection": "CONTINUE", - "streetName": "sidewalk", - "absoluteDirection": "SOUTHWEST", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3466956, - "lat": 47.6520984, - "elevation": "", - "walkingBike": true - } - ], - "rentedBike": false, - "walkingBike": false, - "duration": 180.0 - } - ], - "tooSloped": false, - "arrivedAtDestinationWithRentedBicycle": false - }, - { - "duration": 2571, - "startTime": 1708728369000, - "endTime": 1708730940000, - "walkTime": 1251, - "transitTime": 1320, - "waitingTime": 0, - "walkDistance": 3647.11, - "walkLimitExceeded": false, - "generalizedCost": 5411, - "elevationLost": 0.0, - "elevationGained": 0.0, - "transfers": 0, - "fare": { - "fare": { - "regular": { - "cents": 275, - "currency": { - "currency": "USD", - "defaultFractionDigits": 2, - "currencyCode": "USD", - "symbol": "$" - } - } - }, - "details": { - "regular": [ - { - "fareId": "headway-1080:31", - "price": { - "cents": 275, - "currency": { - "currency": "USD", - "defaultFractionDigits": 2, - "currencyCode": "USD", - "symbol": "$" - } - }, - "routes": [ - "headway-1080:100229" - ] - } - ] - } - }, - "legs": [ - { - "startTime": 1708728369000, - "endTime": 1708729440000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 3441.73, - "generalizedCost": 2710, - "pathway": false, - "mode": "BICYCLE", - "transitLeg": false, - "route": "", - "agencyTimeZoneOffset": -28800000, - "interlineWithPreviousLeg": false, - "from": { - "name": "Origin", - "lon": -122.339414, - "lat": 47.575837, - "departure": 1708728369000, - "vertexType": "NORMAL" - }, - "to": { - "name": "4th Ave S & S Jackson St", - "stopId": "headway-1080:1-619", - "stopCode": "619", - "lon": -122.328972, - "lat": 47.599827, - "arrival": 1708729440000, - "departure": 1708729440000, - "zoneId": "21", - "vertexType": "TRANSIT" - }, - "legGeometry": { - "points": "}ckaHbkuiV`@?J@?L?bA?n@?H?Pa@?}PCcRFq@@}W?{BCe@EYGUM[Um@y@a@g@KQEGq@w@uAiAa@UYQa@Q{EgBk@Q_@MOEsA_@DI]GGE?GSA]Eg@IyAc@YIkAWe@SeA_@y@QKC_AO[ASAcAGaACaCAqA@mEGgA?_@?iEBGAAC?K?QG?I?I?a@@GD_@?_ABm@?G??I?[?i@?i@AwB@mB?M?UBMBG@s@?U?E?M?W?U?cA?QK?E?A?A?iCAA?MA?M?oB?_B?e@?Q?mB?y@?i@?SDE@C@G?G?C@mB?I?{A?I?KD?B?T??G", - "length": 118 - }, - "steps": [ - { - "distance": 26.11, - "relativeDirection": "DEPART", - "streetName": "East Marginal Way South", - "absoluteDirection": "SOUTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3392164, - "lat": 47.5758355, - "elevation": "", - "walkingBike": false - }, - { - "distance": 59.45, - "relativeDirection": "RIGHT", - "streetName": "South Hanford Street", - "absoluteDirection": "WEST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3392243, - "lat": 47.5756008, - "elevation": "", - "walkingBike": false - }, - { - "distance": 1218.07, - "relativeDirection": "RIGHT", - "streetName": "East Marginal Way South", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3400167, - "lat": 47.5756059, - "elevation": "", - "walkingBike": false - }, - { - "distance": 515.99, - "relativeDirection": "CONTINUE", - "streetName": "Alaskan Way South", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3400253, - "lat": 47.58656, - "elevation": "", - "walkingBike": false - }, - { - "distance": 4.97, - "relativeDirection": "HARD_RIGHT", - "streetName": "South Atlantic Street", - "absoluteDirection": "SOUTHEAST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.337345, - "lat": 47.5907595, - "elevation": "", - "walkingBike": false - }, - { - "distance": 186.23, - "relativeDirection": "HARD_LEFT", - "streetName": "Marginal Way Cycle Route", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3372987, - "lat": 47.5907275, - "elevation": "", - "walkingBike": false - }, - { - "distance": 659.69, - "relativeDirection": "CONTINUE", - "streetName": "Portside Trail", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3367458, - "lat": 47.592328, - "elevation": "", - "walkingBike": false - }, - { - "distance": 123.17, - "relativeDirection": "LEFT", - "streetName": "Portside Trail", - "absoluteDirection": "NORTH", - "stayOn": true, - "area": false, - "bogusName": false, - "lon": -122.3360192, - "lat": 47.5981133, - "elevation": "", - "walkingBike": false - }, - { - "distance": 14.34, - "relativeDirection": "RIGHT", - "streetName": "service road", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.336079, - "lat": 47.5992162, - "elevation": "", - "walkingBike": false - }, - { - "distance": 225.92, - "relativeDirection": "CONTINUE", - "streetName": "South Jackson Street", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3358878, - "lat": 47.5992158, - "elevation": "", - "walkingBike": false - }, - { - "distance": 98.22, - "relativeDirection": "LEFT", - "streetName": "Occidental Avenue South", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3328928, - "lat": 47.5991661, - "elevation": "", - "walkingBike": false - }, - { - "distance": 191.02, - "relativeDirection": "RIGHT", - "streetName": "South Main Street", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3328797, - "lat": 47.6000494, - "elevation": "", - "walkingBike": false - }, - { - "distance": 5.75, - "relativeDirection": "RIGHT", - "streetName": "2nd Avenue Cycle Track", - "absoluteDirection": "SOUTHEAST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.330332, - "lat": 47.6000424, - "elevation": "", - "walkingBike": false - }, - { - "distance": 95.22, - "relativeDirection": "SLIGHTLY_LEFT", - "streetName": "South Main Street Cycletrack", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.330282, - "lat": 47.6000051, - "elevation": "", - "walkingBike": false - }, - { - "distance": 17.58, - "relativeDirection": "RIGHT", - "streetName": "4th Avenue South", - "absoluteDirection": "SOUTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.329014, - "lat": 47.5999853, - "elevation": "", - "walkingBike": true - } - ], - "rentedBike": false, - "walkingBike": false, - "duration": 1071.0 - }, - { - "startTime": 1708729440000, - "endTime": 1708730760000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 6464.36, - "generalizedCost": 1920, - "pathway": false, - "mode": "BUS", - "transitLeg": true, - "agencyName": "Metro Transit", - "agencyUrl": "https://kingcounty.gov/en/dept/metro", - "agencyTimeZoneOffset": -28800000, - "routeType": 3, - "routeId": "headway-1080:100229", - "interlineWithPreviousLeg": false, - "tripBlockId": "7026125", - "headsign": "Shoreline Greenwood", - "agencyId": "headway-1080:1", - "tripId": "headway-1080:571900295", - "serviceDate": "2024-02-23", - "from": { - "name": "4th Ave S & S Jackson St", - "stopId": "headway-1080:1-619", - "stopCode": "619", - "lon": -122.328972, - "lat": 47.599827, - "arrival": 1708729440000, - "departure": 1708729440000, - "zoneId": "21", - "stopIndex": 1, - "stopSequence": 12, - "vertexType": "TRANSIT" - }, - "to": { - "name": "Aurora Ave N & N 38th St", - "stopId": "headway-1080:6340", - "stopCode": "6340", - "lon": -122.346619, - "lat": 47.652473, - "arrival": 1708730760000, - "departure": 1708730760000, - "zoneId": "1", - "stopIndex": 15, - "stopSequence": 181, - "vertexType": "TRANSIT" - }, - "legGeometry": { - "points": "}yoaHtksiVQCUMeDBuAtAILKJEJGLo@rBM^GPIHGLa@\\gB~A??_@\\oC`C]ZoBbBoCbCuBlB??WTmC`CoC~BmCbCqDbD??SRgErDwChCm@j@gA`A??cA`AIFGFGHIPMTOVKRO`@aB`DkBvD??k@jAyCdGyCbGiAtB??oAjCaAsAaAwAcAuAy@kAIIaCkDa@?wDZkB@??}BD]IsA?uB?_@?{@C_@As@CY???E?O?K?MBODM@KDMBOFOJIFKDODOFO@O@M?OAUEWG]M]IQEQEWC]E_@E]A_@GcC@[?o@?}CGcAEiAAoBEaBCsCA{@???mGAeJAi@???qDCcFAgCC]?Q?QBO?O@QBOBQDMBMBOFODQJOFMHSHiCbB??IBwCjBsAx@]RMFCB??YN[No@\\m@Va@R[Nc@Ri@Pi@Rs@Te@Nw@T_Cp@SD??}@ReATYHUDYDQDY@_AFo@Dg@Bq@?q@?eNB}Y@i@IYIYKQKOIi@c@US{@?", - "length": 183 - }, - "steps": [], - "routeShortName": "5", - "duration": 1320.0 - }, - { - "startTime": 1708730760000, - "endTime": 1708730940000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 205.38, - "generalizedCost": 779, - "pathway": false, - "mode": "BICYCLE", - "transitLeg": false, - "route": "", - "agencyTimeZoneOffset": -28800000, - "interlineWithPreviousLeg": false, - "from": { - "name": "Aurora Ave N & N 38th St", - "stopId": "headway-1080:6340", - "stopCode": "6340", - "lon": -122.346619, - "lat": 47.652473, - "arrival": 1708730760000, - "departure": 1708730760000, - "zoneId": "1", - "vertexType": "TRANSIT" - }, - "to": { - "name": "Destination", - "lon": -122.347234, - "lat": 47.651048, - "arrival": 1708730940000, - "vertexType": "NORMAL" - }, - "legGeometry": { - "points": "}bzaHjyviVG??A@q@FIDGNXPZLNFDJHFHt@l@d@V\\JNBNBB?L?\\?", - "length": 20 - }, - "steps": [ - { - "distance": 30.56, - "relativeDirection": "DEPART", - "streetName": "sidewalk", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3466171, - "lat": 47.6525198, - "elevation": "", - "walkingBike": true - }, - { - "distance": 37.35, - "relativeDirection": "RIGHT", - "streetName": "sidewalk", - "absoluteDirection": "SOUTHWEST", - "stayOn": true, - "area": false, - "bogusName": true, - "lon": -122.3462617, - "lat": 47.6524313, - "elevation": "", - "walkingBike": true - }, - { - "distance": 12.25, - "relativeDirection": "CONTINUE", - "streetName": "path", - "absoluteDirection": "SOUTHWEST", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.34661, - "lat": 47.6521922, - "elevation": "", - "walkingBike": false - }, - { - "distance": 125.23, - "relativeDirection": "CONTINUE", - "streetName": "sidewalk", - "absoluteDirection": "SOUTHWEST", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3466956, - "lat": 47.6520984, - "elevation": "", - "walkingBike": true - } - ], - "rentedBike": false, - "walkingBike": false, - "duration": 180.0 - } - ], - "tooSloped": false, - "arrivedAtDestinationWithRentedBicycle": false - }, - { - "duration": 2742, - "startTime": 1708728706000, - "endTime": 1708731448000, - "walkTime": 1825, - "transitTime": 917, - "waitingTime": 0, - "walkDistance": 6601.67, - "walkLimitExceeded": false, - "generalizedCost": 6384, - "elevationLost": 0.0, - "elevationGained": 0.0, - "transfers": 0, - "fare": { - "fare": { - "regular": { - "cents": 275, - "currency": { - "currency": "USD", - "defaultFractionDigits": 2, - "currencyCode": "USD", - "symbol": "$" - } - } - }, - "details": { - "regular": [ - { - "fareId": "headway-1080:31", - "price": { - "cents": 275, - "currency": { - "currency": "USD", - "defaultFractionDigits": 2, - "currencyCode": "USD", - "symbol": "$" - } - }, - "routes": [ - "headway-1080:102576" - ] - } - ] - } - }, - "legs": [ - { - "startTime": 1708728706000, - "endTime": 1708729462000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 2893.09, - "generalizedCost": 2146, - "pathway": false, - "mode": "BICYCLE", - "transitLeg": false, - "route": "", - "agencyTimeZoneOffset": -28800000, - "interlineWithPreviousLeg": false, - "from": { - "name": "Origin", - "lon": -122.339414, - "lat": 47.575837, - "departure": 1708728706000, - "vertexType": "NORMAL" - }, - "to": { - "name": "Alaskan Way S & S Jackson St", - "stopId": "headway-1080:1561", - "stopCode": "1561", - "lon": -122.335556, - "lat": 47.599701, - "arrival": 1708729462000, - "departure": 1708729462000, - "zoneId": "21", - "vertexType": "TRANSIT" - }, - "legGeometry": { - "points": "}ckaHbkuiV`@?J@?L?bA?n@?H?Pa@?}PCcRFq@@}W?{BCe@EYGUM[Um@y@a@g@KQEGq@w@uAiAa@UYQa@Q{EgBk@Q_@MOEsA_@DI]GGE?GSA]Eg@IyAc@YIkAWe@SeA_@y@QKC_AO[ASAcAGaACaCAqA@mEGgA?_@?iEBGAAC?K?QG?I?I?a@@GD_@?_ABm@?G??I?[?i@?i@I?C?K@gA??N", - "length": 79 - }, - "steps": [ - { - "distance": 26.11, - "relativeDirection": "DEPART", - "streetName": "East Marginal Way South", - "absoluteDirection": "SOUTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3392164, - "lat": 47.5758355, - "elevation": "", - "walkingBike": false - }, - { - "distance": 59.45, - "relativeDirection": "RIGHT", - "streetName": "South Hanford Street", - "absoluteDirection": "WEST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3392243, - "lat": 47.5756008, - "elevation": "", - "walkingBike": false - }, - { - "distance": 1218.07, - "relativeDirection": "RIGHT", - "streetName": "East Marginal Way South", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3400167, - "lat": 47.5756059, - "elevation": "", - "walkingBike": false - }, - { - "distance": 515.99, - "relativeDirection": "CONTINUE", - "streetName": "Alaskan Way South", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3400253, - "lat": 47.58656, - "elevation": "", - "walkingBike": false - }, - { - "distance": 4.97, - "relativeDirection": "HARD_RIGHT", - "streetName": "South Atlantic Street", - "absoluteDirection": "SOUTHEAST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.337345, - "lat": 47.5907595, - "elevation": "", - "walkingBike": false - }, - { - "distance": 186.23, - "relativeDirection": "HARD_LEFT", - "streetName": "Marginal Way Cycle Route", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3372987, - "lat": 47.5907275, - "elevation": "", - "walkingBike": false - }, - { - "distance": 659.69, - "relativeDirection": "CONTINUE", - "streetName": "Portside Trail", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3367458, - "lat": 47.592328, - "elevation": "", - "walkingBike": false - }, - { - "distance": 123.17, - "relativeDirection": "LEFT", - "streetName": "Portside Trail", - "absoluteDirection": "NORTH", - "stayOn": true, - "area": false, - "bogusName": false, - "lon": -122.3360192, - "lat": 47.5981133, - "elevation": "", - "walkingBike": false - }, - { - "distance": 14.34, - "relativeDirection": "RIGHT", - "streetName": "service road", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.336079, - "lat": 47.5992162, - "elevation": "", - "walkingBike": false - }, - { - "distance": 31.42, - "relativeDirection": "CONTINUE", - "streetName": "South Jackson Street", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3358878, - "lat": 47.5992158, - "elevation": "", - "walkingBike": false - }, - { - "distance": 5.47, - "relativeDirection": "LEFT", - "streetName": "path", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3354687, - "lat": 47.5992185, - "elevation": "", - "walkingBike": false - }, - { - "distance": 48.2, - "relativeDirection": "CONTINUE", - "streetName": "sidewalk", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3354676, - "lat": 47.5992677, - "elevation": "", - "walkingBike": true - } - ], - "rentedBike": false, - "walkingBike": false, - "duration": 756.0 - }, - { - "startTime": 1708729462000, - "endTime": 1708730379000, - "departureDelay": -21, - "arrivalDelay": -21, - "realTime": true, - "distance": 3495.38, - "generalizedCost": 1517, - "pathway": false, - "mode": "BUS", - "transitLeg": true, - "route": "C-Line Rapid Ride", - "agencyName": "Metro Transit", - "agencyUrl": "https://kingcounty.gov/en/dept/metro", - "agencyTimeZoneOffset": -28800000, - "routeType": 3, - "routeId": "headway-1080:102576", - "interlineWithPreviousLeg": false, - "tripBlockId": "7025920", - "headsign": "South Lake Union Downtown Seattle", - "agencyId": "headway-1080:1", - "tripId": "headway-1080:635649735", - "serviceDate": "2024-02-23", - "from": { - "name": "Alaskan Way S & S Jackson St", - "stopId": "headway-1080:1561", - "stopCode": "1561", - "lon": -122.335556, - "lat": 47.599701, - "arrival": 1708729462000, - "departure": 1708729462000, - "zoneId": "21", - "stopIndex": 16, - "stopSequence": 334, - "vertexType": "TRANSIT" - }, - "to": { - "name": "Westlake Ave N & Mercer St", - "stopId": "headway-1080:26730", - "stopCode": "26730", - "lon": -122.338394, - "lat": 47.625504, - "arrival": 1708730379000, - "departure": 1708730379000, - "zoneId": "1", - "stopIndex": 25, - "stopSequence": 411, - "vertexType": "TRANSIT" - }, - "legGeometry": { - "points": "cyoaH|ttiVUCm@?k@?}@Ae@@WBg@Ne@Py@h@a@RqCzB??KJ{@uCSw@Uq@o@yBm@mBm@mBo@sBk@mBsBjB??[VmCbCmC`CgCzB??GBmCbCeEvDiDxC??]XwChCm@j@kCbCIFGFGHIPMTOVKRO`@aB`DkBvD??k@jAyCdGaAwAaAsAcAwAw@iAKM_AsA{@kA??EIcAuAaAuAcAuAc@m@]e@aAuAKIM[GSAYa@B}@J??w@HiFAiFAaE???g@?gFCmGA]?mB?S@UDC@", - "length": 86 - }, - "steps": [], - "routeShortName": "C Line", - "routeLongName": "C-Line Rapid Ride", - "duration": 917.0 - }, - { - "startTime": 1708730379000, - "endTime": 1708731448000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 3708.58, - "generalizedCost": 2720, - "pathway": false, - "mode": "BICYCLE", - "transitLeg": false, - "route": "", - "agencyTimeZoneOffset": -28800000, - "interlineWithPreviousLeg": false, - "from": { - "name": "Westlake Ave N & Mercer St", - "stopId": "headway-1080:26730", - "stopCode": "26730", - "lon": -122.338394, - "lat": 47.625504, - "arrival": 1708730379000, - "departure": 1708730379000, - "zoneId": "1", - "vertexType": "TRANSIT" - }, - "to": { - "name": "Destination", - "lon": -122.347234, - "lat": 47.651048, - "arrival": 1708731448000, - "vertexType": "NORMAL" - }, - "legGeometry": { - "points": "kztaH~euiVBPKBG@GBUJCg@MDE@CBA@ABCBu@ZINMJIDKDQ@OFKFMNUf@GFGDE@A\\?HC?Y@M@MBKBIBC@C@ACKGEACAaBfASLEBC@C?EBGBEBMH[RMLCDGDIFA?KD[LWLc@JSBYDY@CBG@E@GBE@K?G?C?G?E?GECAEEC?cB?M@I@I?o@?QAuC?o@AK@E?ODG@GAC?GAECGCKKECIIGCCAgCo@i@OGAOEICaBOI?]?kB?UBu@@mD@qA@G?cB@C@MHG@[Dc@FOBMDOD]LYJSJKDc@RG@M@G@uCvAIBGDOL_@PMRCBOFCBOBG@ULiAl@[Ry@b@EDCFCFEDIDmAn@E@C@OAC@EB]PWLIFKFGBG@y@d@[NEDq@h@eA|@aAt@u@l@}@t@STSXOVGNO`@Sj@a@hACF@BHF@FBFBP?FADgCfIeAjDIVENGPQb@Yb@i@r@UXIFMHMDGBG@M@W@i@@k@@iEAICKVU?Q?g@?e@?c@?w@@EKACIKAGCIAQ?W?kA@{@B]^qEBe@G?I@iA@}A?oA?qA?@I@QKAA?QDAe@MQSCm@\\mB}@EEC??DFHt@l@d@V\\JNBNBB?L?\\?", - "length": 251 - }, - "steps": [ - { - "distance": 28.54, - "relativeDirection": "DEPART", - "streetName": "Westlake Avenue North", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3384861, - "lat": 47.6254888, - "elevation": "", - "walkingBike": false - }, - { - "distance": 14.8, - "relativeDirection": "RIGHT", - "streetName": "Valley Street", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3385901, - "lat": 47.6257355, - "elevation": "", - "walkingBike": false - }, - { - "distance": 51.75, - "relativeDirection": "LEFT", - "streetName": "path", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3383946, - "lat": 47.6257544, - "elevation": "", - "walkingBike": false - }, - { - "distance": 100.36, - "relativeDirection": "SLIGHTLY_LEFT", - "streetName": "Cheshiahud Lake Union Loop", - "absoluteDirection": "NORTHWEST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.338649, - "lat": 47.6261803, - "elevation": "", - "walkingBike": false - }, - { - "distance": 14.54, - "relativeDirection": "LEFT", - "streetName": "open area", - "absoluteDirection": "WEST", - "stayOn": false, - "area": true, - "bogusName": true, - "lon": -122.3392982, - "lat": 47.6269383, - "elevation": "", - "walkingBike": false - }, - { - "distance": 2.25, - "relativeDirection": "RIGHT", - "streetName": "bike path", - "absoluteDirection": "NORTH", - "stayOn": true, - "area": false, - "bogusName": true, - "lon": -122.3394921, - "lat": 47.6269417, - "elevation": "", - "walkingBike": false - }, - { - "distance": 47.87, - "relativeDirection": "CONTINUE", - "streetName": "Cheshiahud Lake Union Loop", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3394925, - "lat": 47.6269619, - "elevation": "", - "walkingBike": false - }, - { - "distance": 88.27, - "relativeDirection": "RIGHT", - "streetName": "Westlake Protected Bike Lane", - "absoluteDirection": "NORTHEAST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3395952, - "lat": 47.6273847, - "elevation": "", - "walkingBike": false - }, - { - "distance": 208.81, - "relativeDirection": "CONTINUE", - "streetName": "Westlake Cycle Track", - "absoluteDirection": "NORTHWEST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3399451, - "lat": 47.6280979, - "elevation": "", - "walkingBike": false - }, - { - "distance": 1768.5, - "relativeDirection": "SLIGHTLY_RIGHT", - "streetName": "Westlake Protected Bike Lane", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3407137, - "lat": 47.6298708, - "elevation": "", - "walkingBike": false - }, - { - "distance": 575.04, - "relativeDirection": "LEFT", - "streetName": "Cheshiahud Lake Union Loop", - "absoluteDirection": "SOUTHWEST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3456348, - "lat": 47.6446397, - "elevation": "", - "walkingBike": false - }, - { - "distance": 11.44, - "relativeDirection": "LEFT", - "streetName": "Interurban Trail", - "absoluteDirection": "NORTHWEST", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3496341, - "lat": 47.6481797, - "elevation": "", - "walkingBike": false - }, - { - "distance": 44.18, - "relativeDirection": "RIGHT", - "streetName": "Fremont Bridge", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.349758, - "lat": 47.6482398, - "elevation": "", - "walkingBike": false - }, - { - "distance": 72.07, - "relativeDirection": "CONTINUE", - "streetName": "Fremont Avenue North", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3497576, - "lat": 47.6486371, - "elevation": "", - "walkingBike": false - }, - { - "distance": 88.47, - "relativeDirection": "RIGHT", - "streetName": "link", - "absoluteDirection": "NORTHEAST", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3497604, - "lat": 47.6492852, - "elevation": "", - "walkingBike": false - }, - { - "distance": 106.11, - "relativeDirection": "CONTINUE", - "streetName": "North 34th Street", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3486434, - "lat": 47.6494012, - "elevation": "", - "walkingBike": false - }, - { - "distance": 193.79, - "relativeDirection": "LEFT", - "streetName": "Troll Avenue North", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.347258, - "lat": 47.6492019, - "elevation": "", - "walkingBike": false - }, - { - "distance": 11.21, - "relativeDirection": "RIGHT", - "streetName": "North 36th Street", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.347277, - "lat": 47.6509446, - "elevation": "", - "walkingBike": false - }, - { - "distance": 18.03, - "relativeDirection": "LEFT", - "streetName": "path", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3471309, - "lat": 47.6509228, - "elevation": "", - "walkingBike": false - }, - { - "distance": 137.31, - "relativeDirection": "RIGHT", - "streetName": "path", - "absoluteDirection": "EAST", - "stayOn": true, - "area": false, - "bogusName": true, - "lon": -122.3471583, - "lat": 47.6510827, - "elevation": "", - "walkingBike": false, - "alerts": [ - { - "alertHeaderText": "Unpaved surface" - } - ] - }, - { - "distance": 125.23, - "relativeDirection": "HARD_LEFT", - "streetName": "sidewalk", - "absoluteDirection": "SOUTHWEST", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3466956, - "lat": 47.6520984, - "elevation": "", - "walkingBike": true - } - ], - "alerts": [ - { - "alertHeaderText": "Unpaved surface" - } - ], - "rentedBike": false, - "walkingBike": false, - "duration": 1069.0 - } - ], - "tooSloped": false, - "arrivedAtDestinationWithRentedBicycle": false - }, - { - "duration": 2819, - "startTime": 1708728729000, - "endTime": 1708731548000, - "walkTime": 1379, - "transitTime": 1440, - "waitingTime": 0, - "walkDistance": 4142.0, - "walkLimitExceeded": false, - "generalizedCost": 5889, - "elevationLost": 0.0, - "elevationGained": 0.0, - "transfers": 0, - "fare": { - "fare": { - "regular": { - "cents": 275, - "currency": { - "currency": "USD", - "defaultFractionDigits": 2, - "currencyCode": "USD", - "symbol": "$" - } - } - }, - "details": { - "regular": [ - { - "fareId": "headway-1080:31", - "price": { - "cents": 275, - "currency": { - "currency": "USD", - "defaultFractionDigits": 2, - "currencyCode": "USD", - "symbol": "$" - } - }, - "routes": [ - "headway-1080:102574" - ] - } - ] - } - }, - "legs": [ - { - "startTime": 1708728729000, - "endTime": 1708729800000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 3441.73, - "generalizedCost": 2710, - "pathway": false, - "mode": "BICYCLE", - "transitLeg": false, - "route": "", - "agencyTimeZoneOffset": -28800000, - "interlineWithPreviousLeg": false, - "from": { - "name": "Origin", - "lon": -122.339414, - "lat": 47.575837, - "departure": 1708728729000, - "vertexType": "NORMAL" - }, - "to": { - "name": "4th Ave S & S Jackson St", - "stopId": "headway-1080:1-619", - "stopCode": "619", - "lon": -122.328972, - "lat": 47.599827, - "arrival": 1708729800000, - "departure": 1708729800000, - "zoneId": "21", - "vertexType": "TRANSIT" - }, - "legGeometry": { - "points": "}ckaHbkuiV`@?J@?L?bA?n@?H?Pa@?}PCcRFq@@}W?{BCe@EYGUM[Um@y@a@g@KQEGq@w@uAiAa@UYQa@Q{EgBk@Q_@MOEsA_@DI]GGE?GSA]Eg@IyAc@YIkAWe@SeA_@y@QKC_AO[ASAcAGaACaCAqA@mEGgA?_@?iEBGAAC?K?QG?I?I?a@@GD_@?_ABm@?G??I?[?i@?i@AwB@mB?M?UBMBG@s@?U?E?M?W?U?cA?QK?E?A?A?iCAA?MA?M?oB?_B?e@?Q?mB?y@?i@?SDE@C@G?G?C@mB?I?{A?I?KD?B?T??G", - "length": 118 - }, - "steps": [ - { - "distance": 26.11, - "relativeDirection": "DEPART", - "streetName": "East Marginal Way South", - "absoluteDirection": "SOUTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3392164, - "lat": 47.5758355, - "elevation": "", - "walkingBike": false - }, - { - "distance": 59.45, - "relativeDirection": "RIGHT", - "streetName": "South Hanford Street", - "absoluteDirection": "WEST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3392243, - "lat": 47.5756008, - "elevation": "", - "walkingBike": false - }, - { - "distance": 1218.07, - "relativeDirection": "RIGHT", - "streetName": "East Marginal Way South", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3400167, - "lat": 47.5756059, - "elevation": "", - "walkingBike": false - }, - { - "distance": 515.99, - "relativeDirection": "CONTINUE", - "streetName": "Alaskan Way South", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3400253, - "lat": 47.58656, - "elevation": "", - "walkingBike": false - }, - { - "distance": 4.97, - "relativeDirection": "HARD_RIGHT", - "streetName": "South Atlantic Street", - "absoluteDirection": "SOUTHEAST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.337345, - "lat": 47.5907595, - "elevation": "", - "walkingBike": false - }, - { - "distance": 186.23, - "relativeDirection": "HARD_LEFT", - "streetName": "Marginal Way Cycle Route", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3372987, - "lat": 47.5907275, - "elevation": "", - "walkingBike": false - }, - { - "distance": 659.69, - "relativeDirection": "CONTINUE", - "streetName": "Portside Trail", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3367458, - "lat": 47.592328, - "elevation": "", - "walkingBike": false - }, - { - "distance": 123.17, - "relativeDirection": "LEFT", - "streetName": "Portside Trail", - "absoluteDirection": "NORTH", - "stayOn": true, - "area": false, - "bogusName": false, - "lon": -122.3360192, - "lat": 47.5981133, - "elevation": "", - "walkingBike": false - }, - { - "distance": 14.34, - "relativeDirection": "RIGHT", - "streetName": "service road", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.336079, - "lat": 47.5992162, - "elevation": "", - "walkingBike": false - }, - { - "distance": 225.92, - "relativeDirection": "CONTINUE", - "streetName": "South Jackson Street", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3358878, - "lat": 47.5992158, - "elevation": "", - "walkingBike": false - }, - { - "distance": 98.22, - "relativeDirection": "LEFT", - "streetName": "Occidental Avenue South", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3328928, - "lat": 47.5991661, - "elevation": "", - "walkingBike": false - }, - { - "distance": 191.02, - "relativeDirection": "RIGHT", - "streetName": "South Main Street", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3328797, - "lat": 47.6000494, - "elevation": "", - "walkingBike": false - }, - { - "distance": 5.75, - "relativeDirection": "RIGHT", - "streetName": "2nd Avenue Cycle Track", - "absoluteDirection": "SOUTHEAST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.330332, - "lat": 47.6000424, - "elevation": "", - "walkingBike": false - }, - { - "distance": 95.22, - "relativeDirection": "SLIGHTLY_LEFT", - "streetName": "South Main Street Cycletrack", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.330282, - "lat": 47.6000051, - "elevation": "", - "walkingBike": false - }, - { - "distance": 17.58, - "relativeDirection": "RIGHT", - "streetName": "4th Avenue South", - "absoluteDirection": "SOUTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.329014, - "lat": 47.5999853, - "elevation": "", - "walkingBike": true - } - ], - "rentedBike": false, - "walkingBike": false, - "duration": 1071.0 - }, - { - "startTime": 1708729800000, - "endTime": 1708731240000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 6426.73, - "generalizedCost": 2040, - "pathway": false, - "mode": "BUS", - "transitLeg": true, - "agencyName": "Metro Transit", - "agencyUrl": "https://kingcounty.gov/en/dept/metro", - "agencyTimeZoneOffset": -28800000, - "routeType": 3, - "routeId": "headway-1080:102574", - "interlineWithPreviousLeg": false, - "tripBlockId": "7026221", - "agencyId": "headway-1080:1", - "tripId": "headway-1080:628199155", - "serviceDate": "2024-02-23", - "from": { - "name": "4th Ave S & S Jackson St", - "stopId": "headway-1080:1-619", - "stopCode": "619", - "lon": -122.328972, - "lat": 47.599827, - "arrival": 1708729800000, - "departure": 1708729800000, - "zoneId": "21", - "stopIndex": 0, - "stopSequence": 1, - "vertexType": "TRANSIT" - }, - "to": { - "name": "Fremont Ave N & N 34th St", - "stopId": "headway-1080:26860", - "stopCode": "26860", - "lon": -122.349678, - "lat": 47.64986, - "arrival": 1708731240000, - "departure": 1708731240000, - "zoneId": "1", - "stopIndex": 16, - "stopSequence": 146, - "vertexType": "TRANSIT" - }, - "legGeometry": { - "points": "}yoaHtksiVQCUMeDBuAtAILKJEJGLo@rBM^GPIHGLa@\\gB~A??_@\\oC`C]ZoBbBoCbCuBlB??WTmC`CoC~BmCbCqDbD??SRgErDwChCm@j@gA`A??cA`AIFGFGHIPMTOVKRO`@aB`DkBvD??k@jAyCdGaAwAaAsAcAwAw@iAKM_AsA{@kA??EIcAuAaAuAcAuAc@m@]e@aAuAKIM[GSAYa@B}@J??w@HiFAiFAaE???g@?gFCmGA]?mB?S@UDC@??SHWJmAv@mBrAIHEJEJCLAXcA?M?QDQJwAhAwBvASLu@b@g@Pm@Le@@cA?g@???mIAcA?e@AYCOEOAQCk@Q??cBe@_@GYKg@KeAGcGD??cD@_C?y@@iARq@Ps@\\YL??{DfBk@VuAr@{DtBgB`A_FlCo@d@i@b@cBxAsAbAa@\\W`@Sf@A@??eAnCuEvO??Qh@eBnEUDcC?aD?}@Cq@?eF?e@Bw@B", - "length": 161 - }, - "steps": [], - "alerts": [ - { - "alertHeaderText": "Northgate Park & Ride D Partial Closure for TOD Construction – Dec. 22, 2023", - "alertDescriptionText": "The northern section of Northgate Park & Ride D is scheduled to permanently close in December as part of a transit-oriented development (TOD) project that will build affordable housing next to transit service. In addition to housing, the development will also include retail space serving the Northgate community. \r\n\nMore information about the Northgate TOD project and how you can access alternate parking can be found at Metro’s website: Northgate Transit-Oriented Development Project - King County, Washington.\r\n\nTo prepare the site for construction, transit customers will no longer be able to park in the northern section (north of entrance off Third Avenue Northeast) of Northgate Park & Ride D after December 22. Any vehicle parked in the construction area after December 22th is subject to being towed at the owner’s expense.\r\n\nMake sure to move your vehicle by that date to avoid a tow. The remaining southern section of Northgate Park & Ride D is accessible through the south entrance off Northeast 100th Street.\r\n", - "alertUrl": "https://kingcounty.gov/en/dept/metro/projects/transit-oriented-communities/northgate#toc-alternative-travel-options", - "effectiveStartDate": 1702587060000, - "effectiveEndDate": 1711969140000 - } - ], - "routeShortName": "40", - "duration": 1440.0 - }, - { - "startTime": 1708731240000, - "endTime": 1708731548000, - "departureDelay": 0, - "arrivalDelay": 0, - "realTime": false, - "distance": 700.27, - "generalizedCost": 1138, - "pathway": false, - "mode": "BICYCLE", - "transitLeg": false, - "route": "", - "agencyTimeZoneOffset": -28800000, - "interlineWithPreviousLeg": false, - "from": { - "name": "Fremont Ave N & N 34th St", - "stopId": "headway-1080:26860", - "stopCode": "26860", - "lon": -122.349678, - "lat": 47.64986, - "arrival": 1708731240000, - "departure": 1708731240000, - "zoneId": "1", - "vertexType": "TRANSIT" - }, - "to": { - "name": "Destination", - "lon": -122.347234, - "lat": 47.651048, - "arrival": 1708731548000, - "vertexType": "NORMAL" - }, - "legGeometry": { - "points": "sryaHnlwiV?EL?XCDG@?NAVyCB_@B]^qEBe@G?I@iA@}A?oA?qA?@I@QKAA?QDAe@MQSCm@\\mB}@EEC??DFHt@l@d@V\\JNBNBB?L?\\?", - "length": 40 - }, - "steps": [ - { - "distance": 28.49, - "relativeDirection": "DEPART", - "streetName": "sidewalk", - "absoluteDirection": "SOUTH", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3496494, - "lat": 47.6498612, - "elevation": "", - "walkingBike": true - }, - { - "distance": 8.45, - "relativeDirection": "SLIGHTLY_LEFT", - "streetName": "path", - "absoluteDirection": "SOUTH", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3495882, - "lat": 47.6496207, - "elevation": "", - "walkingBike": false - }, - { - "distance": 177.75, - "relativeDirection": "LEFT", - "streetName": "North 34th Street", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.3495756, - "lat": 47.6495452, - "elevation": "", - "walkingBike": false - }, - { - "distance": 193.79, - "relativeDirection": "LEFT", - "streetName": "Troll Avenue North", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.347258, - "lat": 47.6492019, - "elevation": "", - "walkingBike": false - }, - { - "distance": 11.21, - "relativeDirection": "RIGHT", - "streetName": "North 36th Street", - "absoluteDirection": "EAST", - "stayOn": false, - "area": false, - "bogusName": false, - "lon": -122.347277, - "lat": 47.6509446, - "elevation": "", - "walkingBike": false - }, - { - "distance": 18.03, - "relativeDirection": "LEFT", - "streetName": "path", - "absoluteDirection": "NORTH", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3471309, - "lat": 47.6509228, - "elevation": "", - "walkingBike": false - }, - { - "distance": 137.31, - "relativeDirection": "RIGHT", - "streetName": "path", - "absoluteDirection": "EAST", - "stayOn": true, - "area": false, - "bogusName": true, - "lon": -122.3471583, - "lat": 47.6510827, - "elevation": "", - "walkingBike": false, - "alerts": [ - { - "alertHeaderText": "Unpaved surface" - } - ] - }, - { - "distance": 125.23, - "relativeDirection": "HARD_LEFT", - "streetName": "sidewalk", - "absoluteDirection": "SOUTHWEST", - "stayOn": false, - "area": false, - "bogusName": true, - "lon": -122.3466956, - "lat": 47.6520984, - "elevation": "", - "walkingBike": true - } - ], - "alerts": [ - { - "alertHeaderText": "Unpaved surface" - } - ], - "rentedBike": false, - "walkingBike": false, - "duration": 308.0 - } - ], - "tooSloped": false, - "arrivedAtDestinationWithRentedBicycle": false - } - ] - }, - "metadata": { - "searchWindowUsed": 3000, - "nextDateTime": 1708728960000, - "prevDateTime": 1708725099191 - }, - "previousPageCursor": "rO0ABXc1AQANUFJFVklPVVNfUEFHRQfNNMOAAAAAAAAJYAAXU1RSRUVUX0FORF9BUlJJVkFMX1RJTUU=", - "nextPageCursor": "rO0ABXcxAQAJTkVYVF9QQUdFB81BgIAAAAAAAAlgABdTVFJFRVRfQU5EX0FSUklWQUxfVElNRQ==", - "debugOutput": { - "precalculationTime": 89979, - "directStreetRouterTime": 82126165, - "transitRouterTime": 633998557, - "filteringTime": 5131751, - "renderingTime": 915425, - "totalTime": 722290541, - "transitRouterTimes": { - "tripPatternFilterTime": 9894096, - "accessEgressTime": 367799412, - "raptorSearchTime": 240381510, - "itineraryCreationTime": 15870680 - } - }, - "elevationMetadata": { - "ellipsoidToGeoidDifference": -19.32009447665611, - "geoidElevation": false - } -} diff --git a/services/travelmux/tests/fixtures/requests/opentripplanner_transit_plan.json b/services/travelmux/tests/fixtures/requests/opentripplanner_transit_plan.json new file mode 100644 index 000000000..c86455f7f --- /dev/null +++ b/services/travelmux/tests/fixtures/requests/opentripplanner_transit_plan.json @@ -0,0 +1,2271 @@ +{ + "debugOutput": { + "directStreetRouterTime": 95376407, + "filteringTime": 35029087, + "precalculationTime": 1793936, + "renderingTime": 7321496, + "totalTime": 288255035, + "transitRouterTime": 148339938, + "transitRouterTimes": { + "accessEgressTime": 48354648, + "itineraryCreationTime": 27070918, + "raptorSearchTime": 60187401, + "tripPatternFilterTime": 12556094 + } + }, + "elevationMetadata": { + "ellipsoidToGeoidDifference": -19.263942171473552, + "geoidElevation": false + }, + "metadata": { + "nextDateTime": 1715977434000, + "prevDateTime": 1715971434000, + "searchWindowUsed": 3000 + }, + "nextPageCursor": "MXxORVhUX1BBR0V8MjAyNC0wNS0xN1QyMDoyMzo1NFp8fDUwbXxTVFJFRVRfQU5EX0FSUklWQUxfVElNRXx8fHx8fA==", + "plan": { + "date": 1715974434962, + "from": { + "lat": 47.575837, + "lon": -122.339414, + "name": "Origin", + "vertexType": "NORMAL" + }, + "itineraries": [ + { + "arrivedAtDestinationWithRentedBicycle": false, + "duration": 2347, + "elevationGained": 0.0, + "elevationLost": 0.0, + "endTime": 1715976848000, + "fare": { + "details": {}, + "fare": {}, + "legProducts": [ + { + "legIndices": [ + 1 + ], + "products": [ + { + "amount": { + "cents": 0, + "currency": { + "currency": "USD", + "currencyCode": "USD", + "defaultFractionDigits": 2, + "symbol": "$" + } + }, + "id": "headway-1330:300", + "name": "regular" + } + ] + }, + { + "legIndices": [ + 2 + ], + "products": [ + { + "amount": { + "cents": 0, + "currency": { + "currency": "USD", + "currencyCode": "USD", + "defaultFractionDigits": 2, + "symbol": "$" + } + }, + "id": "headway-1330:300", + "name": "regular" + } + ] + } + ] + }, + "generalizedCost": 3407, + "legs": [ + { + "agencyTimeZoneOffset": -25200000, + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 442.8, + "duration": 369.0, + "endTime": 1715974870000, + "from": { + "departure": 1715974501000, + "lat": 47.575837, + "lon": -122.339414, + "name": "Origin", + "vertexType": "NORMAL" + }, + "generalizedCost": 681, + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 42, + "points": "}ckaHbkuiV`@@?[?sDE??E?E?A?A?cB?c@?C?M?C?{@B??C?G?W?K?S?CAMBC?c@?y@?_A?gA?a@?C?U?K?yBAE?CCA@C@]?[?Cq@??H" + }, + "mode": "WALK", + "pathway": false, + "realTime": false, + "rentedBike": false, + "route": "", + "startTime": 1715974501000, + "steps": [ + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": false, + "distance": 19.15, + "elevation": "", + "lat": 47.5758346, + "lon": -122.3392181, + "relativeDirection": "DEPART", + "stayOn": false, + "streetName": "East Marginal Way South", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": true, + "distance": 423.64, + "elevation": "", + "lat": 47.5756624, + "lon": -122.3392227, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "path", + "walkingBike": false + } + ], + "to": { + "arrival": 1715974870000, + "departure": 1715974870000, + "lat": 47.575924, + "lon": -122.334106, + "name": "1st Ave S & S Hanford St", + "stopCode": "15205", + "stopId": "headway-1330:15205", + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": false, + "walkingBike": false + }, + { + "agencyId": "headway-1330:1", + "agencyName": "Metro Transit", + "agencyTimeZoneOffset": -25200000, + "agencyUrl": "https://kingcounty.gov/en/dept/metro", + "alerts": [ + { + "alertDescriptionText": "This is an update to a previous Transit Alert. \r\nThe reroute end time for games on June 1 and June 15 have been updated.\r\n\nReroute will be in effect on each of the games noted below:\r\n- Sunday, April 28 at 3:30 PM-4:30 PM\r\n- Wednesday, May 1 at 3:00 PM-4:00 PM\r\n- Sunday, May 12 at 3:30 PM-4:30 PM\r\n- Wednesday, May 15 at 3:30 PM-4:30 PM\r\n- Thursday, May 30 at 3:30 PM-4:30 PM\r\n- Saturday, June 1 at 6:35 PM-7:35 PM\r\n- Sunday, June 2 at 3:30 PM-4:30 PM\r\n- Saturday, June 15 at 6:35 PM-7:35 PM\r\n- Sunday, June 16 at 3:30 PM-4:30 PM\r\n- Sunday, June 30 at 3:30 PM-4:30 PM\r\n- Thursday, July 4 at 3:30 PM-4:30 PM\r\n- Saturday, July 6 at 3:30 PM-4:30 PM\r\n- Sunday, July 7 at 3:30 PM-4:30 PM\r\n- Sunday, July 21 at 3:30 PM-4:30 PM\r\n- Wednesday, July 24 at 3:00 PM-4:00 PM\r\n- Sunday, August 4 at 3:30 PM-4:30 PM\r\n- Sunday, August 11 at 3:30 PM-4:30 PM\r\n- Saturday, August 24 at 3:30 PM-4:30 PM\r\n- Sunday, August 25 at 3:30 PM-4:30 PM\r\n- Wednesday, August 28 at 3:30 PM-4:30 PM\r\n- Sunday, September 15 at 3:30 PM-4:30 PM\r\n- Thursday, September 19 at 3:30 PM-4:30 PM\r\n- Sunday, September 29 at 2:30 PM-3:30 PM\r\n\nAffected stops:\r\nStop #15190 1st Ave S & S Spokane St (NB)\r\nStop #15205 1st Ave S & S Hanford St (NB)\r\nStop #15230 1st Ave S & S Lander St (NB)\r\nStop #15206 1st Ave S & S Stacy St (NB)\r\nStop #15212 1st Ave S & S Walker St (NB)\r\nStop #15207 1st Ave S & S Holgate St (NB)\r\nStop #15204 Edgar Martinez Dr S & Occidental Ave S (EB)\r\nStop #15201 1st Ave S & S Atlantic St (SB)\r\nStop #15202 1st Ave S & S Holgate St (SB)\r\nStop #15211 1st Ave S & S Walker St (SB)\r\nStop #15203 1st Ave S & S Stacy St (SB)\r\n\nFor Route 21 to Downtown Seattle get on/off buses at:\r\nStop #15145 SW Spokane St & Chelan Ave SW (EB)\r\nStop #30538 4th Ave S & S Spokane St (NB)\r\nStop #30550 4th Ave S & S Hanford St (NB)\r\nStop #30560 4th Ave S & S Forest St (NB)\r\nStop #30570 4th Ave S & S Lander St (NB)\r\nStop #30590 4th Ave S & S Walker St (NB)\r\nStop #30600 4th Ave S & S Holgate St (NB)\r\nStop #30635 4th Ave S & S Royal Brougham Way (NB)\r\n\nFor Route 21 to Westwood Village get on/off buses at:\r\nStop #515 3rd Ave S & S Main St (SB)\r\nStop #30670 4th Ave S & Edgar Martinez Dr S (SB)\r\nStop #30690 4th Ave S & S Holgate St (SB)\r\nStop #30700 4th Ave S & S Walker St (SB)\r\nStop #15380 1st Ave S & S Lander St (SB)", + "alertHeaderText": "Route 21 will be affected in both directions during Mariners afternoon home games; stops on portions of 1st Ave S will not be served; use alternate stops on 4th Ave S instead.", + "alertUrl": "https://drive.google.com/file/d/1rOKsBD54nHp8U1Sh3GGpQQpLMNhXwTDf/view", + "effectiveEndDate": 1727649000000, + "effectiveStartDate": 1711924200000 + } + ], + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 2322.8, + "duration": 410.0, + "endTime": 1715975280000, + "from": { + "arrival": 1715974870000, + "departure": 1715974870000, + "lat": 47.575924, + "lon": -122.334106, + "name": "1st Ave S & S Hanford St", + "stopCode": "15205", + "stopId": "headway-1330:15205", + "stopIndex": 28, + "stopSequence": 194, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 1010, + "headsign": "Downtown Seattle Via 35th Ave SW", + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 41, + "points": "odkaHvktiVgJ?gK???W?aL?cA???}I?s@???qJ?sA???uI?yK?@oC???g@?_AEyB?gB?cA?mC?cCAgBCiBEMACCCCAECI?{FAc@D[RQ?Y?W?G?sD?" + }, + "mode": "BUS", + "pathway": false, + "realTime": false, + "routeId": "headway-1330:100101", + "routeShortName": "21", + "routeType": 3, + "serviceDate": "2024-05-17", + "startTime": 1715974870000, + "steps": [], + "to": { + "arrival": 1715975280000, + "departure": 1715975280000, + "lat": 47.593342, + "lon": -122.328957, + "name": "4th Ave S & S Royal Brougham Way", + "stopCode": "30635", + "stopId": "headway-1330:30635", + "stopIndex": 34, + "stopSequence": 229, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": true, + "tripBlockId": "7142607", + "tripId": "headway-1330:605082596" + }, + { + "agencyId": "headway-1330:1", + "agencyName": "Metro Transit", + "agencyTimeZoneOffset": -25200000, + "agencyUrl": "https://kingcounty.gov/en/dept/metro", + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 7187.77, + "duration": 1380.0, + "endTime": 1715976660000, + "from": { + "arrival": 1715975280000, + "departure": 1715975280000, + "lat": 47.593342, + "lon": -122.328957, + "name": "4th Ave S & S Royal Brougham Way", + "stopCode": "30635", + "stopId": "headway-1330:30635", + "stopIndex": 0, + "stopSequence": 1, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 1380, + "headsign": "Shoreline Greenwood", + "interlineWithPreviousLeg": true, + "legGeometry": { + "length": 195, + "points": "kqnaHpksiVmD?e@?_L?}ECoDCqCAYLY@c@B[CIA??QCUMeDBuAtAILKJEJGLo@rBM^GPIHGLa@\\gB~A??_@\\oC`C]ZoBbBoCbCuBlB??WTmC`CoC~BmCbCqDbD??SRgErDwChCm@j@gA`A??cA`AIFGFGHIPMTOVKRO`@aB`DkBvD??k@jAyCdGyCbGiAtB??oAjCaAsAaAwAcAuAy@kAIIaCkDa@?wDZkB@??}BD]IsA?uB?_@?{@C_@As@CY???E?O?K?MBODM@KDMBOFOJIFKDODOFO@O@M?OAUEWG]M]IQEQEWC]E_@E]A_@GcC@[?o@?}CGcAEiAAoBEaBCsCA{@???mGAeJAi@???qDCcFAgCC]?Q?QBO?O@QBOBQDMBMBOFODQJOFMHSHiCbB??IBwCjBsAx@]RMFCB??YN[No@\\m@Va@R[Nc@Ri@Pi@Rs@Te@Nw@T_Cp@SD??}@ReATYHUDYDQDY@_AFo@Dg@Bq@?q@?eNB}Y@i@IYIYKQKOIi@c@US{@?" + }, + "mode": "BUS", + "pathway": false, + "realTime": false, + "routeId": "headway-1330:100229", + "routeShortName": "5", + "routeType": 3, + "serviceDate": "2024-05-17", + "startTime": 1715975280000, + "steps": [], + "to": { + "arrival": 1715976660000, + "departure": 1715976660000, + "lat": 47.652473, + "lon": -122.346619, + "name": "Aurora Ave N & N 38th St", + "stopCode": "6340", + "stopId": "headway-1330:6340", + "stopIndex": 15, + "stopSequence": 181, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": true, + "tripBlockId": "7142607", + "tripId": "headway-1330:605083836" + }, + { + "agencyTimeZoneOffset": -25200000, + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 204.29, + "duration": 188.0, + "endTime": 1715976848000, + "from": { + "arrival": 1715976660000, + "departure": 1715976660000, + "lat": 47.652473, + "lon": -122.346619, + "name": "Aurora Ave N & N 38th St", + "stopCode": "6340", + "stopId": "headway-1330:6340", + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 335, + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 20, + "points": "}bzaHjyviVG??A@q@FILTFNNRLNHEJHFHt@l@d@V\\JNBNBB?L?\\?" + }, + "mode": "WALK", + "pathway": false, + "realTime": false, + "rentedBike": false, + "route": "", + "startTime": 1715976660000, + "steps": [ + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": true, + "distance": 25.71, + "elevation": "", + "lat": 47.6525198, + "lon": -122.3466171, + "relativeDirection": "DEPART", + "stayOn": false, + "streetName": "sidewalk", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": false, + "distance": 40.3, + "elevation": "", + "lat": 47.6524649, + "lon": -122.3463029, + "relativeDirection": "HARD_RIGHT", + "stayOn": false, + "streetName": "Bridge Way North", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": false, + "distance": 6.19, + "elevation": "", + "lat": 47.6522039, + "lon": -122.3466732, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "Winslow Place North", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": true, + "distance": 132.1, + "elevation": "", + "lat": 47.652151, + "lon": -122.3466476, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "path", + "walkingBike": false + } + ], + "to": { + "arrival": 1715976848000, + "lat": 47.651048, + "lon": -122.347234, + "name": "Destination", + "vertexType": "NORMAL" + }, + "transitLeg": false, + "walkingBike": false + } + ], + "startTime": 1715974501000, + "tooSloped": false, + "transfers": 0, + "transitTime": 1790, + "waitingTime": 0, + "walkDistance": 647.09, + "walkLimitExceeded": false, + "walkTime": 557 + }, + { + "arrivedAtDestinationWithRentedBicycle": false, + "duration": 2676, + "elevationGained": 0.0, + "elevationLost": 0.0, + "endTime": 1715977448000, + "fare": { + "details": {}, + "fare": {}, + "legProducts": [ + { + "legIndices": [ + 1 + ], + "products": [ + { + "amount": { + "cents": 0, + "currency": { + "currency": "USD", + "currencyCode": "USD", + "defaultFractionDigits": 2, + "symbol": "$" + } + }, + "id": "headway-1330:300", + "name": "regular" + } + ] + }, + { + "legIndices": [ + 2 + ], + "products": [ + { + "amount": { + "cents": 0, + "currency": { + "currency": "USD", + "currencyCode": "USD", + "defaultFractionDigits": 2, + "symbol": "$" + } + }, + "id": "headway-1330:300", + "name": "regular" + } + ] + } + ] + }, + "generalizedCost": 4210, + "legs": [ + { + "agencyTimeZoneOffset": -25200000, + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 1080.24, + "duration": 872.0, + "endTime": 1715975644000, + "from": { + "departure": 1715974772000, + "lat": 47.575837, + "lon": -122.339414, + "name": "Origin", + "vertexType": "NORMAL" + }, + "generalizedCost": 1657, + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 51, + "points": "}ckaHbkuiV`@@J?@kB?mB?C?eB?q@?I?eA?W?K@S?E?s@?y@?_A?iA?y@@mCLAB@@A@A`E??ED[B]H??_@?aC@wB?gA?w@?i@?M?K?I?W@iH?u@?_B?g@S?W?oA@W?gA@?g@p@??P" + }, + "mode": "WALK", + "pathway": false, + "realTime": false, + "rentedBike": false, + "route": "", + "startTime": 1715974772000, + "steps": [ + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": false, + "distance": 26.0, + "elevation": "", + "lat": 47.5758346, + "lon": -122.3392181, + "relativeDirection": "DEPART", + "stayOn": false, + "streetName": "East Marginal Way South", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": false, + "distance": 361.7, + "elevation": "", + "lat": 47.5756008, + "lon": -122.3392243, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "South Hanford Street", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": true, + "distance": 146.76, + "elevation": "", + "lat": 47.5755789, + "lon": -122.3344026, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "path", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": true, + "distance": 5.48, + "elevation": "", + "lat": 47.5744409, + "lon": -122.3340608, + "relativeDirection": "RIGHT", + "stayOn": true, + "streetName": "path", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": false, + "distance": 374.98, + "elevation": "", + "lat": 47.5743917, + "lon": -122.334064, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "South Horton Street", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 122.3, + "elevation": "", + "lat": 47.5743713, + "lon": -122.3290654, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "4th Avenue South", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": true, + "distance": 15.3, + "elevation": "", + "lat": 47.5754711, + "lon": -122.3290854, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "service road", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": true, + "distance": 27.71, + "elevation": "", + "lat": 47.5754712, + "lon": -122.3288814, + "relativeDirection": "RIGHT", + "stayOn": true, + "streetName": "sidewalk", + "walkingBike": false + } + ], + "to": { + "arrival": 1715975644000, + "departure": 1715975644000, + "lat": 47.575222, + "lon": -122.328972, + "name": "4th Ave S & S Hanford St", + "stopCode": "30550", + "stopId": "headway-1330:30550", + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": false, + "walkingBike": false + }, + { + "agencyId": "headway-1330:1", + "agencyName": "Metro Transit", + "agencyTimeZoneOffset": -25200000, + "agencyUrl": "https://kingcounty.gov/en/dept/metro", + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 2016.14, + "duration": 356.0, + "endTime": 1715976000000, + "from": { + "arrival": 1715975644000, + "departure": 1715975644000, + "lat": 47.575222, + "lon": -122.328972, + "name": "4th Ave S & S Hanford St", + "stopCode": "30550", + "stopId": "headway-1330:30550", + "stopIndex": 52, + "stopSequence": 427, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 956, + "headsign": "Downtown Seattle South Park", + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 28, + "points": "c`kaHtksiV{@?mL???I?_L?uA???kI?cJ???eA?kL?m@???mJCq@?gAWoCDkDLqB?_CCwAFQ?Y?W?G?sD?" + }, + "mode": "BUS", + "pathway": false, + "realTime": false, + "routeId": "headway-1330:100031", + "routeShortName": "132", + "routeType": 3, + "serviceDate": "2024-05-17", + "startTime": 1715975644000, + "steps": [], + "to": { + "arrival": 1715976000000, + "departure": 1715976000000, + "lat": 47.593342, + "lon": -122.328957, + "name": "4th Ave S & S Royal Brougham Way", + "stopCode": "30635", + "stopId": "headway-1330:30635", + "stopIndex": 57, + "stopSequence": 450, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": true, + "tripBlockId": "7159185", + "tripId": "headway-1330:473763106" + }, + { + "agencyId": "headway-1330:1", + "agencyName": "Metro Transit", + "agencyTimeZoneOffset": -25200000, + "agencyUrl": "https://kingcounty.gov/en/dept/metro", + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 7187.72, + "duration": 1260.0, + "endTime": 1715977260000, + "from": { + "arrival": 1715976000000, + "departure": 1715976000000, + "lat": 47.593342, + "lon": -122.328957, + "name": "4th Ave S & S Royal Brougham Way", + "stopCode": "30635", + "stopId": "headway-1330:30635", + "stopIndex": 0, + "stopSequence": 1, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 1260, + "headsign": "Carkeek Park", + "interlineWithPreviousLeg": true, + "legGeometry": { + "length": 189, + "points": "kqnaHpksiVmD?e@?_L?}ECoDCqCAYLY@c@B[CIA??QCUMeDBuAtAILKJEJGLo@rBM^GPIHGLa@\\gB~A??_@\\oC`C]ZoBbBoCbCuBlB??WTmC`CoC~BmCbCqDbD??SRgErDwChCm@j@gA`A??cA`AIFGFGHIPMTOVKRO`@aB`DkBvD??k@jAyCdGyCbGiAtB??oAjCaAsAaAwAcAuAy@kAIIaCkDa@?wDZkB@??}BD]IsA?uB?_@?{@C_@As@CY???E?O?K?MBODM@KDMBOFOJIFKDODOFO@O@M?OAUEWG]M]IQEQEWC]E_@E]A_@GcC@[?o@?}CGcAEiAAoBEaBCsCAiIAeJAi@???qDCcFAgCC]?Q?QBO?O@QBOBQDMBMBOFODQJOFMHSHsCfBwCjBsAx@]RMFCB??YN[No@\\m@Va@R[Nc@Ri@Pi@Rs@Te@Nw@T_Cp@qAXeATYHUDYDQDY@_AFo@Dg@Bq@?q@?eNB}Y@i@IYIYKQKOIi@c@US{@?" + }, + "mode": "BUS", + "pathway": false, + "realTime": false, + "routeId": "headway-1330:100169", + "routeShortName": "28", + "routeType": 3, + "serviceDate": "2024-05-17", + "startTime": 1715976000000, + "steps": [], + "to": { + "arrival": 1715977260000, + "departure": 1715977260000, + "lat": 47.652473, + "lon": -122.346619, + "name": "Aurora Ave N & N 38th St", + "stopCode": "6340", + "stopId": "headway-1330:6340", + "stopIndex": 12, + "stopSequence": 178, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": true, + "tripBlockId": "7159185", + "tripId": "headway-1330:651759926", + "tripShortName": "EXPRESS" + }, + { + "agencyTimeZoneOffset": -25200000, + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 204.29, + "duration": 188.0, + "endTime": 1715977448000, + "from": { + "arrival": 1715977260000, + "departure": 1715977260000, + "lat": 47.652473, + "lon": -122.346619, + "name": "Aurora Ave N & N 38th St", + "stopCode": "6340", + "stopId": "headway-1330:6340", + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 335, + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 20, + "points": "}bzaHjyviVG??A@q@FILTFNNRLNHEJHFHt@l@d@V\\JNBNBB?L?\\?" + }, + "mode": "WALK", + "pathway": false, + "realTime": false, + "rentedBike": false, + "route": "", + "startTime": 1715977260000, + "steps": [ + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": true, + "distance": 25.71, + "elevation": "", + "lat": 47.6525198, + "lon": -122.3466171, + "relativeDirection": "DEPART", + "stayOn": false, + "streetName": "sidewalk", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": false, + "distance": 40.3, + "elevation": "", + "lat": 47.6524649, + "lon": -122.3463029, + "relativeDirection": "HARD_RIGHT", + "stayOn": false, + "streetName": "Bridge Way North", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": false, + "distance": 6.19, + "elevation": "", + "lat": 47.6522039, + "lon": -122.3466732, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "Winslow Place North", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": true, + "distance": 132.1, + "elevation": "", + "lat": 47.652151, + "lon": -122.3466476, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "path", + "walkingBike": false + } + ], + "to": { + "arrival": 1715977448000, + "lat": 47.651048, + "lon": -122.347234, + "name": "Destination", + "vertexType": "NORMAL" + }, + "transitLeg": false, + "walkingBike": false + } + ], + "startTime": 1715974772000, + "tooSloped": false, + "transfers": 0, + "transitTime": 1616, + "waitingTime": 0, + "walkDistance": 1284.53, + "walkLimitExceeded": false, + "walkTime": 1060 + }, + { + "arrivedAtDestinationWithRentedBicycle": false, + "duration": 2407, + "elevationGained": 0.0, + "elevationLost": 0.0, + "endTime": 1715977748000, + "fare": { + "details": {}, + "fare": {}, + "legProducts": [ + { + "legIndices": [ + 1 + ], + "products": [ + { + "amount": { + "cents": 0, + "currency": { + "currency": "USD", + "currencyCode": "USD", + "defaultFractionDigits": 2, + "symbol": "$" + } + }, + "id": "headway-1330:300", + "name": "regular" + } + ] + }, + { + "legIndices": [ + 2 + ], + "products": [ + { + "amount": { + "cents": 0, + "currency": { + "currency": "USD", + "currencyCode": "USD", + "defaultFractionDigits": 2, + "symbol": "$" + } + }, + "id": "headway-1330:300", + "name": "regular" + } + ] + } + ] + }, + "generalizedCost": 3467, + "legs": [ + { + "agencyTimeZoneOffset": -25200000, + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 442.8, + "duration": 369.0, + "endTime": 1715975710000, + "from": { + "departure": 1715975341000, + "lat": 47.575837, + "lon": -122.339414, + "name": "Origin", + "vertexType": "NORMAL" + }, + "generalizedCost": 681, + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 42, + "points": "}ckaHbkuiV`@@?[?sDE??E?E?A?A?cB?c@?C?M?C?{@B??C?G?W?K?S?CAMBC?c@?y@?_A?gA?a@?C?U?K?yBAE?CCA@C@]?[?Cq@??H" + }, + "mode": "WALK", + "pathway": false, + "realTime": false, + "rentedBike": false, + "route": "", + "startTime": 1715975341000, + "steps": [ + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": false, + "distance": 19.15, + "elevation": "", + "lat": 47.5758346, + "lon": -122.3392181, + "relativeDirection": "DEPART", + "stayOn": false, + "streetName": "East Marginal Way South", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": true, + "distance": 423.64, + "elevation": "", + "lat": 47.5756624, + "lon": -122.3392227, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "path", + "walkingBike": false + } + ], + "to": { + "arrival": 1715975710000, + "departure": 1715975710000, + "lat": 47.575924, + "lon": -122.334106, + "name": "1st Ave S & S Hanford St", + "stopCode": "15205", + "stopId": "headway-1330:15205", + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": false, + "walkingBike": false + }, + { + "agencyId": "headway-1330:1", + "agencyName": "Metro Transit", + "agencyTimeZoneOffset": -25200000, + "agencyUrl": "https://kingcounty.gov/en/dept/metro", + "alerts": [ + { + "alertDescriptionText": "This is an update to a previous Transit Alert. \r\nThe reroute end time for games on June 1 and June 15 have been updated.\r\n\nReroute will be in effect on each of the games noted below:\r\n- Sunday, April 28 at 3:30 PM-4:30 PM\r\n- Wednesday, May 1 at 3:00 PM-4:00 PM\r\n- Sunday, May 12 at 3:30 PM-4:30 PM\r\n- Wednesday, May 15 at 3:30 PM-4:30 PM\r\n- Thursday, May 30 at 3:30 PM-4:30 PM\r\n- Saturday, June 1 at 6:35 PM-7:35 PM\r\n- Sunday, June 2 at 3:30 PM-4:30 PM\r\n- Saturday, June 15 at 6:35 PM-7:35 PM\r\n- Sunday, June 16 at 3:30 PM-4:30 PM\r\n- Sunday, June 30 at 3:30 PM-4:30 PM\r\n- Thursday, July 4 at 3:30 PM-4:30 PM\r\n- Saturday, July 6 at 3:30 PM-4:30 PM\r\n- Sunday, July 7 at 3:30 PM-4:30 PM\r\n- Sunday, July 21 at 3:30 PM-4:30 PM\r\n- Wednesday, July 24 at 3:00 PM-4:00 PM\r\n- Sunday, August 4 at 3:30 PM-4:30 PM\r\n- Sunday, August 11 at 3:30 PM-4:30 PM\r\n- Saturday, August 24 at 3:30 PM-4:30 PM\r\n- Sunday, August 25 at 3:30 PM-4:30 PM\r\n- Wednesday, August 28 at 3:30 PM-4:30 PM\r\n- Sunday, September 15 at 3:30 PM-4:30 PM\r\n- Thursday, September 19 at 3:30 PM-4:30 PM\r\n- Sunday, September 29 at 2:30 PM-3:30 PM\r\n\nAffected stops:\r\nStop #15190 1st Ave S & S Spokane St (NB)\r\nStop #15205 1st Ave S & S Hanford St (NB)\r\nStop #15230 1st Ave S & S Lander St (NB)\r\nStop #15206 1st Ave S & S Stacy St (NB)\r\nStop #15212 1st Ave S & S Walker St (NB)\r\nStop #15207 1st Ave S & S Holgate St (NB)\r\nStop #15204 Edgar Martinez Dr S & Occidental Ave S (EB)\r\nStop #15201 1st Ave S & S Atlantic St (SB)\r\nStop #15202 1st Ave S & S Holgate St (SB)\r\nStop #15211 1st Ave S & S Walker St (SB)\r\nStop #15203 1st Ave S & S Stacy St (SB)\r\n\nFor Route 21 to Downtown Seattle get on/off buses at:\r\nStop #15145 SW Spokane St & Chelan Ave SW (EB)\r\nStop #30538 4th Ave S & S Spokane St (NB)\r\nStop #30550 4th Ave S & S Hanford St (NB)\r\nStop #30560 4th Ave S & S Forest St (NB)\r\nStop #30570 4th Ave S & S Lander St (NB)\r\nStop #30590 4th Ave S & S Walker St (NB)\r\nStop #30600 4th Ave S & S Holgate St (NB)\r\nStop #30635 4th Ave S & S Royal Brougham Way (NB)\r\n\nFor Route 21 to Westwood Village get on/off buses at:\r\nStop #515 3rd Ave S & S Main St (SB)\r\nStop #30670 4th Ave S & Edgar Martinez Dr S (SB)\r\nStop #30690 4th Ave S & S Holgate St (SB)\r\nStop #30700 4th Ave S & S Walker St (SB)\r\nStop #15380 1st Ave S & S Lander St (SB)", + "alertHeaderText": "Route 21 will be affected in both directions during Mariners afternoon home games; stops on portions of 1st Ave S will not be served; use alternate stops on 4th Ave S instead.", + "alertUrl": "https://drive.google.com/file/d/1rOKsBD54nHp8U1Sh3GGpQQpLMNhXwTDf/view", + "effectiveEndDate": 1727649000000, + "effectiveStartDate": 1711924200000 + } + ], + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 2322.8, + "duration": 410.0, + "endTime": 1715976120000, + "from": { + "arrival": 1715975710000, + "departure": 1715975710000, + "lat": 47.575924, + "lon": -122.334106, + "name": "1st Ave S & S Hanford St", + "stopCode": "15205", + "stopId": "headway-1330:15205", + "stopIndex": 26, + "stopSequence": 183, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 1010, + "headsign": "Downtown Seattle Via 35th Ave SW", + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 41, + "points": "odkaHvktiVgJ?gK???W?aL?cA???}I?s@???qJ?sA???uI?yK?@oC???g@?_AEyB?gB?cA?mC?cCAgBCiBEMACCCCAECI?{FAc@D[RQ?Y?W?G?sD?" + }, + "mode": "BUS", + "pathway": false, + "realTime": false, + "routeId": "headway-1330:100101", + "routeShortName": "21", + "routeType": 3, + "serviceDate": "2024-05-17", + "startTime": 1715975710000, + "steps": [], + "to": { + "arrival": 1715976120000, + "departure": 1715976120000, + "lat": 47.593342, + "lon": -122.328957, + "name": "4th Ave S & S Royal Brougham Way", + "stopCode": "30635", + "stopId": "headway-1330:30635", + "stopIndex": 32, + "stopSequence": 218, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": true, + "tripBlockId": "7142679", + "tripId": "headway-1330:605084226" + }, + { + "agencyId": "headway-1330:1", + "agencyName": "Metro Transit", + "agencyTimeZoneOffset": -25200000, + "agencyUrl": "https://kingcounty.gov/en/dept/metro", + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 7187.77, + "duration": 1440.0, + "endTime": 1715977560000, + "from": { + "arrival": 1715976120000, + "departure": 1715976120000, + "lat": 47.593342, + "lon": -122.328957, + "name": "4th Ave S & S Royal Brougham Way", + "stopCode": "30635", + "stopId": "headway-1330:30635", + "stopIndex": 0, + "stopSequence": 1, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 1440, + "headsign": "Shoreline Greenwood", + "interlineWithPreviousLeg": true, + "legGeometry": { + "length": 195, + "points": "kqnaHpksiVmD?e@?_L?}ECoDCqCAYLY@c@B[CIA??QCUMeDBuAtAILKJEJGLo@rBM^GPIHGLa@\\gB~A??_@\\oC`C]ZoBbBoCbCuBlB??WTmC`CoC~BmCbCqDbD??SRgErDwChCm@j@gA`A??cA`AIFGFGHIPMTOVKRO`@aB`DkBvD??k@jAyCdGyCbGiAtB??oAjCaAsAaAwAcAuAy@kAIIaCkDa@?wDZkB@??}BD]IsA?uB?_@?{@C_@As@CY???E?O?K?MBODM@KDMBOFOJIFKDODOFO@O@M?OAUEWG]M]IQEQEWC]E_@E]A_@GcC@[?o@?}CGcAEiAAoBEaBCsCA{@???mGAeJAi@???qDCcFAgCC]?Q?QBO?O@QBOBQDMBMBOFODQJOFMHSHiCbB??IBwCjBsAx@]RMFCB??YN[No@\\m@Va@R[Nc@Ri@Pi@Rs@Te@Nw@T_Cp@SD??}@ReATYHUDYDQDY@_AFo@Dg@Bq@?q@?eNB}Y@i@IYIYKQKOIi@c@US{@?" + }, + "mode": "BUS", + "pathway": false, + "realTime": false, + "routeId": "headway-1330:100229", + "routeShortName": "5", + "routeType": 3, + "serviceDate": "2024-05-17", + "startTime": 1715976120000, + "steps": [], + "to": { + "arrival": 1715977560000, + "departure": 1715977560000, + "lat": 47.652473, + "lon": -122.346619, + "name": "Aurora Ave N & N 38th St", + "stopCode": "6340", + "stopId": "headway-1330:6340", + "stopIndex": 15, + "stopSequence": 181, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": true, + "tripBlockId": "7142679", + "tripId": "headway-1330:571900176" + }, + { + "agencyTimeZoneOffset": -25200000, + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 204.29, + "duration": 188.0, + "endTime": 1715977748000, + "from": { + "arrival": 1715977560000, + "departure": 1715977560000, + "lat": 47.652473, + "lon": -122.346619, + "name": "Aurora Ave N & N 38th St", + "stopCode": "6340", + "stopId": "headway-1330:6340", + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 335, + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 20, + "points": "}bzaHjyviVG??A@q@FILTFNNRLNHEJHFHt@l@d@V\\JNBNBB?L?\\?" + }, + "mode": "WALK", + "pathway": false, + "realTime": false, + "rentedBike": false, + "route": "", + "startTime": 1715977560000, + "steps": [ + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": true, + "distance": 25.71, + "elevation": "", + "lat": 47.6525198, + "lon": -122.3466171, + "relativeDirection": "DEPART", + "stayOn": false, + "streetName": "sidewalk", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": false, + "distance": 40.3, + "elevation": "", + "lat": 47.6524649, + "lon": -122.3463029, + "relativeDirection": "HARD_RIGHT", + "stayOn": false, + "streetName": "Bridge Way North", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": false, + "distance": 6.19, + "elevation": "", + "lat": 47.6522039, + "lon": -122.3466732, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "Winslow Place North", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": true, + "distance": 132.1, + "elevation": "", + "lat": 47.652151, + "lon": -122.3466476, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "path", + "walkingBike": false + } + ], + "to": { + "arrival": 1715977748000, + "lat": 47.651048, + "lon": -122.347234, + "name": "Destination", + "vertexType": "NORMAL" + }, + "transitLeg": false, + "walkingBike": false + } + ], + "startTime": 1715975341000, + "tooSloped": false, + "transfers": 0, + "transitTime": 1850, + "waitingTime": 0, + "walkDistance": 647.09, + "walkLimitExceeded": false, + "walkTime": 557 + }, + { + "arrivedAtDestinationWithRentedBicycle": false, + "duration": 2407, + "elevationGained": 0.0, + "elevationLost": 0.0, + "endTime": 1715978708000, + "fare": { + "details": {}, + "fare": {}, + "legProducts": [ + { + "legIndices": [ + 1 + ], + "products": [ + { + "amount": { + "cents": 0, + "currency": { + "currency": "USD", + "currencyCode": "USD", + "defaultFractionDigits": 2, + "symbol": "$" + } + }, + "id": "headway-1330:300", + "name": "regular" + } + ] + }, + { + "legIndices": [ + 2 + ], + "products": [ + { + "amount": { + "cents": 0, + "currency": { + "currency": "USD", + "currencyCode": "USD", + "defaultFractionDigits": 2, + "symbol": "$" + } + }, + "id": "headway-1330:300", + "name": "regular" + } + ] + } + ] + }, + "generalizedCost": 3467, + "legs": [ + { + "agencyTimeZoneOffset": -25200000, + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 442.8, + "duration": 369.0, + "endTime": 1715976670000, + "from": { + "departure": 1715976301000, + "lat": 47.575837, + "lon": -122.339414, + "name": "Origin", + "vertexType": "NORMAL" + }, + "generalizedCost": 681, + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 42, + "points": "}ckaHbkuiV`@@?[?sDE??E?E?A?A?cB?c@?C?M?C?{@B??C?G?W?K?S?CAMBC?c@?y@?_A?gA?a@?C?U?K?yBAE?CCA@C@]?[?Cq@??H" + }, + "mode": "WALK", + "pathway": false, + "realTime": false, + "rentedBike": false, + "route": "", + "startTime": 1715976301000, + "steps": [ + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": false, + "distance": 19.15, + "elevation": "", + "lat": 47.5758346, + "lon": -122.3392181, + "relativeDirection": "DEPART", + "stayOn": false, + "streetName": "East Marginal Way South", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": true, + "distance": 423.64, + "elevation": "", + "lat": 47.5756624, + "lon": -122.3392227, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "path", + "walkingBike": false + } + ], + "to": { + "arrival": 1715976670000, + "departure": 1715976670000, + "lat": 47.575924, + "lon": -122.334106, + "name": "1st Ave S & S Hanford St", + "stopCode": "15205", + "stopId": "headway-1330:15205", + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": false, + "walkingBike": false + }, + { + "agencyId": "headway-1330:1", + "agencyName": "Metro Transit", + "agencyTimeZoneOffset": -25200000, + "agencyUrl": "https://kingcounty.gov/en/dept/metro", + "alerts": [ + { + "alertDescriptionText": "This is an update to a previous Transit Alert. \r\nThe reroute end time for games on June 1 and June 15 have been updated.\r\n\nReroute will be in effect on each of the games noted below:\r\n- Sunday, April 28 at 3:30 PM-4:30 PM\r\n- Wednesday, May 1 at 3:00 PM-4:00 PM\r\n- Sunday, May 12 at 3:30 PM-4:30 PM\r\n- Wednesday, May 15 at 3:30 PM-4:30 PM\r\n- Thursday, May 30 at 3:30 PM-4:30 PM\r\n- Saturday, June 1 at 6:35 PM-7:35 PM\r\n- Sunday, June 2 at 3:30 PM-4:30 PM\r\n- Saturday, June 15 at 6:35 PM-7:35 PM\r\n- Sunday, June 16 at 3:30 PM-4:30 PM\r\n- Sunday, June 30 at 3:30 PM-4:30 PM\r\n- Thursday, July 4 at 3:30 PM-4:30 PM\r\n- Saturday, July 6 at 3:30 PM-4:30 PM\r\n- Sunday, July 7 at 3:30 PM-4:30 PM\r\n- Sunday, July 21 at 3:30 PM-4:30 PM\r\n- Wednesday, July 24 at 3:00 PM-4:00 PM\r\n- Sunday, August 4 at 3:30 PM-4:30 PM\r\n- Sunday, August 11 at 3:30 PM-4:30 PM\r\n- Saturday, August 24 at 3:30 PM-4:30 PM\r\n- Sunday, August 25 at 3:30 PM-4:30 PM\r\n- Wednesday, August 28 at 3:30 PM-4:30 PM\r\n- Sunday, September 15 at 3:30 PM-4:30 PM\r\n- Thursday, September 19 at 3:30 PM-4:30 PM\r\n- Sunday, September 29 at 2:30 PM-3:30 PM\r\n\nAffected stops:\r\nStop #15190 1st Ave S & S Spokane St (NB)\r\nStop #15205 1st Ave S & S Hanford St (NB)\r\nStop #15230 1st Ave S & S Lander St (NB)\r\nStop #15206 1st Ave S & S Stacy St (NB)\r\nStop #15212 1st Ave S & S Walker St (NB)\r\nStop #15207 1st Ave S & S Holgate St (NB)\r\nStop #15204 Edgar Martinez Dr S & Occidental Ave S (EB)\r\nStop #15201 1st Ave S & S Atlantic St (SB)\r\nStop #15202 1st Ave S & S Holgate St (SB)\r\nStop #15211 1st Ave S & S Walker St (SB)\r\nStop #15203 1st Ave S & S Stacy St (SB)\r\n\nFor Route 21 to Downtown Seattle get on/off buses at:\r\nStop #15145 SW Spokane St & Chelan Ave SW (EB)\r\nStop #30538 4th Ave S & S Spokane St (NB)\r\nStop #30550 4th Ave S & S Hanford St (NB)\r\nStop #30560 4th Ave S & S Forest St (NB)\r\nStop #30570 4th Ave S & S Lander St (NB)\r\nStop #30590 4th Ave S & S Walker St (NB)\r\nStop #30600 4th Ave S & S Holgate St (NB)\r\nStop #30635 4th Ave S & S Royal Brougham Way (NB)\r\n\nFor Route 21 to Westwood Village get on/off buses at:\r\nStop #515 3rd Ave S & S Main St (SB)\r\nStop #30670 4th Ave S & Edgar Martinez Dr S (SB)\r\nStop #30690 4th Ave S & S Holgate St (SB)\r\nStop #30700 4th Ave S & S Walker St (SB)\r\nStop #15380 1st Ave S & S Lander St (SB)", + "alertHeaderText": "Route 21 will be affected in both directions during Mariners afternoon home games; stops on portions of 1st Ave S will not be served; use alternate stops on 4th Ave S instead.", + "alertUrl": "https://drive.google.com/file/d/1rOKsBD54nHp8U1Sh3GGpQQpLMNhXwTDf/view", + "effectiveEndDate": 1727649000000, + "effectiveStartDate": 1711924200000 + } + ], + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 2322.8, + "duration": 410.0, + "endTime": 1715977080000, + "from": { + "arrival": 1715976670000, + "departure": 1715976670000, + "lat": 47.575924, + "lon": -122.334106, + "name": "1st Ave S & S Hanford St", + "stopCode": "15205", + "stopId": "headway-1330:15205", + "stopIndex": 28, + "stopSequence": 194, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 1010, + "headsign": "Downtown Seattle Via 35th Ave SW", + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 41, + "points": "odkaHvktiVgJ?gK???W?aL?cA???}I?s@???qJ?sA???uI?yK?@oC???g@?_AEyB?gB?cA?mC?cCAgBCiBEMACCCCAECI?{FAc@D[RQ?Y?W?G?sD?" + }, + "mode": "BUS", + "pathway": false, + "realTime": false, + "routeId": "headway-1330:100101", + "routeShortName": "21", + "routeType": 3, + "serviceDate": "2024-05-17", + "startTime": 1715976670000, + "steps": [], + "to": { + "arrival": 1715977080000, + "departure": 1715977080000, + "lat": 47.593342, + "lon": -122.328957, + "name": "4th Ave S & S Royal Brougham Way", + "stopCode": "30635", + "stopId": "headway-1330:30635", + "stopIndex": 34, + "stopSequence": 229, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": true, + "tripBlockId": "7142612", + "tripId": "headway-1330:605082006" + }, + { + "agencyId": "headway-1330:1", + "agencyName": "Metro Transit", + "agencyTimeZoneOffset": -25200000, + "agencyUrl": "https://kingcounty.gov/en/dept/metro", + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 7187.77, + "duration": 1440.0, + "endTime": 1715978520000, + "from": { + "arrival": 1715977080000, + "departure": 1715977080000, + "lat": 47.593342, + "lon": -122.328957, + "name": "4th Ave S & S Royal Brougham Way", + "stopCode": "30635", + "stopId": "headway-1330:30635", + "stopIndex": 0, + "stopSequence": 1, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 1440, + "headsign": "Shoreline Greenwood", + "interlineWithPreviousLeg": true, + "legGeometry": { + "length": 195, + "points": "kqnaHpksiVmD?e@?_L?}ECoDCqCAYLY@c@B[CIA??QCUMeDBuAtAILKJEJGLo@rBM^GPIHGLa@\\gB~A??_@\\oC`C]ZoBbBoCbCuBlB??WTmC`CoC~BmCbCqDbD??SRgErDwChCm@j@gA`A??cA`AIFGFGHIPMTOVKRO`@aB`DkBvD??k@jAyCdGyCbGiAtB??oAjCaAsAaAwAcAuAy@kAIIaCkDa@?wDZkB@??}BD]IsA?uB?_@?{@C_@As@CY???E?O?K?MBODM@KDMBOFOJIFKDODOFO@O@M?OAUEWG]M]IQEQEWC]E_@E]A_@GcC@[?o@?}CGcAEiAAoBEaBCsCA{@???mGAeJAi@???qDCcFAgCC]?Q?QBO?O@QBOBQDMBMBOFODQJOFMHSHiCbB??IBwCjBsAx@]RMFCB??YN[No@\\m@Va@R[Nc@Ri@Pi@Rs@Te@Nw@T_Cp@SD??}@ReATYHUDYDQDY@_AFo@Dg@Bq@?q@?eNB}Y@i@IYIYKQKOIi@c@US{@?" + }, + "mode": "BUS", + "pathway": false, + "realTime": false, + "routeId": "headway-1330:100229", + "routeShortName": "5", + "routeType": 3, + "serviceDate": "2024-05-17", + "startTime": 1715977080000, + "steps": [], + "to": { + "arrival": 1715978520000, + "departure": 1715978520000, + "lat": 47.652473, + "lon": -122.346619, + "name": "Aurora Ave N & N 38th St", + "stopCode": "6340", + "stopId": "headway-1330:6340", + "stopIndex": 15, + "stopSequence": 181, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": true, + "tripBlockId": "7142612", + "tripId": "headway-1330:605083816" + }, + { + "agencyTimeZoneOffset": -25200000, + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 204.29, + "duration": 188.0, + "endTime": 1715978708000, + "from": { + "arrival": 1715978520000, + "departure": 1715978520000, + "lat": 47.652473, + "lon": -122.346619, + "name": "Aurora Ave N & N 38th St", + "stopCode": "6340", + "stopId": "headway-1330:6340", + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 335, + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 20, + "points": "}bzaHjyviVG??A@q@FILTFNNRLNHEJHFHt@l@d@V\\JNBNBB?L?\\?" + }, + "mode": "WALK", + "pathway": false, + "realTime": false, + "rentedBike": false, + "route": "", + "startTime": 1715978520000, + "steps": [ + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": true, + "distance": 25.71, + "elevation": "", + "lat": 47.6525198, + "lon": -122.3466171, + "relativeDirection": "DEPART", + "stayOn": false, + "streetName": "sidewalk", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": false, + "distance": 40.3, + "elevation": "", + "lat": 47.6524649, + "lon": -122.3463029, + "relativeDirection": "HARD_RIGHT", + "stayOn": false, + "streetName": "Bridge Way North", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": false, + "distance": 6.19, + "elevation": "", + "lat": 47.6522039, + "lon": -122.3466732, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "Winslow Place North", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": true, + "distance": 132.1, + "elevation": "", + "lat": 47.652151, + "lon": -122.3466476, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "path", + "walkingBike": false + } + ], + "to": { + "arrival": 1715978708000, + "lat": 47.651048, + "lon": -122.347234, + "name": "Destination", + "vertexType": "NORMAL" + }, + "transitLeg": false, + "walkingBike": false + } + ], + "startTime": 1715976301000, + "tooSloped": false, + "transfers": 0, + "transitTime": 1850, + "waitingTime": 0, + "walkDistance": 647.09, + "walkLimitExceeded": false, + "walkTime": 557 + }, + { + "arrivedAtDestinationWithRentedBicycle": false, + "duration": 2676, + "elevationGained": 0.0, + "elevationLost": 0.0, + "endTime": 1715979248000, + "fare": { + "details": {}, + "fare": {}, + "legProducts": [ + { + "legIndices": [ + 1 + ], + "products": [ + { + "amount": { + "cents": 0, + "currency": { + "currency": "USD", + "currencyCode": "USD", + "defaultFractionDigits": 2, + "symbol": "$" + } + }, + "id": "headway-1330:300", + "name": "regular" + } + ] + }, + { + "legIndices": [ + 2 + ], + "products": [ + { + "amount": { + "cents": 0, + "currency": { + "currency": "USD", + "currencyCode": "USD", + "defaultFractionDigits": 2, + "symbol": "$" + } + }, + "id": "headway-1330:300", + "name": "regular" + } + ] + } + ] + }, + "generalizedCost": 4210, + "legs": [ + { + "agencyTimeZoneOffset": -25200000, + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 1080.24, + "duration": 872.0, + "endTime": 1715977444000, + "from": { + "departure": 1715976572000, + "lat": 47.575837, + "lon": -122.339414, + "name": "Origin", + "vertexType": "NORMAL" + }, + "generalizedCost": 1657, + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 51, + "points": "}ckaHbkuiV`@@J?@kB?mB?C?eB?q@?I?eA?W?K@S?E?s@?y@?_A?iA?y@@mCLAB@@A@A`E??ED[B]H??_@?aC@wB?gA?w@?i@?M?K?I?W@iH?u@?_B?g@S?W?oA@W?gA@?g@p@??P" + }, + "mode": "WALK", + "pathway": false, + "realTime": false, + "rentedBike": false, + "route": "", + "startTime": 1715976572000, + "steps": [ + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": false, + "distance": 26.0, + "elevation": "", + "lat": 47.5758346, + "lon": -122.3392181, + "relativeDirection": "DEPART", + "stayOn": false, + "streetName": "East Marginal Way South", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": false, + "distance": 361.7, + "elevation": "", + "lat": 47.5756008, + "lon": -122.3392243, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "South Hanford Street", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": true, + "distance": 146.76, + "elevation": "", + "lat": 47.5755789, + "lon": -122.3344026, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "path", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": true, + "distance": 5.48, + "elevation": "", + "lat": 47.5744409, + "lon": -122.3340608, + "relativeDirection": "RIGHT", + "stayOn": true, + "streetName": "path", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": false, + "distance": 374.98, + "elevation": "", + "lat": 47.5743917, + "lon": -122.334064, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "South Horton Street", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 122.3, + "elevation": "", + "lat": 47.5743713, + "lon": -122.3290654, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "4th Avenue South", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": true, + "distance": 15.3, + "elevation": "", + "lat": 47.5754711, + "lon": -122.3290854, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "service road", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": true, + "distance": 27.71, + "elevation": "", + "lat": 47.5754712, + "lon": -122.3288814, + "relativeDirection": "RIGHT", + "stayOn": true, + "streetName": "sidewalk", + "walkingBike": false + } + ], + "to": { + "arrival": 1715977444000, + "departure": 1715977444000, + "lat": 47.575222, + "lon": -122.328972, + "name": "4th Ave S & S Hanford St", + "stopCode": "30550", + "stopId": "headway-1330:30550", + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": false, + "walkingBike": false + }, + { + "agencyId": "headway-1330:1", + "agencyName": "Metro Transit", + "agencyTimeZoneOffset": -25200000, + "agencyUrl": "https://kingcounty.gov/en/dept/metro", + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 2016.14, + "duration": 356.0, + "endTime": 1715977800000, + "from": { + "arrival": 1715977444000, + "departure": 1715977444000, + "lat": 47.575222, + "lon": -122.328972, + "name": "4th Ave S & S Hanford St", + "stopCode": "30550", + "stopId": "headway-1330:30550", + "stopIndex": 52, + "stopSequence": 427, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 956, + "headsign": "Downtown Seattle South Park", + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 28, + "points": "c`kaHtksiV{@?mL???I?_L?uA???kI?cJ???eA?kL?m@???mJCq@?gAWoCDkDLqB?_CCwAFQ?Y?W?G?sD?" + }, + "mode": "BUS", + "pathway": false, + "realTime": false, + "routeId": "headway-1330:100031", + "routeShortName": "132", + "routeType": 3, + "serviceDate": "2024-05-17", + "startTime": 1715977444000, + "steps": [], + "to": { + "arrival": 1715977800000, + "departure": 1715977800000, + "lat": 47.593342, + "lon": -122.328957, + "name": "4th Ave S & S Royal Brougham Way", + "stopCode": "30635", + "stopId": "headway-1330:30635", + "stopIndex": 57, + "stopSequence": 450, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": true, + "tripBlockId": "7159176", + "tripId": "headway-1330:651761126" + }, + { + "agencyId": "headway-1330:1", + "agencyName": "Metro Transit", + "agencyTimeZoneOffset": -25200000, + "agencyUrl": "https://kingcounty.gov/en/dept/metro", + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 7187.72, + "duration": 1260.0, + "endTime": 1715979060000, + "from": { + "arrival": 1715977800000, + "departure": 1715977800000, + "lat": 47.593342, + "lon": -122.328957, + "name": "4th Ave S & S Royal Brougham Way", + "stopCode": "30635", + "stopId": "headway-1330:30635", + "stopIndex": 0, + "stopSequence": 1, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 1260, + "headsign": "Carkeek Park", + "interlineWithPreviousLeg": true, + "legGeometry": { + "length": 189, + "points": "kqnaHpksiVmD?e@?_L?}ECoDCqCAYLY@c@B[CIA??QCUMeDBuAtAILKJEJGLo@rBM^GPIHGLa@\\gB~A??_@\\oC`C]ZoBbBoCbCuBlB??WTmC`CoC~BmCbCqDbD??SRgErDwChCm@j@gA`A??cA`AIFGFGHIPMTOVKRO`@aB`DkBvD??k@jAyCdGyCbGiAtB??oAjCaAsAaAwAcAuAy@kAIIaCkDa@?wDZkB@??}BD]IsA?uB?_@?{@C_@As@CY???E?O?K?MBODM@KDMBOFOJIFKDODOFO@O@M?OAUEWG]M]IQEQEWC]E_@E]A_@GcC@[?o@?}CGcAEiAAoBEaBCsCAiIAeJAi@???qDCcFAgCC]?Q?QBO?O@QBOBQDMBMBOFODQJOFMHSHsCfBwCjBsAx@]RMFCB??YN[No@\\m@Va@R[Nc@Ri@Pi@Rs@Te@Nw@T_Cp@qAXeATYHUDYDQDY@_AFo@Dg@Bq@?q@?eNB}Y@i@IYIYKQKOIi@c@US{@?" + }, + "mode": "BUS", + "pathway": false, + "realTime": false, + "routeId": "headway-1330:100169", + "routeShortName": "28", + "routeType": 3, + "serviceDate": "2024-05-17", + "startTime": 1715977800000, + "steps": [], + "to": { + "arrival": 1715979060000, + "departure": 1715979060000, + "lat": 47.652473, + "lon": -122.346619, + "name": "Aurora Ave N & N 38th St", + "stopCode": "6340", + "stopId": "headway-1330:6340", + "stopIndex": 12, + "stopSequence": 178, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": true, + "tripBlockId": "7159176", + "tripId": "headway-1330:663596036", + "tripShortName": "EXPRESS" + }, + { + "agencyTimeZoneOffset": -25200000, + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 204.29, + "duration": 188.0, + "endTime": 1715979248000, + "from": { + "arrival": 1715979060000, + "departure": 1715979060000, + "lat": 47.652473, + "lon": -122.346619, + "name": "Aurora Ave N & N 38th St", + "stopCode": "6340", + "stopId": "headway-1330:6340", + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 335, + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 20, + "points": "}bzaHjyviVG??A@q@FILTFNNRLNHEJHFHt@l@d@V\\JNBNBB?L?\\?" + }, + "mode": "WALK", + "pathway": false, + "realTime": false, + "rentedBike": false, + "route": "", + "startTime": 1715979060000, + "steps": [ + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": true, + "distance": 25.71, + "elevation": "", + "lat": 47.6525198, + "lon": -122.3466171, + "relativeDirection": "DEPART", + "stayOn": false, + "streetName": "sidewalk", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": false, + "distance": 40.3, + "elevation": "", + "lat": 47.6524649, + "lon": -122.3463029, + "relativeDirection": "HARD_RIGHT", + "stayOn": false, + "streetName": "Bridge Way North", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": false, + "distance": 6.19, + "elevation": "", + "lat": 47.6522039, + "lon": -122.3466732, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "Winslow Place North", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": true, + "distance": 132.1, + "elevation": "", + "lat": 47.652151, + "lon": -122.3466476, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "path", + "walkingBike": false + } + ], + "to": { + "arrival": 1715979248000, + "lat": 47.651048, + "lon": -122.347234, + "name": "Destination", + "vertexType": "NORMAL" + }, + "transitLeg": false, + "walkingBike": false + } + ], + "startTime": 1715976572000, + "tooSloped": false, + "transfers": 0, + "transitTime": 1616, + "waitingTime": 0, + "walkDistance": 1284.53, + "walkLimitExceeded": false, + "walkTime": 1060 + }, + { + "arrivedAtDestinationWithRentedBicycle": false, + "duration": 2407, + "elevationGained": 0.0, + "elevationLost": 0.0, + "endTime": 1715979548000, + "fare": { + "details": {}, + "fare": {}, + "legProducts": [ + { + "legIndices": [ + 1 + ], + "products": [ + { + "amount": { + "cents": 0, + "currency": { + "currency": "USD", + "currencyCode": "USD", + "defaultFractionDigits": 2, + "symbol": "$" + } + }, + "id": "headway-1330:300", + "name": "regular" + } + ] + }, + { + "legIndices": [ + 2 + ], + "products": [ + { + "amount": { + "cents": 0, + "currency": { + "currency": "USD", + "currencyCode": "USD", + "defaultFractionDigits": 2, + "symbol": "$" + } + }, + "id": "headway-1330:300", + "name": "regular" + } + ] + } + ] + }, + "generalizedCost": 3467, + "legs": [ + { + "agencyTimeZoneOffset": -25200000, + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 442.8, + "duration": 369.0, + "endTime": 1715977510000, + "from": { + "departure": 1715977141000, + "lat": 47.575837, + "lon": -122.339414, + "name": "Origin", + "vertexType": "NORMAL" + }, + "generalizedCost": 681, + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 42, + "points": "}ckaHbkuiV`@@?[?sDE??E?E?A?A?cB?c@?C?M?C?{@B??C?G?W?K?S?CAMBC?c@?y@?_A?gA?a@?C?U?K?yBAE?CCA@C@]?[?Cq@??H" + }, + "mode": "WALK", + "pathway": false, + "realTime": false, + "rentedBike": false, + "route": "", + "startTime": 1715977141000, + "steps": [ + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": false, + "distance": 19.15, + "elevation": "", + "lat": 47.5758346, + "lon": -122.3392181, + "relativeDirection": "DEPART", + "stayOn": false, + "streetName": "East Marginal Way South", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": true, + "distance": 423.64, + "elevation": "", + "lat": 47.5756624, + "lon": -122.3392227, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "path", + "walkingBike": false + } + ], + "to": { + "arrival": 1715977510000, + "departure": 1715977510000, + "lat": 47.575924, + "lon": -122.334106, + "name": "1st Ave S & S Hanford St", + "stopCode": "15205", + "stopId": "headway-1330:15205", + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": false, + "walkingBike": false + }, + { + "agencyId": "headway-1330:1", + "agencyName": "Metro Transit", + "agencyTimeZoneOffset": -25200000, + "agencyUrl": "https://kingcounty.gov/en/dept/metro", + "alerts": [ + { + "alertDescriptionText": "This is an update to a previous Transit Alert. \r\nThe reroute end time for games on June 1 and June 15 have been updated.\r\n\nReroute will be in effect on each of the games noted below:\r\n- Sunday, April 28 at 3:30 PM-4:30 PM\r\n- Wednesday, May 1 at 3:00 PM-4:00 PM\r\n- Sunday, May 12 at 3:30 PM-4:30 PM\r\n- Wednesday, May 15 at 3:30 PM-4:30 PM\r\n- Thursday, May 30 at 3:30 PM-4:30 PM\r\n- Saturday, June 1 at 6:35 PM-7:35 PM\r\n- Sunday, June 2 at 3:30 PM-4:30 PM\r\n- Saturday, June 15 at 6:35 PM-7:35 PM\r\n- Sunday, June 16 at 3:30 PM-4:30 PM\r\n- Sunday, June 30 at 3:30 PM-4:30 PM\r\n- Thursday, July 4 at 3:30 PM-4:30 PM\r\n- Saturday, July 6 at 3:30 PM-4:30 PM\r\n- Sunday, July 7 at 3:30 PM-4:30 PM\r\n- Sunday, July 21 at 3:30 PM-4:30 PM\r\n- Wednesday, July 24 at 3:00 PM-4:00 PM\r\n- Sunday, August 4 at 3:30 PM-4:30 PM\r\n- Sunday, August 11 at 3:30 PM-4:30 PM\r\n- Saturday, August 24 at 3:30 PM-4:30 PM\r\n- Sunday, August 25 at 3:30 PM-4:30 PM\r\n- Wednesday, August 28 at 3:30 PM-4:30 PM\r\n- Sunday, September 15 at 3:30 PM-4:30 PM\r\n- Thursday, September 19 at 3:30 PM-4:30 PM\r\n- Sunday, September 29 at 2:30 PM-3:30 PM\r\n\nAffected stops:\r\nStop #15190 1st Ave S & S Spokane St (NB)\r\nStop #15205 1st Ave S & S Hanford St (NB)\r\nStop #15230 1st Ave S & S Lander St (NB)\r\nStop #15206 1st Ave S & S Stacy St (NB)\r\nStop #15212 1st Ave S & S Walker St (NB)\r\nStop #15207 1st Ave S & S Holgate St (NB)\r\nStop #15204 Edgar Martinez Dr S & Occidental Ave S (EB)\r\nStop #15201 1st Ave S & S Atlantic St (SB)\r\nStop #15202 1st Ave S & S Holgate St (SB)\r\nStop #15211 1st Ave S & S Walker St (SB)\r\nStop #15203 1st Ave S & S Stacy St (SB)\r\n\nFor Route 21 to Downtown Seattle get on/off buses at:\r\nStop #15145 SW Spokane St & Chelan Ave SW (EB)\r\nStop #30538 4th Ave S & S Spokane St (NB)\r\nStop #30550 4th Ave S & S Hanford St (NB)\r\nStop #30560 4th Ave S & S Forest St (NB)\r\nStop #30570 4th Ave S & S Lander St (NB)\r\nStop #30590 4th Ave S & S Walker St (NB)\r\nStop #30600 4th Ave S & S Holgate St (NB)\r\nStop #30635 4th Ave S & S Royal Brougham Way (NB)\r\n\nFor Route 21 to Westwood Village get on/off buses at:\r\nStop #515 3rd Ave S & S Main St (SB)\r\nStop #30670 4th Ave S & Edgar Martinez Dr S (SB)\r\nStop #30690 4th Ave S & S Holgate St (SB)\r\nStop #30700 4th Ave S & S Walker St (SB)\r\nStop #15380 1st Ave S & S Lander St (SB)", + "alertHeaderText": "Route 21 will be affected in both directions during Mariners afternoon home games; stops on portions of 1st Ave S will not be served; use alternate stops on 4th Ave S instead.", + "alertUrl": "https://drive.google.com/file/d/1rOKsBD54nHp8U1Sh3GGpQQpLMNhXwTDf/view", + "effectiveEndDate": 1727649000000, + "effectiveStartDate": 1711924200000 + } + ], + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 2322.8, + "duration": 410.0, + "endTime": 1715977920000, + "from": { + "arrival": 1715977510000, + "departure": 1715977510000, + "lat": 47.575924, + "lon": -122.334106, + "name": "1st Ave S & S Hanford St", + "stopCode": "15205", + "stopId": "headway-1330:15205", + "stopIndex": 28, + "stopSequence": 194, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 1010, + "headsign": "Downtown Seattle Via 35th Ave SW", + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 41, + "points": "odkaHvktiVgJ?gK???W?aL?cA???}I?s@???qJ?sA???uI?yK?@oC???g@?_AEyB?gB?cA?mC?cCAgBCiBEMACCCCAECI?{FAc@D[RQ?Y?W?G?sD?" + }, + "mode": "BUS", + "pathway": false, + "realTime": false, + "routeId": "headway-1330:100101", + "routeShortName": "21", + "routeType": 3, + "serviceDate": "2024-05-17", + "startTime": 1715977510000, + "steps": [], + "to": { + "arrival": 1715977920000, + "departure": 1715977920000, + "lat": 47.593342, + "lon": -122.328957, + "name": "4th Ave S & S Royal Brougham Way", + "stopCode": "30635", + "stopId": "headway-1330:30635", + "stopIndex": 34, + "stopSequence": 229, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": true, + "tripBlockId": "7142599", + "tripId": "headway-1330:605084236" + }, + { + "agencyId": "headway-1330:1", + "agencyName": "Metro Transit", + "agencyTimeZoneOffset": -25200000, + "agencyUrl": "https://kingcounty.gov/en/dept/metro", + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 7187.77, + "duration": 1440.0, + "endTime": 1715979360000, + "from": { + "arrival": 1715977920000, + "departure": 1715977920000, + "lat": 47.593342, + "lon": -122.328957, + "name": "4th Ave S & S Royal Brougham Way", + "stopCode": "30635", + "stopId": "headway-1330:30635", + "stopIndex": 0, + "stopSequence": 1, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 1440, + "headsign": "Shoreline Greenwood", + "interlineWithPreviousLeg": true, + "legGeometry": { + "length": 195, + "points": "kqnaHpksiVmD?e@?_L?}ECoDCqCAYLY@c@B[CIA??QCUMeDBuAtAILKJEJGLo@rBM^GPIHGLa@\\gB~A??_@\\oC`C]ZoBbBoCbCuBlB??WTmC`CoC~BmCbCqDbD??SRgErDwChCm@j@gA`A??cA`AIFGFGHIPMTOVKRO`@aB`DkBvD??k@jAyCdGyCbGiAtB??oAjCaAsAaAwAcAuAy@kAIIaCkDa@?wDZkB@??}BD]IsA?uB?_@?{@C_@As@CY???E?O?K?MBODM@KDMBOFOJIFKDODOFO@O@M?OAUEWG]M]IQEQEWC]E_@E]A_@GcC@[?o@?}CGcAEiAAoBEaBCsCA{@???mGAeJAi@???qDCcFAgCC]?Q?QBO?O@QBOBQDMBMBOFODQJOFMHSHiCbB??IBwCjBsAx@]RMFCB??YN[No@\\m@Va@R[Nc@Ri@Pi@Rs@Te@Nw@T_Cp@SD??}@ReATYHUDYDQDY@_AFo@Dg@Bq@?q@?eNB}Y@i@IYIYKQKOIi@c@US{@?" + }, + "mode": "BUS", + "pathway": false, + "realTime": false, + "routeId": "headway-1330:100229", + "routeShortName": "5", + "routeType": 3, + "serviceDate": "2024-05-17", + "startTime": 1715977920000, + "steps": [], + "to": { + "arrival": 1715979360000, + "departure": 1715979360000, + "lat": 47.652473, + "lon": -122.346619, + "name": "Aurora Ave N & N 38th St", + "stopCode": "6340", + "stopId": "headway-1330:6340", + "stopIndex": 15, + "stopSequence": 181, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": true, + "tripBlockId": "7142599", + "tripId": "headway-1330:571900096" + }, + { + "agencyTimeZoneOffset": -25200000, + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 204.29, + "duration": 188.0, + "endTime": 1715979548000, + "from": { + "arrival": 1715979360000, + "departure": 1715979360000, + "lat": 47.652473, + "lon": -122.346619, + "name": "Aurora Ave N & N 38th St", + "stopCode": "6340", + "stopId": "headway-1330:6340", + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 335, + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 20, + "points": "}bzaHjyviVG??A@q@FILTFNNRLNHEJHFHt@l@d@V\\JNBNBB?L?\\?" + }, + "mode": "WALK", + "pathway": false, + "realTime": false, + "rentedBike": false, + "route": "", + "startTime": 1715979360000, + "steps": [ + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": true, + "distance": 25.71, + "elevation": "", + "lat": 47.6525198, + "lon": -122.3466171, + "relativeDirection": "DEPART", + "stayOn": false, + "streetName": "sidewalk", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": false, + "distance": 40.3, + "elevation": "", + "lat": 47.6524649, + "lon": -122.3463029, + "relativeDirection": "HARD_RIGHT", + "stayOn": false, + "streetName": "Bridge Way North", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": false, + "distance": 6.19, + "elevation": "", + "lat": 47.6522039, + "lon": -122.3466732, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "Winslow Place North", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": true, + "distance": 132.1, + "elevation": "", + "lat": 47.652151, + "lon": -122.3466476, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "path", + "walkingBike": false + } + ], + "to": { + "arrival": 1715979548000, + "lat": 47.651048, + "lon": -122.347234, + "name": "Destination", + "vertexType": "NORMAL" + }, + "transitLeg": false, + "walkingBike": false + } + ], + "startTime": 1715977141000, + "tooSloped": false, + "transfers": 0, + "transitTime": 1850, + "waitingTime": 0, + "walkDistance": 647.09, + "walkLimitExceeded": false, + "walkTime": 557 + } + ], + "to": { + "lat": 47.651048, + "lon": -122.347234, + "name": "Destination", + "vertexType": "NORMAL" + } + }, + "previousPageCursor": "MXxQUkVWSU9VU19QQUdFfDIwMjQtMDUtMTdUMTg6NDM6NTRafHw1MG18U1RSRUVUX0FORF9BUlJJVkFMX1RJTUV8fHx8fHw=", + "requestParameters": { + "fromPlace": "47.575837,-122.339414", + "mode": "TRANSIT", + "toPlace": "47.651048,-122.347234" + } +} diff --git a/services/travelmux/tests/fixtures/requests/opentripplanner_transit_with_bicycle_plan.json b/services/travelmux/tests/fixtures/requests/opentripplanner_transit_with_bicycle_plan.json new file mode 100644 index 000000000..ef5133ee1 --- /dev/null +++ b/services/travelmux/tests/fixtures/requests/opentripplanner_transit_with_bicycle_plan.json @@ -0,0 +1,4169 @@ +{ + "debugOutput": { + "directStreetRouterTime": 122946830, + "filteringTime": 14906952, + "precalculationTime": 146002, + "renderingTime": 6800282, + "totalTime": 327074454, + "transitRouterTime": 182151762, + "transitRouterTimes": { + "accessEgressTime": 67393812, + "itineraryCreationTime": 9806856, + "raptorSearchTime": 96741379, + "tripPatternFilterTime": 8089087 + } + }, + "elevationMetadata": { + "ellipsoidToGeoidDifference": -19.263942171473552, + "geoidElevation": false + }, + "metadata": { + "nextDateTime": 1715977435000, + "prevDateTime": 1715971435000, + "searchWindowUsed": 3000 + }, + "nextPageCursor": "MXxORVhUX1BBR0V8MjAyNC0wNS0xN1QyMDoyMzo1NVp8fDUwbXxTVFJFRVRfQU5EX0FSUklWQUxfVElNRXx8fHx8fA==", + "plan": { + "date": 1715974435298, + "from": { + "lat": 47.575837, + "lon": -122.339414, + "name": "Origin", + "vertexType": "NORMAL" + }, + "itineraries": [ + { + "arrivedAtDestinationWithRentedBicycle": false, + "duration": 3039, + "elevationGained": 0.0, + "elevationLost": 0.0, + "endTime": 1715977474000, + "fare": { + "details": {}, + "fare": {} + }, + "generalizedCost": 6840, + "legs": [ + { + "agencyTimeZoneOffset": -25200000, + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 10392.34, + "duration": 3039.0, + "endTime": 1715977474000, + "from": { + "departure": 1715974435000, + "lat": 47.575837, + "lon": -122.339414, + "name": "Origin", + "vertexType": "NORMAL" + }, + "generalizedCost": 6840, + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 491, + "points": "}ckaHbkuiV`@@J??L?bA?p@?F?Pa@?}PCcRFq@@}W?{BCe@EYGUM[Um@y@a@g@KQEGq@w@uAiAa@UYQa@Q{EgBk@Q_@MOEsA_@DI]GGE?GSA]Eg@IyAc@YIkAWe@SeA_@y@QKC_AO[ASAcAGaACaCAqA@mEGgA?_@?iEBGAAC?K?QG?I?I?a@@GD_@?_ABm@?G?K@_EAGCIC_@?G@Y?U@I@M@OBG@UDWJ[NKFEHURQNABCBMJA@AgA?s@?sAHB?Q?iC?kB?Q?S?K?}A?K?Q?qA?KE??m@Ac@?]?]?C@m@?M?E?cB?e@?qA?O?IAi@?MAQCKCICGCOE[KYO[YYIEG@c@^WR_Av@m@j@SPMLuBjBGFSNML[XgA`AKHKJuBjBIHQJqBfBMJMLsBhBKJSPgB~AQNONoBdBMJQPeDvCMLSNgDzCMLQN}CpCSPSPcDvCKJWf@IR_CzEA@IPKTw@|Ai@fAYj@EHKNINCBEHERcBhDKHEHIPMVADKVqB`EINGIQWq@aAa@i@MSCCG??AMQIMEEyAuBCEEEMIMKACGKk@w@m@{@EEIOEEEGEGIKqAiBKOOUGIEGaAqAEEIGQICCg@GuC?E?OMS?eECM?Q?Y?oDAO?O?gEAO?Q?sB?y@Au@?W?_@AQ?eBAW?U?mB?c@?]?_@?E?Qk@SAG?CCIBC?C?Y@M@MBKBIBC@C@ACKGEACAaBfASLEBC@C?EBGBEBMH[RMLCDGDIFA?KD[LWLc@JSBYDY@CBG@E@GBE@K?G?C?G?E?GECAEEC?cB?M@I@I?o@?QAuC?o@AK@E?ODG@GAC?GAECGCKKECIIGCCAgCo@i@OGAOEICaBOI?]?kB?UBu@@mD@qA@G?cB@C@MHG@[Dc@FOBMDOD]LYJSJKDc@RG@M@G@uCvAIBGDOL_@PMRCBOFCBOBG@ULiAl@[Ry@b@EDCFCFEDIDmAn@E@C@OAC@EB]PWLIFKFGBG@y@d@[NEDq@h@eA|@aAt@u@l@}@t@STSXOVGNO`@Sj@a@hACF@BHF@FBFBP?FADgCfIeAjDIVENGPQb@Yb@i@r@UXIFMHMDGBG@M@W@i@@k@@iEAICuEAEACAGG?CCEA@G@AQ?W?kA@{@B]^qEBe@G?I@iA@}A?oA?cA?A?K?@I@IMAA?OAAe@MQSCm@\\mB}@EEC??DFHt@l@d@V\\JNBNBB?L?\\?" + }, + "mode": "BICYCLE", + "pathway": false, + "realTime": false, + "rentedBike": false, + "route": "", + "startTime": 1715974435000, + "steps": [ + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": false, + "distance": 26.0, + "elevation": "", + "lat": 47.5758346, + "lon": -122.3392181, + "relativeDirection": "DEPART", + "stayOn": false, + "streetName": "East Marginal Way South", + "walkingBike": false + }, + { + "absoluteDirection": "WEST", + "area": false, + "bogusName": false, + "distance": 59.45, + "elevation": "", + "lat": 47.5756008, + "lon": -122.3392243, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "South Hanford Street", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 1218.07, + "elevation": "", + "lat": 47.5756059, + "lon": -122.3400167, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "East Marginal Way South", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 515.99, + "elevation": "", + "lat": 47.58656, + "lon": -122.3400253, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "Alaskan Way South", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTHEAST", + "area": false, + "bogusName": false, + "distance": 4.97, + "elevation": "", + "lat": 47.5907595, + "lon": -122.337345, + "relativeDirection": "HARD_RIGHT", + "stayOn": false, + "streetName": "South Atlantic Street", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": true, + "distance": 186.23, + "elevation": "", + "lat": 47.5907275, + "lon": -122.3372987, + "relativeDirection": "HARD_LEFT", + "stayOn": false, + "streetName": "Marginal Way Cycle Route", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 659.69, + "elevation": "", + "lat": 47.592328, + "lon": -122.3367458, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "Portside Trail", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 129.47, + "elevation": "", + "lat": 47.5981133, + "lon": -122.3360192, + "relativeDirection": "LEFT", + "stayOn": true, + "streetName": "Portside Trail", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": true, + "distance": 111.29, + "elevation": "", + "lat": 47.5992728, + "lon": -122.3360808, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "Marginal Way Cycle Route", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": true, + "distance": 176.79, + "elevation": "", + "lat": 47.6002712, + "lon": -122.3360595, + "relativeDirection": "SLIGHTLY_RIGHT", + "stayOn": true, + "streetName": "Marginal Way Cycle Route", + "walkingBike": false + }, + { + "absoluteDirection": "NORTHWEST", + "area": false, + "bogusName": false, + "distance": 1.96, + "elevation": "", + "lat": 47.6017628, + "lon": -122.3366416, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "Elliott Bay Trail", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": true, + "distance": 77.8, + "elevation": "", + "lat": 47.6017781, + "lon": -122.3366546, + "relativeDirection": "HARD_RIGHT", + "stayOn": false, + "streetName": "bike path", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": true, + "distance": 5.04, + "elevation": "", + "lat": 47.6017831, + "lon": -122.3356171, + "relativeDirection": "RIGHT", + "stayOn": true, + "streetName": "path", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": false, + "distance": 199.61, + "elevation": "", + "lat": 47.6017396, + "lon": -122.335636, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "Yesler Way", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": true, + "distance": 4.21, + "elevation": "", + "lat": 47.601731, + "lon": -122.3329736, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "service road", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": false, + "distance": 219.26, + "elevation": "", + "lat": 47.6017688, + "lon": -122.3329762, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "Yesler Way Cycletrack", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": false, + "distance": 1895.86, + "elevation": "", + "lat": 47.6018657, + "lon": -122.3300857, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "4th Avenue Cycletrack", + "walkingBike": false + }, + { + "absoluteDirection": "NORTHEAST", + "area": false, + "bogusName": false, + "distance": 94.94, + "elevation": "", + "lat": 47.6153211, + "lon": -122.3439964, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "Bell Street", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 375.08, + "elevation": "", + "lat": 47.6159653, + "lon": -122.343165, + "relativeDirection": "SLIGHTLY_LEFT", + "stayOn": false, + "streetName": "Bell St Cycletrack", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 132.43, + "elevation": "", + "lat": 47.6185462, + "lon": -122.3399723, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "9th Ave Cycletrack", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 768.81, + "elevation": "", + "lat": 47.6197152, + "lon": -122.339792, + "relativeDirection": "SLIGHTLY_LEFT", + "stayOn": false, + "streetName": "9th Avenue North", + "walkingBike": false + }, + { + "absoluteDirection": "NORTHEAST", + "area": false, + "bogusName": true, + "distance": 19.74, + "elevation": "", + "lat": 47.6266292, + "lon": -122.3397288, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "bike path", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": true, + "distance": 27.65, + "elevation": "", + "lat": 47.6267195, + "lon": -122.339502, + "relativeDirection": "LEFT", + "stayOn": true, + "streetName": "bike path", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 47.87, + "elevation": "", + "lat": 47.6269619, + "lon": -122.3394925, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "Cheshiahud Lake Union Loop", + "walkingBike": false + }, + { + "absoluteDirection": "NORTHEAST", + "area": false, + "bogusName": false, + "distance": 88.27, + "elevation": "", + "lat": 47.6273847, + "lon": -122.3395952, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "Westlake Protected Bike Lane", + "walkingBike": false + }, + { + "absoluteDirection": "NORTHWEST", + "area": false, + "bogusName": false, + "distance": 208.81, + "elevation": "", + "lat": 47.6280979, + "lon": -122.3399451, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "Westlake Cycle Track", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 1768.5, + "elevation": "", + "lat": 47.6298708, + "lon": -122.3407137, + "relativeDirection": "SLIGHTLY_RIGHT", + "stayOn": false, + "streetName": "Westlake Protected Bike Lane", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": false, + "distance": 781.14, + "elevation": "", + "lat": 47.6446397, + "lon": -122.3456348, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "Cheshiahud Lake Union Loop", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": false, + "distance": 106.11, + "elevation": "", + "lat": 47.6494012, + "lon": -122.3486434, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "North 34th Street", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 193.79, + "elevation": "", + "lat": 47.6492019, + "lon": -122.347258, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "Troll Avenue North", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": false, + "distance": 7.88, + "elevation": "", + "lat": 47.6509446, + "lon": -122.347277, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "North 36th Street", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": true, + "distance": 17.1, + "elevation": "", + "lat": 47.6509293, + "lon": -122.3471743, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "path", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "alerts": [ + { + "alertHeaderText": "Unpaved surface" + } + ], + "area": false, + "bogusName": true, + "distance": 137.31, + "elevation": "", + "lat": 47.6510827, + "lon": -122.3471583, + "relativeDirection": "RIGHT", + "stayOn": true, + "streetName": "path", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": true, + "distance": 125.23, + "elevation": "", + "lat": 47.6520984, + "lon": -122.3466956, + "relativeDirection": "HARD_LEFT", + "stayOn": false, + "streetName": "sidewalk", + "walkingBike": true + } + ], + "to": { + "arrival": 1715977474000, + "lat": 47.651048, + "lon": -122.347234, + "name": "Destination", + "vertexType": "NORMAL" + }, + "transitLeg": false, + "walkingBike": false + } + ], + "startTime": 1715974435000, + "tooSloped": false, + "transfers": 0, + "transitTime": 0, + "waitingTime": 0, + "walkDistance": 10392.34, + "walkLimitExceeded": false, + "walkTime": 3039 + }, + { + "arrivedAtDestinationWithRentedBicycle": false, + "duration": 2125, + "elevationGained": 0.0, + "elevationLost": 0.0, + "endTime": 1715976840000, + "fare": { + "details": {}, + "fare": {}, + "legProducts": [ + { + "legIndices": [ + 1 + ], + "products": [ + { + "amount": { + "cents": 0, + "currency": { + "currency": "USD", + "currencyCode": "USD", + "defaultFractionDigits": 2, + "symbol": "$" + } + }, + "id": "headway-1330:300", + "name": "regular" + } + ] + }, + { + "legIndices": [ + 2 + ], + "products": [ + { + "amount": { + "cents": 0, + "currency": { + "currency": "USD", + "currencyCode": "USD", + "defaultFractionDigits": 2, + "symbol": "$" + } + }, + "id": "headway-1330:300", + "name": "regular" + } + ] + } + ] + }, + "generalizedCost": 3745, + "legs": [ + { + "agencyTimeZoneOffset": -25200000, + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 452.53, + "duration": 155.0, + "endTime": 1715974870000, + "from": { + "departure": 1715974715000, + "lat": 47.575837, + "lon": -122.339414, + "name": "Origin", + "vertexType": "NORMAL" + }, + "generalizedCost": 574, + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 26, + "points": "}ckaHbkuiV`@@J?@kB?mB?C?eB?q@?I?eA?W?K@S?E?s@?y@?_A?iA?y@@mC?g@S@?[?Cq@??H" + }, + "mode": "BICYCLE", + "pathway": false, + "realTime": false, + "rentedBike": false, + "route": "", + "startTime": 1715974715000, + "steps": [ + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": false, + "distance": 26.0, + "elevation": "", + "lat": 47.5758346, + "lon": -122.3392181, + "relativeDirection": "DEPART", + "stayOn": false, + "streetName": "East Marginal Way South", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": false, + "distance": 376.3, + "elevation": "", + "lat": 47.5756008, + "lon": -122.3392243, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "South Hanford Street", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 11.12, + "elevation": "", + "lat": 47.5755779, + "lon": -122.334208, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "1st Avenue South", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": true, + "distance": 9.87, + "elevation": "", + "lat": 47.5756779, + "lon": -122.3342102, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "path", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": true, + "distance": 29.24, + "elevation": "", + "lat": 47.575676, + "lon": -122.3340787, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "sidewalk", + "walkingBike": true + } + ], + "to": { + "arrival": 1715974870000, + "departure": 1715974870000, + "lat": 47.575924, + "lon": -122.334106, + "name": "1st Ave S & S Hanford St", + "stopCode": "15205", + "stopId": "headway-1330:15205", + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": false, + "walkingBike": false + }, + { + "agencyId": "headway-1330:1", + "agencyName": "Metro Transit", + "agencyTimeZoneOffset": -25200000, + "agencyUrl": "https://kingcounty.gov/en/dept/metro", + "alerts": [ + { + "alertDescriptionText": "This is an update to a previous Transit Alert. \r\nThe reroute end time for games on June 1 and June 15 have been updated.\r\n\nReroute will be in effect on each of the games noted below:\r\n- Sunday, April 28 at 3:30 PM-4:30 PM\r\n- Wednesday, May 1 at 3:00 PM-4:00 PM\r\n- Sunday, May 12 at 3:30 PM-4:30 PM\r\n- Wednesday, May 15 at 3:30 PM-4:30 PM\r\n- Thursday, May 30 at 3:30 PM-4:30 PM\r\n- Saturday, June 1 at 6:35 PM-7:35 PM\r\n- Sunday, June 2 at 3:30 PM-4:30 PM\r\n- Saturday, June 15 at 6:35 PM-7:35 PM\r\n- Sunday, June 16 at 3:30 PM-4:30 PM\r\n- Sunday, June 30 at 3:30 PM-4:30 PM\r\n- Thursday, July 4 at 3:30 PM-4:30 PM\r\n- Saturday, July 6 at 3:30 PM-4:30 PM\r\n- Sunday, July 7 at 3:30 PM-4:30 PM\r\n- Sunday, July 21 at 3:30 PM-4:30 PM\r\n- Wednesday, July 24 at 3:00 PM-4:00 PM\r\n- Sunday, August 4 at 3:30 PM-4:30 PM\r\n- Sunday, August 11 at 3:30 PM-4:30 PM\r\n- Saturday, August 24 at 3:30 PM-4:30 PM\r\n- Sunday, August 25 at 3:30 PM-4:30 PM\r\n- Wednesday, August 28 at 3:30 PM-4:30 PM\r\n- Sunday, September 15 at 3:30 PM-4:30 PM\r\n- Thursday, September 19 at 3:30 PM-4:30 PM\r\n- Sunday, September 29 at 2:30 PM-3:30 PM\r\n\nAffected stops:\r\nStop #15190 1st Ave S & S Spokane St (NB)\r\nStop #15205 1st Ave S & S Hanford St (NB)\r\nStop #15230 1st Ave S & S Lander St (NB)\r\nStop #15206 1st Ave S & S Stacy St (NB)\r\nStop #15212 1st Ave S & S Walker St (NB)\r\nStop #15207 1st Ave S & S Holgate St (NB)\r\nStop #15204 Edgar Martinez Dr S & Occidental Ave S (EB)\r\nStop #15201 1st Ave S & S Atlantic St (SB)\r\nStop #15202 1st Ave S & S Holgate St (SB)\r\nStop #15211 1st Ave S & S Walker St (SB)\r\nStop #15203 1st Ave S & S Stacy St (SB)\r\n\nFor Route 21 to Downtown Seattle get on/off buses at:\r\nStop #15145 SW Spokane St & Chelan Ave SW (EB)\r\nStop #30538 4th Ave S & S Spokane St (NB)\r\nStop #30550 4th Ave S & S Hanford St (NB)\r\nStop #30560 4th Ave S & S Forest St (NB)\r\nStop #30570 4th Ave S & S Lander St (NB)\r\nStop #30590 4th Ave S & S Walker St (NB)\r\nStop #30600 4th Ave S & S Holgate St (NB)\r\nStop #30635 4th Ave S & S Royal Brougham Way (NB)\r\n\nFor Route 21 to Westwood Village get on/off buses at:\r\nStop #515 3rd Ave S & S Main St (SB)\r\nStop #30670 4th Ave S & Edgar Martinez Dr S (SB)\r\nStop #30690 4th Ave S & S Holgate St (SB)\r\nStop #30700 4th Ave S & S Walker St (SB)\r\nStop #15380 1st Ave S & S Lander St (SB)", + "alertHeaderText": "Route 21 will be affected in both directions during Mariners afternoon home games; stops on portions of 1st Ave S will not be served; use alternate stops on 4th Ave S instead.", + "alertUrl": "https://drive.google.com/file/d/1rOKsBD54nHp8U1Sh3GGpQQpLMNhXwTDf/view", + "effectiveEndDate": 1727649000000, + "effectiveStartDate": 1711924200000 + } + ], + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 2322.8, + "duration": 410.0, + "endTime": 1715975280000, + "from": { + "arrival": 1715974870000, + "departure": 1715974870000, + "lat": 47.575924, + "lon": -122.334106, + "name": "1st Ave S & S Hanford St", + "stopCode": "15205", + "stopId": "headway-1330:15205", + "stopIndex": 28, + "stopSequence": 194, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 1010, + "headsign": "Downtown Seattle Via 35th Ave SW", + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 41, + "points": "odkaHvktiVgJ?gK???W?aL?cA???}I?s@???qJ?sA???uI?yK?@oC???g@?_AEyB?gB?cA?mC?cCAgBCiBEMACCCCAECI?{FAc@D[RQ?Y?W?G?sD?" + }, + "mode": "BUS", + "pathway": false, + "realTime": false, + "routeId": "headway-1330:100101", + "routeShortName": "21", + "routeType": 3, + "serviceDate": "2024-05-17", + "startTime": 1715974870000, + "steps": [], + "to": { + "arrival": 1715975280000, + "departure": 1715975280000, + "lat": 47.593342, + "lon": -122.328957, + "name": "4th Ave S & S Royal Brougham Way", + "stopCode": "30635", + "stopId": "headway-1330:30635", + "stopIndex": 34, + "stopSequence": 229, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": true, + "tripBlockId": "7142607", + "tripId": "headway-1330:605082596" + }, + { + "agencyId": "headway-1330:1", + "agencyName": "Metro Transit", + "agencyTimeZoneOffset": -25200000, + "agencyUrl": "https://kingcounty.gov/en/dept/metro", + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 7187.77, + "duration": 1380.0, + "endTime": 1715976660000, + "from": { + "arrival": 1715975280000, + "departure": 1715975280000, + "lat": 47.593342, + "lon": -122.328957, + "name": "4th Ave S & S Royal Brougham Way", + "stopCode": "30635", + "stopId": "headway-1330:30635", + "stopIndex": 0, + "stopSequence": 1, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 1380, + "headsign": "Shoreline Greenwood", + "interlineWithPreviousLeg": true, + "legGeometry": { + "length": 195, + "points": "kqnaHpksiVmD?e@?_L?}ECoDCqCAYLY@c@B[CIA??QCUMeDBuAtAILKJEJGLo@rBM^GPIHGLa@\\gB~A??_@\\oC`C]ZoBbBoCbCuBlB??WTmC`CoC~BmCbCqDbD??SRgErDwChCm@j@gA`A??cA`AIFGFGHIPMTOVKRO`@aB`DkBvD??k@jAyCdGyCbGiAtB??oAjCaAsAaAwAcAuAy@kAIIaCkDa@?wDZkB@??}BD]IsA?uB?_@?{@C_@As@CY???E?O?K?MBODM@KDMBOFOJIFKDODOFO@O@M?OAUEWG]M]IQEQEWC]E_@E]A_@GcC@[?o@?}CGcAEiAAoBEaBCsCA{@???mGAeJAi@???qDCcFAgCC]?Q?QBO?O@QBOBQDMBMBOFODQJOFMHSHiCbB??IBwCjBsAx@]RMFCB??YN[No@\\m@Va@R[Nc@Ri@Pi@Rs@Te@Nw@T_Cp@SD??}@ReATYHUDYDQDY@_AFo@Dg@Bq@?q@?eNB}Y@i@IYIYKQKOIi@c@US{@?" + }, + "mode": "BUS", + "pathway": false, + "realTime": false, + "routeId": "headway-1330:100229", + "routeShortName": "5", + "routeType": 3, + "serviceDate": "2024-05-17", + "startTime": 1715975280000, + "steps": [], + "to": { + "arrival": 1715976660000, + "departure": 1715976660000, + "lat": 47.652473, + "lon": -122.346619, + "name": "Aurora Ave N & N 38th St", + "stopCode": "6340", + "stopId": "headway-1330:6340", + "stopIndex": 15, + "stopSequence": 181, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": true, + "tripBlockId": "7142607", + "tripId": "headway-1330:605083836" + }, + { + "agencyTimeZoneOffset": -25200000, + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 205.38, + "duration": 180.0, + "endTime": 1715976840000, + "from": { + "arrival": 1715976660000, + "departure": 1715976660000, + "lat": 47.652473, + "lon": -122.346619, + "name": "Aurora Ave N & N 38th St", + "stopCode": "6340", + "stopId": "headway-1330:6340", + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 779, + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 20, + "points": "}bzaHjyviVG??A@q@FIDGNXPZLNFDJHFHt@l@d@V\\JNBNBB?L?\\?" + }, + "mode": "BICYCLE", + "pathway": false, + "realTime": false, + "rentedBike": false, + "route": "", + "startTime": 1715976660000, + "steps": [ + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": true, + "distance": 30.56, + "elevation": "", + "lat": 47.6525198, + "lon": -122.3466171, + "relativeDirection": "DEPART", + "stayOn": false, + "streetName": "sidewalk", + "walkingBike": true + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": true, + "distance": 37.35, + "elevation": "", + "lat": 47.6524313, + "lon": -122.3462617, + "relativeDirection": "RIGHT", + "stayOn": true, + "streetName": "sidewalk", + "walkingBike": true + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": true, + "distance": 12.25, + "elevation": "", + "lat": 47.6521922, + "lon": -122.34661, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "path", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": true, + "distance": 125.23, + "elevation": "", + "lat": 47.6520984, + "lon": -122.3466956, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "sidewalk", + "walkingBike": true + } + ], + "to": { + "arrival": 1715976840000, + "lat": 47.651048, + "lon": -122.347234, + "name": "Destination", + "vertexType": "NORMAL" + }, + "transitLeg": false, + "walkingBike": false + } + ], + "startTime": 1715974715000, + "tooSloped": false, + "transfers": 0, + "transitTime": 1790, + "waitingTime": 0, + "walkDistance": 657.91, + "walkLimitExceeded": false, + "walkTime": 335 + }, + { + "arrivedAtDestinationWithRentedBicycle": false, + "duration": 2138, + "elevationGained": 0.0, + "elevationLost": 0.0, + "endTime": 1715977440000, + "fare": { + "details": {}, + "fare": {}, + "legProducts": [ + { + "legIndices": [ + 1 + ], + "products": [ + { + "amount": { + "cents": 0, + "currency": { + "currency": "USD", + "currencyCode": "USD", + "defaultFractionDigits": 2, + "symbol": "$" + } + }, + "id": "headway-1330:300", + "name": "regular" + } + ] + }, + { + "legIndices": [ + 2 + ], + "products": [ + { + "amount": { + "cents": 0, + "currency": { + "currency": "USD", + "currencyCode": "USD", + "defaultFractionDigits": 2, + "symbol": "$" + } + }, + "id": "headway-1330:300", + "name": "regular" + } + ] + } + ] + }, + "generalizedCost": 4211, + "legs": [ + { + "agencyTimeZoneOffset": -25200000, + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 1094.22, + "duration": 342.0, + "endTime": 1715975644000, + "from": { + "departure": 1715975302000, + "lat": 47.575837, + "lon": -122.339414, + "name": "Origin", + "vertexType": "NORMAL" + }, + "generalizedCost": 1215, + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 46, + "points": "}ckaHbkuiV`@@J?@kB?mB?C?eB?q@?I?eA?W?K@S?E?s@?y@?_A?iA?y@@mC?g@?_@?]?{@?M?u@jF?@wB?gA?w@?i@?M?K?I?W@iHgA?o@?C?iB??wC?a@H??g@p@??P" + }, + "mode": "BICYCLE", + "pathway": false, + "realTime": false, + "rentedBike": false, + "route": "", + "startTime": 1715975302000, + "steps": [ + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": false, + "distance": 26.0, + "elevation": "", + "lat": 47.5758346, + "lon": -122.3392181, + "relativeDirection": "DEPART", + "stayOn": false, + "streetName": "East Marginal Way South", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": false, + "distance": 448.07, + "elevation": "", + "lat": 47.5756008, + "lon": -122.3392243, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "South Hanford Street", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": false, + "distance": 131.92, + "elevation": "", + "lat": 47.5755778, + "lon": -122.3332512, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "Occidental Avenue South", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": false, + "distance": 242.59, + "elevation": "", + "lat": 47.5743914, + "lon": -122.3332503, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "South Horton Street", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 127.13, + "elevation": "", + "lat": 47.5743767, + "lon": -122.3300166, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "3rd Avenue South", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": false, + "distance": 69.81, + "elevation": "", + "lat": 47.57552, + "lon": -122.3300166, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "South Hanford Street", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": false, + "distance": 5.69, + "elevation": "", + "lat": 47.5755223, + "lon": -122.329086, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "4th Avenue South", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": true, + "distance": 15.3, + "elevation": "", + "lat": 47.5754711, + "lon": -122.3290854, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "service road", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": true, + "distance": 27.71, + "elevation": "", + "lat": 47.5754712, + "lon": -122.3288814, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "sidewalk", + "walkingBike": true + } + ], + "to": { + "arrival": 1715975644000, + "departure": 1715975644000, + "lat": 47.575222, + "lon": -122.328972, + "name": "4th Ave S & S Hanford St", + "stopCode": "30550", + "stopId": "headway-1330:30550", + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": false, + "walkingBike": false + }, + { + "agencyId": "headway-1330:1", + "agencyName": "Metro Transit", + "agencyTimeZoneOffset": -25200000, + "agencyUrl": "https://kingcounty.gov/en/dept/metro", + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 2016.14, + "duration": 356.0, + "endTime": 1715976000000, + "from": { + "arrival": 1715975644000, + "departure": 1715975644000, + "lat": 47.575222, + "lon": -122.328972, + "name": "4th Ave S & S Hanford St", + "stopCode": "30550", + "stopId": "headway-1330:30550", + "stopIndex": 52, + "stopSequence": 427, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 956, + "headsign": "Downtown Seattle South Park", + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 28, + "points": "c`kaHtksiV{@?mL???I?_L?uA???kI?cJ???eA?kL?m@???mJCq@?gAWoCDkDLqB?_CCwAFQ?Y?W?G?sD?" + }, + "mode": "BUS", + "pathway": false, + "realTime": false, + "routeId": "headway-1330:100031", + "routeShortName": "132", + "routeType": 3, + "serviceDate": "2024-05-17", + "startTime": 1715975644000, + "steps": [], + "to": { + "arrival": 1715976000000, + "departure": 1715976000000, + "lat": 47.593342, + "lon": -122.328957, + "name": "4th Ave S & S Royal Brougham Way", + "stopCode": "30635", + "stopId": "headway-1330:30635", + "stopIndex": 57, + "stopSequence": 450, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": true, + "tripBlockId": "7159185", + "tripId": "headway-1330:473763106" + }, + { + "agencyId": "headway-1330:1", + "agencyName": "Metro Transit", + "agencyTimeZoneOffset": -25200000, + "agencyUrl": "https://kingcounty.gov/en/dept/metro", + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 7187.72, + "duration": 1260.0, + "endTime": 1715977260000, + "from": { + "arrival": 1715976000000, + "departure": 1715976000000, + "lat": 47.593342, + "lon": -122.328957, + "name": "4th Ave S & S Royal Brougham Way", + "stopCode": "30635", + "stopId": "headway-1330:30635", + "stopIndex": 0, + "stopSequence": 1, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 1260, + "headsign": "Carkeek Park", + "interlineWithPreviousLeg": true, + "legGeometry": { + "length": 189, + "points": "kqnaHpksiVmD?e@?_L?}ECoDCqCAYLY@c@B[CIA??QCUMeDBuAtAILKJEJGLo@rBM^GPIHGLa@\\gB~A??_@\\oC`C]ZoBbBoCbCuBlB??WTmC`CoC~BmCbCqDbD??SRgErDwChCm@j@gA`A??cA`AIFGFGHIPMTOVKRO`@aB`DkBvD??k@jAyCdGyCbGiAtB??oAjCaAsAaAwAcAuAy@kAIIaCkDa@?wDZkB@??}BD]IsA?uB?_@?{@C_@As@CY???E?O?K?MBODM@KDMBOFOJIFKDODOFO@O@M?OAUEWG]M]IQEQEWC]E_@E]A_@GcC@[?o@?}CGcAEiAAoBEaBCsCAiIAeJAi@???qDCcFAgCC]?Q?QBO?O@QBOBQDMBMBOFODQJOFMHSHsCfBwCjBsAx@]RMFCB??YN[No@\\m@Va@R[Nc@Ri@Pi@Rs@Te@Nw@T_Cp@qAXeATYHUDYDQDY@_AFo@Dg@Bq@?q@?eNB}Y@i@IYIYKQKOIi@c@US{@?" + }, + "mode": "BUS", + "pathway": false, + "realTime": false, + "routeId": "headway-1330:100169", + "routeShortName": "28", + "routeType": 3, + "serviceDate": "2024-05-17", + "startTime": 1715976000000, + "steps": [], + "to": { + "arrival": 1715977260000, + "departure": 1715977260000, + "lat": 47.652473, + "lon": -122.346619, + "name": "Aurora Ave N & N 38th St", + "stopCode": "6340", + "stopId": "headway-1330:6340", + "stopIndex": 12, + "stopSequence": 178, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": true, + "tripBlockId": "7159185", + "tripId": "headway-1330:651759926", + "tripShortName": "EXPRESS" + }, + { + "agencyTimeZoneOffset": -25200000, + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 205.38, + "duration": 180.0, + "endTime": 1715977440000, + "from": { + "arrival": 1715977260000, + "departure": 1715977260000, + "lat": 47.652473, + "lon": -122.346619, + "name": "Aurora Ave N & N 38th St", + "stopCode": "6340", + "stopId": "headway-1330:6340", + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 779, + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 20, + "points": "}bzaHjyviVG??A@q@FIDGNXPZLNFDJHFHt@l@d@V\\JNBNBB?L?\\?" + }, + "mode": "BICYCLE", + "pathway": false, + "realTime": false, + "rentedBike": false, + "route": "", + "startTime": 1715977260000, + "steps": [ + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": true, + "distance": 30.56, + "elevation": "", + "lat": 47.6525198, + "lon": -122.3466171, + "relativeDirection": "DEPART", + "stayOn": false, + "streetName": "sidewalk", + "walkingBike": true + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": true, + "distance": 37.35, + "elevation": "", + "lat": 47.6524313, + "lon": -122.3462617, + "relativeDirection": "RIGHT", + "stayOn": true, + "streetName": "sidewalk", + "walkingBike": true + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": true, + "distance": 12.25, + "elevation": "", + "lat": 47.6521922, + "lon": -122.34661, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "path", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": true, + "distance": 125.23, + "elevation": "", + "lat": 47.6520984, + "lon": -122.3466956, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "sidewalk", + "walkingBike": true + } + ], + "to": { + "arrival": 1715977440000, + "lat": 47.651048, + "lon": -122.347234, + "name": "Destination", + "vertexType": "NORMAL" + }, + "transitLeg": false, + "walkingBike": false + } + ], + "startTime": 1715975302000, + "tooSloped": false, + "transfers": 0, + "transitTime": 1616, + "waitingTime": 0, + "walkDistance": 1299.6, + "walkLimitExceeded": false, + "walkTime": 522 + }, + { + "arrivedAtDestinationWithRentedBicycle": false, + "duration": 2185, + "elevationGained": 0.0, + "elevationLost": 0.0, + "endTime": 1715977740000, + "fare": { + "details": {}, + "fare": {}, + "legProducts": [ + { + "legIndices": [ + 1 + ], + "products": [ + { + "amount": { + "cents": 0, + "currency": { + "currency": "USD", + "currencyCode": "USD", + "defaultFractionDigits": 2, + "symbol": "$" + } + }, + "id": "headway-1330:300", + "name": "regular" + } + ] + }, + { + "legIndices": [ + 2 + ], + "products": [ + { + "amount": { + "cents": 0, + "currency": { + "currency": "USD", + "currencyCode": "USD", + "defaultFractionDigits": 2, + "symbol": "$" + } + }, + "id": "headway-1330:300", + "name": "regular" + } + ] + } + ] + }, + "generalizedCost": 3805, + "legs": [ + { + "agencyTimeZoneOffset": -25200000, + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 452.53, + "duration": 155.0, + "endTime": 1715975710000, + "from": { + "departure": 1715975555000, + "lat": 47.575837, + "lon": -122.339414, + "name": "Origin", + "vertexType": "NORMAL" + }, + "generalizedCost": 574, + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 26, + "points": "}ckaHbkuiV`@@J?@kB?mB?C?eB?q@?I?eA?W?K@S?E?s@?y@?_A?iA?y@@mC?g@S@?[?Cq@??H" + }, + "mode": "BICYCLE", + "pathway": false, + "realTime": false, + "rentedBike": false, + "route": "", + "startTime": 1715975555000, + "steps": [ + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": false, + "distance": 26.0, + "elevation": "", + "lat": 47.5758346, + "lon": -122.3392181, + "relativeDirection": "DEPART", + "stayOn": false, + "streetName": "East Marginal Way South", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": false, + "distance": 376.3, + "elevation": "", + "lat": 47.5756008, + "lon": -122.3392243, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "South Hanford Street", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 11.12, + "elevation": "", + "lat": 47.5755779, + "lon": -122.334208, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "1st Avenue South", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": true, + "distance": 9.87, + "elevation": "", + "lat": 47.5756779, + "lon": -122.3342102, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "path", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": true, + "distance": 29.24, + "elevation": "", + "lat": 47.575676, + "lon": -122.3340787, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "sidewalk", + "walkingBike": true + } + ], + "to": { + "arrival": 1715975710000, + "departure": 1715975710000, + "lat": 47.575924, + "lon": -122.334106, + "name": "1st Ave S & S Hanford St", + "stopCode": "15205", + "stopId": "headway-1330:15205", + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": false, + "walkingBike": false + }, + { + "agencyId": "headway-1330:1", + "agencyName": "Metro Transit", + "agencyTimeZoneOffset": -25200000, + "agencyUrl": "https://kingcounty.gov/en/dept/metro", + "alerts": [ + { + "alertDescriptionText": "This is an update to a previous Transit Alert. \r\nThe reroute end time for games on June 1 and June 15 have been updated.\r\n\nReroute will be in effect on each of the games noted below:\r\n- Sunday, April 28 at 3:30 PM-4:30 PM\r\n- Wednesday, May 1 at 3:00 PM-4:00 PM\r\n- Sunday, May 12 at 3:30 PM-4:30 PM\r\n- Wednesday, May 15 at 3:30 PM-4:30 PM\r\n- Thursday, May 30 at 3:30 PM-4:30 PM\r\n- Saturday, June 1 at 6:35 PM-7:35 PM\r\n- Sunday, June 2 at 3:30 PM-4:30 PM\r\n- Saturday, June 15 at 6:35 PM-7:35 PM\r\n- Sunday, June 16 at 3:30 PM-4:30 PM\r\n- Sunday, June 30 at 3:30 PM-4:30 PM\r\n- Thursday, July 4 at 3:30 PM-4:30 PM\r\n- Saturday, July 6 at 3:30 PM-4:30 PM\r\n- Sunday, July 7 at 3:30 PM-4:30 PM\r\n- Sunday, July 21 at 3:30 PM-4:30 PM\r\n- Wednesday, July 24 at 3:00 PM-4:00 PM\r\n- Sunday, August 4 at 3:30 PM-4:30 PM\r\n- Sunday, August 11 at 3:30 PM-4:30 PM\r\n- Saturday, August 24 at 3:30 PM-4:30 PM\r\n- Sunday, August 25 at 3:30 PM-4:30 PM\r\n- Wednesday, August 28 at 3:30 PM-4:30 PM\r\n- Sunday, September 15 at 3:30 PM-4:30 PM\r\n- Thursday, September 19 at 3:30 PM-4:30 PM\r\n- Sunday, September 29 at 2:30 PM-3:30 PM\r\n\nAffected stops:\r\nStop #15190 1st Ave S & S Spokane St (NB)\r\nStop #15205 1st Ave S & S Hanford St (NB)\r\nStop #15230 1st Ave S & S Lander St (NB)\r\nStop #15206 1st Ave S & S Stacy St (NB)\r\nStop #15212 1st Ave S & S Walker St (NB)\r\nStop #15207 1st Ave S & S Holgate St (NB)\r\nStop #15204 Edgar Martinez Dr S & Occidental Ave S (EB)\r\nStop #15201 1st Ave S & S Atlantic St (SB)\r\nStop #15202 1st Ave S & S Holgate St (SB)\r\nStop #15211 1st Ave S & S Walker St (SB)\r\nStop #15203 1st Ave S & S Stacy St (SB)\r\n\nFor Route 21 to Downtown Seattle get on/off buses at:\r\nStop #15145 SW Spokane St & Chelan Ave SW (EB)\r\nStop #30538 4th Ave S & S Spokane St (NB)\r\nStop #30550 4th Ave S & S Hanford St (NB)\r\nStop #30560 4th Ave S & S Forest St (NB)\r\nStop #30570 4th Ave S & S Lander St (NB)\r\nStop #30590 4th Ave S & S Walker St (NB)\r\nStop #30600 4th Ave S & S Holgate St (NB)\r\nStop #30635 4th Ave S & S Royal Brougham Way (NB)\r\n\nFor Route 21 to Westwood Village get on/off buses at:\r\nStop #515 3rd Ave S & S Main St (SB)\r\nStop #30670 4th Ave S & Edgar Martinez Dr S (SB)\r\nStop #30690 4th Ave S & S Holgate St (SB)\r\nStop #30700 4th Ave S & S Walker St (SB)\r\nStop #15380 1st Ave S & S Lander St (SB)", + "alertHeaderText": "Route 21 will be affected in both directions during Mariners afternoon home games; stops on portions of 1st Ave S will not be served; use alternate stops on 4th Ave S instead.", + "alertUrl": "https://drive.google.com/file/d/1rOKsBD54nHp8U1Sh3GGpQQpLMNhXwTDf/view", + "effectiveEndDate": 1727649000000, + "effectiveStartDate": 1711924200000 + } + ], + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 2322.8, + "duration": 410.0, + "endTime": 1715976120000, + "from": { + "arrival": 1715975710000, + "departure": 1715975710000, + "lat": 47.575924, + "lon": -122.334106, + "name": "1st Ave S & S Hanford St", + "stopCode": "15205", + "stopId": "headway-1330:15205", + "stopIndex": 26, + "stopSequence": 183, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 1010, + "headsign": "Downtown Seattle Via 35th Ave SW", + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 41, + "points": "odkaHvktiVgJ?gK???W?aL?cA???}I?s@???qJ?sA???uI?yK?@oC???g@?_AEyB?gB?cA?mC?cCAgBCiBEMACCCCAECI?{FAc@D[RQ?Y?W?G?sD?" + }, + "mode": "BUS", + "pathway": false, + "realTime": false, + "routeId": "headway-1330:100101", + "routeShortName": "21", + "routeType": 3, + "serviceDate": "2024-05-17", + "startTime": 1715975710000, + "steps": [], + "to": { + "arrival": 1715976120000, + "departure": 1715976120000, + "lat": 47.593342, + "lon": -122.328957, + "name": "4th Ave S & S Royal Brougham Way", + "stopCode": "30635", + "stopId": "headway-1330:30635", + "stopIndex": 32, + "stopSequence": 218, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": true, + "tripBlockId": "7142679", + "tripId": "headway-1330:605084226" + }, + { + "agencyId": "headway-1330:1", + "agencyName": "Metro Transit", + "agencyTimeZoneOffset": -25200000, + "agencyUrl": "https://kingcounty.gov/en/dept/metro", + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 7187.77, + "duration": 1440.0, + "endTime": 1715977560000, + "from": { + "arrival": 1715976120000, + "departure": 1715976120000, + "lat": 47.593342, + "lon": -122.328957, + "name": "4th Ave S & S Royal Brougham Way", + "stopCode": "30635", + "stopId": "headway-1330:30635", + "stopIndex": 0, + "stopSequence": 1, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 1440, + "headsign": "Shoreline Greenwood", + "interlineWithPreviousLeg": true, + "legGeometry": { + "length": 195, + "points": "kqnaHpksiVmD?e@?_L?}ECoDCqCAYLY@c@B[CIA??QCUMeDBuAtAILKJEJGLo@rBM^GPIHGLa@\\gB~A??_@\\oC`C]ZoBbBoCbCuBlB??WTmC`CoC~BmCbCqDbD??SRgErDwChCm@j@gA`A??cA`AIFGFGHIPMTOVKRO`@aB`DkBvD??k@jAyCdGyCbGiAtB??oAjCaAsAaAwAcAuAy@kAIIaCkDa@?wDZkB@??}BD]IsA?uB?_@?{@C_@As@CY???E?O?K?MBODM@KDMBOFOJIFKDODOFO@O@M?OAUEWG]M]IQEQEWC]E_@E]A_@GcC@[?o@?}CGcAEiAAoBEaBCsCA{@???mGAeJAi@???qDCcFAgCC]?Q?QBO?O@QBOBQDMBMBOFODQJOFMHSHiCbB??IBwCjBsAx@]RMFCB??YN[No@\\m@Va@R[Nc@Ri@Pi@Rs@Te@Nw@T_Cp@SD??}@ReATYHUDYDQDY@_AFo@Dg@Bq@?q@?eNB}Y@i@IYIYKQKOIi@c@US{@?" + }, + "mode": "BUS", + "pathway": false, + "realTime": false, + "routeId": "headway-1330:100229", + "routeShortName": "5", + "routeType": 3, + "serviceDate": "2024-05-17", + "startTime": 1715976120000, + "steps": [], + "to": { + "arrival": 1715977560000, + "departure": 1715977560000, + "lat": 47.652473, + "lon": -122.346619, + "name": "Aurora Ave N & N 38th St", + "stopCode": "6340", + "stopId": "headway-1330:6340", + "stopIndex": 15, + "stopSequence": 181, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": true, + "tripBlockId": "7142679", + "tripId": "headway-1330:571900176" + }, + { + "agencyTimeZoneOffset": -25200000, + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 205.38, + "duration": 180.0, + "endTime": 1715977740000, + "from": { + "arrival": 1715977560000, + "departure": 1715977560000, + "lat": 47.652473, + "lon": -122.346619, + "name": "Aurora Ave N & N 38th St", + "stopCode": "6340", + "stopId": "headway-1330:6340", + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 779, + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 20, + "points": "}bzaHjyviVG??A@q@FIDGNXPZLNFDJHFHt@l@d@V\\JNBNBB?L?\\?" + }, + "mode": "BICYCLE", + "pathway": false, + "realTime": false, + "rentedBike": false, + "route": "", + "startTime": 1715977560000, + "steps": [ + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": true, + "distance": 30.56, + "elevation": "", + "lat": 47.6525198, + "lon": -122.3466171, + "relativeDirection": "DEPART", + "stayOn": false, + "streetName": "sidewalk", + "walkingBike": true + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": true, + "distance": 37.35, + "elevation": "", + "lat": 47.6524313, + "lon": -122.3462617, + "relativeDirection": "RIGHT", + "stayOn": true, + "streetName": "sidewalk", + "walkingBike": true + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": true, + "distance": 12.25, + "elevation": "", + "lat": 47.6521922, + "lon": -122.34661, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "path", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": true, + "distance": 125.23, + "elevation": "", + "lat": 47.6520984, + "lon": -122.3466956, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "sidewalk", + "walkingBike": true + } + ], + "to": { + "arrival": 1715977740000, + "lat": 47.651048, + "lon": -122.347234, + "name": "Destination", + "vertexType": "NORMAL" + }, + "transitLeg": false, + "walkingBike": false + } + ], + "startTime": 1715975555000, + "tooSloped": false, + "transfers": 0, + "transitTime": 1850, + "waitingTime": 0, + "walkDistance": 657.91, + "walkLimitExceeded": false, + "walkTime": 335 + }, + { + "arrivedAtDestinationWithRentedBicycle": false, + "duration": 2602, + "elevationGained": 0.0, + "elevationLost": 0.0, + "endTime": 1715978485000, + "fare": { + "details": {}, + "fare": {}, + "legProducts": [ + { + "legIndices": [ + 1 + ], + "products": [ + { + "amount": { + "cents": 0, + "currency": { + "currency": "USD", + "currencyCode": "USD", + "defaultFractionDigits": 2, + "symbol": "$" + } + }, + "id": "headway-1330:300", + "name": "regular" + } + ] + } + ] + }, + "generalizedCost": 6547, + "legs": [ + { + "agencyTimeZoneOffset": -25200000, + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 3611.11, + "duration": 1017.0, + "endTime": 1715976900000, + "from": { + "departure": 1715975883000, + "lat": 47.575837, + "lon": -122.339414, + "name": "Origin", + "vertexType": "NORMAL" + }, + "generalizedCost": 2644, + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 130, + "points": "}ckaHbkuiV`@@J??L?bA?p@?F?Pa@?}PCcRFq@@}W?{BCe@EYGUM[Um@y@a@g@KQEGq@w@uAiAa@UYQa@Q{EgBk@Q_@MOEsA_@DI]GGE?GSA]Eg@IyAc@YIkAWe@SeA_@y@QKC_AO[ASAcAGaACaCAqA@mEGgA?_@?iEBGAAC?K?QG?I?I?a@@GD_@?_ABm@?G?K@_EAGCIC_@?G@Y?U@I@M@OBG@UDWJ[NKFEHURQNABCBMJA@AgA?s@?sAHB?Q?iC?kB?Q?S?K?}A?K?Q?qA?KE??m@Ac@?]?]?C@m@?M?E?cB?e@?qA?O?IAi@?MHY?k@LI@A?IA?" + }, + "mode": "BICYCLE", + "pathway": false, + "realTime": false, + "rentedBike": false, + "route": "", + "startTime": 1715975883000, + "steps": [ + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": false, + "distance": 26.0, + "elevation": "", + "lat": 47.5758346, + "lon": -122.3392181, + "relativeDirection": "DEPART", + "stayOn": false, + "streetName": "East Marginal Way South", + "walkingBike": false + }, + { + "absoluteDirection": "WEST", + "area": false, + "bogusName": false, + "distance": 59.45, + "elevation": "", + "lat": 47.5756008, + "lon": -122.3392243, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "South Hanford Street", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 1218.07, + "elevation": "", + "lat": 47.5756059, + "lon": -122.3400167, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "East Marginal Way South", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 515.99, + "elevation": "", + "lat": 47.58656, + "lon": -122.3400253, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "Alaskan Way South", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTHEAST", + "area": false, + "bogusName": false, + "distance": 4.97, + "elevation": "", + "lat": 47.5907595, + "lon": -122.337345, + "relativeDirection": "HARD_RIGHT", + "stayOn": false, + "streetName": "South Atlantic Street", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": true, + "distance": 186.23, + "elevation": "", + "lat": 47.5907275, + "lon": -122.3372987, + "relativeDirection": "HARD_LEFT", + "stayOn": false, + "streetName": "Marginal Way Cycle Route", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 659.69, + "elevation": "", + "lat": 47.592328, + "lon": -122.3367458, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "Portside Trail", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 129.47, + "elevation": "", + "lat": 47.5981133, + "lon": -122.3360192, + "relativeDirection": "LEFT", + "stayOn": true, + "streetName": "Portside Trail", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": true, + "distance": 111.29, + "elevation": "", + "lat": 47.5992728, + "lon": -122.3360808, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "Marginal Way Cycle Route", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": true, + "distance": 176.79, + "elevation": "", + "lat": 47.6002712, + "lon": -122.3360595, + "relativeDirection": "SLIGHTLY_RIGHT", + "stayOn": true, + "streetName": "Marginal Way Cycle Route", + "walkingBike": false + }, + { + "absoluteDirection": "NORTHWEST", + "area": false, + "bogusName": false, + "distance": 1.96, + "elevation": "", + "lat": 47.6017628, + "lon": -122.3366416, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "Elliott Bay Trail", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": true, + "distance": 77.8, + "elevation": "", + "lat": 47.6017781, + "lon": -122.3366546, + "relativeDirection": "HARD_RIGHT", + "stayOn": false, + "streetName": "bike path", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": true, + "distance": 5.04, + "elevation": "", + "lat": 47.6017831, + "lon": -122.3356171, + "relativeDirection": "RIGHT", + "stayOn": true, + "streetName": "path", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": false, + "distance": 199.61, + "elevation": "", + "lat": 47.6017396, + "lon": -122.335636, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "Yesler Way", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": true, + "distance": 4.21, + "elevation": "", + "lat": 47.601731, + "lon": -122.3329736, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "service road", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": false, + "distance": 192.52, + "elevation": "", + "lat": 47.6017688, + "lon": -122.3329762, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "Yesler Way Cycletrack", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTHEAST", + "area": false, + "bogusName": false, + "distance": 11.83, + "elevation": "", + "lat": 47.601778, + "lon": -122.3304095, + "relativeDirection": "SLIGHTLY_RIGHT", + "stayOn": false, + "streetName": "3rd Avenue", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": false, + "distance": 16.38, + "elevation": "", + "lat": 47.6017203, + "lon": -122.330277, + "relativeDirection": "SLIGHTLY_LEFT", + "stayOn": false, + "streetName": "Yesler Way", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTHEAST", + "area": false, + "bogusName": true, + "distance": 7.8, + "elevation": "", + "lat": 47.6017205, + "lon": -122.3300585, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "path", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTHEAST", + "area": false, + "bogusName": true, + "distance": 6.05, + "elevation": "", + "lat": 47.601659, + "lon": -122.3300084, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "sidewalk", + "walkingBike": true + } + ], + "to": { + "arrival": 1715976900000, + "departure": 1715976900000, + "lat": 47.601658, + "lon": -122.329941, + "name": "Prefontaine Pl S & Yesler Way", + "stopCode": "1610", + "stopId": "headway-1330:1610", + "vertexType": "TRANSIT", + "zoneId": "21" + }, + "transitLeg": false, + "walkingBike": false + }, + { + "agencyId": "headway-1330:1", + "agencyName": "Metro Transit", + "agencyTimeZoneOffset": -25200000, + "agencyUrl": "https://kingcounty.gov/en/dept/metro", + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 7252.61, + "duration": 1020.0, + "endTime": 1715977920000, + "from": { + "arrival": 1715976900000, + "departure": 1715976900000, + "lat": 47.601658, + "lon": -122.329941, + "name": "Prefontaine Pl S & Yesler Way", + "stopCode": "1610", + "stopId": "headway-1330:1610", + "stopIndex": 0, + "stopSequence": 1, + "vertexType": "TRANSIT", + "zoneId": "21" + }, + "generalizedCost": 1620, + "headsign": "Aurora Village Transit Center", + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 172, + "points": "_epaHnqsiVUt@M^GPIHGLa@\\gC|BoC`C]ZuAjA??YVoCbCmCbCmC`CgCzB??GBmCbCeEvDiDxC??]XwChCm@j@kCbCIFGFGHIPMTOVKRO`@aB`DkBvD??k@jAyCdGyCbGiAtB??oAjCaAsAaAwAcAuAy@kAIIaCkDa@?wDZkB@??}BD]IsA?uB?_@?{@C_@As@CY???E?O?K?MBODM@KDMBOFOJIFKDODOFO@O@M?OAUEWG]M]IQEQEWC]E_@E]A_@GcC@[?o@?}CGcAEiAAoBEaBCsCAiIAeJAi@???qDCcFAgCC]?Q?QBO?O@QBOBQDMBMBOFODQJOFMHSHsCfBwCjBsAx@]RMFCB??YN[No@\\m@Va@R[Nc@Ri@Pi@Rs@Te@Nw@T_Cp@qAXeATYHUDYDQDY@_AFo@Dg@Bq@?q@?eNB}Y@sB?qA?c@BwA?iAC}D?yF?{FByE?yD?uD?cE?aF?" + }, + "mode": "BUS", + "pathway": false, + "realTime": false, + "route": "E-Line Rapid Ride", + "routeId": "headway-1330:102615", + "routeLongName": "E-Line Rapid Ride", + "routeShortName": "E Line", + "routeType": 3, + "serviceDate": "2024-05-17", + "startTime": 1715976900000, + "steps": [], + "to": { + "arrival": 1715977920000, + "departure": 1715977920000, + "lat": 47.66169, + "lon": -122.347176, + "name": "Aurora Ave N & N 46th St", + "stopCode": "75409", + "stopId": "headway-1330:75409", + "stopIndex": 10, + "stopSequence": 163, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": true, + "tripBlockId": "7142580", + "tripId": "headway-1330:628189746" + }, + { + "agencyTimeZoneOffset": -25200000, + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 1359.75, + "duration": 565.0, + "endTime": 1715978485000, + "from": { + "arrival": 1715977920000, + "departure": 1715977920000, + "lat": 47.66169, + "lon": -122.347176, + "name": "Aurora Ave N & N 46th St", + "stopCode": "75409", + "stopId": "headway-1330:75409", + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 2282, + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 49, + "points": "q|{aHz|viV@N??jE@PA@mBdECrD@vAAtA?J??O?cCjE?L?L?`F@J?bA@jDBL?J@tA?~CBDVHAFJBZ?x@R?HAD?@q@FIDGNXPZLNFDJHFHt@l@d@V\\JNBNBB?L?\\?" + }, + "mode": "BICYCLE", + "pathway": false, + "realTime": false, + "rentedBike": false, + "route": "", + "startTime": 1715977920000, + "steps": [ + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": false, + "distance": 124.4, + "elevation": "", + "lat": 47.66169, + "lon": -122.347259, + "relativeDirection": "DEPART", + "stayOn": false, + "streetName": "Aurora Avenue North", + "walkingBike": true + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": false, + "distance": 41.92, + "elevation": "", + "lat": 47.6605712, + "lon": -122.3472598, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "North Allen Place", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": false, + "distance": 313.31, + "elevation": "", + "lat": 47.6605668, + "lon": -122.3467001, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "Winslow Place North", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": false, + "distance": 55.24, + "elevation": "", + "lat": 47.6577493, + "lon": -122.3466839, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "North 42nd Street", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": false, + "distance": 546.92, + "elevation": "", + "lat": 47.6577493, + "lon": -122.3459463, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "Whitman Avenue North", + "walkingBike": false + }, + { + "absoluteDirection": "WEST", + "area": false, + "bogusName": false, + "distance": 9.25, + "elevation": "", + "lat": 47.6528309, + "lon": -122.3460133, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "North 38th Street", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": true, + "distance": 64.13, + "elevation": "", + "lat": 47.6528087, + "lon": -122.3461323, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "path", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": true, + "distance": 29.74, + "elevation": "", + "lat": 47.6525196, + "lon": -122.3466064, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "sidewalk", + "walkingBike": true + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": true, + "distance": 37.35, + "elevation": "", + "lat": 47.6524313, + "lon": -122.3462617, + "relativeDirection": "RIGHT", + "stayOn": true, + "streetName": "sidewalk", + "walkingBike": true + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": true, + "distance": 12.25, + "elevation": "", + "lat": 47.6521922, + "lon": -122.34661, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "path", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": true, + "distance": 125.23, + "elevation": "", + "lat": 47.6520984, + "lon": -122.3466956, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "sidewalk", + "walkingBike": true + } + ], + "to": { + "arrival": 1715978485000, + "lat": 47.651048, + "lon": -122.347234, + "name": "Destination", + "vertexType": "NORMAL" + }, + "transitLeg": false, + "walkingBike": false + } + ], + "startTime": 1715975883000, + "tooSloped": false, + "transfers": 0, + "transitTime": 1020, + "waitingTime": 0, + "walkDistance": 4970.86, + "walkLimitExceeded": false, + "walkTime": 1582 + }, + { + "arrivedAtDestinationWithRentedBicycle": false, + "duration": 2670, + "elevationGained": 0.0, + "elevationLost": 0.0, + "endTime": 1715978688000, + "fare": { + "details": {}, + "fare": {}, + "legProducts": [ + { + "legIndices": [ + 1 + ], + "products": [ + { + "amount": { + "cents": 0, + "currency": { + "currency": "USD", + "currencyCode": "USD", + "defaultFractionDigits": 2, + "symbol": "$" + } + }, + "id": "headway-1330:300", + "name": "regular" + } + ] + } + ] + }, + "generalizedCost": 6319, + "legs": [ + { + "agencyTimeZoneOffset": -25200000, + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 2892.98, + "duration": 754.0, + "endTime": 1715976772000, + "from": { + "departure": 1715976018000, + "lat": 47.575837, + "lon": -122.339414, + "name": "Origin", + "vertexType": "NORMAL" + }, + "generalizedCost": 2146, + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 79, + "points": "}ckaHbkuiV`@@J??L?bA?p@?F?Pa@?}PCcRFq@@}W?{BCe@EYGUM[Um@y@a@g@KQEGq@w@uAiAa@UYQa@Q{EgBk@Q_@MOEsA_@DI]GGE?GSA]Eg@IyAc@YIkAWe@SeA_@y@QKC_AO[ASAcAGaACaCAqA@mEGgA?_@?iEBGAAC?K?QG?I?I?a@@GD_@?_ABm@?G??I?[?i@?i@I?C?K@gA??N" + }, + "mode": "BICYCLE", + "pathway": false, + "realTime": false, + "rentedBike": false, + "route": "", + "startTime": 1715976018000, + "steps": [ + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": false, + "distance": 26.0, + "elevation": "", + "lat": 47.5758346, + "lon": -122.3392181, + "relativeDirection": "DEPART", + "stayOn": false, + "streetName": "East Marginal Way South", + "walkingBike": false + }, + { + "absoluteDirection": "WEST", + "area": false, + "bogusName": false, + "distance": 59.45, + "elevation": "", + "lat": 47.5756008, + "lon": -122.3392243, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "South Hanford Street", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 1218.07, + "elevation": "", + "lat": 47.5756059, + "lon": -122.3400167, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "East Marginal Way South", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 515.99, + "elevation": "", + "lat": 47.58656, + "lon": -122.3400253, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "Alaskan Way South", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTHEAST", + "area": false, + "bogusName": false, + "distance": 4.97, + "elevation": "", + "lat": 47.5907595, + "lon": -122.337345, + "relativeDirection": "HARD_RIGHT", + "stayOn": false, + "streetName": "South Atlantic Street", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": true, + "distance": 186.23, + "elevation": "", + "lat": 47.5907275, + "lon": -122.3372987, + "relativeDirection": "HARD_LEFT", + "stayOn": false, + "streetName": "Marginal Way Cycle Route", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 659.69, + "elevation": "", + "lat": 47.592328, + "lon": -122.3367458, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "Portside Trail", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 123.17, + "elevation": "", + "lat": 47.5981133, + "lon": -122.3360192, + "relativeDirection": "LEFT", + "stayOn": true, + "streetName": "Portside Trail", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": true, + "distance": 14.34, + "elevation": "", + "lat": 47.5992162, + "lon": -122.336079, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "service road", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": false, + "distance": 31.42, + "elevation": "", + "lat": 47.5992158, + "lon": -122.3358878, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "South Jackson Street", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": true, + "distance": 5.47, + "elevation": "", + "lat": 47.5992185, + "lon": -122.3354687, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "path", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": true, + "distance": 48.2, + "elevation": "", + "lat": 47.5992677, + "lon": -122.3354676, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "sidewalk", + "walkingBike": true + } + ], + "to": { + "arrival": 1715976772000, + "departure": 1715976772000, + "lat": 47.599701, + "lon": -122.335556, + "name": "Alaskan Way S & S Jackson St", + "stopCode": "1561", + "stopId": "headway-1330:1561", + "vertexType": "TRANSIT", + "zoneId": "21" + }, + "transitLeg": false, + "walkingBike": false + }, + { + "agencyId": "headway-1330:1", + "agencyName": "Metro Transit", + "agencyTimeZoneOffset": -25200000, + "agencyUrl": "https://kingcounty.gov/en/dept/metro", + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 3495.38, + "duration": 848.0, + "endTime": 1715977620000, + "from": { + "arrival": 1715976772000, + "departure": 1715976772000, + "lat": 47.599701, + "lon": -122.335556, + "name": "Alaskan Way S & S Jackson St", + "stopCode": "1561", + "stopId": "headway-1330:1561", + "stopIndex": 16, + "stopSequence": 334, + "vertexType": "TRANSIT", + "zoneId": "21" + }, + "generalizedCost": 1448, + "headsign": "South Lake Union Downtown Seattle", + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 86, + "points": "cyoaH|ttiVUCm@?k@?}@Ae@@WBg@Ne@Py@h@a@RqCzB??KJ{@uCSw@Uq@o@yBm@mBm@mBo@sBk@mBsBjB??[VmCbCmC`CgCzB??GBmCbCeEvDiDxC??]XwChCm@j@kCbCIFGFGHIPMTOVKRO`@aB`DkBvD??k@jAyCdGaAwAaAsAcAwAw@iAKM_AsA{@kA??EIcAuAaAuAcAuAc@m@]e@aAuAKIM[GSAYa@B}@J??w@HiFAiFAaE???g@?gFCmGA]?mB?S@UDC@" + }, + "mode": "BUS", + "pathway": false, + "realTime": false, + "route": "C-Line Rapid Ride", + "routeId": "headway-1330:102576", + "routeLongName": "C-Line Rapid Ride", + "routeShortName": "C Line", + "routeType": 3, + "serviceDate": "2024-05-17", + "startTime": 1715976772000, + "steps": [], + "to": { + "arrival": 1715977620000, + "departure": 1715977620000, + "lat": 47.625504, + "lon": -122.338394, + "name": "Westlake Ave N & Mercer St", + "stopCode": "26730", + "stopId": "headway-1330:26730", + "stopIndex": 25, + "stopSequence": 411, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": true, + "tripBlockId": "7160078", + "tripId": "headway-1330:635650436" + }, + { + "agencyTimeZoneOffset": -25200000, + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 3704.32, + "duration": 1068.0, + "endTime": 1715978688000, + "from": { + "arrival": 1715977620000, + "departure": 1715977620000, + "lat": 47.625504, + "lon": -122.338394, + "name": "Westlake Ave N & Mercer St", + "stopCode": "26730", + "stopId": "headway-1330:26730", + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 2724, + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 253, + "points": "kztaH~euiVBPKBG@GBUJCg@MDE@CBA@ABCBu@ZINMJIDKDQ@OFKFMNUf@GFGDE@A\\?HC?Y@M@MBKBIBC@C@ACKGEACAaBfASLEBC@C?EBGBEBMH[RMLCDGDIFA?KD[LWLc@JSBYDY@CBG@E@GBE@K?G?C?G?E?GECAEEC?cB?M@I@I?o@?QAuC?o@AK@E?ODG@GAC?GAECGCKKECIIGCCAgCo@i@OGAOEICaBOI?]?kB?UBu@@mD@qA@G?cB@C@MHG@[Dc@FOBMDOD]LYJSJKDc@RG@M@G@uCvAIBGDOL_@PMRCBOFCBOBG@ULiAl@[Ry@b@EDCFCFEDIDmAn@E@C@OAC@EB]PWLIFKFGBG@y@d@[NEDq@h@eA|@aAt@u@l@}@t@STSXOVGNO`@Sj@a@hACF@BHF@FBFBP?FADgCfIeAjDIVENGPQb@Yb@i@r@UXIFMHMDGBG@M@W@i@@k@@iEAICKVU?Q?g@?e@?c@?w@@EKACIKAGCIAQ?W?kA@{@B]^qEBe@G?I@iA@}A?oA?cA?A?K?@I@IMAA?OAAe@MQSCm@\\mB}@EEC??DFHt@l@d@V\\JNBNBB?L?\\?" + }, + "mode": "BICYCLE", + "pathway": false, + "realTime": false, + "rentedBike": false, + "route": "", + "startTime": 1715977620000, + "steps": [ + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 28.54, + "elevation": "", + "lat": 47.6254888, + "lon": -122.3384861, + "relativeDirection": "DEPART", + "stayOn": false, + "streetName": "Westlake Avenue North", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": false, + "distance": 14.8, + "elevation": "", + "lat": 47.6257355, + "lon": -122.3385901, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "Valley Street", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": true, + "distance": 51.75, + "elevation": "", + "lat": 47.6257544, + "lon": -122.3383946, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "path", + "walkingBike": false + }, + { + "absoluteDirection": "NORTHWEST", + "area": false, + "bogusName": false, + "distance": 100.36, + "elevation": "", + "lat": 47.6261803, + "lon": -122.338649, + "relativeDirection": "SLIGHTLY_LEFT", + "stayOn": false, + "streetName": "Cheshiahud Lake Union Loop", + "walkingBike": false + }, + { + "absoluteDirection": "WEST", + "area": true, + "bogusName": true, + "distance": 14.54, + "elevation": "", + "lat": 47.6269383, + "lon": -122.3392982, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "open area", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": true, + "distance": 2.25, + "elevation": "", + "lat": 47.6269417, + "lon": -122.3394921, + "relativeDirection": "RIGHT", + "stayOn": true, + "streetName": "bike path", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 47.87, + "elevation": "", + "lat": 47.6269619, + "lon": -122.3394925, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "Cheshiahud Lake Union Loop", + "walkingBike": false + }, + { + "absoluteDirection": "NORTHEAST", + "area": false, + "bogusName": false, + "distance": 88.27, + "elevation": "", + "lat": 47.6273847, + "lon": -122.3395952, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "Westlake Protected Bike Lane", + "walkingBike": false + }, + { + "absoluteDirection": "NORTHWEST", + "area": false, + "bogusName": false, + "distance": 208.81, + "elevation": "", + "lat": 47.6280979, + "lon": -122.3399451, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "Westlake Cycle Track", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 1768.5, + "elevation": "", + "lat": 47.6298708, + "lon": -122.3407137, + "relativeDirection": "SLIGHTLY_RIGHT", + "stayOn": false, + "streetName": "Westlake Protected Bike Lane", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": false, + "distance": 575.04, + "elevation": "", + "lat": 47.6446397, + "lon": -122.3456348, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "Cheshiahud Lake Union Loop", + "walkingBike": false + }, + { + "absoluteDirection": "NORTHWEST", + "area": false, + "bogusName": true, + "distance": 11.44, + "elevation": "", + "lat": 47.6481797, + "lon": -122.3496341, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "Interurban Trail", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 44.18, + "elevation": "", + "lat": 47.6482398, + "lon": -122.349758, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "Fremont Bridge", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 72.07, + "elevation": "", + "lat": 47.6486371, + "lon": -122.3497576, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "Fremont Avenue North", + "walkingBike": false + }, + { + "absoluteDirection": "NORTHEAST", + "area": false, + "bogusName": true, + "distance": 88.47, + "elevation": "", + "lat": 47.6492852, + "lon": -122.3497604, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "link", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": false, + "distance": 106.11, + "elevation": "", + "lat": 47.6494012, + "lon": -122.3486434, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "North 34th Street", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 193.79, + "elevation": "", + "lat": 47.6492019, + "lon": -122.347258, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "Troll Avenue North", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": false, + "distance": 7.88, + "elevation": "", + "lat": 47.6509446, + "lon": -122.347277, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "North 36th Street", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": true, + "distance": 17.1, + "elevation": "", + "lat": 47.6509293, + "lon": -122.3471743, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "path", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "alerts": [ + { + "alertHeaderText": "Unpaved surface" + } + ], + "area": false, + "bogusName": true, + "distance": 137.31, + "elevation": "", + "lat": 47.6510827, + "lon": -122.3471583, + "relativeDirection": "RIGHT", + "stayOn": true, + "streetName": "path", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": true, + "distance": 125.23, + "elevation": "", + "lat": 47.6520984, + "lon": -122.3466956, + "relativeDirection": "HARD_LEFT", + "stayOn": false, + "streetName": "sidewalk", + "walkingBike": true + } + ], + "to": { + "arrival": 1715978688000, + "lat": 47.651048, + "lon": -122.347234, + "name": "Destination", + "vertexType": "NORMAL" + }, + "transitLeg": false, + "walkingBike": false + } + ], + "startTime": 1715976018000, + "tooSloped": false, + "transfers": 0, + "transitTime": 848, + "waitingTime": 0, + "walkDistance": 6597.3, + "walkLimitExceeded": false, + "walkTime": 1822 + }, + { + "arrivedAtDestinationWithRentedBicycle": false, + "duration": 2185, + "elevationGained": 0.0, + "elevationLost": 0.0, + "endTime": 1715978700000, + "fare": { + "details": {}, + "fare": {}, + "legProducts": [ + { + "legIndices": [ + 1 + ], + "products": [ + { + "amount": { + "cents": 0, + "currency": { + "currency": "USD", + "currencyCode": "USD", + "defaultFractionDigits": 2, + "symbol": "$" + } + }, + "id": "headway-1330:300", + "name": "regular" + } + ] + }, + { + "legIndices": [ + 2 + ], + "products": [ + { + "amount": { + "cents": 0, + "currency": { + "currency": "USD", + "currencyCode": "USD", + "defaultFractionDigits": 2, + "symbol": "$" + } + }, + "id": "headway-1330:300", + "name": "regular" + } + ] + } + ] + }, + "generalizedCost": 3805, + "legs": [ + { + "agencyTimeZoneOffset": -25200000, + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 452.53, + "duration": 155.0, + "endTime": 1715976670000, + "from": { + "departure": 1715976515000, + "lat": 47.575837, + "lon": -122.339414, + "name": "Origin", + "vertexType": "NORMAL" + }, + "generalizedCost": 574, + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 26, + "points": "}ckaHbkuiV`@@J?@kB?mB?C?eB?q@?I?eA?W?K@S?E?s@?y@?_A?iA?y@@mC?g@S@?[?Cq@??H" + }, + "mode": "BICYCLE", + "pathway": false, + "realTime": false, + "rentedBike": false, + "route": "", + "startTime": 1715976515000, + "steps": [ + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": false, + "distance": 26.0, + "elevation": "", + "lat": 47.5758346, + "lon": -122.3392181, + "relativeDirection": "DEPART", + "stayOn": false, + "streetName": "East Marginal Way South", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": false, + "distance": 376.3, + "elevation": "", + "lat": 47.5756008, + "lon": -122.3392243, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "South Hanford Street", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 11.12, + "elevation": "", + "lat": 47.5755779, + "lon": -122.334208, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "1st Avenue South", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": true, + "distance": 9.87, + "elevation": "", + "lat": 47.5756779, + "lon": -122.3342102, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "path", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": true, + "distance": 29.24, + "elevation": "", + "lat": 47.575676, + "lon": -122.3340787, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "sidewalk", + "walkingBike": true + } + ], + "to": { + "arrival": 1715976670000, + "departure": 1715976670000, + "lat": 47.575924, + "lon": -122.334106, + "name": "1st Ave S & S Hanford St", + "stopCode": "15205", + "stopId": "headway-1330:15205", + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": false, + "walkingBike": false + }, + { + "agencyId": "headway-1330:1", + "agencyName": "Metro Transit", + "agencyTimeZoneOffset": -25200000, + "agencyUrl": "https://kingcounty.gov/en/dept/metro", + "alerts": [ + { + "alertDescriptionText": "This is an update to a previous Transit Alert. \r\nThe reroute end time for games on June 1 and June 15 have been updated.\r\n\nReroute will be in effect on each of the games noted below:\r\n- Sunday, April 28 at 3:30 PM-4:30 PM\r\n- Wednesday, May 1 at 3:00 PM-4:00 PM\r\n- Sunday, May 12 at 3:30 PM-4:30 PM\r\n- Wednesday, May 15 at 3:30 PM-4:30 PM\r\n- Thursday, May 30 at 3:30 PM-4:30 PM\r\n- Saturday, June 1 at 6:35 PM-7:35 PM\r\n- Sunday, June 2 at 3:30 PM-4:30 PM\r\n- Saturday, June 15 at 6:35 PM-7:35 PM\r\n- Sunday, June 16 at 3:30 PM-4:30 PM\r\n- Sunday, June 30 at 3:30 PM-4:30 PM\r\n- Thursday, July 4 at 3:30 PM-4:30 PM\r\n- Saturday, July 6 at 3:30 PM-4:30 PM\r\n- Sunday, July 7 at 3:30 PM-4:30 PM\r\n- Sunday, July 21 at 3:30 PM-4:30 PM\r\n- Wednesday, July 24 at 3:00 PM-4:00 PM\r\n- Sunday, August 4 at 3:30 PM-4:30 PM\r\n- Sunday, August 11 at 3:30 PM-4:30 PM\r\n- Saturday, August 24 at 3:30 PM-4:30 PM\r\n- Sunday, August 25 at 3:30 PM-4:30 PM\r\n- Wednesday, August 28 at 3:30 PM-4:30 PM\r\n- Sunday, September 15 at 3:30 PM-4:30 PM\r\n- Thursday, September 19 at 3:30 PM-4:30 PM\r\n- Sunday, September 29 at 2:30 PM-3:30 PM\r\n\nAffected stops:\r\nStop #15190 1st Ave S & S Spokane St (NB)\r\nStop #15205 1st Ave S & S Hanford St (NB)\r\nStop #15230 1st Ave S & S Lander St (NB)\r\nStop #15206 1st Ave S & S Stacy St (NB)\r\nStop #15212 1st Ave S & S Walker St (NB)\r\nStop #15207 1st Ave S & S Holgate St (NB)\r\nStop #15204 Edgar Martinez Dr S & Occidental Ave S (EB)\r\nStop #15201 1st Ave S & S Atlantic St (SB)\r\nStop #15202 1st Ave S & S Holgate St (SB)\r\nStop #15211 1st Ave S & S Walker St (SB)\r\nStop #15203 1st Ave S & S Stacy St (SB)\r\n\nFor Route 21 to Downtown Seattle get on/off buses at:\r\nStop #15145 SW Spokane St & Chelan Ave SW (EB)\r\nStop #30538 4th Ave S & S Spokane St (NB)\r\nStop #30550 4th Ave S & S Hanford St (NB)\r\nStop #30560 4th Ave S & S Forest St (NB)\r\nStop #30570 4th Ave S & S Lander St (NB)\r\nStop #30590 4th Ave S & S Walker St (NB)\r\nStop #30600 4th Ave S & S Holgate St (NB)\r\nStop #30635 4th Ave S & S Royal Brougham Way (NB)\r\n\nFor Route 21 to Westwood Village get on/off buses at:\r\nStop #515 3rd Ave S & S Main St (SB)\r\nStop #30670 4th Ave S & Edgar Martinez Dr S (SB)\r\nStop #30690 4th Ave S & S Holgate St (SB)\r\nStop #30700 4th Ave S & S Walker St (SB)\r\nStop #15380 1st Ave S & S Lander St (SB)", + "alertHeaderText": "Route 21 will be affected in both directions during Mariners afternoon home games; stops on portions of 1st Ave S will not be served; use alternate stops on 4th Ave S instead.", + "alertUrl": "https://drive.google.com/file/d/1rOKsBD54nHp8U1Sh3GGpQQpLMNhXwTDf/view", + "effectiveEndDate": 1727649000000, + "effectiveStartDate": 1711924200000 + } + ], + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 2322.8, + "duration": 410.0, + "endTime": 1715977080000, + "from": { + "arrival": 1715976670000, + "departure": 1715976670000, + "lat": 47.575924, + "lon": -122.334106, + "name": "1st Ave S & S Hanford St", + "stopCode": "15205", + "stopId": "headway-1330:15205", + "stopIndex": 28, + "stopSequence": 194, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 1010, + "headsign": "Downtown Seattle Via 35th Ave SW", + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 41, + "points": "odkaHvktiVgJ?gK???W?aL?cA???}I?s@???qJ?sA???uI?yK?@oC???g@?_AEyB?gB?cA?mC?cCAgBCiBEMACCCCAECI?{FAc@D[RQ?Y?W?G?sD?" + }, + "mode": "BUS", + "pathway": false, + "realTime": false, + "routeId": "headway-1330:100101", + "routeShortName": "21", + "routeType": 3, + "serviceDate": "2024-05-17", + "startTime": 1715976670000, + "steps": [], + "to": { + "arrival": 1715977080000, + "departure": 1715977080000, + "lat": 47.593342, + "lon": -122.328957, + "name": "4th Ave S & S Royal Brougham Way", + "stopCode": "30635", + "stopId": "headway-1330:30635", + "stopIndex": 34, + "stopSequence": 229, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": true, + "tripBlockId": "7142612", + "tripId": "headway-1330:605082006" + }, + { + "agencyId": "headway-1330:1", + "agencyName": "Metro Transit", + "agencyTimeZoneOffset": -25200000, + "agencyUrl": "https://kingcounty.gov/en/dept/metro", + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 7187.77, + "duration": 1440.0, + "endTime": 1715978520000, + "from": { + "arrival": 1715977080000, + "departure": 1715977080000, + "lat": 47.593342, + "lon": -122.328957, + "name": "4th Ave S & S Royal Brougham Way", + "stopCode": "30635", + "stopId": "headway-1330:30635", + "stopIndex": 0, + "stopSequence": 1, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 1440, + "headsign": "Shoreline Greenwood", + "interlineWithPreviousLeg": true, + "legGeometry": { + "length": 195, + "points": "kqnaHpksiVmD?e@?_L?}ECoDCqCAYLY@c@B[CIA??QCUMeDBuAtAILKJEJGLo@rBM^GPIHGLa@\\gB~A??_@\\oC`C]ZoBbBoCbCuBlB??WTmC`CoC~BmCbCqDbD??SRgErDwChCm@j@gA`A??cA`AIFGFGHIPMTOVKRO`@aB`DkBvD??k@jAyCdGyCbGiAtB??oAjCaAsAaAwAcAuAy@kAIIaCkDa@?wDZkB@??}BD]IsA?uB?_@?{@C_@As@CY???E?O?K?MBODM@KDMBOFOJIFKDODOFO@O@M?OAUEWG]M]IQEQEWC]E_@E]A_@GcC@[?o@?}CGcAEiAAoBEaBCsCA{@???mGAeJAi@???qDCcFAgCC]?Q?QBO?O@QBOBQDMBMBOFODQJOFMHSHiCbB??IBwCjBsAx@]RMFCB??YN[No@\\m@Va@R[Nc@Ri@Pi@Rs@Te@Nw@T_Cp@SD??}@ReATYHUDYDQDY@_AFo@Dg@Bq@?q@?eNB}Y@i@IYIYKQKOIi@c@US{@?" + }, + "mode": "BUS", + "pathway": false, + "realTime": false, + "routeId": "headway-1330:100229", + "routeShortName": "5", + "routeType": 3, + "serviceDate": "2024-05-17", + "startTime": 1715977080000, + "steps": [], + "to": { + "arrival": 1715978520000, + "departure": 1715978520000, + "lat": 47.652473, + "lon": -122.346619, + "name": "Aurora Ave N & N 38th St", + "stopCode": "6340", + "stopId": "headway-1330:6340", + "stopIndex": 15, + "stopSequence": 181, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": true, + "tripBlockId": "7142612", + "tripId": "headway-1330:605083816" + }, + { + "agencyTimeZoneOffset": -25200000, + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 205.38, + "duration": 180.0, + "endTime": 1715978700000, + "from": { + "arrival": 1715978520000, + "departure": 1715978520000, + "lat": 47.652473, + "lon": -122.346619, + "name": "Aurora Ave N & N 38th St", + "stopCode": "6340", + "stopId": "headway-1330:6340", + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 779, + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 20, + "points": "}bzaHjyviVG??A@q@FIDGNXPZLNFDJHFHt@l@d@V\\JNBNBB?L?\\?" + }, + "mode": "BICYCLE", + "pathway": false, + "realTime": false, + "rentedBike": false, + "route": "", + "startTime": 1715978520000, + "steps": [ + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": true, + "distance": 30.56, + "elevation": "", + "lat": 47.6525198, + "lon": -122.3466171, + "relativeDirection": "DEPART", + "stayOn": false, + "streetName": "sidewalk", + "walkingBike": true + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": true, + "distance": 37.35, + "elevation": "", + "lat": 47.6524313, + "lon": -122.3462617, + "relativeDirection": "RIGHT", + "stayOn": true, + "streetName": "sidewalk", + "walkingBike": true + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": true, + "distance": 12.25, + "elevation": "", + "lat": 47.6521922, + "lon": -122.34661, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "path", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": true, + "distance": 125.23, + "elevation": "", + "lat": 47.6520984, + "lon": -122.3466956, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "sidewalk", + "walkingBike": true + } + ], + "to": { + "arrival": 1715978700000, + "lat": 47.651048, + "lon": -122.347234, + "name": "Destination", + "vertexType": "NORMAL" + }, + "transitLeg": false, + "walkingBike": false + } + ], + "startTime": 1715976515000, + "tooSloped": false, + "transfers": 0, + "transitTime": 1850, + "waitingTime": 0, + "walkDistance": 657.91, + "walkLimitExceeded": false, + "walkTime": 335 + }, + { + "arrivedAtDestinationWithRentedBicycle": false, + "duration": 2138, + "elevationGained": 0.0, + "elevationLost": 0.0, + "endTime": 1715979240000, + "fare": { + "details": {}, + "fare": {}, + "legProducts": [ + { + "legIndices": [ + 1 + ], + "products": [ + { + "amount": { + "cents": 0, + "currency": { + "currency": "USD", + "currencyCode": "USD", + "defaultFractionDigits": 2, + "symbol": "$" + } + }, + "id": "headway-1330:300", + "name": "regular" + } + ] + }, + { + "legIndices": [ + 2 + ], + "products": [ + { + "amount": { + "cents": 0, + "currency": { + "currency": "USD", + "currencyCode": "USD", + "defaultFractionDigits": 2, + "symbol": "$" + } + }, + "id": "headway-1330:300", + "name": "regular" + } + ] + } + ] + }, + "generalizedCost": 4211, + "legs": [ + { + "agencyTimeZoneOffset": -25200000, + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 1094.22, + "duration": 342.0, + "endTime": 1715977444000, + "from": { + "departure": 1715977102000, + "lat": 47.575837, + "lon": -122.339414, + "name": "Origin", + "vertexType": "NORMAL" + }, + "generalizedCost": 1215, + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 46, + "points": "}ckaHbkuiV`@@J?@kB?mB?C?eB?q@?I?eA?W?K@S?E?s@?y@?_A?iA?y@@mC?g@?_@?]?{@?M?u@jF?@wB?gA?w@?i@?M?K?I?W@iHgA?o@?C?iB??wC?a@H??g@p@??P" + }, + "mode": "BICYCLE", + "pathway": false, + "realTime": false, + "rentedBike": false, + "route": "", + "startTime": 1715977102000, + "steps": [ + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": false, + "distance": 26.0, + "elevation": "", + "lat": 47.5758346, + "lon": -122.3392181, + "relativeDirection": "DEPART", + "stayOn": false, + "streetName": "East Marginal Way South", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": false, + "distance": 448.07, + "elevation": "", + "lat": 47.5756008, + "lon": -122.3392243, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "South Hanford Street", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": false, + "distance": 131.92, + "elevation": "", + "lat": 47.5755778, + "lon": -122.3332512, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "Occidental Avenue South", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": false, + "distance": 242.59, + "elevation": "", + "lat": 47.5743914, + "lon": -122.3332503, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "South Horton Street", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 127.13, + "elevation": "", + "lat": 47.5743767, + "lon": -122.3300166, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "3rd Avenue South", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": false, + "distance": 69.81, + "elevation": "", + "lat": 47.57552, + "lon": -122.3300166, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "South Hanford Street", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": false, + "distance": 5.69, + "elevation": "", + "lat": 47.5755223, + "lon": -122.329086, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "4th Avenue South", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": true, + "distance": 15.3, + "elevation": "", + "lat": 47.5754711, + "lon": -122.3290854, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "service road", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": true, + "distance": 27.71, + "elevation": "", + "lat": 47.5754712, + "lon": -122.3288814, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "sidewalk", + "walkingBike": true + } + ], + "to": { + "arrival": 1715977444000, + "departure": 1715977444000, + "lat": 47.575222, + "lon": -122.328972, + "name": "4th Ave S & S Hanford St", + "stopCode": "30550", + "stopId": "headway-1330:30550", + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": false, + "walkingBike": false + }, + { + "agencyId": "headway-1330:1", + "agencyName": "Metro Transit", + "agencyTimeZoneOffset": -25200000, + "agencyUrl": "https://kingcounty.gov/en/dept/metro", + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 2016.14, + "duration": 356.0, + "endTime": 1715977800000, + "from": { + "arrival": 1715977444000, + "departure": 1715977444000, + "lat": 47.575222, + "lon": -122.328972, + "name": "4th Ave S & S Hanford St", + "stopCode": "30550", + "stopId": "headway-1330:30550", + "stopIndex": 52, + "stopSequence": 427, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 956, + "headsign": "Downtown Seattle South Park", + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 28, + "points": "c`kaHtksiV{@?mL???I?_L?uA???kI?cJ???eA?kL?m@???mJCq@?gAWoCDkDLqB?_CCwAFQ?Y?W?G?sD?" + }, + "mode": "BUS", + "pathway": false, + "realTime": false, + "routeId": "headway-1330:100031", + "routeShortName": "132", + "routeType": 3, + "serviceDate": "2024-05-17", + "startTime": 1715977444000, + "steps": [], + "to": { + "arrival": 1715977800000, + "departure": 1715977800000, + "lat": 47.593342, + "lon": -122.328957, + "name": "4th Ave S & S Royal Brougham Way", + "stopCode": "30635", + "stopId": "headway-1330:30635", + "stopIndex": 57, + "stopSequence": 450, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": true, + "tripBlockId": "7159176", + "tripId": "headway-1330:651761126" + }, + { + "agencyId": "headway-1330:1", + "agencyName": "Metro Transit", + "agencyTimeZoneOffset": -25200000, + "agencyUrl": "https://kingcounty.gov/en/dept/metro", + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 7187.72, + "duration": 1260.0, + "endTime": 1715979060000, + "from": { + "arrival": 1715977800000, + "departure": 1715977800000, + "lat": 47.593342, + "lon": -122.328957, + "name": "4th Ave S & S Royal Brougham Way", + "stopCode": "30635", + "stopId": "headway-1330:30635", + "stopIndex": 0, + "stopSequence": 1, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 1260, + "headsign": "Carkeek Park", + "interlineWithPreviousLeg": true, + "legGeometry": { + "length": 189, + "points": "kqnaHpksiVmD?e@?_L?}ECoDCqCAYLY@c@B[CIA??QCUMeDBuAtAILKJEJGLo@rBM^GPIHGLa@\\gB~A??_@\\oC`C]ZoBbBoCbCuBlB??WTmC`CoC~BmCbCqDbD??SRgErDwChCm@j@gA`A??cA`AIFGFGHIPMTOVKRO`@aB`DkBvD??k@jAyCdGyCbGiAtB??oAjCaAsAaAwAcAuAy@kAIIaCkDa@?wDZkB@??}BD]IsA?uB?_@?{@C_@As@CY???E?O?K?MBODM@KDMBOFOJIFKDODOFO@O@M?OAUEWG]M]IQEQEWC]E_@E]A_@GcC@[?o@?}CGcAEiAAoBEaBCsCAiIAeJAi@???qDCcFAgCC]?Q?QBO?O@QBOBQDMBMBOFODQJOFMHSHsCfBwCjBsAx@]RMFCB??YN[No@\\m@Va@R[Nc@Ri@Pi@Rs@Te@Nw@T_Cp@qAXeATYHUDYDQDY@_AFo@Dg@Bq@?q@?eNB}Y@i@IYIYKQKOIi@c@US{@?" + }, + "mode": "BUS", + "pathway": false, + "realTime": false, + "routeId": "headway-1330:100169", + "routeShortName": "28", + "routeType": 3, + "serviceDate": "2024-05-17", + "startTime": 1715977800000, + "steps": [], + "to": { + "arrival": 1715979060000, + "departure": 1715979060000, + "lat": 47.652473, + "lon": -122.346619, + "name": "Aurora Ave N & N 38th St", + "stopCode": "6340", + "stopId": "headway-1330:6340", + "stopIndex": 12, + "stopSequence": 178, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": true, + "tripBlockId": "7159176", + "tripId": "headway-1330:663596036", + "tripShortName": "EXPRESS" + }, + { + "agencyTimeZoneOffset": -25200000, + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 205.38, + "duration": 180.0, + "endTime": 1715979240000, + "from": { + "arrival": 1715979060000, + "departure": 1715979060000, + "lat": 47.652473, + "lon": -122.346619, + "name": "Aurora Ave N & N 38th St", + "stopCode": "6340", + "stopId": "headway-1330:6340", + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 779, + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 20, + "points": "}bzaHjyviVG??A@q@FIDGNXPZLNFDJHFHt@l@d@V\\JNBNBB?L?\\?" + }, + "mode": "BICYCLE", + "pathway": false, + "realTime": false, + "rentedBike": false, + "route": "", + "startTime": 1715979060000, + "steps": [ + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": true, + "distance": 30.56, + "elevation": "", + "lat": 47.6525198, + "lon": -122.3466171, + "relativeDirection": "DEPART", + "stayOn": false, + "streetName": "sidewalk", + "walkingBike": true + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": true, + "distance": 37.35, + "elevation": "", + "lat": 47.6524313, + "lon": -122.3462617, + "relativeDirection": "RIGHT", + "stayOn": true, + "streetName": "sidewalk", + "walkingBike": true + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": true, + "distance": 12.25, + "elevation": "", + "lat": 47.6521922, + "lon": -122.34661, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "path", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": true, + "distance": 125.23, + "elevation": "", + "lat": 47.6520984, + "lon": -122.3466956, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "sidewalk", + "walkingBike": true + } + ], + "to": { + "arrival": 1715979240000, + "lat": 47.651048, + "lon": -122.347234, + "name": "Destination", + "vertexType": "NORMAL" + }, + "transitLeg": false, + "walkingBike": false + } + ], + "startTime": 1715977102000, + "tooSloped": false, + "transfers": 0, + "transitTime": 1616, + "waitingTime": 0, + "walkDistance": 1299.6, + "walkLimitExceeded": false, + "walkTime": 522 + }, + { + "arrivedAtDestinationWithRentedBicycle": false, + "duration": 2185, + "elevationGained": 0.0, + "elevationLost": 0.0, + "endTime": 1715979540000, + "fare": { + "details": {}, + "fare": {}, + "legProducts": [ + { + "legIndices": [ + 1 + ], + "products": [ + { + "amount": { + "cents": 0, + "currency": { + "currency": "USD", + "currencyCode": "USD", + "defaultFractionDigits": 2, + "symbol": "$" + } + }, + "id": "headway-1330:300", + "name": "regular" + } + ] + }, + { + "legIndices": [ + 2 + ], + "products": [ + { + "amount": { + "cents": 0, + "currency": { + "currency": "USD", + "currencyCode": "USD", + "defaultFractionDigits": 2, + "symbol": "$" + } + }, + "id": "headway-1330:300", + "name": "regular" + } + ] + } + ] + }, + "generalizedCost": 3805, + "legs": [ + { + "agencyTimeZoneOffset": -25200000, + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 452.53, + "duration": 155.0, + "endTime": 1715977510000, + "from": { + "departure": 1715977355000, + "lat": 47.575837, + "lon": -122.339414, + "name": "Origin", + "vertexType": "NORMAL" + }, + "generalizedCost": 574, + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 26, + "points": "}ckaHbkuiV`@@J?@kB?mB?C?eB?q@?I?eA?W?K@S?E?s@?y@?_A?iA?y@@mC?g@S@?[?Cq@??H" + }, + "mode": "BICYCLE", + "pathway": false, + "realTime": false, + "rentedBike": false, + "route": "", + "startTime": 1715977355000, + "steps": [ + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": false, + "distance": 26.0, + "elevation": "", + "lat": 47.5758346, + "lon": -122.3392181, + "relativeDirection": "DEPART", + "stayOn": false, + "streetName": "East Marginal Way South", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": false, + "distance": 376.3, + "elevation": "", + "lat": 47.5756008, + "lon": -122.3392243, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "South Hanford Street", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 11.12, + "elevation": "", + "lat": 47.5755779, + "lon": -122.334208, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "1st Avenue South", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": true, + "distance": 9.87, + "elevation": "", + "lat": 47.5756779, + "lon": -122.3342102, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "path", + "walkingBike": false + }, + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": true, + "distance": 29.24, + "elevation": "", + "lat": 47.575676, + "lon": -122.3340787, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "sidewalk", + "walkingBike": true + } + ], + "to": { + "arrival": 1715977510000, + "departure": 1715977510000, + "lat": 47.575924, + "lon": -122.334106, + "name": "1st Ave S & S Hanford St", + "stopCode": "15205", + "stopId": "headway-1330:15205", + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": false, + "walkingBike": false + }, + { + "agencyId": "headway-1330:1", + "agencyName": "Metro Transit", + "agencyTimeZoneOffset": -25200000, + "agencyUrl": "https://kingcounty.gov/en/dept/metro", + "alerts": [ + { + "alertDescriptionText": "This is an update to a previous Transit Alert. \r\nThe reroute end time for games on June 1 and June 15 have been updated.\r\n\nReroute will be in effect on each of the games noted below:\r\n- Sunday, April 28 at 3:30 PM-4:30 PM\r\n- Wednesday, May 1 at 3:00 PM-4:00 PM\r\n- Sunday, May 12 at 3:30 PM-4:30 PM\r\n- Wednesday, May 15 at 3:30 PM-4:30 PM\r\n- Thursday, May 30 at 3:30 PM-4:30 PM\r\n- Saturday, June 1 at 6:35 PM-7:35 PM\r\n- Sunday, June 2 at 3:30 PM-4:30 PM\r\n- Saturday, June 15 at 6:35 PM-7:35 PM\r\n- Sunday, June 16 at 3:30 PM-4:30 PM\r\n- Sunday, June 30 at 3:30 PM-4:30 PM\r\n- Thursday, July 4 at 3:30 PM-4:30 PM\r\n- Saturday, July 6 at 3:30 PM-4:30 PM\r\n- Sunday, July 7 at 3:30 PM-4:30 PM\r\n- Sunday, July 21 at 3:30 PM-4:30 PM\r\n- Wednesday, July 24 at 3:00 PM-4:00 PM\r\n- Sunday, August 4 at 3:30 PM-4:30 PM\r\n- Sunday, August 11 at 3:30 PM-4:30 PM\r\n- Saturday, August 24 at 3:30 PM-4:30 PM\r\n- Sunday, August 25 at 3:30 PM-4:30 PM\r\n- Wednesday, August 28 at 3:30 PM-4:30 PM\r\n- Sunday, September 15 at 3:30 PM-4:30 PM\r\n- Thursday, September 19 at 3:30 PM-4:30 PM\r\n- Sunday, September 29 at 2:30 PM-3:30 PM\r\n\nAffected stops:\r\nStop #15190 1st Ave S & S Spokane St (NB)\r\nStop #15205 1st Ave S & S Hanford St (NB)\r\nStop #15230 1st Ave S & S Lander St (NB)\r\nStop #15206 1st Ave S & S Stacy St (NB)\r\nStop #15212 1st Ave S & S Walker St (NB)\r\nStop #15207 1st Ave S & S Holgate St (NB)\r\nStop #15204 Edgar Martinez Dr S & Occidental Ave S (EB)\r\nStop #15201 1st Ave S & S Atlantic St (SB)\r\nStop #15202 1st Ave S & S Holgate St (SB)\r\nStop #15211 1st Ave S & S Walker St (SB)\r\nStop #15203 1st Ave S & S Stacy St (SB)\r\n\nFor Route 21 to Downtown Seattle get on/off buses at:\r\nStop #15145 SW Spokane St & Chelan Ave SW (EB)\r\nStop #30538 4th Ave S & S Spokane St (NB)\r\nStop #30550 4th Ave S & S Hanford St (NB)\r\nStop #30560 4th Ave S & S Forest St (NB)\r\nStop #30570 4th Ave S & S Lander St (NB)\r\nStop #30590 4th Ave S & S Walker St (NB)\r\nStop #30600 4th Ave S & S Holgate St (NB)\r\nStop #30635 4th Ave S & S Royal Brougham Way (NB)\r\n\nFor Route 21 to Westwood Village get on/off buses at:\r\nStop #515 3rd Ave S & S Main St (SB)\r\nStop #30670 4th Ave S & Edgar Martinez Dr S (SB)\r\nStop #30690 4th Ave S & S Holgate St (SB)\r\nStop #30700 4th Ave S & S Walker St (SB)\r\nStop #15380 1st Ave S & S Lander St (SB)", + "alertHeaderText": "Route 21 will be affected in both directions during Mariners afternoon home games; stops on portions of 1st Ave S will not be served; use alternate stops on 4th Ave S instead.", + "alertUrl": "https://drive.google.com/file/d/1rOKsBD54nHp8U1Sh3GGpQQpLMNhXwTDf/view", + "effectiveEndDate": 1727649000000, + "effectiveStartDate": 1711924200000 + } + ], + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 2322.8, + "duration": 410.0, + "endTime": 1715977920000, + "from": { + "arrival": 1715977510000, + "departure": 1715977510000, + "lat": 47.575924, + "lon": -122.334106, + "name": "1st Ave S & S Hanford St", + "stopCode": "15205", + "stopId": "headway-1330:15205", + "stopIndex": 28, + "stopSequence": 194, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 1010, + "headsign": "Downtown Seattle Via 35th Ave SW", + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 41, + "points": "odkaHvktiVgJ?gK???W?aL?cA???}I?s@???qJ?sA???uI?yK?@oC???g@?_AEyB?gB?cA?mC?cCAgBCiBEMACCCCAECI?{FAc@D[RQ?Y?W?G?sD?" + }, + "mode": "BUS", + "pathway": false, + "realTime": false, + "routeId": "headway-1330:100101", + "routeShortName": "21", + "routeType": 3, + "serviceDate": "2024-05-17", + "startTime": 1715977510000, + "steps": [], + "to": { + "arrival": 1715977920000, + "departure": 1715977920000, + "lat": 47.593342, + "lon": -122.328957, + "name": "4th Ave S & S Royal Brougham Way", + "stopCode": "30635", + "stopId": "headway-1330:30635", + "stopIndex": 34, + "stopSequence": 229, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": true, + "tripBlockId": "7142599", + "tripId": "headway-1330:605084236" + }, + { + "agencyId": "headway-1330:1", + "agencyName": "Metro Transit", + "agencyTimeZoneOffset": -25200000, + "agencyUrl": "https://kingcounty.gov/en/dept/metro", + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 7187.77, + "duration": 1440.0, + "endTime": 1715979360000, + "from": { + "arrival": 1715977920000, + "departure": 1715977920000, + "lat": 47.593342, + "lon": -122.328957, + "name": "4th Ave S & S Royal Brougham Way", + "stopCode": "30635", + "stopId": "headway-1330:30635", + "stopIndex": 0, + "stopSequence": 1, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 1440, + "headsign": "Shoreline Greenwood", + "interlineWithPreviousLeg": true, + "legGeometry": { + "length": 195, + "points": "kqnaHpksiVmD?e@?_L?}ECoDCqCAYLY@c@B[CIA??QCUMeDBuAtAILKJEJGLo@rBM^GPIHGLa@\\gB~A??_@\\oC`C]ZoBbBoCbCuBlB??WTmC`CoC~BmCbCqDbD??SRgErDwChCm@j@gA`A??cA`AIFGFGHIPMTOVKRO`@aB`DkBvD??k@jAyCdGyCbGiAtB??oAjCaAsAaAwAcAuAy@kAIIaCkDa@?wDZkB@??}BD]IsA?uB?_@?{@C_@As@CY???E?O?K?MBODM@KDMBOFOJIFKDODOFO@O@M?OAUEWG]M]IQEQEWC]E_@E]A_@GcC@[?o@?}CGcAEiAAoBEaBCsCA{@???mGAeJAi@???qDCcFAgCC]?Q?QBO?O@QBOBQDMBMBOFODQJOFMHSHiCbB??IBwCjBsAx@]RMFCB??YN[No@\\m@Va@R[Nc@Ri@Pi@Rs@Te@Nw@T_Cp@SD??}@ReATYHUDYDQDY@_AFo@Dg@Bq@?q@?eNB}Y@i@IYIYKQKOIi@c@US{@?" + }, + "mode": "BUS", + "pathway": false, + "realTime": false, + "routeId": "headway-1330:100229", + "routeShortName": "5", + "routeType": 3, + "serviceDate": "2024-05-17", + "startTime": 1715977920000, + "steps": [], + "to": { + "arrival": 1715979360000, + "departure": 1715979360000, + "lat": 47.652473, + "lon": -122.346619, + "name": "Aurora Ave N & N 38th St", + "stopCode": "6340", + "stopId": "headway-1330:6340", + "stopIndex": 15, + "stopSequence": 181, + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "transitLeg": true, + "tripBlockId": "7142599", + "tripId": "headway-1330:571900096" + }, + { + "agencyTimeZoneOffset": -25200000, + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 205.38, + "duration": 180.0, + "endTime": 1715979540000, + "from": { + "arrival": 1715979360000, + "departure": 1715979360000, + "lat": 47.652473, + "lon": -122.346619, + "name": "Aurora Ave N & N 38th St", + "stopCode": "6340", + "stopId": "headway-1330:6340", + "vertexType": "TRANSIT", + "zoneId": "1" + }, + "generalizedCost": 779, + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 20, + "points": "}bzaHjyviVG??A@q@FIDGNXPZLNFDJHFHt@l@d@V\\JNBNBB?L?\\?" + }, + "mode": "BICYCLE", + "pathway": false, + "realTime": false, + "rentedBike": false, + "route": "", + "startTime": 1715979360000, + "steps": [ + { + "absoluteDirection": "EAST", + "area": false, + "bogusName": true, + "distance": 30.56, + "elevation": "", + "lat": 47.6525198, + "lon": -122.3466171, + "relativeDirection": "DEPART", + "stayOn": false, + "streetName": "sidewalk", + "walkingBike": true + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": true, + "distance": 37.35, + "elevation": "", + "lat": 47.6524313, + "lon": -122.3462617, + "relativeDirection": "RIGHT", + "stayOn": true, + "streetName": "sidewalk", + "walkingBike": true + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": true, + "distance": 12.25, + "elevation": "", + "lat": 47.6521922, + "lon": -122.34661, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "path", + "walkingBike": false + }, + { + "absoluteDirection": "SOUTHWEST", + "area": false, + "bogusName": true, + "distance": 125.23, + "elevation": "", + "lat": 47.6520984, + "lon": -122.3466956, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "sidewalk", + "walkingBike": true + } + ], + "to": { + "arrival": 1715979540000, + "lat": 47.651048, + "lon": -122.347234, + "name": "Destination", + "vertexType": "NORMAL" + }, + "transitLeg": false, + "walkingBike": false + } + ], + "startTime": 1715977355000, + "tooSloped": false, + "transfers": 0, + "transitTime": 1850, + "waitingTime": 0, + "walkDistance": 657.91, + "walkLimitExceeded": false, + "walkTime": 335 + } + ], + "to": { + "lat": 47.651048, + "lon": -122.347234, + "name": "Destination", + "vertexType": "NORMAL" + } + }, + "previousPageCursor": "MXxQUkVWSU9VU19QQUdFfDIwMjQtMDUtMTdUMTg6NDM6NTVafHw1MG18U1RSRUVUX0FORF9BUlJJVkFMX1RJTUV8fHx8fHw=", + "requestParameters": { + "fromPlace": "47.575837,-122.339414", + "mode": "TRANSIT,BICYCLE", + "toPlace": "47.651048,-122.347234" + } +} diff --git a/services/travelmux/tests/fixtures/requests/opentripplanner_walk_plan.json b/services/travelmux/tests/fixtures/requests/opentripplanner_walk_plan.json new file mode 100644 index 000000000..15338315c --- /dev/null +++ b/services/travelmux/tests/fixtures/requests/opentripplanner_walk_plan.json @@ -0,0 +1,388 @@ +{ + "debugOutput": { + "directStreetRouterTime": 160311692, + "filteringTime": 16741306, + "precalculationTime": 130127, + "renderingTime": 13512812, + "totalTime": 212795218, + "transitRouterTime": 21627150, + "transitRouterTimes": { + "accessEgressTime": 0, + "itineraryCreationTime": 0, + "raptorSearchTime": 0, + "tripPatternFilterTime": 0 + } + }, + "elevationMetadata": { + "ellipsoidToGeoidDifference": -19.263942171473552, + "geoidElevation": false + }, + "plan": { + "date": 1715974434146, + "from": { + "lat": 47.575837, + "lon": -122.339414, + "name": "Origin", + "vertexType": "NORMAL" + }, + "itineraries": [ + { + "arrivedAtDestinationWithRentedBicycle": false, + "duration": 7505, + "elevationGained": 0.0, + "elevationLost": 0.0, + "endTime": 1715981939000, + "fare": { + "details": {}, + "fare": {} + }, + "generalizedCost": 14164, + "legs": [ + { + "agencyTimeZoneOffset": -25200000, + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 9165.05, + "duration": 7505.0, + "endTime": 1715981939000, + "from": { + "departure": 1715974434000, + "lat": 47.575837, + "lon": -122.339414, + "name": "Origin", + "vertexType": "NORMAL" + }, + "generalizedCost": 14164, + "interlineWithPreviousLeg": false, + "legGeometry": { + "length": 380, + "points": "}ckaHbkuiV`@@?J?~@ADANG\\AFEB?@AN}PCcRFq@@}W?{BCe@EYGUM[Um@y@a@g@KQEGq@w@uAiAa@UYQa@Q{EgBuAy@a@I[EUE]GGE?GSA]Eg@IyAc@YIkAWe@SeA_@y@QKC_AO[ASAcAGaACaCAqA@mEGgA?_@?iEBGAAC?K?QG?I?I?a@@GD_@?_ABm@?G?K@_EAGCIFS?MAW?g@BMBOFKBA@KHg@XM@c@`@A?CCCBILEFEFKLCDEDkAbACDA@c@ZIFoAfAMHQLEDGDGFGPCDuAlAC@QPQNSPwApA]XCBmAfA[X]XEDEDKHy@t@_@\\SNIH}@v@sAhA]\\YXORY^[b@ADW`@EFK[O]MNMRa@^sAlAIFGNEDSPQNON_@^k@l@i@j@WXUT_A`AWXQ\\_@t@m@jAA@GLS^CFININIOQXOV{@dAIJODs@x@EGKLKLC@EDEBCAKHUL]JEFKBGBOFCBC?E?GDMJIJADCAMTw@gAGJGPABo@pAIN]H]BWAG?CE]n@ACKKKMCCCCo@}@s@aA@AEGGGKM??CEAAs@aAm@_AAACEKMKQCCA?AAo@}@o@_AACCCCCEIMQAACAAAo@}@m@{@AC?ECACEMOEGCEC?aBaCABCEKOIMCEA@C?ACk@}@o@aA?C?EC@KCCACC}@?E?A@?DCAKCKCC@CAw@?y@?a@?o@AA@C?MAI?C@AAmAAwB?A@ACM?G?EFCEqA?G?Y?{@?S?A?C?M?K?A?AAA?gBAuA?C@CAIAM?I?AB@@?rA?bB_@?g@E_@CW?u@AuADM?wBCCA?EC@G?I?CCAFC@qACAX[Ag@Cg@CgCC_ECcFE[?eA?Q?{CC{ICi@?gDAkDAiA?gC?q@AcAFa@H[HqAh@cDrBeGrDIF_Ah@EB{Av@KDoChAmA^wGfBu@Pq@TUSO@k@PEDG@M@{@HyADw@@oCBiB@oa@A" + }, + "mode": "WALK", + "pathway": false, + "realTime": false, + "rentedBike": false, + "route": "", + "startTime": 1715974434000, + "steps": [ + { + "absoluteDirection": "SOUTH", + "area": false, + "bogusName": false, + "distance": 19.15, + "elevation": "", + "lat": 47.5758346, + "lon": -122.3392181, + "relativeDirection": "DEPART", + "stayOn": false, + "streetName": "East Marginal Way South", + "walkingBike": false + }, + { + "absoluteDirection": "WEST", + "area": false, + "bogusName": true, + "distance": 62.82, + "elevation": "", + "lat": 47.5756624, + "lon": -122.3392227, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "path", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 1198.9, + "elevation": "", + "lat": 47.5757783, + "lon": -122.3400157, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "East Marginal Way South", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 414.27, + "elevation": "", + "lat": 47.58656, + "lon": -122.3400253, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "Alaskan Way South", + "walkingBike": false + }, + { + "absoluteDirection": "NORTHEAST", + "area": false, + "bogusName": true, + "distance": 286.33, + "elevation": "", + "lat": 47.589876, + "lon": -122.3376963, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "Marginal Way Cycle Route", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 659.69, + "elevation": "", + "lat": 47.592328, + "lon": -122.3367458, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "Portside Trail", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 129.47, + "elevation": "", + "lat": 47.5981133, + "lon": -122.3360192, + "relativeDirection": "LEFT", + "stayOn": true, + "streetName": "Portside Trail", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": true, + "distance": 111.29, + "elevation": "", + "lat": 47.5992728, + "lon": -122.3360808, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "Marginal Way Cycle Route", + "walkingBike": false + }, + { + "absoluteDirection": "NORTHWEST", + "area": false, + "bogusName": true, + "distance": 156.24, + "elevation": "", + "lat": 47.6002712, + "lon": -122.3360595, + "relativeDirection": "SLIGHTLY_LEFT", + "stayOn": true, + "streetName": "sidewalk", + "walkingBike": false + }, + { + "absoluteDirection": "NORTHWEST", + "area": false, + "bogusName": true, + "distance": 1253.45, + "elevation": "", + "lat": 47.6015907, + "lon": -122.3365504, + "relativeDirection": "LEFT", + "stayOn": true, + "streetName": "path", + "walkingBike": false + }, + { + "absoluteDirection": "NORTHEAST", + "area": false, + "bogusName": true, + "distance": 8.17, + "elevation": "", + "lat": 47.6107169, + "lon": -122.3455955, + "relativeDirection": "RIGHT", + "stayOn": true, + "streetName": "path", + "walkingBike": false + }, + { + "absoluteDirection": "NORTHWEST", + "area": false, + "bogusName": false, + "distance": 120.75, + "elevation": "", + "lat": 47.610769, + "lon": -122.3455186, + "relativeDirection": "LEFT", + "stayOn": false, + "streetName": "Elliott Way", + "walkingBike": false + }, + { + "absoluteDirection": "NORTHEAST", + "area": false, + "bogusName": true, + "distance": 4.47, + "elevation": "", + "lat": 47.6116213, + "lon": -122.346496, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "path", + "walkingBike": false + }, + { + "absoluteDirection": "NORTHWEST", + "area": false, + "bogusName": true, + "distance": 124.31, + "elevation": "", + "lat": 47.6116517, + "lon": -122.3464569, + "relativeDirection": "LEFT", + "stayOn": true, + "streetName": "bike path", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": true, + "distance": 53.96, + "elevation": "", + "lat": 47.6126463, + "lon": -122.3471407, + "relativeDirection": "RIGHT", + "stayOn": true, + "streetName": "sidewalk", + "walkingBike": false + }, + { + "absoluteDirection": "NORTHWEST", + "area": false, + "bogusName": true, + "distance": 120.37, + "elevation": "", + "lat": 47.6130108, + "lon": -122.3468803, + "relativeDirection": "LEFT", + "stayOn": true, + "streetName": "alley", + "walkingBike": false + }, + { + "absoluteDirection": "NORTHWEST", + "area": false, + "bogusName": true, + "distance": 1218.05, + "elevation": "", + "lat": 47.6138772, + "lon": -122.3475726, + "relativeDirection": "LEFT", + "stayOn": true, + "streetName": "sidewalk", + "walkingBike": false + }, + { + "absoluteDirection": "NORTHWEST", + "area": false, + "bogusName": true, + "distance": 365.79, + "elevation": "", + "lat": 47.6233135, + "lon": -122.3425506, + "relativeDirection": "LEFT", + "stayOn": true, + "streetName": "sidewalk", + "walkingBike": false + }, + { + "absoluteDirection": "WEST", + "area": false, + "bogusName": true, + "distance": 9.21, + "elevation": "", + "lat": 47.6259112, + "lon": -122.3434396, + "relativeDirection": "LEFT", + "stayOn": true, + "streetName": "service road", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": false, + "distance": 1915.66, + "elevation": "", + "lat": 47.6259225, + "lon": -122.3435614, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "Aurora Avenue North", + "walkingBike": false + }, + { + "absoluteDirection": "NORTHEAST", + "area": false, + "bogusName": true, + "distance": 14.02, + "elevation": "", + "lat": 47.642699, + "lon": -122.3470338, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "service road", + "walkingBike": false + }, + { + "absoluteDirection": "NORTH", + "area": false, + "bogusName": true, + "distance": 918.7, + "elevation": "", + "lat": 47.6428041, + "lon": -122.3469305, + "relativeDirection": "LEFT", + "stayOn": true, + "streetName": "sidewalk", + "walkingBike": false + } + ], + "to": { + "arrival": 1715981939000, + "lat": 47.651048, + "lon": -122.347234, + "name": "Destination", + "vertexType": "NORMAL" + }, + "transitLeg": false, + "walkingBike": false + } + ], + "startTime": 1715974434000, + "tooSloped": false, + "transfers": 0, + "transitTime": 0, + "waitingTime": 0, + "walkDistance": 9165.05, + "walkLimitExceeded": false, + "walkTime": 7505 + } + ], + "to": { + "lat": 47.651048, + "lon": -122.347234, + "name": "Destination", + "vertexType": "NORMAL" + } + }, + "requestParameters": { + "fromPlace": "47.575837,-122.339414", + "mode": "WALK", + "toPlace": "47.651048,-122.347234" + } +} diff --git a/services/travelmux/tests/fixtures/requests/refresh.sh b/services/travelmux/tests/fixtures/requests/refresh.sh new file mode 100755 index 000000000..244db06c8 --- /dev/null +++ b/services/travelmux/tests/fixtures/requests/refresh.sh @@ -0,0 +1,45 @@ +set -ex + +# Fetch and format real responses from a locally running server - i.e. when the schema changes +function fetch_valhalla { + mode=$1 + output_prefix="valhalla_$(echo "$mode" | tr '[:upper:]' '[:lower:]')" + + # from realFine coffee in West Seattle to Zeitgeist downtown Seattle + json_data="{ + \"locations\": [ + {\"lat\": 47.575837, \"lon\": -122.339414}, + {\"lat\": 47.651048, \"lon\": -122.347234} + ], + \"costing\": \"$mode\", + \"alternates\": 3, + \"units\": \"miles\" + }" + encoded_json=$(echo $json_data | jq -c . | sed 's/ /%20/g; s/{/%7B/g; s/}/%7D/g; s/:/%3A/g; s/,/%2C/g; s/\"/%22/g; s/\[/%5B/g; s/\]/%5D/g') + curl "http://localhost:9001/route?json=$encoded_json" | jq -S . > "${output_prefix}_route.json" +} + +fetch_valhalla pedestrian +fetch_valhalla bicycle +fetch_valhalla auto # car + +function fetch_opentripplanner { + mode=$1 + output_prefix="opentripplanner_$(echo "$mode" | tr '[:upper:]' '[:lower:]' | sed 's/,/_with_/')" + + start_lat=47.575837 + start_lon=-122.339414 + end_lat=47.651048 + end_lon=-122.347234 + + request_url="http://localhost:9002/otp/routers/default/plan?fromPlace=$start_lat,$start_lon&toPlace=$end_lat,$end_lon&mode=${mode}" + + # Make the request and save the output to a file + curl "$request_url" | jq -S . > "${output_prefix}_plan.json" +} + +fetch_opentripplanner WALK +fetch_opentripplanner BICYCLE +fetch_opentripplanner TRANSIT +fetch_opentripplanner "TRANSIT,BICYCLE" + diff --git a/services/travelmux/tests/fixtures/requests/valhalla_auto_route.json b/services/travelmux/tests/fixtures/requests/valhalla_auto_route.json new file mode 100644 index 000000000..a26b9120a --- /dev/null +++ b/services/travelmux/tests/fixtures/requests/valhalla_auto_route.json @@ -0,0 +1,107 @@ +{ + "trip": { + "language": "en-US", + "legs": [ + { + "maneuvers": [ + { + "begin_shape_index": 0, + "cost": 105.738, + "end_shape_index": 21, + "highway": true, + "instruction": "Drive north on WA 99/SR 99.", + "length": 1.094, + "street_names": [ + "WA 99", + "SR 99" + ], + "time": 98.008, + "travel_mode": "drive", + "travel_type": "car", + "type": 1, + "verbal_post_transition_instruction": "Continue for 1 mile.", + "verbal_pre_transition_instruction": "Drive north on WA 99, SR 99.", + "verbal_succinct_transition_instruction": "Drive north." + }, + { + "begin_shape_index": 21, + "cost": 434.58, + "end_shape_index": 133, + "highway": true, + "instruction": "Keep left to stay on WA 99.", + "length": 4.269, + "street_names": [ + "WA 99" + ], + "time": 372.288, + "toll": true, + "travel_mode": "drive", + "travel_type": "car", + "type": 24, + "verbal_post_transition_instruction": "Continue for 4 miles.", + "verbal_pre_transition_instruction": "Keep left to stay on WA 99.", + "verbal_transition_alert_instruction": "Keep left to stay on WA 99." + }, + { + "begin_shape_index": 133, + "cost": 0.0, + "end_shape_index": 133, + "instruction": "Your destination is on the right.", + "length": 0.0, + "time": 0.0, + "travel_mode": "drive", + "travel_type": "car", + "type": 5, + "verbal_pre_transition_instruction": "Your destination is on the right.", + "verbal_transition_alert_instruction": "Your destination will be on the right." + } + ], + "shape": "ypxvyAt`_jhFev@?kgBEop@g@s~CMqnC@ycAxBmRRkZmAiP_BkK{BkHeCkLkF_ToKmSqNuVyQs[}SoTcL}PyI{pBer@ic@eOmBs@e}@_Zi^{LuMoDwNyC{J}AsO_BuOqAiOk@uR[wQDml@P}Ox@oK~@o`@tE_d@hEgd@~Bsd@t@k{AG}g@bAil@bDec@fEeq@|Jgp@jNi~@lXgs@~Xwz@rb@og@zZqc@~Zgd@~_@csCbeC}i@hc@}h@v^er@`a@u{@la@aw@fYyu@zS}v@nOso@tIue@~Dwu@zCkaADa~@wEgO_CyRiCuY{CqHaBgEuA}CiAwOqIoO_KsCiBaQyJ{JaFiGsCmGaCqGoBuG}AwGkAyGy@{Gg@aIYwCAyMLyNdBcCXeSAkQUe_@o@yGOmKa@oK_@ui@{@uz@e@s{@g@yRGsTGuDA_p@YukBe@cLG_s@Ucu@UaVAoi@CyNAySlA}ItAoGhBoXfLir@fc@eqAlw@iBfAoRzKgAf@y[pPiBt@cl@rVqW`IqvAb_@{OrDwN|E{HzAiHjBkHvCcHr@aHj@eHb@cHZeHPmbI@eND}kB`@", + "summary": { + "cost": 540.319, + "has_ferry": false, + "has_highway": true, + "has_time_restrictions": false, + "has_toll": true, + "length": 5.364, + "max_lat": 47.651047, + "max_lon": -122.3355, + "min_lat": 47.575837, + "min_lon": -122.34732, + "time": 470.296 + } + } + ], + "locations": [ + { + "lat": 47.575837, + "lon": -122.339414, + "original_index": 0, + "type": "break" + }, + { + "lat": 47.651048, + "lon": -122.347234, + "original_index": 1, + "side_of_street": "right", + "type": "break" + } + ], + "status": 0, + "status_message": "Found route between points", + "summary": { + "cost": 540.319, + "has_ferry": false, + "has_highway": true, + "has_time_restrictions": false, + "has_toll": true, + "length": 5.364, + "max_lat": 47.651047, + "max_lon": -122.3355, + "min_lat": 47.575837, + "min_lon": -122.34732, + "time": 470.296 + }, + "units": "miles" + } +} diff --git a/services/travelmux/tests/fixtures/requests/valhalla_bicycle_route.json b/services/travelmux/tests/fixtures/requests/valhalla_bicycle_route.json new file mode 100644 index 000000000..a51e4674a --- /dev/null +++ b/services/travelmux/tests/fixtures/requests/valhalla_bicycle_route.json @@ -0,0 +1,332 @@ +{ + "trip": { + "language": "en-US", + "legs": [ + { + "maneuvers": [ + { + "begin_shape_index": 0, + "cost": 15.308, + "end_shape_index": 2, + "instruction": "Bike south on East Marginal Way South.", + "length": 0.016, + "street_names": [ + "East Marginal Way South" + ], + "time": 7.494, + "travel_mode": "bicycle", + "travel_type": "hybrid", + "type": 2, + "verbal_multi_cue": true, + "verbal_post_transition_instruction": "Continue for 90 feet.", + "verbal_pre_transition_instruction": "Bike south on East Marginal Way South. Then Turn right onto South Hanford Street.", + "verbal_succinct_transition_instruction": "Bike south. Then Turn right onto South Hanford Street." + }, + { + "begin_shape_index": 2, + "cost": 66.207, + "end_shape_index": 7, + "instruction": "Turn right onto South Hanford Street.", + "length": 0.036, + "street_names": [ + "South Hanford Street" + ], + "time": 17.599, + "travel_mode": "bicycle", + "travel_type": "hybrid", + "type": 10, + "verbal_multi_cue": true, + "verbal_post_transition_instruction": "Continue for 200 feet.", + "verbal_pre_transition_instruction": "Turn right onto South Hanford Street. Then Turn right onto East Marginal Way South.", + "verbal_succinct_transition_instruction": "Turn right. Then Turn right onto East Marginal Way South.", + "verbal_transition_alert_instruction": "Turn right onto South Hanford Street." + }, + { + "begin_shape_index": 7, + "cost": 630.508, + "end_shape_index": 32, + "instruction": "Turn right onto East Marginal Way South.", + "length": 1.092, + "street_names": [ + "East Marginal Way South" + ], + "time": 359.199, + "travel_mode": "bicycle", + "travel_type": "hybrid", + "type": 10, + "verbal_post_transition_instruction": "Continue for 1 mile.", + "verbal_pre_transition_instruction": "Turn right onto East Marginal Way South.", + "verbal_succinct_transition_instruction": "Turn right.", + "verbal_transition_alert_instruction": "Turn right onto East Marginal Way South." + }, + { + "begin_shape_index": 32, + "cost": 11.259, + "end_shape_index": 33, + "instruction": "Turn left onto the cycleway.", + "length": 0.006, + "time": 3.1, + "travel_mode": "bicycle", + "travel_type": "hybrid", + "type": 15, + "verbal_multi_cue": true, + "verbal_post_transition_instruction": "Continue for 40 feet.", + "verbal_pre_transition_instruction": "Turn left onto the cycleway. Then Bear right onto Portside Trail.", + "verbal_succinct_transition_instruction": "Turn left. Then Bear right onto Portside Trail.", + "verbal_transition_alert_instruction": "Turn left onto the cycleway." + }, + { + "begin_shape_index": 33, + "cost": 285.359, + "end_shape_index": 76, + "instruction": "Bear right onto Portside Trail.", + "length": 0.59, + "street_names": [ + "Portside Trail" + ], + "time": 199.65, + "travel_mode": "bicycle", + "travel_type": "hybrid", + "type": 9, + "verbal_post_transition_instruction": "Continue for a half mile.", + "verbal_pre_transition_instruction": "Bear right onto Portside Trail.", + "verbal_succinct_transition_instruction": "Bear right.", + "verbal_transition_alert_instruction": "Bear right onto Portside Trail." + }, + { + "begin_shape_index": 76, + "cost": 123.604, + "end_shape_index": 97, + "instruction": "Continue on the cycleway.", + "length": 0.178, + "time": 75.55, + "travel_mode": "bicycle", + "travel_type": "hybrid", + "type": 8, + "verbal_post_transition_instruction": "Continue for 900 feet.", + "verbal_pre_transition_instruction": "Continue on the cycleway.", + "verbal_transition_alert_instruction": "Continue on the cycleway." + }, + { + "begin_shape_index": 97, + "cost": 6.632, + "end_shape_index": 98, + "instruction": "Continue on Elliott Bay Trail.", + "length": 0.001, + "street_names": [ + "Elliott Bay Trail" + ], + "time": 0.849, + "travel_mode": "bicycle", + "travel_type": "hybrid", + "type": 8, + "verbal_multi_cue": true, + "verbal_post_transition_instruction": "Continue for less than 10 feet.", + "verbal_pre_transition_instruction": "Continue on Elliott Bay Trail. Then Turn right onto the cycleway.", + "verbal_transition_alert_instruction": "Continue on Elliott Bay Trail." + }, + { + "begin_shape_index": 98, + "cost": 30.257, + "end_shape_index": 99, + "instruction": "Turn right onto the cycleway.", + "length": 0.016, + "time": 9.4, + "travel_mode": "bicycle", + "travel_type": "hybrid", + "type": 10, + "verbal_multi_cue": true, + "verbal_post_transition_instruction": "Continue for 90 feet.", + "verbal_pre_transition_instruction": "Turn right onto the cycleway. Then Turn left onto Alaskan Way.", + "verbal_succinct_transition_instruction": "Turn right. Then Turn left onto Alaskan Way.", + "verbal_transition_alert_instruction": "Turn right onto the cycleway." + }, + { + "begin_shape_index": 99, + "cost": 148.922, + "end_shape_index": 105, + "instruction": "Turn left onto Alaskan Way.", + "length": 0.113, + "street_names": [ + "Alaskan Way" + ], + "time": 38.55, + "travel_mode": "bicycle", + "travel_type": "hybrid", + "type": 15, + "verbal_post_transition_instruction": "Continue for 600 feet.", + "verbal_pre_transition_instruction": "Turn left onto Alaskan Way.", + "verbal_succinct_transition_instruction": "Turn left.", + "verbal_transition_alert_instruction": "Turn left onto Alaskan Way." + }, + { + "begin_shape_index": 105, + "cost": 54.772, + "end_shape_index": 108, + "instruction": "Turn right onto Marion Street.", + "length": 0.045, + "street_names": [ + "Marion Street" + ], + "time": 17.6, + "travel_mode": "bicycle", + "travel_type": "hybrid", + "type": 10, + "verbal_post_transition_instruction": "Continue for 200 feet.", + "verbal_pre_transition_instruction": "Turn right onto Marion Street.", + "verbal_succinct_transition_instruction": "Turn right.", + "verbal_transition_alert_instruction": "Turn right onto Marion Street." + }, + { + "begin_shape_index": 108, + "cost": 537.5, + "end_shape_index": 156, + "instruction": "Turn left onto Western Avenue.", + "length": 0.733, + "street_names": [ + "Western Avenue" + ], + "time": 260.699, + "travel_mode": "bicycle", + "travel_type": "hybrid", + "type": 15, + "verbal_post_transition_instruction": "Continue for a half mile.", + "verbal_pre_transition_instruction": "Turn left onto Western Avenue.", + "verbal_succinct_transition_instruction": "Turn left.", + "verbal_transition_alert_instruction": "Turn left onto Western Avenue." + }, + { + "begin_shape_index": 156, + "cost": 397.137, + "end_shape_index": 193, + "instruction": "Turn right onto Blanchard Street.", + "length": 0.426, + "street_names": [ + "Blanchard Street" + ], + "time": 196.599, + "travel_mode": "bicycle", + "travel_type": "hybrid", + "type": 10, + "verbal_post_transition_instruction": "Continue for a half mile.", + "verbal_pre_transition_instruction": "Turn right onto Blanchard Street.", + "verbal_succinct_transition_instruction": "Turn right.", + "verbal_transition_alert_instruction": "Turn right onto Blanchard Street." + }, + { + "begin_shape_index": 193, + "cost": 1661.323, + "end_shape_index": 387, + "instruction": "Turn left onto 7th Avenue.", + "length": 2.718, + "street_names": [ + "7th Avenue" + ], + "time": 956.249, + "travel_mode": "bicycle", + "travel_type": "hybrid", + "type": 15, + "verbal_post_transition_instruction": "Continue for 3 miles.", + "verbal_pre_transition_instruction": "Turn left onto 7th Avenue.", + "verbal_succinct_transition_instruction": "Turn left.", + "verbal_transition_alert_instruction": "Turn left onto 7th Avenue." + }, + { + "begin_shape_index": 387, + "cost": 125.572, + "end_shape_index": 398, + "instruction": "Make a sharp right onto Fremont Way North.", + "length": 0.131, + "street_names": [ + "Fremont Way North" + ], + "time": 48.4, + "travel_mode": "bicycle", + "travel_type": "hybrid", + "type": 11, + "verbal_post_transition_instruction": "Continue for 700 feet.", + "verbal_pre_transition_instruction": "Make a sharp right onto Fremont Way North.", + "verbal_succinct_transition_instruction": "Make a sharp right.", + "verbal_transition_alert_instruction": "Make a sharp right onto Fremont Way North." + }, + { + "begin_shape_index": 398, + "cost": 134.911, + "end_shape_index": 408, + "instruction": "Continue on Fremont Way North.", + "length": 0.102, + "street_names": [ + "Fremont Way North" + ], + "time": 33.898, + "travel_mode": "bicycle", + "travel_type": "hybrid", + "type": 8, + "verbal_post_transition_instruction": "Continue for 500 feet.", + "verbal_pre_transition_instruction": "Continue on Fremont Way North.", + "verbal_transition_alert_instruction": "Continue on Fremont Way North." + }, + { + "begin_shape_index": 408, + "cost": 0.0, + "end_shape_index": 408, + "instruction": "Your destination is on the left.", + "length": 0.0, + "time": 0.0, + "travel_mode": "bicycle", + "travel_type": "hybrid", + "type": 6, + "verbal_pre_transition_instruction": "Your destination is on the left.", + "verbal_transition_alert_instruction": "Your destination will be on the left." + } + ], + "shape": "wpxvyA~w~ihFxIDzBHCdCAvTA|MC~A@tDoIAorD_@k~DnAkNDoxFAke@a@iKq@cG}AuEaCqG}EwMcQiI{K_CmDcAkAqNgPoYcVqIeFaGiD}IaEwcAi_@yLuDaIeCcD_AeYaIyKa@kBnFc@b@aBdBs@d@q@B_AO}@[mAcAgG{FoDyC}Am@qI_CqCgAoImFyXeQ{JmE{T}HwPoDyBa@}RcDeH[yDEeT_BqSe@ch@GqXJm_AkAcU?mIIa~@d@aAKYm@GyBHcDqAEiBAwA?uIJ{A|@yHH{R`@sMFmA@qBB_{@AkAg@aBu@iILuADwFBiFNyALsC\\aDf@oARyEbAmFpByG`DuBtAy@|A{EhEiDzCc@^e@d@aCxB]XEmU}BtAq_@zYyFrFyBlBw_@~\\cFnEsBsG{Mmb@eBmF}BnBsJpIaOrMqFzEuChC_DrC}b@h`@cB|AeCvBqD|Cc^`[cCtBeDzCya@x_@cC|ByBjBwu@zo@oC|BoAfAsEdEil@nh@yEdE[ZwCpC_GnF_O`NyLbL{BrBq@l@yAtA_EpE{KbUcBjDgGvL{Sbb@_LvS}BnBkC~BoCvBwMhKoCrCkAxB_X`h@gAdCoBlC}N|YyWrj@}AtC}AwBiP{TiOmTiCsDyBcD{OkUwNoSoC{DwBqAaAg@KQ{NgV{MgRyCgEkD{E_OmSmIyL}C_F}AsBeAuAaDsEwJeN_CeDcOuSo@w@oDeFcAyAmOiTaN{RqBsC_AuAcDoEoCwDeJeMoNwRs@_AeCiDwBhEkh@vdA{AzC_AnBg@bAiYll@iAzBuAlB_BxAeBdAkBn@oBXoBBcA?mMCeEAcDAeQGqQGmIEqFAwFE_E?wCCuWIme@KsCAyDA{b@EsIA{@?yDAsFA{DA}C?}NBwO?g\\?yC@wDAiL@sF?aUByD?Y?eH@sCAqD?}FDiC?kA?oOQ_CAaCAeGAuEA_BAmD?wCCyi@YgDCuEA{XImQGsCAqCA_H?sAAaj@Iwz@MeVCiSAeBAiD?qe@MeEAaFA}WI_OEiA?qAAoe@M_LEkA?_C?uC?aEAiF?oLA}EA{B?{D@iH@uE@mb@GeECg`AMqFAy@?_QEoC@oCPgANgANmCl@mC~@iClAeD|AkFhCeGvCaUtKs@\\sHtDgb@lSyL`G{CvAcErBsQ~IgKdFkN~Ec`AhTcB^ib@lJwMvCcNlCyIlA_CZiPbAaq@~DeIx@yEj@aALcFvAkDrBi`Ajv@uKvI{BlCqBbC{BjCgD`EsBdDgC`EwEpJwBvG}IpZ{F`TmGrUeOhb@sUdf@cB|BuKxOyEbGwArAw@j@u@f@qCzAgAh@eARwFn@yHXgCD_BBwDH{Yf@eQCgr@AmNAoE?yD?oK?qJ?uJ?gPB{A@gA?yFAcDD_FZaGNU@uLf@kJ^aIZoGXyERsQr@cK`@yFToDNiD?cg@Dsi@BqBAgp@UgDmC}@kBSsBTqF|TmVt]u_@dAiAdBiB~A_BjOsPrDaEpO_PfGoGtAwAdEoE|DgDzDsBxDuA~JcDlIcBjHC", + "summary": { + "cost": 4229.279, + "has_ferry": false, + "has_highway": false, + "has_time_restrictions": false, + "has_toll": false, + "length": 6.211, + "max_lat": 47.653717, + "max_lon": -122.336014, + "min_lat": 47.575601, + "min_lon": -122.349926, + "time": 2224.842 + } + } + ], + "locations": [ + { + "lat": 47.575837, + "lon": -122.339414, + "original_index": 0, + "side_of_street": "right", + "type": "break" + }, + { + "lat": 47.651048, + "lon": -122.347234, + "original_index": 1, + "side_of_street": "left", + "type": "break" + } + ], + "status": 0, + "status_message": "Found route between points", + "summary": { + "cost": 4229.279, + "has_ferry": false, + "has_highway": false, + "has_time_restrictions": false, + "has_toll": false, + "length": 6.211, + "max_lat": 47.653717, + "max_lon": -122.336014, + "min_lat": 47.575601, + "min_lon": -122.349926, + "time": 2224.842 + }, + "units": "miles" + } +} diff --git a/services/travelmux/tests/fixtures/requests/valhalla_route_walk.json b/services/travelmux/tests/fixtures/requests/valhalla_pedestrian_route.json similarity index 85% rename from services/travelmux/tests/fixtures/requests/valhalla_route_walk.json rename to services/travelmux/tests/fixtures/requests/valhalla_pedestrian_route.json index 67ded7391..d5b2aae31 100644 --- a/services/travelmux/tests/fixtures/requests/valhalla_route_walk.json +++ b/services/travelmux/tests/fixtures/requests/valhalla_pedestrian_route.json @@ -1,1666 +1,1666 @@ { - "trip": { - "locations": [ - { - "type": "break", - "lat": 47.575837, - "lon": -122.339414, - "side_of_street": "right", - "original_index": 0 - }, - { - "type": "break", - "lat": 47.651048, - "lon": -122.347234, - "original_index": 1 - } - ], - "legs": [ - { - "maneuvers": [ - { - "type": 2, - "instruction": "Walk south on East Marginal Way South.", - "verbal_succinct_transition_instruction": "Walk south. Then Turn right onto the walkway.", - "verbal_pre_transition_instruction": "Walk south on East Marginal Way South. Then Turn right onto the walkway.", - "verbal_post_transition_instruction": "Continue for 20 meters.", - "street_names": [ - "East Marginal Way South" - ], - "time": 13.567, - "length": 0.019, - "cost": 13.567, - "begin_shape_index": 0, - "end_shape_index": 1, - "verbal_multi_cue": true, - "travel_mode": "pedestrian", - "travel_type": "foot" - }, - { - "type": 10, - "instruction": "Turn right onto the walkway.", - "verbal_transition_alert_instruction": "Turn right onto the walkway.", - "verbal_succinct_transition_instruction": "Turn right.", - "verbal_pre_transition_instruction": "Turn right onto the walkway.", - "verbal_post_transition_instruction": "Continue for 60 meters.", - "time": 44.47, - "length": 0.063, - "cost": 49.47, - "begin_shape_index": 1, - "end_shape_index": 9, - "rough": true, - "travel_mode": "pedestrian", - "travel_type": "foot" - }, - { - "type": 10, - "instruction": "Turn right onto East Marginal Way South.", - "verbal_transition_alert_instruction": "Turn right onto East Marginal Way South.", - "verbal_succinct_transition_instruction": "Turn right.", - "verbal_pre_transition_instruction": "Turn right onto East Marginal Way South.", - "verbal_post_transition_instruction": "Continue for 1.5 kilometers.", - "street_names": [ - "East Marginal Way South" - ], - "time": 1139.294, - "length": 1.614, - "cost": 1149.294, - "begin_shape_index": 9, - "end_shape_index": 28, - "travel_mode": "pedestrian", - "travel_type": "foot" - }, - { - "type": 24, - "instruction": "Keep left to take Alaskan Way South.", - "verbal_transition_alert_instruction": "Keep left to take Alaskan Way South.", - "verbal_pre_transition_instruction": "Keep left to take Alaskan Way South.", - "verbal_post_transition_instruction": "Continue for 800 meters.", - "street_names": [ - "Alaskan Way South" - ], - "time": 542.117, - "length": 0.768, - "cost": 542.117, - "begin_shape_index": 28, - "end_shape_index": 52, - "travel_mode": "pedestrian", - "travel_type": "foot" - }, - { - "type": 15, - "instruction": "Turn left to stay on Alaskan Way South.", - "verbal_transition_alert_instruction": "Turn left to stay on Alaskan Way South.", - "verbal_succinct_transition_instruction": "Turn left.", - "verbal_pre_transition_instruction": "Turn left to stay on Alaskan Way South.", - "verbal_post_transition_instruction": "Continue for 800 meters.", - "street_names": [ - "Alaskan Way South" - ], - "time": 551.588, - "length": 0.78, - "cost": 556.588, - "begin_shape_index": 52, - "end_shape_index": 92, - "travel_mode": "pedestrian", - "travel_type": "foot" - }, - { - "type": 10, - "instruction": "Turn right onto Marion Street.", - "verbal_transition_alert_instruction": "Turn right onto Marion Street.", - "verbal_succinct_transition_instruction": "Turn right.", - "verbal_pre_transition_instruction": "Turn right onto Marion Street.", - "verbal_post_transition_instruction": "Continue for 200 meters.", - "street_names": [ - "Marion Street" - ], - "time": 120.293, - "length": 0.168, - "cost": 125.293, - "begin_shape_index": 92, - "end_shape_index": 101, - "travel_mode": "pedestrian", - "travel_type": "foot" - }, - { - "type": 15, - "instruction": "Turn left onto 1st Avenue.", - "verbal_transition_alert_instruction": "Turn left onto 1st Avenue.", - "verbal_succinct_transition_instruction": "Turn left.", - "verbal_pre_transition_instruction": "Turn left onto 1st Avenue.", - "verbal_post_transition_instruction": "Continue for 800 meters.", - "street_names": [ - "1st Avenue" - ], - "time": 584.53, - "length": 0.82, - "cost": 589.53, - "begin_shape_index": 101, - "end_shape_index": 134, - "travel_mode": "pedestrian", - "travel_type": "foot" - }, - { - "type": 10, - "instruction": "Turn right onto Stewart Street.", - "verbal_transition_alert_instruction": "Turn right onto Stewart Street.", - "verbal_succinct_transition_instruction": "Turn right.", - "verbal_pre_transition_instruction": "Turn right onto Stewart Street.", - "verbal_post_transition_instruction": "Continue for 200 meters.", - "street_names": [ - "Stewart Street" - ], - "time": 157.765, - "length": 0.215, - "cost": 162.765, - "begin_shape_index": 134, - "end_shape_index": 148, - "travel_mode": "pedestrian", - "travel_type": "foot" - }, - { - "type": 24, - "instruction": "Keep left to stay on Stewart Street.", - "verbal_transition_alert_instruction": "Keep left to stay on Stewart Street.", - "verbal_pre_transition_instruction": "Keep left to stay on Stewart Street.", - "verbal_post_transition_instruction": "Continue for 200 meters.", - "street_names": [ - "Stewart Street" - ], - "time": 161.823, - "length": 0.225, - "cost": 161.823, - "begin_shape_index": 148, - "end_shape_index": 161, - "travel_mode": "pedestrian", - "travel_type": "foot" - }, - { - "type": 15, - "instruction": "Turn left onto Westlake Avenue.", - "verbal_transition_alert_instruction": "Turn left onto Westlake Avenue.", - "verbal_succinct_transition_instruction": "Turn left.", - "verbal_pre_transition_instruction": "Turn left onto Westlake Avenue.", - "verbal_post_transition_instruction": "Continue for 80 meters.", - "street_names": [ - "Westlake Avenue" - ], - "time": 52.941, - "length": 0.075, - "cost": 57.941, - "begin_shape_index": 161, - "end_shape_index": 166, - "travel_mode": "pedestrian", - "travel_type": "foot" - }, - { - "type": 22, - "instruction": "Keep straight to stay on Westlake Avenue.", - "verbal_transition_alert_instruction": "Keep straight to stay on Westlake Avenue.", - "verbal_pre_transition_instruction": "Keep straight to stay on Westlake Avenue.", - "verbal_post_transition_instruction": "Continue for 200 meters.", - "street_names": [ - "Westlake Avenue" - ], - "time": 108.588, - "length": 0.151, - "cost": 108.588, - "begin_shape_index": 166, - "end_shape_index": 176, - "travel_mode": "pedestrian", - "travel_type": "foot" - }, - { - "type": 24, - "instruction": "Keep left to take 7th Avenue.", - "verbal_transition_alert_instruction": "Keep left to take 7th Avenue.", - "verbal_pre_transition_instruction": "Keep left to take 7th Avenue.", - "verbal_post_transition_instruction": "Continue for 400 meters.", - "street_names": [ - "7th Avenue" - ], - "time": 307.235, - "length": 0.43, - "cost": 312.235, - "begin_shape_index": 176, - "end_shape_index": 193, - "travel_mode": "pedestrian", - "travel_type": "foot" - }, - { - "type": 23, - "instruction": "Keep right to take Dexter Avenue.", - "verbal_transition_alert_instruction": "Keep right to take Dexter Avenue.", - "verbal_pre_transition_instruction": "Keep right to take Dexter Avenue.", - "verbal_post_transition_instruction": "Continue for 2.5 kilometers.", - "street_names": [ - "Dexter Avenue" - ], - "time": 1802.647, - "length": 2.541, - "cost": 1812.646, - "begin_shape_index": 193, - "end_shape_index": 311, - "travel_mode": "pedestrian", - "travel_type": "foot" - }, - { - "type": 15, - "instruction": "Turn left onto 6th Avenue North.", - "verbal_transition_alert_instruction": "Turn left onto 6th Avenue North.", - "verbal_succinct_transition_instruction": "Turn left. Then, in 20 meters, Turn right to stay on 6th Avenue North.", - "verbal_pre_transition_instruction": "Turn left onto 6th Avenue North. Then, in 20 meters, Turn right to stay on 6th Avenue North.", - "verbal_post_transition_instruction": "Continue for 20 meters.", - "street_names": [ - "6th Avenue North" - ], - "time": 10.588, - "length": 0.015, - "cost": 15.588, - "begin_shape_index": 311, - "end_shape_index": 315, - "verbal_multi_cue": true, - "travel_mode": "pedestrian", - "travel_type": "foot" - }, - { - "type": 10, - "instruction": "Turn right to stay on 6th Avenue North.", - "verbal_transition_alert_instruction": "Turn right to stay on 6th Avenue North.", - "verbal_succinct_transition_instruction": "Turn right.", - "verbal_pre_transition_instruction": "Turn right to stay on 6th Avenue North.", - "verbal_post_transition_instruction": "Continue for 200 meters.", - "street_names": [ - "6th Avenue North" - ], - "time": 161.646, - "length": 0.229, - "cost": 161.646, - "begin_shape_index": 315, - "end_shape_index": 329, - "travel_mode": "pedestrian", - "travel_type": "foot" - }, - { - "type": 15, - "instruction": "Turn left onto Halladay Street.", - "verbal_transition_alert_instruction": "Turn left onto Halladay Street.", - "verbal_succinct_transition_instruction": "Turn left.", - "verbal_pre_transition_instruction": "Turn left onto Halladay Street.", - "verbal_post_transition_instruction": "Continue for 30 meters.", - "street_names": [ - "Halladay Street" - ], - "time": 22.588, - "length": 0.032, - "cost": 27.588, - "begin_shape_index": 329, - "end_shape_index": 330, - "travel_mode": "pedestrian", - "travel_type": "foot" - }, - { - "type": 23, - "instruction": "Keep right at the fork.", - "verbal_transition_alert_instruction": "Keep right at the fork.", - "verbal_pre_transition_instruction": "Keep right at the fork.", - "verbal_post_transition_instruction": "Continue for 30 meters.", - "time": 19.058, - "length": 0.027, - "cost": 19.058, - "begin_shape_index": 330, - "end_shape_index": 332, - "travel_mode": "pedestrian", - "travel_type": "foot" - }, - { - "type": 10, - "instruction": "Turn right onto the walkway.", - "verbal_transition_alert_instruction": "Turn right onto the walkway.", - "verbal_succinct_transition_instruction": "Turn right. Then Turn left onto the walkway.", - "verbal_pre_transition_instruction": "Turn right onto the walkway. Then Turn left onto the walkway.", - "verbal_post_transition_instruction": "Continue for less than 10 meters.", - "time": 4.235, - "length": 0.006, - "cost": 4.235, - "begin_shape_index": 332, - "end_shape_index": 334, - "verbal_multi_cue": true, - "travel_mode": "pedestrian", - "travel_type": "foot" - }, - { - "type": 15, - "instruction": "Turn left onto the walkway.", - "verbal_transition_alert_instruction": "Turn left onto the walkway.", - "verbal_succinct_transition_instruction": "Turn left.", - "verbal_pre_transition_instruction": "Turn left onto the walkway.", - "verbal_post_transition_instruction": "Continue for 80 meters.", - "time": 58.882, - "length": 0.082, - "cost": 58.882, - "begin_shape_index": 334, - "end_shape_index": 340, - "travel_mode": "pedestrian", - "travel_type": "foot" - }, - { - "type": 24, - "instruction": "Keep left to take the walkway.", - "verbal_transition_alert_instruction": "Keep left to take the walkway.", - "verbal_pre_transition_instruction": "Keep left to take the walkway.", - "verbal_post_transition_instruction": "Continue for 900 meters.", - "time": 624.581, - "length": 0.884, - "cost": 624.581, - "begin_shape_index": 340, - "end_shape_index": 349, - "travel_mode": "pedestrian", - "travel_type": "foot" - }, - { - "type": 4, - "instruction": "You have arrived at your destination.", - "verbal_transition_alert_instruction": "You will arrive at your destination.", - "verbal_pre_transition_instruction": "You have arrived at your destination.", - "time": 0.0, - "length": 0.0, - "cost": 0.0, - "begin_shape_index": 349, - "end_shape_index": 349, - "travel_mode": "pedestrian", - "travel_type": "foot" - } - ], - "summary": { - "has_time_restrictions": false, - "has_toll": false, - "has_highway": false, - "has_ferry": false, - "min_lat": 47.575663, - "min_lon": -122.347201, - "max_lat": 47.651047, - "max_lon": -122.335618, - "time": 6488.443, - "length": 9.148, - "cost": 6553.443 - }, - "shape": "wpxvyA~w~ihFxIDAhDEnQCnA]nC_BdJ]Va@hASvCorD_@k~DnAkNDoxFAke@a@iKq@cG}AuEaCqG}EwMcQiI{K_CmDcAkAqNgPoYcVqIeFaGiD}IaEwcAi_@yLuDaIeCcD_AeYaIyKa@aEMyOmDmSyH}t@u[cEiCuFkFkDmDuAyAaSoJcNuD}OcAwVMoU?klBCmEUeDqA_C}Bg@{AAuDqHg@aAG_Uf@_YhBqMx@_I`@cM`@oAAiJCiIVgCDqYf@oJJeFP}FFwYPaKTgDDuCByGIyJQeNGeE\\_@DmD`@gCp@cATaB^mDfA_DfAwD`B_MnHcJYmCbB}BtAq_@zYyFrFyBlBw_@~\\cFnEsBsG{Mmb@eBmFcBqFqF}PkAoDaBcFmHyToBaG_CtByJvIoUjS}CnCmC~Bid@z`@{ApAaBxAud@da@YTaBvAwChCwc@j`@iB`BkCbCsDjDeUxR{X|UcCrBqC`Cyg@zc@qIpHOLyEfEoC~B{CnCkPxNaGbFyU~RwCbCcCvBmSrQuCfCwB{CeCoDoKgOEIeMsQoDcFmBmCsAoBwNqSwAoBsK_OcDqEwD{EgAkBiJqL{F}H{GsJwB_DwAsBmCyDaOwSkNeS}@oA}CuE{@oAqHoKcE}FuEf@iEd@yTrB{BRqBRwGf@oE`@kKfAuANqMzAeCXkD`@gUrC_BRcCZc@x@{@dB_CtE{@dBiTtb@oBxDkCjF_HfNaJvQiSja@iBrDwBhEkh@vdA{AzC_AnBg@bAiYll@iAzBuAlB_BxAeBdAkBn@oBXoBBcA?mMCeEAcDAeQGqQGmIEqFAwFE_E?wCCuWIme@KsCAyDA{b@EsIA{@?yDAsFA{DA}C?}NBwO?g\\?yC@wDAiL@sF?aUByD?Y?eH@sCAqD?}FDiC?kA?oOQ_CAaCAeGAuEA_BAmD?wCCyi@YgDCuEA{XImQGsCAqCA_H?sAAaj@Iwz@MeVCiSAeBAiD?qe@MeEAaFA}WI_OEiA?qAAoe@M_LEkA?_C?uC?aEAiF?oLA}EA{B?{D@iH@uE@mb@GeECg`AMqFAy@?_QEoC@oCPgANgANmCl@mC~@iClAeD|AkFhCeGvCaUtKs@\\sHtDgb@lSyL`G{CvAcErBsQ~IgKdFkN~Ec`AhTcB^ib@lJwMvCq@|DUvASpAGZ}CzAyLrCsQhEeE`AuCx@qCjAmCzAiClBuLtI}AxAwA`BuAfBqAnBgX`c@`@vYeDbR]n@_B[O[o@bAy@v@{PbE}B^{Cd@eMlD{@z@eATuCToQtA}ZfAuPRel@Z}_@PexIC" - } - ], - "summary": { - "has_time_restrictions": false, - "has_toll": false, - "has_highway": false, - "has_ferry": false, - "min_lat": 47.575663, - "min_lon": -122.347201, - "max_lat": 47.651047, - "max_lon": -122.335618, - "time": 6488.443, - "length": 9.148, - "cost": 6553.443 - }, - "status_message": "Found route between points", - "status": 0, - "units": "kilometers", - "language": "en-US" - }, "alternates": [ { - "trip": { - "locations": [ - { - "type": "break", - "lat": 47.575837, - "lon": -122.339414, - "side_of_street": "right", - "original_index": 0 - }, - { - "type": "break", - "lat": 47.651048, - "lon": -122.347234, - "original_index": 1 - } - ], + "trip": { + "language": "en-US", "legs": [ { "maneuvers": [ { - "type": 2, + "begin_shape_index": 0, + "cost": 13.567, + "end_shape_index": 1, "instruction": "Walk south on East Marginal Way South.", - "verbal_succinct_transition_instruction": "Walk south. Then Turn left onto the walkway.", - "verbal_pre_transition_instruction": "Walk south on East Marginal Way South. Then Turn left onto the walkway.", - "verbal_post_transition_instruction": "Continue for 20 meters.", + "length": 0.011, "street_names": [ "East Marginal Way South" ], "time": 13.567, - "length": 0.019, - "cost": 13.567, - "begin_shape_index": 0, - "end_shape_index": 1, - "verbal_multi_cue": true, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 2, + "verbal_multi_cue": true, + "verbal_post_transition_instruction": "Continue for 60 feet.", + "verbal_pre_transition_instruction": "Walk south on East Marginal Way South. Then Turn left onto the walkway.", + "verbal_succinct_transition_instruction": "Walk south. Then Turn left onto the walkway." }, { - "type": 15, - "instruction": "Turn left onto the walkway.", - "verbal_transition_alert_instruction": "Turn left onto the walkway.", - "verbal_succinct_transition_instruction": "Turn left.", - "verbal_pre_transition_instruction": "Turn left onto the walkway.", - "verbal_post_transition_instruction": "Continue for 400 meters.", - "time": 278.764, - "length": 0.385, - "cost": 283.764, "begin_shape_index": 1, + "cost": 283.764, "end_shape_index": 33, + "instruction": "Turn left onto the walkway.", + "length": 0.239, "rough": true, + "time": 278.764, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 15, + "verbal_post_transition_instruction": "Continue for a quarter mile.", + "verbal_pre_transition_instruction": "Turn left onto the walkway.", + "verbal_succinct_transition_instruction": "Turn left.", + "verbal_transition_alert_instruction": "Turn left onto the walkway." }, { - "type": 15, + "begin_shape_index": 33, + "cost": 824.823, + "end_shape_index": 92, "instruction": "Turn left onto 1st Avenue South.", - "verbal_transition_alert_instruction": "Turn left onto 1st Avenue South.", - "verbal_succinct_transition_instruction": "Turn left.", - "verbal_pre_transition_instruction": "Turn left onto 1st Avenue South.", - "verbal_post_transition_instruction": "Continue for 1 kilometer.", + "length": 0.72, "street_names": [ "1st Avenue South" ], "time": 819.823, - "length": 1.16, - "cost": 824.823, - "begin_shape_index": 33, - "end_shape_index": 92, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 15, + "verbal_post_transition_instruction": "Continue for a half mile.", + "verbal_pre_transition_instruction": "Turn left onto 1st Avenue South.", + "verbal_succinct_transition_instruction": "Turn left.", + "verbal_transition_alert_instruction": "Turn left onto 1st Avenue South." }, { - "type": 10, + "begin_shape_index": 92, + "cost": 374.058, + "end_shape_index": 131, "instruction": "Turn right onto South Holgate Street.", - "verbal_transition_alert_instruction": "Turn right onto South Holgate Street.", - "verbal_succinct_transition_instruction": "Turn right.", - "verbal_pre_transition_instruction": "Turn right onto South Holgate Street.", - "verbal_post_transition_instruction": "Continue for 500 meters.", + "length": 0.323, "street_names": [ "South Holgate Street" ], "time": 369.058, - "length": 0.52, - "cost": 374.058, - "begin_shape_index": 92, - "end_shape_index": 131, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 10, + "verbal_post_transition_instruction": "Continue for a quarter mile.", + "verbal_pre_transition_instruction": "Turn right onto South Holgate Street.", + "verbal_succinct_transition_instruction": "Turn right.", + "verbal_transition_alert_instruction": "Turn right onto South Holgate Street." }, { - "type": 15, + "begin_shape_index": 131, + "cost": 506.647, + "end_shape_index": 150, "instruction": "Turn left onto SODO Trail.", - "verbal_transition_alert_instruction": "Turn left onto SODO Trail.", - "verbal_succinct_transition_instruction": "Turn left.", - "verbal_pre_transition_instruction": "Turn left onto SODO Trail.", - "verbal_post_transition_instruction": "Continue for 700 meters.", + "length": 0.438, "street_names": [ "SODO Trail" ], "time": 501.647, - "length": 0.705, - "cost": 506.647, - "begin_shape_index": 131, - "end_shape_index": 150, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 15, + "verbal_post_transition_instruction": "Continue for a half mile.", + "verbal_pre_transition_instruction": "Turn left onto SODO Trail.", + "verbal_succinct_transition_instruction": "Turn left.", + "verbal_transition_alert_instruction": "Turn left onto SODO Trail." }, { - "type": 8, - "instruction": "Continue on the walkway.", - "verbal_transition_alert_instruction": "Continue on the walkway.", - "verbal_pre_transition_instruction": "Continue on the walkway. Then Turn right onto the walkway.", - "verbal_post_transition_instruction": "Continue for less than 10 meters.", - "time": 2.823, - "length": 0.004, - "cost": 7.823, "begin_shape_index": 150, + "cost": 7.823, "end_shape_index": 151, - "verbal_multi_cue": true, + "instruction": "Continue on the walkway.", + "length": 0.002, + "time": 2.823, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 8, + "verbal_multi_cue": true, + "verbal_post_transition_instruction": "Continue for 10 feet.", + "verbal_pre_transition_instruction": "Continue on the walkway. Then Turn right onto the walkway.", + "verbal_transition_alert_instruction": "Continue on the walkway." }, { - "type": 10, - "instruction": "Turn right onto the walkway.", - "verbal_transition_alert_instruction": "Turn right onto the walkway.", - "verbal_succinct_transition_instruction": "Turn right.", - "verbal_pre_transition_instruction": "Turn right onto the walkway.", - "verbal_post_transition_instruction": "Continue for 70 meters.", - "time": 46.588, - "length": 0.065, - "cost": 46.588, "begin_shape_index": 151, + "cost": 46.588, "end_shape_index": 153, + "instruction": "Turn right onto the walkway.", + "length": 0.041, + "time": 46.588, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 10, + "verbal_post_transition_instruction": "Continue for 200 feet.", + "verbal_pre_transition_instruction": "Turn right onto the walkway.", + "verbal_succinct_transition_instruction": "Turn right.", + "verbal_transition_alert_instruction": "Turn right onto the walkway." }, { - "type": 15, - "instruction": "Turn left onto the walkway.", - "verbal_transition_alert_instruction": "Turn left onto the walkway.", - "verbal_succinct_transition_instruction": "Turn left.", - "verbal_pre_transition_instruction": "Turn left onto the walkway.", - "verbal_post_transition_instruction": "Continue for 300 meters.", - "time": 289.353, - "length": 0.315, - "cost": 309.353, "begin_shape_index": 153, + "cost": 309.353, "end_shape_index": 169, + "instruction": "Turn left onto the walkway.", + "length": 0.195, + "time": 289.353, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 15, + "verbal_post_transition_instruction": "Continue for a quarter mile.", + "verbal_pre_transition_instruction": "Turn left onto the walkway.", + "verbal_succinct_transition_instruction": "Turn left.", + "verbal_transition_alert_instruction": "Turn left onto the walkway." }, { - "type": 10, - "instruction": "Turn right onto the walkway.", - "verbal_transition_alert_instruction": "Turn right onto the walkway.", - "verbal_succinct_transition_instruction": "Turn right.", - "verbal_pre_transition_instruction": "Turn right onto the walkway.", - "verbal_post_transition_instruction": "Continue for 70 meters.", - "time": 54.117, - "length": 0.071, - "cost": 54.117, "begin_shape_index": 169, + "cost": 54.117, "end_shape_index": 176, + "instruction": "Turn right onto the walkway.", + "length": 0.044, + "time": 54.117, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 10, + "verbal_post_transition_instruction": "Continue for 200 feet.", + "verbal_pre_transition_instruction": "Turn right onto the walkway.", + "verbal_succinct_transition_instruction": "Turn right.", + "verbal_transition_alert_instruction": "Turn right onto the walkway." }, { - "type": 10, - "instruction": "Turn right onto the walkway.", - "verbal_transition_alert_instruction": "Turn right onto the walkway.", - "verbal_succinct_transition_instruction": "Turn right. Then Turn left onto 6th Avenue South.", - "verbal_pre_transition_instruction": "Turn right onto the walkway. Then Turn left onto 6th Avenue South.", - "verbal_post_transition_instruction": "Continue for less than 10 meters.", - "time": 6.353, - "length": 0.009, - "cost": 6.353, "begin_shape_index": 176, + "cost": 6.353, "end_shape_index": 178, - "verbal_multi_cue": true, + "instruction": "Turn right onto the walkway.", + "length": 0.005, + "time": 6.353, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 10, + "verbal_multi_cue": true, + "verbal_post_transition_instruction": "Continue for 30 feet.", + "verbal_pre_transition_instruction": "Turn right onto the walkway. Then Turn left onto 6th Avenue South.", + "verbal_succinct_transition_instruction": "Turn right. Then Turn left onto 6th Avenue South.", + "verbal_transition_alert_instruction": "Turn right onto the walkway." }, { - "type": 15, + "begin_shape_index": 178, + "cost": 928.118, + "end_shape_index": 259, "instruction": "Turn left onto 6th Avenue South.", - "verbal_transition_alert_instruction": "Turn left onto 6th Avenue South.", - "verbal_succinct_transition_instruction": "Turn left.", - "verbal_pre_transition_instruction": "Turn left onto 6th Avenue South.", - "verbal_post_transition_instruction": "Continue for 1.5 kilometers.", + "length": 0.783, "street_names": [ "6th Avenue South" ], "time": 918.118, - "length": 1.261, - "cost": 928.118, - "begin_shape_index": 178, - "end_shape_index": 259, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 15, + "verbal_post_transition_instruction": "Continue for 1 mile.", + "verbal_pre_transition_instruction": "Turn left onto 6th Avenue South.", + "verbal_succinct_transition_instruction": "Turn left.", + "verbal_transition_alert_instruction": "Turn left onto 6th Avenue South." }, { - "type": 10, + "begin_shape_index": 259, + "cost": 742.294, + "end_shape_index": 311, "instruction": "Turn right onto 6th Avenue.", - "verbal_transition_alert_instruction": "Turn right onto 6th Avenue.", - "verbal_succinct_transition_instruction": "Turn right.", - "verbal_pre_transition_instruction": "Turn right onto 6th Avenue.", - "verbal_post_transition_instruction": "Continue for 1 kilometer.", + "length": 0.643, "street_names": [ "6th Avenue" ], "time": 742.294, - "length": 1.036, - "cost": 742.294, - "begin_shape_index": 259, - "end_shape_index": 311, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 10, + "verbal_post_transition_instruction": "Continue for a half mile.", + "verbal_pre_transition_instruction": "Turn right onto 6th Avenue.", + "verbal_succinct_transition_instruction": "Turn right.", + "verbal_transition_alert_instruction": "Turn right onto 6th Avenue." }, { - "type": 23, + "begin_shape_index": 311, + "cost": 112.587, + "end_shape_index": 321, "instruction": "Keep right to take Westlake Avenue.", - "verbal_transition_alert_instruction": "Keep right to take Westlake Avenue.", - "verbal_pre_transition_instruction": "Keep right to take Westlake Avenue.", - "verbal_post_transition_instruction": "Continue for 200 meters.", + "length": 0.093, "street_names": [ "Westlake Avenue" ], "time": 107.587, - "length": 0.151, - "cost": 112.587, - "begin_shape_index": 311, - "end_shape_index": 321, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 23, + "verbal_post_transition_instruction": "Continue for 500 feet.", + "verbal_pre_transition_instruction": "Keep right to take Westlake Avenue.", + "verbal_transition_alert_instruction": "Keep right to take Westlake Avenue." }, { - "type": 24, + "begin_shape_index": 321, + "cost": 312.235, + "end_shape_index": 338, "instruction": "Keep left to take 7th Avenue.", - "verbal_transition_alert_instruction": "Keep left to take 7th Avenue.", - "verbal_pre_transition_instruction": "Keep left to take 7th Avenue.", - "verbal_post_transition_instruction": "Continue for 400 meters.", + "length": 0.267, "street_names": [ "7th Avenue" ], "time": 307.235, - "length": 0.43, - "cost": 312.235, - "begin_shape_index": 321, - "end_shape_index": 338, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 24, + "verbal_post_transition_instruction": "Continue for a quarter mile.", + "verbal_pre_transition_instruction": "Keep left to take 7th Avenue.", + "verbal_transition_alert_instruction": "Keep left to take 7th Avenue." }, { - "type": 23, + "begin_shape_index": 338, + "cost": 1812.645, + "end_shape_index": 456, "instruction": "Keep right to take Dexter Avenue.", - "verbal_transition_alert_instruction": "Keep right to take Dexter Avenue.", - "verbal_pre_transition_instruction": "Keep right to take Dexter Avenue.", - "verbal_post_transition_instruction": "Continue for 2.5 kilometers.", + "length": 1.578, "street_names": [ "Dexter Avenue" ], "time": 1802.645, - "length": 2.541, - "cost": 1812.645, - "begin_shape_index": 338, - "end_shape_index": 456, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 23, + "verbal_post_transition_instruction": "Continue for 1.5 miles.", + "verbal_pre_transition_instruction": "Keep right to take Dexter Avenue.", + "verbal_transition_alert_instruction": "Keep right to take Dexter Avenue." }, { - "type": 15, + "begin_shape_index": 456, + "cost": 15.588, + "end_shape_index": 460, "instruction": "Turn left onto 6th Avenue North.", - "verbal_transition_alert_instruction": "Turn left onto 6th Avenue North.", - "verbal_succinct_transition_instruction": "Turn left. Then, in 20 meters, Turn right to stay on 6th Avenue North.", - "verbal_pre_transition_instruction": "Turn left onto 6th Avenue North. Then, in 20 meters, Turn right to stay on 6th Avenue North.", - "verbal_post_transition_instruction": "Continue for 20 meters.", + "length": 0.009, "street_names": [ "6th Avenue North" ], "time": 10.588, - "length": 0.015, - "cost": 15.588, - "begin_shape_index": 456, - "end_shape_index": 460, - "verbal_multi_cue": true, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 15, + "verbal_multi_cue": true, + "verbal_post_transition_instruction": "Continue for 50 feet.", + "verbal_pre_transition_instruction": "Turn left onto 6th Avenue North. Then, in 50 feet, Turn right to stay on 6th Avenue North.", + "verbal_succinct_transition_instruction": "Turn left. Then, in 50 feet, Turn right to stay on 6th Avenue North.", + "verbal_transition_alert_instruction": "Turn left onto 6th Avenue North." }, { - "type": 10, + "begin_shape_index": 460, + "cost": 161.646, + "end_shape_index": 474, "instruction": "Turn right to stay on 6th Avenue North.", - "verbal_transition_alert_instruction": "Turn right to stay on 6th Avenue North.", - "verbal_succinct_transition_instruction": "Turn right.", - "verbal_pre_transition_instruction": "Turn right to stay on 6th Avenue North.", - "verbal_post_transition_instruction": "Continue for 200 meters.", + "length": 0.142, "street_names": [ "6th Avenue North" ], "time": 161.646, - "length": 0.229, - "cost": 161.646, - "begin_shape_index": 460, - "end_shape_index": 474, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 10, + "verbal_post_transition_instruction": "Continue for 800 feet.", + "verbal_pre_transition_instruction": "Turn right to stay on 6th Avenue North.", + "verbal_succinct_transition_instruction": "Turn right.", + "verbal_transition_alert_instruction": "Turn right to stay on 6th Avenue North." }, { - "type": 15, + "begin_shape_index": 474, + "cost": 27.588, + "end_shape_index": 475, "instruction": "Turn left onto Halladay Street.", - "verbal_transition_alert_instruction": "Turn left onto Halladay Street.", - "verbal_succinct_transition_instruction": "Turn left.", - "verbal_pre_transition_instruction": "Turn left onto Halladay Street.", - "verbal_post_transition_instruction": "Continue for 30 meters.", + "length": 0.019, "street_names": [ "Halladay Street" ], "time": 22.588, - "length": 0.032, - "cost": 27.588, - "begin_shape_index": 474, - "end_shape_index": 475, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 15, + "verbal_post_transition_instruction": "Continue for 100 feet.", + "verbal_pre_transition_instruction": "Turn left onto Halladay Street.", + "verbal_succinct_transition_instruction": "Turn left.", + "verbal_transition_alert_instruction": "Turn left onto Halladay Street." }, { - "type": 23, - "instruction": "Keep right at the fork.", - "verbal_transition_alert_instruction": "Keep right at the fork.", - "verbal_pre_transition_instruction": "Keep right at the fork.", - "verbal_post_transition_instruction": "Continue for 30 meters.", - "time": 19.058, - "length": 0.027, - "cost": 19.058, "begin_shape_index": 475, + "cost": 19.058, "end_shape_index": 477, + "instruction": "Keep right at the fork.", + "length": 0.016, + "time": 19.058, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 23, + "verbal_post_transition_instruction": "Continue for 90 feet.", + "verbal_pre_transition_instruction": "Keep right at the fork.", + "verbal_transition_alert_instruction": "Keep right at the fork." }, { - "type": 10, - "instruction": "Turn right onto the walkway.", - "verbal_transition_alert_instruction": "Turn right onto the walkway.", - "verbal_succinct_transition_instruction": "Turn right. Then Turn left onto the walkway.", - "verbal_pre_transition_instruction": "Turn right onto the walkway. Then Turn left onto the walkway.", - "verbal_post_transition_instruction": "Continue for less than 10 meters.", - "time": 4.235, - "length": 0.006, - "cost": 4.235, "begin_shape_index": 477, + "cost": 4.235, "end_shape_index": 479, - "verbal_multi_cue": true, + "instruction": "Turn right onto the walkway.", + "length": 0.003, + "time": 4.235, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 10, + "verbal_multi_cue": true, + "verbal_post_transition_instruction": "Continue for 20 feet.", + "verbal_pre_transition_instruction": "Turn right onto the walkway. Then Turn left onto the walkway.", + "verbal_succinct_transition_instruction": "Turn right. Then Turn left onto the walkway.", + "verbal_transition_alert_instruction": "Turn right onto the walkway." }, { - "type": 15, - "instruction": "Turn left onto the walkway.", - "verbal_transition_alert_instruction": "Turn left onto the walkway.", - "verbal_succinct_transition_instruction": "Turn left.", - "verbal_pre_transition_instruction": "Turn left onto the walkway.", - "verbal_post_transition_instruction": "Continue for 80 meters.", - "time": 58.882, - "length": 0.082, - "cost": 58.882, "begin_shape_index": 479, + "cost": 58.882, "end_shape_index": 485, + "instruction": "Turn left onto the walkway.", + "length": 0.05, + "time": 58.882, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 15, + "verbal_post_transition_instruction": "Continue for 300 feet.", + "verbal_pre_transition_instruction": "Turn left onto the walkway.", + "verbal_succinct_transition_instruction": "Turn left.", + "verbal_transition_alert_instruction": "Turn left onto the walkway." }, { - "type": 24, - "instruction": "Keep left to take the walkway.", - "verbal_transition_alert_instruction": "Keep left to take the walkway.", - "verbal_pre_transition_instruction": "Keep left to take the walkway.", - "verbal_post_transition_instruction": "Continue for 900 meters.", - "time": 624.581, - "length": 0.884, - "cost": 624.581, "begin_shape_index": 485, + "cost": 624.581, "end_shape_index": 494, + "instruction": "Keep left to take the walkway.", + "length": 0.549, + "time": 624.581, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 24, + "verbal_post_transition_instruction": "Continue for a half mile.", + "verbal_pre_transition_instruction": "Keep left to take the walkway.", + "verbal_transition_alert_instruction": "Keep left to take the walkway." }, { - "type": 4, - "instruction": "You have arrived at your destination.", - "verbal_transition_alert_instruction": "You will arrive at your destination.", - "verbal_pre_transition_instruction": "You have arrived at your destination.", - "time": 0.0, - "length": 0.0, - "cost": 0.0, "begin_shape_index": 494, + "cost": 0.0, "end_shape_index": 494, + "instruction": "You have arrived at your destination.", + "length": 0.0, + "time": 0.0, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 4, + "verbal_pre_transition_instruction": "You have arrived at your destination.", + "verbal_transition_alert_instruction": "You will arrive at your destination." } ], + "shape": "wpxvyA~w~ihFxID?wGDcw@mA@?c@@oA?G?k@?u]?gI?kA?mC@wA?wPv@E?wAAsF?yB?iEGyACoAPu@A}IAmQ@qR?}U?mI@e@@yE@{BI{f@]cAJ{IsCBuDBiE?cYAiNAii@BkJ?oIFoEDyDUcIe@uF?{A?cF?uI?sI?}H?qO?wH?kC?uD?{M@wDAgG@aDAkA?a[@sA?iF?kN?uB?se@@qB?eD?{@?gIAiE@sD?iI@oC?cE@gO@uI@k^D_J?oXDi@AmD@wHAu@?kD?aC?gGAi|@EkJAsHA}LAY@iGAAiJE}RCaKCiDI{HG}CoA}h@A_HAoG?gBAcBAcBAkB@uBDeVBiRHsV@sE@kBBiNBoE?sB?qB@wBBwB?qJ?sN?uF?{S?mKAiKAkS?_J@gU@_Q?eI?kG@qB?oDwD?y@@EPIJmC`@}lBKmAGaLq@oJ}AqPuCkLmB{JiA_HYgbDEcI]cGo@_B?cDAqCBcA?Jcs@e@_AOIeM@_D?weABmD?eC?oQ@sG?qP?e@Nk@z@cAbCiEhKw@fC]nA_EvJu@}@cDmDmCuCs@w@Kg@_UESL?q@EwD_FCuE?gMAiOEkBDyEEwCCaD?cEAuZEwEAgF?iECsg@E{DAkC?eNCaIAcA?cLCiDAo@@a@AS?eDAaf@EeEA_DA[@oLCsIA}JAmEBcDD}CGmDG{TCuLCsDAwBd@Y?eIBmBLiBVcBXu@RqA^aBv@aCxAiD|BkF|DsCnBwLxHcKzFy[jYil@xf@qDzCiGjF_JxHkPlNmAfAyCfCkK`JoGjF}@v@uPnNmLvJkT`Si@n@g@r@c@v@Un@Ur@Wt@GVE`@IvA?^@|@Jz@Rz@{@}@{@i@OKy@Mq@N{c@n^yBpB_DpCsYfW]ZqKjJaCzBqLxKaAz@yPtOaErDeEpDqPzNcFlEqGvFcDpCkE|DkHvGaAz@iPbOaRtP_BvA}CrC_EnDkRxPi]zZaFpEeEpDof@bc@mKhJmBbB{CjCaQbOeA|@eEnDoM~KgKzI_DxDyApBUZcK|QsHxNaCtEqCjFgHbN{BjEqBxDwGf@oE`@kKfAuANqMzAeCXkD`@gUrC_BRcCZc@x@{@dB_CtE{@dBiTtb@oBxDkCjF_HfNaJvQiSja@iBrDwBhEkh@vdA{AzC_AnBg@bAiYll@iAzBuAlB_BxAeBdAkBn@oBXoBBcA?mMCeEAcDAeQGqQGmIEqFAwFE_E?wCCuWIme@KsCAyDA{b@EsIA{@?yDAsFA{DA}C?}NBwO?g\\?yC@wDAiL@sF?aUByD?Y?eH@sCAqD?}FDiC?kA?oOQ_CAaCAeGAuEA_BAmD?wCCyi@YgDCuEA{XImQGsCAqCA_H?sAAaj@Iwz@MeVCiSAeBAiD?qe@MeEAaFA}WI_OEiA?qAAoe@M_LEkA?_C?uC?aEAiF?oLA}EA{B?{D@iH@uE@mb@GeECg`AMqFAy@?_QEoC@oCPgANgANmCl@mC~@iClAeD|AkFhCeGvCaUtKs@\\sHtDgb@lSyL`G{CvAcErBsQ~IgKdFkN~Ec`AhTcB^ib@lJwMvCq@|DUvASpAGZ}CzAyLrCsQhEeE`AuCx@qCjAmCzAiClBuLtI}AxAwA`BuAfBqAnBgX`c@`@vYeDbR]n@_B[O[o@bAy@v@{PbE}B^{Cd@eMlD{@z@eATuCToQtA}ZfAuPRel@Z}_@PexIC", "summary": { + "cost": 7246.559, + "has_ferry": false, + "has_highway": false, "has_time_restrictions": false, "has_toll": false, - "has_highway": false, - "has_ferry": false, - "min_lat": 47.57566, - "min_lon": -122.347201, + "length": 6.182, "max_lat": 47.651047, "max_lon": -122.326145, - "time": 7161.559, - "length": 9.95, - "cost": 7246.559 - }, - "shape": "wpxvyA~w~ihFxID?wGDcw@mA@?c@@oA?G?k@?u]?gI?kA?mC@wA?wPv@E?wAAsF?yB?iEGyACoAPu@A}IAmQ@qR?}U?mI@e@@yE@{BI{f@]cAJ{IsCBuDBiE?cYAiNAii@BkJ?oIFoEDyDUcIe@uF?{A?cF?uI?sI?}H?qO?wH?kC?uD?{M@wDAgG@aDAkA?a[@sA?iF?kN?uB?se@@qB?eD?{@?gIAiE@sD?iI@oC?cE@gO@uI@k^D_J?oXDi@AmD@wHAu@?kD?aC?gGAi|@EkJAsHA}LAY@iGAAiJE}RCaKCiDI{HG}CoA}h@A_HAoG?gBAcBAcBAkB@uBDeVBiRHsV@sE@kBBiNBoE?sB?qB@wBBwB?qJ?sN?uF?{S?mKAiKAkS?_J@gU@_Q?eI?kG@qB?oDwD?y@@EPIJmC`@}lBKmAGaLq@oJ}AqPuCkLmB{JiA_HYgbDEcI]cGo@_B?cDAqCBcA?Jcs@e@_AOIeM@_D?weABmD?eC?oQ@sG?qP?e@Nk@z@cAbCiEhKw@fC]nA_EvJu@}@cDmDmCuCs@w@Kg@_UESL?q@EwD_FCuE?gMAiOEkBDyEEwCCaD?cEAuZEwEAgF?iECsg@E{DAkC?eNCaIAcA?cLCiDAo@@a@AS?eDAaf@EeEA_DA[@oLCsIA}JAmEBcDD}CGmDG{TCuLCsDAwBd@Y?eIBmBLiBVcBXu@RqA^aBv@aCxAiD|BkF|DsCnBwLxHcKzFy[jYil@xf@qDzCiGjF_JxHkPlNmAfAyCfCkK`JoGjF}@v@uPnNmLvJkT`Si@n@g@r@c@v@Un@Ur@Wt@GVE`@IvA?^@|@Jz@Rz@{@}@{@i@OKy@Mq@N{c@n^yBpB_DpCsYfW]ZqKjJaCzBqLxKaAz@yPtOaErDeEpDqPzNcFlEqGvFcDpCkE|DkHvGaAz@iPbOaRtP_BvA}CrC_EnDkRxPi]zZaFpEeEpDof@bc@mKhJmBbB{CjCaQbOeA|@eEnDoM~KgKzI_DxDyApBUZcK|QsHxNaCtEqCjFgHbN{BjEqBxDwGf@oE`@kKfAuANqMzAeCXkD`@gUrC_BRcCZc@x@{@dB_CtE{@dBiTtb@oBxDkCjF_HfNaJvQiSja@iBrDwBhEkh@vdA{AzC_AnBg@bAiYll@iAzBuAlB_BxAeBdAkBn@oBXoBBcA?mMCeEAcDAeQGqQGmIEqFAwFE_E?wCCuWIme@KsCAyDA{b@EsIA{@?yDAsFA{DA}C?}NBwO?g\\?yC@wDAiL@sF?aUByD?Y?eH@sCAqD?}FDiC?kA?oOQ_CAaCAeGAuEA_BAmD?wCCyi@YgDCuEA{XImQGsCAqCA_H?sAAaj@Iwz@MeVCiSAeBAiD?qe@MeEAaFA}WI_OEiA?qAAoe@M_LEkA?_C?uC?aEAiF?oLA}EA{B?{D@iH@uE@mb@GeECg`AMqFAy@?_QEoC@oCPgANgANmCl@mC~@iClAeD|AkFhCeGvCaUtKs@\\sHtDgb@lSyL`G{CvAcErBsQ~IgKdFkN~Ec`AhTcB^ib@lJwMvCq@|DUvASpAGZ}CzAyLrCsQhEeE`AuCx@qCjAmCzAiClBuLtI}AxAwA`BuAfBqAnBgX`c@`@vYeDbR]n@_B[O[o@bAy@v@{PbE}B^{Cd@eMlD{@z@eATuCToQtA}ZfAuPRel@Z}_@PexIC" + "min_lat": 47.57566, + "min_lon": -122.347201, + "time": 7161.559 + } } ], - "summary": { - "has_time_restrictions": false, - "has_toll": false, - "has_highway": false, - "has_ferry": false, - "min_lat": 47.57566, - "min_lon": -122.347201, - "max_lat": 47.651047, - "max_lon": -122.326145, - "time": 7161.559, - "length": 9.95, - "cost": 7246.559 - }, - "status_message": "Found route between points", - "status": 0, - "units": "kilometers", - "language": "en-US" - } - }, - { - "trip": { "locations": [ { - "type": "break", "lat": 47.575837, "lon": -122.339414, + "original_index": 0, "side_of_street": "right", - "original_index": 0 + "type": "break" }, { - "type": "break", "lat": 47.651048, "lon": -122.347234, - "original_index": 1 + "original_index": 1, + "type": "break" } ], + "status": 0, + "status_message": "Found route between points", + "summary": { + "cost": 7246.559, + "has_ferry": false, + "has_highway": false, + "has_time_restrictions": false, + "has_toll": false, + "length": 6.182, + "max_lat": 47.651047, + "max_lon": -122.326145, + "min_lat": 47.57566, + "min_lon": -122.347201, + "time": 7161.559 + }, + "units": "miles" + } + }, + { + "trip": { + "language": "en-US", "legs": [ { "maneuvers": [ { - "type": 2, + "begin_shape_index": 0, + "cost": 13.567, + "end_shape_index": 1, "instruction": "Walk south on East Marginal Way South.", - "verbal_succinct_transition_instruction": "Walk south. Then Turn left onto the walkway.", - "verbal_pre_transition_instruction": "Walk south on East Marginal Way South. Then Turn left onto the walkway.", - "verbal_post_transition_instruction": "Continue for 20 meters.", + "length": 0.011, "street_names": [ "East Marginal Way South" ], "time": 13.567, - "length": 0.019, - "cost": 13.567, - "begin_shape_index": 0, - "end_shape_index": 1, - "verbal_multi_cue": true, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 2, + "verbal_multi_cue": true, + "verbal_post_transition_instruction": "Continue for 60 feet.", + "verbal_pre_transition_instruction": "Walk south on East Marginal Way South. Then Turn left onto the walkway.", + "verbal_succinct_transition_instruction": "Walk south. Then Turn left onto the walkway." }, { - "type": 15, - "instruction": "Turn left onto the walkway.", - "verbal_transition_alert_instruction": "Turn left onto the walkway.", - "verbal_succinct_transition_instruction": "Turn left.", - "verbal_pre_transition_instruction": "Turn left onto the walkway.", - "verbal_post_transition_instruction": "Continue for 400 meters.", - "time": 278.764, - "length": 0.385, - "cost": 283.764, "begin_shape_index": 1, + "cost": 283.764, "end_shape_index": 33, + "instruction": "Turn left onto the walkway.", + "length": 0.239, "rough": true, + "time": 278.764, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 15, + "verbal_post_transition_instruction": "Continue for a quarter mile.", + "verbal_pre_transition_instruction": "Turn left onto the walkway.", + "verbal_succinct_transition_instruction": "Turn left.", + "verbal_transition_alert_instruction": "Turn left onto the walkway." }, { - "type": 15, + "begin_shape_index": 33, + "cost": 824.823, + "end_shape_index": 92, "instruction": "Turn left onto 1st Avenue South.", - "verbal_transition_alert_instruction": "Turn left onto 1st Avenue South.", - "verbal_succinct_transition_instruction": "Turn left.", - "verbal_pre_transition_instruction": "Turn left onto 1st Avenue South.", - "verbal_post_transition_instruction": "Continue for 1 kilometer.", + "length": 0.72, "street_names": [ "1st Avenue South" ], "time": 819.823, - "length": 1.16, - "cost": 824.823, - "begin_shape_index": 33, - "end_shape_index": 92, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 15, + "verbal_post_transition_instruction": "Continue for a half mile.", + "verbal_pre_transition_instruction": "Turn left onto 1st Avenue South.", + "verbal_succinct_transition_instruction": "Turn left.", + "verbal_transition_alert_instruction": "Turn left onto 1st Avenue South." }, { - "type": 10, + "begin_shape_index": 92, + "cost": 567.764, + "end_shape_index": 144, "instruction": "Turn right onto South Holgate Street.", - "verbal_transition_alert_instruction": "Turn right onto South Holgate Street.", - "verbal_succinct_transition_instruction": "Turn right.", - "verbal_pre_transition_instruction": "Turn right onto South Holgate Street.", - "verbal_post_transition_instruction": "Continue for 800 meters.", + "length": 0.492, "street_names": [ "South Holgate Street" ], "time": 562.764, - "length": 0.792, - "cost": 567.764, - "begin_shape_index": 92, - "end_shape_index": 144, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 10, + "verbal_post_transition_instruction": "Continue for a half mile.", + "verbal_pre_transition_instruction": "Turn right onto South Holgate Street.", + "verbal_succinct_transition_instruction": "Turn right.", + "verbal_transition_alert_instruction": "Turn right onto South Holgate Street." }, { - "type": 22, + "begin_shape_index": 144, + "cost": 250.235, + "end_shape_index": 158, "instruction": "Keep straight to stay on South Holgate Street.", - "verbal_transition_alert_instruction": "Keep straight to stay on South Holgate Street.", - "verbal_pre_transition_instruction": "Keep straight to stay on South Holgate Street.", - "verbal_post_transition_instruction": "Continue for 300 meters.", + "length": 0.214, "street_names": [ "South Holgate Street" ], "time": 245.235, - "length": 0.346, - "cost": 250.235, - "begin_shape_index": 144, - "end_shape_index": 158, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 22, + "verbal_post_transition_instruction": "Continue for a quarter mile.", + "verbal_pre_transition_instruction": "Keep straight to stay on South Holgate Street.", + "verbal_transition_alert_instruction": "Keep straight to stay on South Holgate Street." }, { - "type": 15, + "begin_shape_index": 158, + "cost": 471.176, + "end_shape_index": 191, "instruction": "Turn left onto Mountains-to-Sound Trail.", - "verbal_transition_alert_instruction": "Turn left onto Mountains-to-Sound Trail.", - "verbal_succinct_transition_instruction": "Turn left.", - "verbal_pre_transition_instruction": "Turn left onto Mountains-to-Sound Trail.", - "verbal_post_transition_instruction": "Continue for 700 meters.", + "length": 0.409, "street_names": [ "Mountains-to-Sound Trail" ], "time": 466.176, - "length": 0.659, - "cost": 471.176, - "begin_shape_index": 158, - "end_shape_index": 191, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 15, + "verbal_post_transition_instruction": "Continue for a half mile.", + "verbal_pre_transition_instruction": "Turn left onto Mountains-to-Sound Trail.", + "verbal_succinct_transition_instruction": "Turn left.", + "verbal_transition_alert_instruction": "Turn left onto Mountains-to-Sound Trail." }, { - "type": 10, - "instruction": "Turn right onto the walkway.", - "verbal_transition_alert_instruction": "Turn right onto the walkway.", - "verbal_succinct_transition_instruction": "Turn right.", - "verbal_pre_transition_instruction": "Turn right onto the walkway.", - "verbal_post_transition_instruction": "Continue for 100 meters.", - "time": 83.294, - "length": 0.118, - "cost": 88.294, "begin_shape_index": 191, + "cost": 88.294, "end_shape_index": 201, + "instruction": "Turn right onto the walkway.", + "length": 0.073, "rough": true, + "time": 83.294, "travel_mode": "pedestrian", - "travel_type": "foot" - }, - { + "travel_type": "foot", "type": 10, - "instruction": "Turn right onto the walkway.", - "verbal_transition_alert_instruction": "Turn right onto the walkway.", - "verbal_succinct_transition_instruction": "Turn right.", + "verbal_post_transition_instruction": "Continue for 400 feet.", "verbal_pre_transition_instruction": "Turn right onto the walkway.", - "verbal_post_transition_instruction": "Continue for 100 meters.", - "time": 91.764, - "length": 0.13, - "cost": 91.764, + "verbal_succinct_transition_instruction": "Turn right.", + "verbal_transition_alert_instruction": "Turn right onto the walkway." + }, + { "begin_shape_index": 201, + "cost": 91.764, "end_shape_index": 222, + "instruction": "Turn right onto the walkway.", + "length": 0.08, "rough": true, + "time": 91.764, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 10, + "verbal_post_transition_instruction": "Continue for 400 feet.", + "verbal_pre_transition_instruction": "Turn right onto the walkway.", + "verbal_succinct_transition_instruction": "Turn right.", + "verbal_transition_alert_instruction": "Turn right onto the walkway." }, { - "type": 15, - "instruction": "Turn left onto the walkway.", - "verbal_transition_alert_instruction": "Turn left onto the walkway.", - "verbal_succinct_transition_instruction": "Turn left.", - "verbal_pre_transition_instruction": "Turn left onto the walkway.", - "verbal_post_transition_instruction": "Continue for 700 meters.", - "time": 474.647, - "length": 0.671, - "cost": 474.647, "begin_shape_index": 222, + "cost": 474.647, "end_shape_index": 265, + "instruction": "Turn left onto the walkway.", + "length": 0.416, + "time": 474.647, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 15, + "verbal_post_transition_instruction": "Continue for a half mile.", + "verbal_pre_transition_instruction": "Turn left onto the walkway.", + "verbal_succinct_transition_instruction": "Turn left.", + "verbal_transition_alert_instruction": "Turn left onto the walkway." }, { - "type": 15, - "instruction": "Turn left onto the walkway.", - "verbal_transition_alert_instruction": "Turn left onto the walkway.", - "verbal_succinct_transition_instruction": "Turn left. Then Turn right onto the walkway.", - "verbal_pre_transition_instruction": "Turn left onto the walkway. Then Turn right onto the walkway.", - "verbal_post_transition_instruction": "Continue for less than 10 meters.", - "time": 1.411, - "length": 0.002, - "cost": 1.411, "begin_shape_index": 265, + "cost": 1.411, "end_shape_index": 266, - "verbal_multi_cue": true, + "instruction": "Turn left onto the walkway.", + "length": 0.001, + "time": 1.411, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 15, + "verbal_multi_cue": true, + "verbal_post_transition_instruction": "Continue for less than 10 feet.", + "verbal_pre_transition_instruction": "Turn left onto the walkway. Then Turn right onto the walkway.", + "verbal_succinct_transition_instruction": "Turn left. Then Turn right onto the walkway.", + "verbal_transition_alert_instruction": "Turn left onto the walkway." }, { - "type": 10, - "instruction": "Turn right onto the walkway.", - "verbal_transition_alert_instruction": "Turn right onto the walkway.", - "verbal_succinct_transition_instruction": "Turn right.", - "verbal_pre_transition_instruction": "Turn right onto the walkway.", - "verbal_post_transition_instruction": "Continue for 20 meters.", - "time": 12.999, - "length": 0.017, - "cost": 12.999, "begin_shape_index": 266, + "cost": 12.999, "end_shape_index": 270, + "instruction": "Turn right onto the walkway.", + "length": 0.01, + "time": 12.999, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 10, + "verbal_post_transition_instruction": "Continue for 60 feet.", + "verbal_pre_transition_instruction": "Turn right onto the walkway.", + "verbal_succinct_transition_instruction": "Turn right.", + "verbal_transition_alert_instruction": "Turn right onto the walkway." }, { - "type": 10, - "instruction": "Turn right onto the walkway.", - "verbal_transition_alert_instruction": "Turn right onto the walkway.", - "verbal_succinct_transition_instruction": "Turn right. Then Turn left onto the walkway.", - "verbal_pre_transition_instruction": "Turn right onto the walkway. Then Turn left onto the walkway.", - "verbal_post_transition_instruction": "Continue for less than 10 meters.", - "time": 3.529, - "length": 0.005, - "cost": 3.529, "begin_shape_index": 270, + "cost": 3.529, "end_shape_index": 274, - "verbal_multi_cue": true, + "instruction": "Turn right onto the walkway.", + "length": 0.003, + "time": 3.529, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 10, + "verbal_multi_cue": true, + "verbal_post_transition_instruction": "Continue for 20 feet.", + "verbal_pre_transition_instruction": "Turn right onto the walkway. Then Turn left onto the walkway.", + "verbal_succinct_transition_instruction": "Turn right. Then Turn left onto the walkway.", + "verbal_transition_alert_instruction": "Turn right onto the walkway." }, { - "type": 15, - "instruction": "Turn left onto the walkway.", - "verbal_transition_alert_instruction": "Turn left onto the walkway.", - "verbal_succinct_transition_instruction": "Turn left.", - "verbal_pre_transition_instruction": "Turn left onto the walkway.", - "verbal_post_transition_instruction": "Continue for 100 meters.", - "time": 74.47, - "length": 0.097, - "cost": 74.47, "begin_shape_index": 274, + "cost": 74.47, "end_shape_index": 285, + "instruction": "Turn left onto the walkway.", + "length": 0.06, + "time": 74.47, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 15, + "verbal_post_transition_instruction": "Continue for 300 feet.", + "verbal_pre_transition_instruction": "Turn left onto the walkway.", + "verbal_succinct_transition_instruction": "Turn left.", + "verbal_transition_alert_instruction": "Turn left onto the walkway." }, { - "type": 10, - "instruction": "Turn right onto the walkway.", - "verbal_transition_alert_instruction": "Turn right onto the walkway.", - "verbal_succinct_transition_instruction": "Turn right. Then Turn left onto the walkway.", - "verbal_pre_transition_instruction": "Turn right onto the walkway. Then Turn left onto the walkway.", - "verbal_post_transition_instruction": "Continue for less than 10 meters.", - "time": 2.117, - "length": 0.003, - "cost": 2.117, "begin_shape_index": 285, + "cost": 2.117, "end_shape_index": 288, - "verbal_multi_cue": true, + "instruction": "Turn right onto the walkway.", + "length": 0.001, + "time": 2.117, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 10, + "verbal_multi_cue": true, + "verbal_post_transition_instruction": "Continue for 10 feet.", + "verbal_pre_transition_instruction": "Turn right onto the walkway. Then Turn left onto the walkway.", + "verbal_succinct_transition_instruction": "Turn right. Then Turn left onto the walkway.", + "verbal_transition_alert_instruction": "Turn right onto the walkway." }, { - "type": 15, - "instruction": "Turn left onto the walkway.", - "verbal_transition_alert_instruction": "Turn left onto the walkway.", - "verbal_succinct_transition_instruction": "Turn left.", - "verbal_pre_transition_instruction": "Turn left onto the walkway.", - "verbal_post_transition_instruction": "Continue for 70 meters.", - "time": 51.529, - "length": 0.073, - "cost": 51.529, "begin_shape_index": 288, + "cost": 51.529, "end_shape_index": 292, + "instruction": "Turn left onto the walkway.", + "length": 0.045, + "time": 51.529, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 15, + "verbal_post_transition_instruction": "Continue for 200 feet.", + "verbal_pre_transition_instruction": "Turn left onto the walkway.", + "verbal_succinct_transition_instruction": "Turn left.", + "verbal_transition_alert_instruction": "Turn left onto the walkway." }, { - "type": 10, - "instruction": "Turn right onto the walkway.", - "verbal_transition_alert_instruction": "Turn right onto the walkway.", - "verbal_succinct_transition_instruction": "Turn right. Then Turn left onto the crosswalk.", - "verbal_pre_transition_instruction": "Turn right onto the walkway. Then Turn left onto the crosswalk.", - "verbal_post_transition_instruction": "Continue for less than 10 meters.", - "time": 1.411, - "length": 0.002, - "cost": 1.411, "begin_shape_index": 292, + "cost": 1.411, "end_shape_index": 293, - "verbal_multi_cue": true, + "instruction": "Turn right onto the walkway.", + "length": 0.001, + "time": 1.411, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 10, + "verbal_multi_cue": true, + "verbal_post_transition_instruction": "Continue for less than 10 feet.", + "verbal_pre_transition_instruction": "Turn right onto the walkway. Then Turn left onto the crosswalk.", + "verbal_succinct_transition_instruction": "Turn right. Then Turn left onto the crosswalk.", + "verbal_transition_alert_instruction": "Turn right onto the walkway." }, { - "type": 15, - "instruction": "Turn left onto the crosswalk.", - "verbal_transition_alert_instruction": "Turn left onto the crosswalk.", - "verbal_succinct_transition_instruction": "Turn left.", - "verbal_pre_transition_instruction": "Turn left onto the crosswalk.", - "verbal_post_transition_instruction": "Continue for 200 meters.", - "time": 140.647, - "length": 0.195, - "cost": 140.647, "begin_shape_index": 293, + "cost": 140.647, "end_shape_index": 310, + "instruction": "Turn left onto the crosswalk.", + "length": 0.121, + "time": 140.647, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 15, + "verbal_post_transition_instruction": "Continue for 600 feet.", + "verbal_pre_transition_instruction": "Turn left onto the crosswalk.", + "verbal_succinct_transition_instruction": "Turn left.", + "verbal_transition_alert_instruction": "Turn left onto the crosswalk." }, { - "type": 23, - "instruction": "Keep right to take the walkway.", - "verbal_transition_alert_instruction": "Keep right to take the walkway.", - "verbal_pre_transition_instruction": "Keep right to take the walkway. Then Turn right onto the walkway.", - "verbal_post_transition_instruction": "Continue for less than 10 meters.", - "time": 6.646, - "length": 0.008, - "cost": 6.646, "begin_shape_index": 310, + "cost": 6.646, "end_shape_index": 313, - "verbal_multi_cue": true, + "instruction": "Keep right to take the walkway.", + "length": 0.004, + "time": 6.646, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 23, + "verbal_multi_cue": true, + "verbal_post_transition_instruction": "Continue for 30 feet.", + "verbal_pre_transition_instruction": "Keep right to take the walkway. Then Turn right onto the walkway.", + "verbal_transition_alert_instruction": "Keep right to take the walkway." }, { - "type": 10, - "instruction": "Turn right onto the walkway.", - "verbal_transition_alert_instruction": "Turn right onto the walkway.", - "verbal_succinct_transition_instruction": "Turn right.", - "verbal_pre_transition_instruction": "Turn right onto the walkway.", - "verbal_post_transition_instruction": "Continue for 20 meters.", - "time": 19.941, - "length": 0.024, - "cost": 19.941, "begin_shape_index": 313, + "cost": 19.941, "end_shape_index": 323, + "instruction": "Turn right onto the walkway.", + "length": 0.014, + "time": 19.941, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 10, + "verbal_post_transition_instruction": "Continue for 80 feet.", + "verbal_pre_transition_instruction": "Turn right onto the walkway.", + "verbal_succinct_transition_instruction": "Turn right.", + "verbal_transition_alert_instruction": "Turn right onto the walkway." }, { - "type": 10, - "instruction": "Turn right.", - "verbal_transition_alert_instruction": "Turn right.", - "verbal_succinct_transition_instruction": "Turn right. Then Turn left onto Boren Avenue.", - "verbal_pre_transition_instruction": "Turn right. Then Turn left onto Boren Avenue.", - "verbal_post_transition_instruction": "Continue for 20 meters.", - "time": 11.294, - "length": 0.016, - "cost": 11.294, "begin_shape_index": 323, + "cost": 11.294, "end_shape_index": 327, - "verbal_multi_cue": true, + "instruction": "Turn right.", + "length": 0.009, + "time": 11.294, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 10, + "verbal_multi_cue": true, + "verbal_post_transition_instruction": "Continue for 50 feet.", + "verbal_pre_transition_instruction": "Turn right. Then Turn left onto Boren Avenue.", + "verbal_succinct_transition_instruction": "Turn right. Then Turn left onto Boren Avenue.", + "verbal_transition_alert_instruction": "Turn right." }, { - "type": 15, + "begin_shape_index": 327, + "cost": 258.411, + "end_shape_index": 341, "instruction": "Turn left onto Boren Avenue.", - "verbal_transition_alert_instruction": "Turn left onto Boren Avenue.", - "verbal_succinct_transition_instruction": "Turn left.", - "verbal_pre_transition_instruction": "Turn left onto Boren Avenue.", - "verbal_post_transition_instruction": "Continue for 400 meters.", + "length": 0.223, "street_names": [ "Boren Avenue" ], "time": 253.411, - "length": 0.358, - "cost": 258.411, - "begin_shape_index": 327, - "end_shape_index": 341, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 15, + "verbal_post_transition_instruction": "Continue for a quarter mile.", + "verbal_pre_transition_instruction": "Turn left onto Boren Avenue.", + "verbal_succinct_transition_instruction": "Turn left.", + "verbal_transition_alert_instruction": "Turn left onto Boren Avenue." }, { - "type": 24, + "begin_shape_index": 341, + "cost": 486.823, + "end_shape_index": 375, "instruction": "Keep left to stay on Boren Avenue.", - "verbal_transition_alert_instruction": "Keep left to stay on Boren Avenue.", - "verbal_pre_transition_instruction": "Keep left to stay on Boren Avenue.", - "verbal_post_transition_instruction": "Continue for 700 meters.", + "length": 0.425, "street_names": [ "Boren Avenue" ], "time": 486.824, - "length": 0.684, - "cost": 486.823, - "begin_shape_index": 341, - "end_shape_index": 375, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 24, + "verbal_post_transition_instruction": "Continue for a half mile.", + "verbal_pre_transition_instruction": "Keep left to stay on Boren Avenue.", + "verbal_transition_alert_instruction": "Keep left to stay on Boren Avenue." }, { - "type": 23, + "begin_shape_index": 375, + "cost": 878.119, + "end_shape_index": 436, "instruction": "Keep right to stay on Boren Avenue.", - "verbal_transition_alert_instruction": "Keep right to stay on Boren Avenue.", - "verbal_pre_transition_instruction": "Keep right to stay on Boren Avenue.", - "verbal_post_transition_instruction": "Continue for 1 kilometer.", + "length": 0.762, "street_names": [ "Boren Avenue" ], "time": 878.119, - "length": 1.227, - "cost": 878.119, - "begin_shape_index": 375, - "end_shape_index": 436, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 23, + "verbal_post_transition_instruction": "Continue for 1 mile.", + "verbal_pre_transition_instruction": "Keep right to stay on Boren Avenue.", + "verbal_transition_alert_instruction": "Keep right to stay on Boren Avenue." }, { - "type": 16, + "begin_shape_index": 436, + "cost": 66.588, + "end_shape_index": 438, "instruction": "Bear left onto Denny Way.", - "verbal_transition_alert_instruction": "Bear left onto Denny Way.", - "verbal_succinct_transition_instruction": "Bear left.", - "verbal_pre_transition_instruction": "Bear left onto Denny Way.", - "verbal_post_transition_instruction": "Continue for 80 meters.", + "length": 0.051, "street_names": [ "Denny Way" ], "time": 61.588, - "length": 0.083, - "cost": 66.588, - "begin_shape_index": 436, - "end_shape_index": 438, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 16, + "verbal_post_transition_instruction": "Continue for 300 feet.", + "verbal_pre_transition_instruction": "Bear left onto Denny Way.", + "verbal_succinct_transition_instruction": "Bear left.", + "verbal_transition_alert_instruction": "Bear left onto Denny Way." }, { - "type": 23, + "begin_shape_index": 438, + "cost": 48.706, + "end_shape_index": 441, "instruction": "Keep right to stay on Denny Way.", - "verbal_transition_alert_instruction": "Keep right to stay on Denny Way.", - "verbal_pre_transition_instruction": "Keep right to stay on Denny Way.", - "verbal_post_transition_instruction": "Continue for 70 meters.", + "length": 0.042, "street_names": [ "Denny Way" ], "time": 48.706, - "length": 0.069, - "cost": 48.706, - "begin_shape_index": 438, - "end_shape_index": 441, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 23, + "verbal_post_transition_instruction": "Continue for 200 feet.", + "verbal_pre_transition_instruction": "Keep right to stay on Denny Way.", + "verbal_transition_alert_instruction": "Keep right to stay on Denny Way." }, { - "type": 10, - "instruction": "Turn right onto the walkway.", - "verbal_transition_alert_instruction": "Turn right onto the walkway.", - "verbal_succinct_transition_instruction": "Turn right. Then Turn left onto the walkway.", - "verbal_pre_transition_instruction": "Turn right onto the walkway. Then Turn left onto the walkway.", - "verbal_post_transition_instruction": "Continue for less than 10 meters.", - "time": 4.235, - "length": 0.006, - "cost": 9.235, "begin_shape_index": 441, + "cost": 9.235, "end_shape_index": 443, - "verbal_multi_cue": true, + "instruction": "Turn right onto the walkway.", + "length": 0.003, + "time": 4.235, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 10, + "verbal_multi_cue": true, + "verbal_post_transition_instruction": "Continue for 20 feet.", + "verbal_pre_transition_instruction": "Turn right onto the walkway. Then Turn left onto the walkway.", + "verbal_succinct_transition_instruction": "Turn right. Then Turn left onto the walkway.", + "verbal_transition_alert_instruction": "Turn right onto the walkway." }, { - "type": 15, - "instruction": "Turn left onto the walkway.", - "verbal_transition_alert_instruction": "Turn left onto the walkway.", - "verbal_succinct_transition_instruction": "Turn left.", - "verbal_pre_transition_instruction": "Turn left onto the walkway.", - "verbal_post_transition_instruction": "Continue for 80 meters.", - "time": 56.059, - "length": 0.078, - "cost": 56.059, "begin_shape_index": 443, + "cost": 56.059, "end_shape_index": 447, + "instruction": "Turn left onto the walkway.", + "length": 0.048, + "time": 56.059, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 15, + "verbal_post_transition_instruction": "Continue for 300 feet.", + "verbal_pre_transition_instruction": "Turn left onto the walkway.", + "verbal_succinct_transition_instruction": "Turn left.", + "verbal_transition_alert_instruction": "Turn left onto the walkway." }, { - "type": 10, - "instruction": "Turn right onto the walkway.", - "verbal_transition_alert_instruction": "Turn right onto the walkway.", - "verbal_succinct_transition_instruction": "Turn right.", - "verbal_pre_transition_instruction": "Turn right onto the walkway.", - "verbal_post_transition_instruction": "Continue for 100 meters.", - "time": 76.235, - "length": 0.108, - "cost": 76.235, "begin_shape_index": 447, + "cost": 76.235, "end_shape_index": 448, + "instruction": "Turn right onto the walkway.", + "length": 0.067, + "time": 76.235, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 10, + "verbal_post_transition_instruction": "Continue for 400 feet.", + "verbal_pre_transition_instruction": "Turn right onto the walkway.", + "verbal_succinct_transition_instruction": "Turn right.", + "verbal_transition_alert_instruction": "Turn right onto the walkway." }, { - "type": 15, - "instruction": "Turn left onto the walkway.", - "verbal_transition_alert_instruction": "Turn left onto the walkway.", - "verbal_succinct_transition_instruction": "Turn left. Then Turn right onto Westlake Avenue North.", - "verbal_pre_transition_instruction": "Turn left onto the walkway. Then Turn right onto Westlake Avenue North.", - "verbal_post_transition_instruction": "Continue for 10 meters.", - "time": 7.059, - "length": 0.009, - "cost": 7.059, "begin_shape_index": 448, + "cost": 7.059, "end_shape_index": 451, - "verbal_multi_cue": true, + "instruction": "Turn left onto the walkway.", + "length": 0.006, + "time": 7.059, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 15, + "verbal_multi_cue": true, + "verbal_post_transition_instruction": "Continue for 30 feet.", + "verbal_pre_transition_instruction": "Turn left onto the walkway. Then Turn right onto Westlake Avenue North.", + "verbal_succinct_transition_instruction": "Turn left. Then Turn right onto Westlake Avenue North.", + "verbal_transition_alert_instruction": "Turn left onto the walkway." }, { - "type": 10, + "begin_shape_index": 451, + "cost": 591.234, + "end_shape_index": 497, "instruction": "Turn right onto Westlake Avenue North.", - "verbal_transition_alert_instruction": "Turn right onto Westlake Avenue North.", - "verbal_succinct_transition_instruction": "Turn right.", - "verbal_pre_transition_instruction": "Turn right onto Westlake Avenue North.", - "verbal_post_transition_instruction": "Continue for 800 meters.", + "length": 0.51, "street_names": [ "Westlake Avenue North" ], "time": 586.234, - "length": 0.821, - "cost": 591.234, - "begin_shape_index": 451, - "end_shape_index": 497, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 10, + "verbal_post_transition_instruction": "Continue for a half mile.", + "verbal_pre_transition_instruction": "Turn right onto Westlake Avenue North.", + "verbal_succinct_transition_instruction": "Turn right.", + "verbal_transition_alert_instruction": "Turn right onto Westlake Avenue North." }, { - "type": 16, + "begin_shape_index": 497, + "cost": 11.293, + "end_shape_index": 499, "instruction": "Bear left to stay on Westlake Avenue North.", - "verbal_transition_alert_instruction": "Bear left to stay on Westlake Avenue North.", - "verbal_succinct_transition_instruction": "Bear left. Then Turn right to stay on Westlake Avenue North.", - "verbal_pre_transition_instruction": "Bear left to stay on Westlake Avenue North. Then Turn right to stay on Westlake Avenue North.", - "verbal_post_transition_instruction": "Continue for 20 meters.", + "length": 0.009, "street_names": [ "Westlake Avenue North" ], "time": 11.293, - "length": 0.016, - "cost": 11.293, - "begin_shape_index": 497, - "end_shape_index": 499, - "verbal_multi_cue": true, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 16, + "verbal_multi_cue": true, + "verbal_post_transition_instruction": "Continue for 50 feet.", + "verbal_pre_transition_instruction": "Bear left to stay on Westlake Avenue North. Then Turn right to stay on Westlake Avenue North.", + "verbal_succinct_transition_instruction": "Bear left. Then Turn right to stay on Westlake Avenue North.", + "verbal_transition_alert_instruction": "Bear left to stay on Westlake Avenue North." }, { - "type": 10, + "begin_shape_index": 499, + "cost": 459.823, + "end_shape_index": 530, "instruction": "Turn right to stay on Westlake Avenue North.", - "verbal_transition_alert_instruction": "Turn right to stay on Westlake Avenue North.", - "verbal_succinct_transition_instruction": "Turn right.", - "verbal_pre_transition_instruction": "Turn right to stay on Westlake Avenue North.", - "verbal_post_transition_instruction": "Continue for 700 meters.", + "length": 0.403, "street_names": [ "Westlake Avenue North" ], "time": 459.823, - "length": 0.65, - "cost": 459.823, - "begin_shape_index": 499, - "end_shape_index": 530, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 10, + "verbal_post_transition_instruction": "Continue for a half mile.", + "verbal_pre_transition_instruction": "Turn right to stay on Westlake Avenue North.", + "verbal_succinct_transition_instruction": "Turn right.", + "verbal_transition_alert_instruction": "Turn right to stay on Westlake Avenue North." }, { - "type": 15, - "instruction": "Turn left onto the walkway.", - "verbal_transition_alert_instruction": "Turn left onto the walkway.", - "verbal_succinct_transition_instruction": "Turn left. Then Turn right onto the walkway.", - "verbal_pre_transition_instruction": "Turn left onto the walkway. Then Turn right onto the walkway.", - "verbal_post_transition_instruction": "Continue for 10 meters.", - "time": 7.764, - "length": 0.011, - "cost": 12.764, "begin_shape_index": 530, + "cost": 12.764, "end_shape_index": 532, - "verbal_multi_cue": true, + "instruction": "Turn left onto the walkway.", + "length": 0.006, + "time": 7.764, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 15, + "verbal_multi_cue": true, + "verbal_post_transition_instruction": "Continue for 40 feet.", + "verbal_pre_transition_instruction": "Turn left onto the walkway. Then Turn right onto the walkway.", + "verbal_succinct_transition_instruction": "Turn left. Then Turn right onto the walkway.", + "verbal_transition_alert_instruction": "Turn left onto the walkway." }, { - "type": 10, - "instruction": "Turn right onto the walkway.", - "verbal_transition_alert_instruction": "Turn right onto the walkway.", - "verbal_succinct_transition_instruction": "Turn right.", - "verbal_pre_transition_instruction": "Turn right onto the walkway.", - "verbal_post_transition_instruction": "Continue for 60 meters.", - "time": 41.235, - "length": 0.057, - "cost": 41.235, "begin_shape_index": 532, + "cost": 41.235, "end_shape_index": 541, + "instruction": "Turn right onto the walkway.", + "length": 0.035, + "time": 41.235, "travel_mode": "pedestrian", - "travel_type": "foot" - }, - { - "type": 15, - "instruction": "Turn left onto the walkway.", - "verbal_transition_alert_instruction": "Turn left onto the walkway.", - "verbal_succinct_transition_instruction": "Turn left.", - "verbal_pre_transition_instruction": "Turn left onto the walkway.", - "verbal_post_transition_instruction": "Continue for 70 meters.", - "time": 49.411, - "length": 0.07, - "cost": 49.411, + "travel_type": "foot", + "type": 10, + "verbal_post_transition_instruction": "Continue for 200 feet.", + "verbal_pre_transition_instruction": "Turn right onto the walkway.", + "verbal_succinct_transition_instruction": "Turn right.", + "verbal_transition_alert_instruction": "Turn right onto the walkway." + }, + { "begin_shape_index": 541, + "cost": 49.411, "end_shape_index": 542, + "instruction": "Turn left onto the walkway.", + "length": 0.043, + "time": 49.411, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 15, + "verbal_post_transition_instruction": "Continue for 200 feet.", + "verbal_pre_transition_instruction": "Turn left onto the walkway.", + "verbal_succinct_transition_instruction": "Turn left.", + "verbal_transition_alert_instruction": "Turn left onto the walkway." }, { - "type": 10, - "instruction": "Turn right onto the walkway.", - "verbal_transition_alert_instruction": "Turn right onto the walkway.", - "verbal_succinct_transition_instruction": "Turn right. Then, in 20 meters, Turn left onto the walkway.", - "verbal_pre_transition_instruction": "Turn right onto the walkway. Then, in 20 meters, Turn left onto the walkway.", - "verbal_post_transition_instruction": "Continue for 20 meters.", - "time": 11.587, - "length": 0.015, - "cost": 11.587, "begin_shape_index": 542, + "cost": 11.587, "end_shape_index": 546, - "verbal_multi_cue": true, + "instruction": "Turn right onto the walkway.", + "length": 0.009, + "time": 11.587, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 10, + "verbal_multi_cue": true, + "verbal_post_transition_instruction": "Continue for 50 feet.", + "verbal_pre_transition_instruction": "Turn right onto the walkway. Then, in 50 feet, Turn left onto the walkway.", + "verbal_succinct_transition_instruction": "Turn right. Then, in 50 feet, Turn left onto the walkway.", + "verbal_transition_alert_instruction": "Turn right onto the walkway." }, { - "type": 15, - "instruction": "Turn left onto the walkway.", - "verbal_transition_alert_instruction": "Turn left onto the walkway.", - "verbal_succinct_transition_instruction": "Turn left.", - "verbal_pre_transition_instruction": "Turn left onto the walkway.", - "verbal_post_transition_instruction": "Continue for 100 meters.", - "time": 79.353, - "length": 0.111, - "cost": 79.353, "begin_shape_index": 546, + "cost": 79.353, "end_shape_index": 553, + "instruction": "Turn left onto the walkway.", + "length": 0.068, + "time": 79.353, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 15, + "verbal_post_transition_instruction": "Continue for 400 feet.", + "verbal_pre_transition_instruction": "Turn left onto the walkway.", + "verbal_succinct_transition_instruction": "Turn left.", + "verbal_transition_alert_instruction": "Turn left onto the walkway." }, { - "type": 15, - "instruction": "Turn left onto the walkway.", - "verbal_transition_alert_instruction": "Turn left onto the walkway.", - "verbal_succinct_transition_instruction": "Turn left. Then Turn right onto Dexter Avenue North.", - "verbal_pre_transition_instruction": "Turn left onto the walkway. Then Turn right onto Dexter Avenue North.", - "verbal_post_transition_instruction": "Continue for less than 10 meters.", - "time": 5.646, - "length": 0.008, - "cost": 5.646, "begin_shape_index": 553, + "cost": 5.646, "end_shape_index": 555, - "verbal_multi_cue": true, + "instruction": "Turn left onto the walkway.", + "length": 0.004, + "time": 5.646, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 15, + "verbal_multi_cue": true, + "verbal_post_transition_instruction": "Continue for 30 feet.", + "verbal_pre_transition_instruction": "Turn left onto the walkway. Then Turn right onto Dexter Avenue North.", + "verbal_succinct_transition_instruction": "Turn left. Then Turn right onto Dexter Avenue North.", + "verbal_transition_alert_instruction": "Turn left onto the walkway." }, { - "type": 10, + "begin_shape_index": 555, + "cost": 523.117, + "end_shape_index": 583, "instruction": "Turn right onto Dexter Avenue North.", - "verbal_transition_alert_instruction": "Turn right onto Dexter Avenue North.", - "verbal_succinct_transition_instruction": "Turn right.", - "verbal_pre_transition_instruction": "Turn right onto Dexter Avenue North.", - "verbal_post_transition_instruction": "Continue for 700 meters.", + "length": 0.456, "street_names": [ "Dexter Avenue North" ], "time": 518.117, - "length": 0.734, - "cost": 523.117, - "begin_shape_index": 555, - "end_shape_index": 583, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 10, + "verbal_post_transition_instruction": "Continue for a half mile.", + "verbal_pre_transition_instruction": "Turn right onto Dexter Avenue North.", + "verbal_succinct_transition_instruction": "Turn right.", + "verbal_transition_alert_instruction": "Turn right onto Dexter Avenue North." }, { - "type": 15, + "begin_shape_index": 583, + "cost": 15.588, + "end_shape_index": 587, "instruction": "Turn left onto 6th Avenue North.", - "verbal_transition_alert_instruction": "Turn left onto 6th Avenue North.", - "verbal_succinct_transition_instruction": "Turn left. Then, in 20 meters, Turn right to stay on 6th Avenue North.", - "verbal_pre_transition_instruction": "Turn left onto 6th Avenue North. Then, in 20 meters, Turn right to stay on 6th Avenue North.", - "verbal_post_transition_instruction": "Continue for 20 meters.", + "length": 0.009, "street_names": [ "6th Avenue North" ], "time": 10.588, - "length": 0.015, - "cost": 15.588, - "begin_shape_index": 583, - "end_shape_index": 587, - "verbal_multi_cue": true, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 15, + "verbal_multi_cue": true, + "verbal_post_transition_instruction": "Continue for 50 feet.", + "verbal_pre_transition_instruction": "Turn left onto 6th Avenue North. Then, in 50 feet, Turn right to stay on 6th Avenue North.", + "verbal_succinct_transition_instruction": "Turn left. Then, in 50 feet, Turn right to stay on 6th Avenue North.", + "verbal_transition_alert_instruction": "Turn left onto 6th Avenue North." }, { - "type": 10, + "begin_shape_index": 587, + "cost": 161.646, + "end_shape_index": 601, "instruction": "Turn right to stay on 6th Avenue North.", - "verbal_transition_alert_instruction": "Turn right to stay on 6th Avenue North.", - "verbal_succinct_transition_instruction": "Turn right.", - "verbal_pre_transition_instruction": "Turn right to stay on 6th Avenue North.", - "verbal_post_transition_instruction": "Continue for 200 meters.", + "length": 0.142, "street_names": [ "6th Avenue North" ], "time": 161.646, - "length": 0.229, - "cost": 161.646, - "begin_shape_index": 587, - "end_shape_index": 601, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 10, + "verbal_post_transition_instruction": "Continue for 800 feet.", + "verbal_pre_transition_instruction": "Turn right to stay on 6th Avenue North.", + "verbal_succinct_transition_instruction": "Turn right.", + "verbal_transition_alert_instruction": "Turn right to stay on 6th Avenue North." }, { - "type": 15, + "begin_shape_index": 601, + "cost": 27.588, + "end_shape_index": 602, "instruction": "Turn left onto Halladay Street.", - "verbal_transition_alert_instruction": "Turn left onto Halladay Street.", - "verbal_succinct_transition_instruction": "Turn left.", - "verbal_pre_transition_instruction": "Turn left onto Halladay Street.", - "verbal_post_transition_instruction": "Continue for 30 meters.", + "length": 0.019, "street_names": [ "Halladay Street" ], "time": 22.588, - "length": 0.032, - "cost": 27.588, - "begin_shape_index": 601, - "end_shape_index": 602, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 15, + "verbal_post_transition_instruction": "Continue for 100 feet.", + "verbal_pre_transition_instruction": "Turn left onto Halladay Street.", + "verbal_succinct_transition_instruction": "Turn left.", + "verbal_transition_alert_instruction": "Turn left onto Halladay Street." }, { - "type": 23, - "instruction": "Keep right at the fork.", - "verbal_transition_alert_instruction": "Keep right at the fork.", - "verbal_pre_transition_instruction": "Keep right at the fork.", - "verbal_post_transition_instruction": "Continue for 30 meters.", - "time": 19.058, - "length": 0.027, - "cost": 19.058, "begin_shape_index": 602, + "cost": 19.058, "end_shape_index": 604, + "instruction": "Keep right at the fork.", + "length": 0.016, + "time": 19.058, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 23, + "verbal_post_transition_instruction": "Continue for 90 feet.", + "verbal_pre_transition_instruction": "Keep right at the fork.", + "verbal_transition_alert_instruction": "Keep right at the fork." }, { - "type": 10, - "instruction": "Turn right onto the walkway.", - "verbal_transition_alert_instruction": "Turn right onto the walkway.", - "verbal_succinct_transition_instruction": "Turn right. Then Turn left onto the walkway.", - "verbal_pre_transition_instruction": "Turn right onto the walkway. Then Turn left onto the walkway.", - "verbal_post_transition_instruction": "Continue for less than 10 meters.", - "time": 4.235, - "length": 0.006, - "cost": 4.235, "begin_shape_index": 604, + "cost": 4.235, "end_shape_index": 606, - "verbal_multi_cue": true, + "instruction": "Turn right onto the walkway.", + "length": 0.003, + "time": 4.235, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 10, + "verbal_multi_cue": true, + "verbal_post_transition_instruction": "Continue for 20 feet.", + "verbal_pre_transition_instruction": "Turn right onto the walkway. Then Turn left onto the walkway.", + "verbal_succinct_transition_instruction": "Turn right. Then Turn left onto the walkway.", + "verbal_transition_alert_instruction": "Turn right onto the walkway." }, { - "type": 15, - "instruction": "Turn left onto the walkway.", - "verbal_transition_alert_instruction": "Turn left onto the walkway.", - "verbal_succinct_transition_instruction": "Turn left.", - "verbal_pre_transition_instruction": "Turn left onto the walkway.", - "verbal_post_transition_instruction": "Continue for 80 meters.", - "time": 58.882, - "length": 0.082, - "cost": 58.882, "begin_shape_index": 606, + "cost": 58.882, "end_shape_index": 612, + "instruction": "Turn left onto the walkway.", + "length": 0.05, + "time": 58.882, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 15, + "verbal_post_transition_instruction": "Continue for 300 feet.", + "verbal_pre_transition_instruction": "Turn left onto the walkway.", + "verbal_succinct_transition_instruction": "Turn left.", + "verbal_transition_alert_instruction": "Turn left onto the walkway." }, { - "type": 24, - "instruction": "Keep left to take the walkway.", - "verbal_transition_alert_instruction": "Keep left to take the walkway.", - "verbal_pre_transition_instruction": "Keep left to take the walkway.", - "verbal_post_transition_instruction": "Continue for 900 meters.", - "time": 624.581, - "length": 0.884, - "cost": 624.581, "begin_shape_index": 612, + "cost": 624.581, "end_shape_index": 621, + "instruction": "Keep left to take the walkway.", + "length": 0.549, + "time": 624.581, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 24, + "verbal_post_transition_instruction": "Continue for a half mile.", + "verbal_pre_transition_instruction": "Keep left to take the walkway.", + "verbal_transition_alert_instruction": "Keep left to take the walkway." }, { - "type": 4, - "instruction": "You have arrived at your destination.", - "verbal_transition_alert_instruction": "You will arrive at your destination.", - "verbal_pre_transition_instruction": "You have arrived at your destination.", - "time": 0.0, - "length": 0.0, - "cost": 0.0, "begin_shape_index": 621, + "cost": 0.0, "end_shape_index": 621, + "instruction": "You have arrived at your destination.", + "length": 0.0, + "time": 0.0, "travel_mode": "pedestrian", - "travel_type": "foot" + "travel_type": "foot", + "type": 4, + "verbal_pre_transition_instruction": "You have arrived at your destination.", + "verbal_transition_alert_instruction": "You will arrive at your destination." } ], - "summary": { - "has_time_restrictions": false, - "has_toll": false, - "has_highway": false, - "has_ferry": false, - "min_lat": 47.57566, - "min_lon": -122.347201, - "max_lat": 47.651047, - "max_lon": -122.316908, - "time": 7906.326, - "length": 11.117, - "cost": 7976.325 - }, - "shape": "wpxvyA~w~ihFxID?wGDcw@mA@?c@@oA?G?k@?u]?gI?kA?mC@wA?wPv@E?wAAsF?yB?iEGyACoAPu@A}IAmQ@qR?}U?mI@e@@yE@{BI{f@]cAJ{IsCBuDBiE?cYAiNAii@BkJ?oIFoEDyDUcIe@uF?{A?cF?uI?sI?}H?qO?wH?kC?uD?{M@wDAgG@aDAkA?a[@sA?iF?kN?uB?se@@qB?eD?{@?gIAiE@sD?iI@oC?cE@gO@uI@k^D_J?oXDi@AmD@wHAu@?kD?aC?gGAi|@EkJAsHA}LAY@iGAAiJE}RCaKCiDI{HG}CoA}h@A_HAoG?gBAcBAcBAkB@uBDeVBiRHsV@sE@kBBiNBoE?sB?qB@wBBwB?qJ?sN?uF?{S?mKAiKAkS?_J@gU@_Q?eI?kG@qB?oD@c[@cP?sV@mK?kIBee@?qCAsFCy`@?kBRqOVeRLkKRcb@@}s@JqaBAcXDaDNqDZwEn@{Fp@mEdA}F~AcGbB}EfDcHfAyBg@q@_@g@Y_@g@@mBzCqGtCsEtDiMnH{OfIkBn@cB\\_b@r@wEQuELiEr@wJlCqEv@kDZeOp@{Mb@wXJuIz@qFZqZTsJz@gENeCAmEUoe@oEqHWwZImKFyBPIiCOmA_@cAi@u@iDaCsKiHkKiKwIiLyByBaCqBzAuK|AyLM_DF}@`@o@dAc@t@aAw@cBeAeBsAgF`@{Bz@oEe@aCuBaDgAImAsF]w@e@Qu@FeAoA?aAiYBmJgHUIqMa@{@gA_XCgC?uASsA_@kAo@gAaAaAkA{@uAm@}AcDgLa@{@q@o@y@g@c@Mi@Ag@Li@Rc@\\iD|E_D|D{DvDkD|ByDbBwD~@_Ef@eDHq|A}AuH?yQB}UI_@TWN_@?uAEmCI_@?QSei@E?|@iA@cAA_BC}@j@?{@C[MKOEsF@aV?gH?IPi@z@W?yBIy@Cw@C}AGq@?A]ISOIcPCEL}UMUNUa@cBTiAP[T_@??UCOIGU?s\\FgJ@iACg@Ae@?wKCm@Lw]t^{@xAg@j@ET?hBe@?qA~@e@\\]VyAfAW?O?Qb@?b@E`AaCk@g@Q{@Mq@E_AbAiAlAqOjP}@M_WvVgEpEiS|SsYhZmt@ju@cBdBm@l@]\\}C|CiAhAwBjBuCjCsS`R}CjCyb@r_@qCdCoC`CgJhI{ClCaHfGgGnFoE|DmEjBMFka@l^gCzBsAjAs@n@sc@n`@qBfBgCzBwQfPgCvFgCxBkFrEaCtBiCvBuGvFuJjIiOjM_CrBuDfDoIzHwLxKeCRwFfFqBhB_@^mBbBiFzEgD|FcIxG}KzJ}BtBiCzBuQhPiPzNkC`CuBhBmUfSiJdI{SvQeCzB}BnBeh@vd@qKnJa@\\mBdBkBbBaAz@YVyOnNeOrMgQlOqBhBmC~ByQlPmKnJoIrHmObN_CvBqQxPeRfQqBlBaB`DoJdRaFxJ}JzR}IhQqB~DoCpFkCnFiChFaMzVwEnJgBpDuF|KcBlDiDlHmCbGqExJ{B`FwS|`@i@fAiGzEAvd@Ct^w@nCCfm@?|DmAXc@^Cz]En]Kf@_@Xs{@HOdBCh@ItByC?aFC}]GgPCiKCoDA_EAe^GoVGwEAsDAuB?yd@BiF@sQ?oD?eD@oL@wG@kD?kGAeXA{JCcE?mH@{C?qN?wE?sF?sEFyDXiCp@yA^oAb@yEdBs@ZSJcDnB_HdE{QfMaQrL}DvDiAnA{A`CmAzBWv@IxGCvCeDC}KJeDR}Af@}DpAoExB_JxFyUdOk[bSqAv@oDrBkAn@kCvAoF`CmEzAs@RqJnCyHrAqIl@kRZmDE{DA{F@eh@HuHCwDAo_AWuOYaDc@_De@wBi@S`FAl@q@RQh@]|Ac@fAw@dAw@f@k@HkNC{BAApy@Y`@{@AqCCWa@}s@XS@iA?wA?[?KDeA??z@?hDg`AMqFAy@?_QEoC@oCPgANgANmCl@mC~@iClAeD|AkFhCeGvCaUtKs@\\sHtDgb@lSyL`G{CvAcErBsQ~IgKdFkN~Ec`AhTcB^ib@lJwMvCq@|DUvASpAGZ}CzAyLrCsQhEeE`AuCx@qCjAmCzAiClBuLtI}AxAwA`BuAfBqAnBgX`c@`@vYeDbR]n@_B[O[o@bAy@v@{PbE}B^{Cd@eMlD{@z@eATuCToQtA}ZfAuPRel@Z}_@PexIC" + "shape": "wpxvyA~w~ihFxID?wGDcw@mA@?c@@oA?G?k@?u]?gI?kA?mC@wA?wPv@E?wAAsF?yB?iEGyACoAPu@A}IAmQ@qR?}U?mI@e@@yE@{BI{f@]cAJ{IsCBuDBiE?cYAiNAii@BkJ?oIFoEDyDUcIe@uF?{A?cF?uI?sI?}H?qO?wH?kC?uD?{M@wDAgG@aDAkA?a[@sA?iF?kN?uB?se@@qB?eD?{@?gIAiE@sD?iI@oC?cE@gO@uI@k^D_J?oXDi@AmD@wHAu@?kD?aC?gGAi|@EkJAsHA}LAY@iGAAiJE}RCaKCiDI{HG}CoA}h@A_HAoG?gBAcBAcBAkB@uBDeVBiRHsV@sE@kBBiNBoE?sB?qB@wBBwB?qJ?sN?uF?{S?mKAiKAkS?_J@gU@_Q?eI?kG@qB?oD@c[@cP?sV@mK?kIBee@?qCAsFCy`@?kBRqOVeRLkKRcb@@}s@JqaBAcXDaDNqDZwEn@{Fp@mEdA}F~AcGbB}EfDcHfAyBg@q@_@g@Y_@g@@mBzCqGtCsEtDiMnH{OfIkBn@cB\\_b@r@wEQuELiEr@wJlCqEv@kDZeOp@{Mb@wXJuIz@qFZqZTsJz@gENeCAmEUoe@oEqHWwZImKFyBPIiCOmA_@cAi@u@iDaCsKiHkKiKwIiLyByBaCqBzAuK|AyLM_DF}@`@o@dAc@t@aAw@cBeAeBsAgF`@{Bz@oEe@aCuBaDgAImAsF]w@e@Qu@FeAoA?aAiYBmJgHUIqMa@{@gA_XCgC?uASsA_@kAo@gAaAaAkA{@uAm@}AcDgLa@{@q@o@y@g@c@Mi@Ag@Li@Rc@\\iD|E_D|D{DvDkD|ByDbBwD~@_Ef@eDHq|A}AuH?yQB}UI_@TWN_@?uAEmCI_@?QSei@E?|@iA@cAA_BC}@j@?{@C[MKOEsF@aV?gH?IPi@z@W?yBIy@Cw@C}AGq@?A]ISOIcPCEL}UMUNUa@cBTiAP[T_@??UCOIGU?s\\FgJ@iACg@Ae@?wKCm@Lw]t^{@xAg@j@ET?hBe@?qA~@e@\\]VyAfAW?O?Qb@?b@E`AaCk@g@Q{@Mq@E_AbAiAlAqOjP}@M_WvVgEpEiS|SsYhZmt@ju@cBdBm@l@]\\}C|CiAhAwBjBuCjCsS`R}CjCyb@r_@qCdCoC`CgJhI{ClCaHfGgGnFoE|DmEjBMFka@l^gCzBsAjAs@n@sc@n`@qBfBgCzBwQfPgCvFgCxBkFrEaCtBiCvBuGvFuJjIiOjM_CrBuDfDoIzHwLxKeCRwFfFqBhB_@^mBbBiFzEgD|FcIxG}KzJ}BtBiCzBuQhPiPzNkC`CuBhBmUfSiJdI{SvQeCzB}BnBeh@vd@qKnJa@\\mBdBkBbBaAz@YVyOnNeOrMgQlOqBhBmC~ByQlPmKnJoIrHmObN_CvBqQxPeRfQqBlBaB`DoJdRaFxJ}JzR}IhQqB~DoCpFkCnFiChFaMzVwEnJgBpDuF|KcBlDiDlHmCbGqExJ{B`FwS|`@i@fAiGzEAvd@Ct^w@nCCfm@?|DmAXc@^Cz]En]Kf@_@Xs{@HOdBCh@ItByC?aFC}]GgPCiKCoDA_EAe^GoVGwEAsDAuB?yd@BiF@sQ?oD?eD@oL@wG@kD?kGAeXA{JCcE?mH@{C?qN?wE?sF?sEFyDXiCp@yA^oAb@yEdBs@ZSJcDnB_HdE{QfMaQrL}DvDiAnA{A`CmAzBWv@IxGCvCeDC}KJeDR}Af@}DpAoExB_JxFyUdOk[bSqAv@oDrBkAn@kCvAoF`CmEzAs@RqJnCyHrAqIl@kRZmDE{DA{F@eh@HuHCwDAo_AWuOYaDc@_De@wBi@S`FAl@q@RQh@]|Ac@fAw@dAw@f@k@HkNC{BAApy@Y`@{@AqCCWa@}s@XS@iA?wA?[?KDeA??z@?hDg`AMqFAy@?_QEoC@oCPgANgANmCl@mC~@iClAeD|AkFhCeGvCaUtKs@\\sHtDgb@lSyL`G{CvAcErBsQ~IgKdFkN~Ec`AhTcB^ib@lJwMvCq@|DUvASpAGZ}CzAyLrCsQhEeE`AuCx@qCjAmCzAiClBuLtI}AxAwA`BuAfBqAnBgX`c@`@vYeDbR]n@_B[O[o@bAy@v@{PbE}B^{Cd@eMlD{@z@eATuCToQtA}ZfAuPRel@Z}_@PexIC", + "summary": { + "cost": 7976.325, + "has_ferry": false, + "has_highway": false, + "has_time_restrictions": false, + "has_toll": false, + "length": 6.907, + "max_lat": 47.651047, + "max_lon": -122.316908, + "min_lat": 47.57566, + "min_lon": -122.347201, + "time": 7906.326 + } + } + ], + "locations": [ + { + "lat": 47.575837, + "lon": -122.339414, + "original_index": 0, + "side_of_street": "right", + "type": "break" + }, + { + "lat": 47.651048, + "lon": -122.347234, + "original_index": 1, + "type": "break" + } + ], + "status": 0, + "status_message": "Found route between points", + "summary": { + "cost": 7976.325, + "has_ferry": false, + "has_highway": false, + "has_time_restrictions": false, + "has_toll": false, + "length": 6.907, + "max_lat": 47.651047, + "max_lon": -122.316908, + "min_lat": 47.57566, + "min_lon": -122.347201, + "time": 7906.326 + }, + "units": "miles" + } + } + ], + "trip": { + "language": "en-US", + "legs": [ + { + "maneuvers": [ + { + "begin_shape_index": 0, + "cost": 13.567, + "end_shape_index": 1, + "instruction": "Walk south on East Marginal Way South.", + "length": 0.011, + "street_names": [ + "East Marginal Way South" + ], + "time": 13.567, + "travel_mode": "pedestrian", + "travel_type": "foot", + "type": 2, + "verbal_multi_cue": true, + "verbal_post_transition_instruction": "Continue for 60 feet.", + "verbal_pre_transition_instruction": "Walk south on East Marginal Way South. Then Turn right onto the walkway.", + "verbal_succinct_transition_instruction": "Walk south. Then Turn right onto the walkway." + }, + { + "begin_shape_index": 1, + "cost": 49.47, + "end_shape_index": 9, + "instruction": "Turn right onto the walkway.", + "length": 0.039, + "rough": true, + "time": 44.47, + "travel_mode": "pedestrian", + "travel_type": "foot", + "type": 10, + "verbal_post_transition_instruction": "Continue for 200 feet.", + "verbal_pre_transition_instruction": "Turn right onto the walkway.", + "verbal_succinct_transition_instruction": "Turn right.", + "verbal_transition_alert_instruction": "Turn right onto the walkway." + }, + { + "begin_shape_index": 9, + "cost": 1149.294, + "end_shape_index": 28, + "instruction": "Turn right onto East Marginal Way South.", + "length": 1.002, + "street_names": [ + "East Marginal Way South" + ], + "time": 1139.294, + "travel_mode": "pedestrian", + "travel_type": "foot", + "type": 10, + "verbal_post_transition_instruction": "Continue for 1 mile.", + "verbal_pre_transition_instruction": "Turn right onto East Marginal Way South.", + "verbal_succinct_transition_instruction": "Turn right.", + "verbal_transition_alert_instruction": "Turn right onto East Marginal Way South." + }, + { + "begin_shape_index": 28, + "cost": 542.117, + "end_shape_index": 52, + "instruction": "Keep left to take Alaskan Way South.", + "length": 0.477, + "street_names": [ + "Alaskan Way South" + ], + "time": 542.117, + "travel_mode": "pedestrian", + "travel_type": "foot", + "type": 24, + "verbal_post_transition_instruction": "Continue for a half mile.", + "verbal_pre_transition_instruction": "Keep left to take Alaskan Way South.", + "verbal_transition_alert_instruction": "Keep left to take Alaskan Way South." + }, + { + "begin_shape_index": 52, + "cost": 556.588, + "end_shape_index": 92, + "instruction": "Turn left to stay on Alaskan Way South.", + "length": 0.484, + "street_names": [ + "Alaskan Way South" + ], + "time": 551.588, + "travel_mode": "pedestrian", + "travel_type": "foot", + "type": 15, + "verbal_post_transition_instruction": "Continue for a half mile.", + "verbal_pre_transition_instruction": "Turn left to stay on Alaskan Way South.", + "verbal_succinct_transition_instruction": "Turn left.", + "verbal_transition_alert_instruction": "Turn left to stay on Alaskan Way South." + }, + { + "begin_shape_index": 92, + "cost": 125.293, + "end_shape_index": 101, + "instruction": "Turn right onto Marion Street.", + "length": 0.105, + "street_names": [ + "Marion Street" + ], + "time": 120.293, + "travel_mode": "pedestrian", + "travel_type": "foot", + "type": 10, + "verbal_post_transition_instruction": "Continue for 600 feet.", + "verbal_pre_transition_instruction": "Turn right onto Marion Street.", + "verbal_succinct_transition_instruction": "Turn right.", + "verbal_transition_alert_instruction": "Turn right onto Marion Street." + }, + { + "begin_shape_index": 101, + "cost": 589.53, + "end_shape_index": 134, + "instruction": "Turn left onto 1st Avenue.", + "length": 0.51, + "street_names": [ + "1st Avenue" + ], + "time": 584.53, + "travel_mode": "pedestrian", + "travel_type": "foot", + "type": 15, + "verbal_post_transition_instruction": "Continue for a half mile.", + "verbal_pre_transition_instruction": "Turn left onto 1st Avenue.", + "verbal_succinct_transition_instruction": "Turn left.", + "verbal_transition_alert_instruction": "Turn left onto 1st Avenue." + }, + { + "begin_shape_index": 134, + "cost": 162.765, + "end_shape_index": 148, + "instruction": "Turn right onto Stewart Street.", + "length": 0.133, + "street_names": [ + "Stewart Street" + ], + "time": 157.765, + "travel_mode": "pedestrian", + "travel_type": "foot", + "type": 10, + "verbal_post_transition_instruction": "Continue for 700 feet.", + "verbal_pre_transition_instruction": "Turn right onto Stewart Street.", + "verbal_succinct_transition_instruction": "Turn right.", + "verbal_transition_alert_instruction": "Turn right onto Stewart Street." + }, + { + "begin_shape_index": 148, + "cost": 161.823, + "end_shape_index": 161, + "instruction": "Keep left to stay on Stewart Street.", + "length": 0.139, + "street_names": [ + "Stewart Street" + ], + "time": 161.823, + "travel_mode": "pedestrian", + "travel_type": "foot", + "type": 24, + "verbal_post_transition_instruction": "Continue for 700 feet.", + "verbal_pre_transition_instruction": "Keep left to stay on Stewart Street.", + "verbal_transition_alert_instruction": "Keep left to stay on Stewart Street." + }, + { + "begin_shape_index": 161, + "cost": 57.941, + "end_shape_index": 166, + "instruction": "Turn left onto Westlake Avenue.", + "length": 0.046, + "street_names": [ + "Westlake Avenue" + ], + "time": 52.941, + "travel_mode": "pedestrian", + "travel_type": "foot", + "type": 15, + "verbal_post_transition_instruction": "Continue for 200 feet.", + "verbal_pre_transition_instruction": "Turn left onto Westlake Avenue.", + "verbal_succinct_transition_instruction": "Turn left.", + "verbal_transition_alert_instruction": "Turn left onto Westlake Avenue." + }, + { + "begin_shape_index": 166, + "cost": 108.588, + "end_shape_index": 176, + "instruction": "Keep straight to stay on Westlake Avenue.", + "length": 0.093, + "street_names": [ + "Westlake Avenue" + ], + "time": 108.588, + "travel_mode": "pedestrian", + "travel_type": "foot", + "type": 22, + "verbal_post_transition_instruction": "Continue for 500 feet.", + "verbal_pre_transition_instruction": "Keep straight to stay on Westlake Avenue.", + "verbal_transition_alert_instruction": "Keep straight to stay on Westlake Avenue." + }, + { + "begin_shape_index": 176, + "cost": 312.235, + "end_shape_index": 193, + "instruction": "Keep left to take 7th Avenue.", + "length": 0.267, + "street_names": [ + "7th Avenue" + ], + "time": 307.235, + "travel_mode": "pedestrian", + "travel_type": "foot", + "type": 24, + "verbal_post_transition_instruction": "Continue for a quarter mile.", + "verbal_pre_transition_instruction": "Keep left to take 7th Avenue.", + "verbal_transition_alert_instruction": "Keep left to take 7th Avenue." + }, + { + "begin_shape_index": 193, + "cost": 1812.646, + "end_shape_index": 311, + "instruction": "Keep right to take Dexter Avenue.", + "length": 1.578, + "street_names": [ + "Dexter Avenue" + ], + "time": 1802.647, + "travel_mode": "pedestrian", + "travel_type": "foot", + "type": 23, + "verbal_post_transition_instruction": "Continue for 1.5 miles.", + "verbal_pre_transition_instruction": "Keep right to take Dexter Avenue.", + "verbal_transition_alert_instruction": "Keep right to take Dexter Avenue." + }, + { + "begin_shape_index": 311, + "cost": 15.588, + "end_shape_index": 315, + "instruction": "Turn left onto 6th Avenue North.", + "length": 0.009, + "street_names": [ + "6th Avenue North" + ], + "time": 10.588, + "travel_mode": "pedestrian", + "travel_type": "foot", + "type": 15, + "verbal_multi_cue": true, + "verbal_post_transition_instruction": "Continue for 50 feet.", + "verbal_pre_transition_instruction": "Turn left onto 6th Avenue North. Then, in 50 feet, Turn right to stay on 6th Avenue North.", + "verbal_succinct_transition_instruction": "Turn left. Then, in 50 feet, Turn right to stay on 6th Avenue North.", + "verbal_transition_alert_instruction": "Turn left onto 6th Avenue North." + }, + { + "begin_shape_index": 315, + "cost": 161.646, + "end_shape_index": 329, + "instruction": "Turn right to stay on 6th Avenue North.", + "length": 0.142, + "street_names": [ + "6th Avenue North" + ], + "time": 161.646, + "travel_mode": "pedestrian", + "travel_type": "foot", + "type": 10, + "verbal_post_transition_instruction": "Continue for 800 feet.", + "verbal_pre_transition_instruction": "Turn right to stay on 6th Avenue North.", + "verbal_succinct_transition_instruction": "Turn right.", + "verbal_transition_alert_instruction": "Turn right to stay on 6th Avenue North." + }, + { + "begin_shape_index": 329, + "cost": 27.588, + "end_shape_index": 330, + "instruction": "Turn left onto Halladay Street.", + "length": 0.019, + "street_names": [ + "Halladay Street" + ], + "time": 22.588, + "travel_mode": "pedestrian", + "travel_type": "foot", + "type": 15, + "verbal_post_transition_instruction": "Continue for 100 feet.", + "verbal_pre_transition_instruction": "Turn left onto Halladay Street.", + "verbal_succinct_transition_instruction": "Turn left.", + "verbal_transition_alert_instruction": "Turn left onto Halladay Street." + }, + { + "begin_shape_index": 330, + "cost": 19.058, + "end_shape_index": 332, + "instruction": "Keep right at the fork.", + "length": 0.016, + "time": 19.058, + "travel_mode": "pedestrian", + "travel_type": "foot", + "type": 23, + "verbal_post_transition_instruction": "Continue for 90 feet.", + "verbal_pre_transition_instruction": "Keep right at the fork.", + "verbal_transition_alert_instruction": "Keep right at the fork." + }, + { + "begin_shape_index": 332, + "cost": 4.235, + "end_shape_index": 334, + "instruction": "Turn right onto the walkway.", + "length": 0.003, + "time": 4.235, + "travel_mode": "pedestrian", + "travel_type": "foot", + "type": 10, + "verbal_multi_cue": true, + "verbal_post_transition_instruction": "Continue for 20 feet.", + "verbal_pre_transition_instruction": "Turn right onto the walkway. Then Turn left onto the walkway.", + "verbal_succinct_transition_instruction": "Turn right. Then Turn left onto the walkway.", + "verbal_transition_alert_instruction": "Turn right onto the walkway." + }, + { + "begin_shape_index": 334, + "cost": 58.882, + "end_shape_index": 340, + "instruction": "Turn left onto the walkway.", + "length": 0.05, + "time": 58.882, + "travel_mode": "pedestrian", + "travel_type": "foot", + "type": 15, + "verbal_post_transition_instruction": "Continue for 300 feet.", + "verbal_pre_transition_instruction": "Turn left onto the walkway.", + "verbal_succinct_transition_instruction": "Turn left.", + "verbal_transition_alert_instruction": "Turn left onto the walkway." + }, + { + "begin_shape_index": 340, + "cost": 624.581, + "end_shape_index": 349, + "instruction": "Keep left to take the walkway.", + "length": 0.549, + "time": 624.581, + "travel_mode": "pedestrian", + "travel_type": "foot", + "type": 24, + "verbal_post_transition_instruction": "Continue for a half mile.", + "verbal_pre_transition_instruction": "Keep left to take the walkway.", + "verbal_transition_alert_instruction": "Keep left to take the walkway." + }, + { + "begin_shape_index": 349, + "cost": 0.0, + "end_shape_index": 349, + "instruction": "You have arrived at your destination.", + "length": 0.0, + "time": 0.0, + "travel_mode": "pedestrian", + "travel_type": "foot", + "type": 4, + "verbal_pre_transition_instruction": "You have arrived at your destination.", + "verbal_transition_alert_instruction": "You will arrive at your destination." } ], + "shape": "wpxvyA~w~ihFxIDAhDEnQCnA]nC_BdJ]Va@hASvCorD_@k~DnAkNDoxFAke@a@iKq@cG}AuEaCqG}EwMcQiI{K_CmDcAkAqNgPoYcVqIeFaGiD}IaEwcAi_@yLuDaIeCcD_AeYaIyKa@aEMyOmDmSyH}t@u[cEiCuFkFkDmDuAyAaSoJcNuD}OcAwVMoU?klBCmEUeDqA_C}Bg@{AAuDqHg@aAG_Uf@_YhBqMx@_I`@cM`@oAAiJCiIVgCDqYf@oJJeFP}FFwYPaKTgDDuCByGIyJQeNGeE\\_@DmD`@gCp@cATaB^mDfA_DfAwD`B_MnHcJYmCbB}BtAq_@zYyFrFyBlBw_@~\\cFnEsBsG{Mmb@eBmFcBqFqF}PkAoDaBcFmHyToBaG_CtByJvIoUjS}CnCmC~Bid@z`@{ApAaBxAud@da@YTaBvAwChCwc@j`@iB`BkCbCsDjDeUxR{X|UcCrBqC`Cyg@zc@qIpHOLyEfEoC~B{CnCkPxNaGbFyU~RwCbCcCvBmSrQuCfCwB{CeCoDoKgOEIeMsQoDcFmBmCsAoBwNqSwAoBsK_OcDqEwD{EgAkBiJqL{F}H{GsJwB_DwAsBmCyDaOwSkNeS}@oA}CuE{@oAqHoKcE}FuEf@iEd@yTrB{BRqBRwGf@oE`@kKfAuANqMzAeCXkD`@gUrC_BRcCZc@x@{@dB_CtE{@dBiTtb@oBxDkCjF_HfNaJvQiSja@iBrDwBhEkh@vdA{AzC_AnBg@bAiYll@iAzBuAlB_BxAeBdAkBn@oBXoBBcA?mMCeEAcDAeQGqQGmIEqFAwFE_E?wCCuWIme@KsCAyDA{b@EsIA{@?yDAsFA{DA}C?}NBwO?g\\?yC@wDAiL@sF?aUByD?Y?eH@sCAqD?}FDiC?kA?oOQ_CAaCAeGAuEA_BAmD?wCCyi@YgDCuEA{XImQGsCAqCA_H?sAAaj@Iwz@MeVCiSAeBAiD?qe@MeEAaFA}WI_OEiA?qAAoe@M_LEkA?_C?uC?aEAiF?oLA}EA{B?{D@iH@uE@mb@GeECg`AMqFAy@?_QEoC@oCPgANgANmCl@mC~@iClAeD|AkFhCeGvCaUtKs@\\sHtDgb@lSyL`G{CvAcErBsQ~IgKdFkN~Ec`AhTcB^ib@lJwMvCq@|DUvASpAGZ}CzAyLrCsQhEeE`AuCx@qCjAmCzAiClBuLtI}AxAwA`BuAfBqAnBgX`c@`@vYeDbR]n@_B[O[o@bAy@v@{PbE}B^{Cd@eMlD{@z@eATuCToQtA}ZfAuPRel@Z}_@PexIC", "summary": { + "cost": 6553.443, + "has_ferry": false, + "has_highway": false, "has_time_restrictions": false, "has_toll": false, - "has_highway": false, - "has_ferry": false, - "min_lat": 47.57566, - "min_lon": -122.347201, + "length": 5.684, "max_lat": 47.651047, - "max_lon": -122.316908, - "time": 7906.326, - "length": 11.117, - "cost": 7976.325 - }, - "status_message": "Found route between points", - "status": 0, - "units": "kilometers", - "language": "en-US" + "max_lon": -122.335618, + "min_lat": 47.575663, + "min_lon": -122.347201, + "time": 6488.443 + } } - } - ] + ], + "locations": [ + { + "lat": 47.575837, + "lon": -122.339414, + "original_index": 0, + "side_of_street": "right", + "type": "break" + }, + { + "lat": 47.651048, + "lon": -122.347234, + "original_index": 1, + "type": "break" + } + ], + "status": 0, + "status_message": "Found route between points", + "summary": { + "cost": 6553.443, + "has_ferry": false, + "has_highway": false, + "has_time_restrictions": false, + "has_toll": false, + "length": 5.684, + "max_lat": 47.651047, + "max_lon": -122.335618, + "min_lat": 47.575663, + "min_lon": -122.347201, + "time": 6488.443 + }, + "units": "miles" + } } From a5d7de9e6d87e0f0cd05a39df3538d3e8a7efba8 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Fri, 17 May 2024 13:16:51 -0700 Subject: [PATCH 10/24] OTP directions test passing --- services/travelmux/src/api/v5/directions.rs | 28 ++++++++++----------- services/travelmux/src/api/v5/plan.rs | 17 ++++++------- services/travelmux/src/otp/otp_api.rs | 6 +++++ 3 files changed, 27 insertions(+), 24 deletions(-) diff --git a/services/travelmux/src/api/v5/directions.rs b/services/travelmux/src/api/v5/directions.rs index 277f72ce4..b52368619 100644 --- a/services/travelmux/src/api/v5/directions.rs +++ b/services/travelmux/src/api/v5/directions.rs @@ -71,41 +71,41 @@ mod tests { let first_route = &directions_response.routes[0]; // distance is always in meters for OSRM responses - assert_relative_eq!(first_route.distance, 9148.0); - assert_relative_eq!(first_route.duration, 6488.443); + assert_relative_eq!(first_route.distance, 9165.05); + assert_relative_eq!(first_route.duration, 7505.0); assert_relative_eq!( first_route.geometry.0[0], - geo::coord!(x: -122.339216, y: 47.575836) + geo::coord!(x: -122.33922, y: 47.57583) ); assert_relative_eq!( first_route.geometry.0.last().unwrap(), - &geo::coord!(x: -122.347199, y: 47.651048) + &geo::coord!(x:-122.3472, y: 47.65104) ); let legs = &first_route.legs; assert_eq!(legs.len(), 1); let first_leg = &legs[0]; - assert_eq!(first_leg.distance, 9148.0); - assert_eq!(first_leg.duration, 6488.443); + assert_eq!(first_leg.distance, 9165.05); + assert_eq!(first_leg.duration, 7505.0); assert_eq!( first_leg.summary, - "Dexter Avenue, East Marginal Way South, Alaskan Way South" + "Aurora Avenue North, East Marginal Way South", ); - assert_eq!(first_leg.steps.len(), 21); + assert_eq!(first_leg.steps.len(), 23); let first_step = &first_leg.steps[0]; - assert_eq!(first_step.distance, 19.0); - assert_eq!(first_step.duration, 13.567); + assert_eq!(first_step.distance, 19.15); + assert_eq!(first_step.duration, 15.681392900202399); assert_eq!(first_step.name, "East Marginal Way South"); assert_eq!(first_step.mode, TravelMode::Walk); let banner_instructions = first_step.banner_instructions.as_ref().unwrap(); assert_eq!(banner_instructions.len(), 1); let banner_instruction = &banner_instructions[0]; - assert_relative_eq!(banner_instruction.distance_along_geometry, 19.0); + assert_relative_eq!(banner_instruction.distance_along_geometry, 19.15); let primary = &banner_instruction.primary; - assert_eq!(primary.text, "Turn right onto the walkway."); + assert_eq!(primary.text, "Turn right onto path."); let Some(osrm_api::BannerComponent::Text(first_component)) = &primary.components.first() else { @@ -116,13 +116,13 @@ mod tests { }; assert_eq!( first_component.text, - Some("Turn right onto the walkway.".to_string()) + Some("Turn right onto path.".to_string()) ); let step_maneuver = &first_step.maneuver; assert_eq!( step_maneuver.location, - geo::point!(x: -122.339216, y: 47.575836) + geo::point!(x: -122.3392181, y: 47.5758346) ); // assert_eq!(step_maneuver.r#type, "my type"); // assert_eq!(step_maneuver.modifier, "my modifier"); diff --git a/services/travelmux/src/api/v5/plan.rs b/services/travelmux/src/api/v5/plan.rs index c969da157..0118ada45 100644 --- a/services/travelmux/src/api/v5/plan.rs +++ b/services/travelmux/src/api/v5/plan.rs @@ -367,14 +367,9 @@ impl Maneuver { } } - fn from_otp( - otp: otp_api::Step, - mode: otp_api::TransitMode, - distance_unit: DistanceUnit, - // leg_geometry: &LineString, - ) -> Self { + fn from_otp(otp: otp_api::Step, leg: &otp_api::Leg, distance_unit: DistanceUnit) -> Self { let instruction = build_instruction( - mode, + leg.mode, otp.relative_direction, otp.absolute_direction, &otp.street_name, @@ -389,6 +384,8 @@ impl Maneuver { Some(vec![otp.street_name]) }; + let duration_seconds = otp.distance / leg.distance * leg.duration_seconds(); + log::error!("TODO: synthesize geometry for OTP steps"); let geometry = LineString::new(vec![]); Self { @@ -397,7 +394,7 @@ impl Maneuver { street_names, verbal_post_transition_instruction, distance: convert_from_meters(otp.distance, distance_unit), - duration_seconds: 666.0, // TODO: OTP doesn't provide this at a granular level - only at the Leg level + duration_seconds, start_point: Point::new(otp.lon, otp.lat).into(), geometry, } @@ -505,7 +502,7 @@ impl Leg { .steps .iter() .cloned() - .map(|otp_step| Maneuver::from_otp(otp_step, otp.mode, distance_unit)) + .map(|otp_step| Maneuver::from_otp(otp_step, otp, distance_unit)) .collect(); // OTP doesn't include an arrival step like valhalla, so we synthesize one @@ -539,7 +536,7 @@ impl Leg { geometry, mode: otp.mode.into(), distance: convert_from_meters(otp.distance, distance_unit), - duration_seconds: (otp.end_time - otp.start_time) as f64 / 1000.0, + duration_seconds: otp.duration_seconds(), mode_leg, }) } diff --git a/services/travelmux/src/otp/otp_api.rs b/services/travelmux/src/otp/otp_api.rs index 331baaefd..ab5d0bf50 100644 --- a/services/travelmux/src/otp/otp_api.rs +++ b/services/travelmux/src/otp/otp_api.rs @@ -87,6 +87,12 @@ pub struct Leg { pub extra: HashMap, } +impl Leg { + pub(crate) fn duration_seconds(&self) -> f64 { + (self.end_time - self.start_time) as f64 / 1000.0 + } +} + #[derive(Debug, Deserialize, Serialize, PartialEq, Clone)] #[serde(rename_all = "camelCase")] pub struct Step { From bf70fae397c4a5a74826d71fc564491ff0dd6c38 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Fri, 17 May 2024 15:59:08 -0700 Subject: [PATCH 11/24] more maneuvers --- services/travelmux/src/api/v5/osrm_api.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/services/travelmux/src/api/v5/osrm_api.rs b/services/travelmux/src/api/v5/osrm_api.rs index 9c545a89f..96e1e0b5b 100644 --- a/services/travelmux/src/api/v5/osrm_api.rs +++ b/services/travelmux/src/api/v5/osrm_api.rs @@ -238,8 +238,10 @@ impl BannerInstruction { ManeuverType::StayLeft => (Fork, Some(Left)), /* ManeuverType::Merge => {} - ManeuverType::RoundaboutEnter => {} - ManeuverType::RoundaboutExit => {} + */ + ManeuverType::RoundaboutEnter => (RoundaboutEnter, None), // Enter/Exit? + ManeuverType::RoundaboutExit => (RoundaboutExit, None), // Enter/Exit? + /* ManeuverType::FerryEnter => {} ManeuverType::FerryExit => {} ManeuverType::Transit => {} @@ -257,7 +259,7 @@ impl BannerInstruction { ManeuverType::BuildingEnter => {} ManeuverType::BuildingExit => {} */ - _ => todo!("implement manuever type: {:?}", maneuver.r#type), + other => todo!("implement maneuver type: {other:?}"), }; Some(BannerManeuver { r#type: banner_type, @@ -327,7 +329,10 @@ pub enum BannerManeuverType { OnRamp, #[serde(rename = "off ramp")] OffRamp, - RoundAbout, + #[serde(rename = "roundabout")] + RoundaboutEnter, + #[serde(rename = "exit roundabout")] + RoundaboutExit, } #[derive(Debug, Serialize, PartialEq, Clone)] From aa3d5104eab78cf224894973b446e6d65628ab9e Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Fri, 17 May 2024 15:59:30 -0700 Subject: [PATCH 12/24] Per step geometry --- .../src/api/v5/haversine_segmenter.rs | 104 ++++++++++++++++++ services/travelmux/src/api/v5/mod.rs | 1 + services/travelmux/src/api/v5/osrm_api.rs | 2 +- services/travelmux/src/api/v5/plan.rs | 21 +++- 4 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 services/travelmux/src/api/v5/haversine_segmenter.rs diff --git a/services/travelmux/src/api/v5/haversine_segmenter.rs b/services/travelmux/src/api/v5/haversine_segmenter.rs new file mode 100644 index 000000000..d66d99fc2 --- /dev/null +++ b/services/travelmux/src/api/v5/haversine_segmenter.rs @@ -0,0 +1,104 @@ +use geo::{ + algorithm::{HaversineDistance, HaversineIntermediate}, + geometry::{Coord, LineString, Point}, +}; + +pub struct HaversineSegmenter { + geometry: LineString, + next_index: usize, +} + +impl HaversineSegmenter { + pub fn new(geometry: LineString) -> Self { + Self { + geometry, + next_index: 0, + } + } + pub fn next_segment(&mut self, distance_meters: f64) -> Option { + // REVIEW: Handle case with linestring of 1 point? + if self.next_index == self.geometry.0.len() - 1 { + return None; + } + let mut distance_remaining = distance_meters; + let mut start = self.geometry.0[self.next_index]; + let mut output = vec![start]; + while self.next_index < self.geometry.0.len() - 1 { + let end = self.geometry.0[self.next_index + 1]; + let segment_length = Point::from(start).haversine_distance(&Point::from(end)); + if segment_length > distance_remaining { + // take whatever portion of the segment we can fit + let ratio = distance_remaining / segment_length; + let intermediate = + Point::from(start).haversine_intermediate(&Point::from(end), ratio); + output.push(Coord::from(intermediate)); + if self.geometry.0[self.next_index] == Coord::from(intermediate) { + debug_assert!( + false, + "intermediate point is the same as the start point - inifinite loop?" + ); + // skip a point rather than risk infinite loop + self.next_index += 1; + } + // overwrite the last point with the intermediate value + self.geometry.0[self.next_index] = Coord::from(intermediate); + break; + } + + output.push(end); + distance_remaining -= segment_length; + start = end; + self.next_index += 1; + } + Some(LineString::new(output)) + } +} + +#[cfg(test)] +mod test { + use super::*; + use approx::assert_relative_eq; + use geo::{point, wkt, HaversineDestination}; + + #[test] + fn test_segmenter() { + // paris to berlin (878km) to prague + let paris = point!(x: 2.3514, y: 48.8575); + let berlin = point!(x: 13.4050, y: 52.5200); + let prague = point!(x: 14.4378, y: 50.0755); + + let paris_to_berlin_distance = LineString::new(vec![paris.0, berlin.0]).haversine_length(); + assert_relative_eq!(paris_to_berlin_distance, 877461.0, epsilon = 1.0); + + let line_string = LineString::new(vec![paris.0, berlin.0, prague.0]); + let total_distance = line_string.haversine_length(); + assert_relative_eq!(total_distance, 1_158_595.0, epsilon = 1.0); + + let mut segmenter = HaversineSegmenter::new(line_string); + + let east_of_paris = point!(x: 2.467660089582291, y: 48.90485360250366); + let segment_1 = segmenter.next_segment(10_000.0).unwrap(); + assert_relative_eq!(segment_1.haversine_length(), 10_000.0, epsilon = 1e-9); + assert_relative_eq!(segment_1, LineString::new(vec![paris.0, east_of_paris.0])); + + // next one should pick up where the last one left off + let segment_2 = segmenter.next_segment(10_000.0).unwrap(); + assert_eq!(segment_1.0.last(), segment_2.0.first()); + + let east_of_berlin = point!(x: 13.482210264987538, y: 52.34640526357316); + let segment_3 = segmenter.next_segment(paris_to_berlin_distance).unwrap(); + let expected = LineString::new(vec![ + *segment_2.0.last().unwrap(), + berlin.0, + east_of_berlin.0, + ]); + assert_relative_eq!(segment_3, expected); + + // overshoot it + let next = segmenter.next_segment(total_distance).unwrap(); + assert_relative_eq!(next, LineString::new(vec![east_of_berlin.0, prague.0])); + + let next = segmenter.next_segment(4.0); + assert!(next.is_none()); + } +} diff --git a/services/travelmux/src/api/v5/mod.rs b/services/travelmux/src/api/v5/mod.rs index 45cd1f61b..9496d312e 100644 --- a/services/travelmux/src/api/v5/mod.rs +++ b/services/travelmux/src/api/v5/mod.rs @@ -1,5 +1,6 @@ pub mod directions; mod error; +mod haversine_segmenter; mod osrm_api; pub mod plan; mod travel_modes; diff --git a/services/travelmux/src/api/v5/osrm_api.rs b/services/travelmux/src/api/v5/osrm_api.rs index 96e1e0b5b..ca10d0cd3 100644 --- a/services/travelmux/src/api/v5/osrm_api.rs +++ b/services/travelmux/src/api/v5/osrm_api.rs @@ -240,7 +240,7 @@ impl BannerInstruction { ManeuverType::Merge => {} */ ManeuverType::RoundaboutEnter => (RoundaboutEnter, None), // Enter/Exit? - ManeuverType::RoundaboutExit => (RoundaboutExit, None), // Enter/Exit? + ManeuverType::RoundaboutExit => (RoundaboutExit, None), // Enter/Exit? /* ManeuverType::FerryEnter => {} ManeuverType::FerryExit => {} diff --git a/services/travelmux/src/api/v5/plan.rs b/services/travelmux/src/api/v5/plan.rs index 0118ada45..50ece2e60 100644 --- a/services/travelmux/src/api/v5/plan.rs +++ b/services/travelmux/src/api/v5/plan.rs @@ -10,6 +10,7 @@ use std::time::{Duration, SystemTime}; use super::error::{PlanResponseErr, PlanResponseOk}; use super::TravelModes; +use crate::api::v5::haversine_segmenter::HaversineSegmenter; use crate::api::AppState; use crate::error::ErrorType; use crate::otp::otp_api; @@ -367,7 +368,12 @@ impl Maneuver { } } - fn from_otp(otp: otp_api::Step, leg: &otp_api::Leg, distance_unit: DistanceUnit) -> Self { + fn from_otp( + otp: otp_api::Step, + geometry: LineString, + leg: &otp_api::Leg, + distance_unit: DistanceUnit, + ) -> Self { let instruction = build_instruction( leg.mode, otp.relative_direction, @@ -385,9 +391,6 @@ impl Maneuver { }; let duration_seconds = otp.distance / leg.distance * leg.duration_seconds(); - - log::error!("TODO: synthesize geometry for OTP steps"); - let geometry = LineString::new(vec![]); Self { instruction, r#type: otp.relative_direction.into(), @@ -494,6 +497,8 @@ impl Leg { let from_place: Place = (&otp.from).into(); let to_place: Place = (&otp.to).into(); + let mut distance_so_far = 0.0; + let mut segmenter = HaversineSegmenter::new(geometry.clone()); let mode_leg = match otp.mode { otp_api::TransitMode::Walk | otp_api::TransitMode::Bicycle @@ -502,7 +507,13 @@ impl Leg { .steps .iter() .cloned() - .map(|otp_step| Maneuver::from_otp(otp_step, otp, distance_unit)) + .map(|otp_step| { + // compute step geometry by distance along leg geometry + let step_geometry = + segmenter.next_segment(otp_step.distance).expect("TODO"); + distance_so_far += otp_step.distance; + Maneuver::from_otp(otp_step, step_geometry, otp, distance_unit) + }) .collect(); // OTP doesn't include an arrival step like valhalla, so we synthesize one From da71c865f126641a06763c23a423e31b3a33b967 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Fri, 17 May 2024 18:11:00 -0700 Subject: [PATCH 13/24] revert api/v5 and put new changes in api/v6 --- services/travelmux/src/api/mod.rs | 1 + services/travelmux/src/api/v5/error.rs | 4 +- services/travelmux/src/api/v5/mod.rs | 6 - services/travelmux/src/api/v5/plan.rs | 219 ++-- .../src/api/{v5 => v6}/directions.rs | 6 +- services/travelmux/src/api/v6/error.rs | 208 +++ services/travelmux/src/api/v6/mod.rs | 9 + .../travelmux/src/api/{v5 => v6}/osrm_api.rs | 0 services/travelmux/src/api/v6/plan.rs | 1110 +++++++++++++++++ .../src/api/{v5 => v6}/travel_modes.rs | 0 .../src/bin/travelmux-server/main.rs | 3 +- .../{api/v5 => util}/haversine_segmenter.rs | 4 +- services/travelmux/src/util/mod.rs | 1 + 13 files changed, 1427 insertions(+), 144 deletions(-) rename services/travelmux/src/api/{v5 => v6}/directions.rs (98%) create mode 100644 services/travelmux/src/api/v6/error.rs create mode 100644 services/travelmux/src/api/v6/mod.rs rename services/travelmux/src/api/{v5 => v6}/osrm_api.rs (100%) create mode 100644 services/travelmux/src/api/v6/plan.rs rename services/travelmux/src/api/{v5 => v6}/travel_modes.rs (100%) rename services/travelmux/src/{api/v5 => util}/haversine_segmenter.rs (97%) diff --git a/services/travelmux/src/api/mod.rs b/services/travelmux/src/api/mod.rs index 1d6fdf9f8..d9d9c1a38 100644 --- a/services/travelmux/src/api/mod.rs +++ b/services/travelmux/src/api/mod.rs @@ -4,3 +4,4 @@ pub use app_state::AppState; pub mod health; pub mod v4; pub mod v5; +pub mod v6; diff --git a/services/travelmux/src/api/v5/error.rs b/services/travelmux/src/api/v5/error.rs index 8e33623e8..d300f1af4 100644 --- a/services/travelmux/src/api/v5/error.rs +++ b/services/travelmux/src/api/v5/error.rs @@ -2,7 +2,7 @@ use crate::otp::otp_api; use crate::valhalla::valhalla_api; use crate::{DistanceUnit, Error, TravelMode}; use actix_web::HttpResponseBuilder; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use super::{Itinerary, Plan}; use crate::error::ErrorType; @@ -32,7 +32,7 @@ pub struct PlanError { pub message: String, } -#[derive(Debug, Serialize, Clone, PartialEq)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub struct PlanResponseOk { pub(crate) plan: Plan, diff --git a/services/travelmux/src/api/v5/mod.rs b/services/travelmux/src/api/v5/mod.rs index 9496d312e..20c8b18b4 100644 --- a/services/travelmux/src/api/v5/mod.rs +++ b/services/travelmux/src/api/v5/mod.rs @@ -1,10 +1,4 @@ -pub mod directions; mod error; -mod haversine_segmenter; -mod osrm_api; pub mod plan; -mod travel_modes; - -pub use travel_modes::TravelModes; pub use plan::{Itinerary, Plan}; diff --git a/services/travelmux/src/api/v5/plan.rs b/services/travelmux/src/api/v5/plan.rs index 50ece2e60..80746b1f3 100644 --- a/services/travelmux/src/api/v5/plan.rs +++ b/services/travelmux/src/api/v5/plan.rs @@ -3,44 +3,26 @@ use geo::algorithm::BoundingRect; use geo::geometry::{LineString, Point, Rect}; use polyline::decode_polyline; use reqwest::header::{HeaderName, HeaderValue}; -use serde::{Deserialize, Serialize}; +use serde::{de, de::IntoDeserializer, de::Visitor, Deserialize, Deserializer, Serialize}; use std::collections::HashMap; +use std::fmt; use std::time::{Duration, SystemTime}; use super::error::{PlanResponseErr, PlanResponseOk}; -use super::TravelModes; - -use crate::api::v5::haversine_segmenter::HaversineSegmenter; use crate::api::AppState; use crate::error::ErrorType; use crate::otp::otp_api; use crate::otp::otp_api::{AbsoluteDirection, RelativeDirection}; use crate::util::format::format_meters; use crate::util::{ - deserialize_point_from_lat_lon, extend_bounds, serialize_line_string_as_polyline6, - serialize_rect_to_lng_lat, serialize_system_time_as_millis, system_time_from_millis, + deserialize_point_from_lat_lon, extend_bounds, serialize_rect_to_lng_lat, + serialize_system_time_as_millis, system_time_from_millis, }; use crate::valhalla::valhalla_api; use crate::valhalla::valhalla_api::{LonLat, ManeuverType}; use crate::{DistanceUnit, Error, TravelMode}; -const METERS_PER_MILE: f64 = 1609.34; - -fn convert_from_meters(meters: f64, output_units: DistanceUnit) -> f64 { - match output_units { - DistanceUnit::Kilometers => meters / 1000.0, - DistanceUnit::Miles => meters / METERS_PER_MILE, - } -} - -fn convert_to_meters(distance: f64, input_units: DistanceUnit) -> f64 { - match input_units { - DistanceUnit::Kilometers => distance * 1000.0, - DistanceUnit::Miles => distance * METERS_PER_MILE, - } -} - -#[derive(Debug, Deserialize, Clone, PartialEq)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub struct PlanQuery { #[serde(deserialize_with = "deserialize_point_from_lat_lon")] @@ -58,51 +40,32 @@ pub struct PlanQuery { preferred_distance_units: Option, } -#[derive(Debug, Serialize, Clone, PartialEq)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub struct Plan { pub(crate) itineraries: Vec, } -#[derive(Debug, Serialize, Clone, PartialEq)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub struct Itinerary { mode: TravelMode, /// seconds - pub(crate) duration: f64, + duration: f64, /// unix millis, UTC #[serde(serialize_with = "serialize_system_time_as_millis")] start_time: SystemTime, /// unix millis, UTC #[serde(serialize_with = "serialize_system_time_as_millis")] end_time: SystemTime, - /// Units are in `distance_units` distance: f64, - /// FIXME: I think we're returning meters even though distance unit is "Kilometers" - /// Probably we should rename DistanceUnit::Kilometers to DistanceUnit::Meters - /// This is passed as a parameter though, so it'd be a breaking change. - pub(crate) distance_units: DistanceUnit, + distance_units: DistanceUnit, #[serde(serialize_with = "serialize_rect_to_lng_lat")] bounds: Rect, - pub(crate) legs: Vec, + legs: Vec, } impl Itinerary { - pub fn distance_meters(&self) -> f64 { - match self.distance_units { - DistanceUnit::Kilometers => self.distance * 1000.0, - DistanceUnit::Miles => self.distance * METERS_PER_MILE, - } - } - - pub fn combined_geometry(&self) -> LineString { - let mut combined_geometry = LineString::new(vec![]); - for leg in &self.legs { - combined_geometry.0.extend(&leg.geometry.0); - } - combined_geometry - } - pub fn from_valhalla(valhalla: &valhalla_api::Trip, mode: TravelMode) -> Self { let bounds = Rect::new( geo::coord!(x: valhalla.summary.min_lon, y: valhalla.summary.min_lat), @@ -173,11 +136,11 @@ impl Itinerary { let Some(first_leg) = legs_iter.next() else { return Err(Error::server("itinerary had no legs")); }; - let Some(mut itinerary_bounds) = first_leg.bounding_rect() else { + let Ok(Some(mut itinerary_bounds)) = first_leg.bounding_rect() else { return Err(Error::server("first leg has no bounding_rect")); }; for leg in legs_iter { - let Some(leg_bounds) = leg.bounding_rect() else { + let Ok(Some(leg_bounds)) = leg.bounding_rect() else { return Err(Error::server("leg has no bounding_rect")); }; extend_bounds(&mut itinerary_bounds, &leg_bounds); @@ -188,8 +151,8 @@ impl Itinerary { start_time: system_time_from_millis(itinerary.start_time), end_time: system_time_from_millis(itinerary.end_time), mode, - distance: convert_from_meters(distance_meters, distance_unit), - distance_units: distance_unit, + distance: distance_meters / 1000.0, + distance_units: DistanceUnit::Kilometers, bounds: itinerary_bounds, legs, }) @@ -222,18 +185,17 @@ impl From for Place { } } -#[derive(Debug, Serialize, Clone, PartialEq)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] -pub(crate) struct Leg { +struct Leg { /// encoded polyline. 1e-6 scale, (lat, lon) - #[serde(serialize_with = "serialize_line_string_as_polyline6")] - geometry: LineString, + geometry: String, /// Which mode is this leg of the journey? - pub(crate) mode: TravelMode, + mode: TravelMode, #[serde(flatten)] - pub(crate) mode_leg: ModeLeg, + mode_leg: ModeLeg, /// Beginning of the leg from_place: Place, @@ -252,20 +214,14 @@ pub(crate) struct Leg { /// Start time of the leg #[serde(serialize_with = "serialize_system_time_as_millis")] end_time: SystemTime, - - /// Length of this leg, in units of the `distance_unit` of the Itinerary - distance: f64, - - /// Duration of this leg - pub(crate) duration_seconds: f64, } // Should we just pass the entire OTP leg? type TransitLeg = otp_api::Leg; -#[derive(Debug, Serialize, Clone, PartialEq)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] -pub(crate) enum ModeLeg { +enum ModeLeg { // REVIEW: rename? There is a boolean field for OTP called TransitLeg #[serde(rename = "transitLeg")] Transit(Box), @@ -274,13 +230,13 @@ pub(crate) enum ModeLeg { NonTransit(Box), } -#[derive(Debug, Serialize, Clone, PartialEq)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] -pub struct NonTransitLeg { - pub(crate) maneuvers: Vec, +struct NonTransitLeg { + maneuvers: Vec, /// The substantial road names along the route - pub(crate) substantial_street_names: Vec, + substantial_street_names: Vec, } impl NonTransitLeg { @@ -326,21 +282,18 @@ impl NonTransitLeg { // Eventually we might want to coalesce this into something not valhalla specific // but for now we only use it for valhalla trips -#[derive(Debug, Serialize, Clone, PartialEq)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub struct Maneuver { pub instruction: Option, // pub cost: f64, // pub begin_shape_index: u64, // pub end_shape_index: u64, - #[serde(skip_serializing)] - pub geometry: LineString, // pub highway: Option, /// In units of the `distance_unit` of the trip leg pub distance: f64, pub street_names: Option>, - #[serde(skip_serializing)] - pub duration_seconds: f64, + // pub time: f64, // pub travel_mode: String, // pub travel_type: String, pub r#type: ManeuverType, @@ -352,30 +305,23 @@ pub struct Maneuver { impl Maneuver { fn from_valhalla(valhalla: valhalla_api::Maneuver, leg_geometry: &LineString) -> Self { - let coords = leg_geometry.0 - [valhalla.begin_shape_index as usize..=valhalla.end_shape_index as usize] - .to_owned(); - let geometry = LineString::from(coords); Self { instruction: Some(valhalla.instruction), street_names: valhalla.street_names, - duration_seconds: valhalla.time, r#type: valhalla.r#type, start_point: Point(leg_geometry[valhalla.begin_shape_index as usize]).into(), verbal_post_transition_instruction: valhalla.verbal_post_transition_instruction, distance: valhalla.length, - geometry, } } fn from_otp( otp: otp_api::Step, - geometry: LineString, - leg: &otp_api::Leg, + mode: otp_api::TransitMode, distance_unit: DistanceUnit, ) -> Self { let instruction = build_instruction( - leg.mode, + mode, otp.relative_direction, otp.absolute_direction, &otp.street_name, @@ -389,23 +335,20 @@ impl Maneuver { } else { Some(vec![otp.street_name]) }; - - let duration_seconds = otp.distance / leg.distance * leg.duration_seconds(); + let localized_distance = match distance_unit { + DistanceUnit::Kilometers => otp.distance, + // round to the nearest ten-thousandth + DistanceUnit::Miles => (otp.distance * 0.621371 * 10_000.0).round() / 10_000.0, + }; Self { instruction, r#type: otp.relative_direction.into(), street_names, verbal_post_transition_instruction, - distance: convert_from_meters(otp.distance, distance_unit), - duration_seconds, + distance: localized_distance, start_point: Point::new(otp.lon, otp.lat).into(), - geometry, } } - - pub(crate) fn distance_meters(&self, distance_unit: DistanceUnit) -> f64 { - convert_to_meters(self.distance, distance_unit) - } } // We could do so much better. Look at Valhalla's Odin. @@ -479,12 +422,13 @@ impl Leg { const VALHALLA_GEOMETRY_PRECISION: u32 = 6; const OTP_GEOMETRY_PRECISION: u32 = 5; - fn bounding_rect(&self) -> Option { - self.geometry.bounding_rect() + fn decoded_geometry(&self) -> std::result::Result { + decode_polyline(&self.geometry, Self::GEOMETRY_PRECISION) } - pub(crate) fn distance_meters(&self, itinerary_units: DistanceUnit) -> f64 { - convert_to_meters(self.distance, itinerary_units) + fn bounding_rect(&self) -> std::result::Result, String> { + let line_string = self.decoded_geometry()?; + Ok(line_string.bounding_rect()) } fn from_otp( @@ -493,12 +437,11 @@ impl Leg { distance_unit: DistanceUnit, ) -> std::result::Result { debug_assert_ne!(Self::OTP_GEOMETRY_PRECISION, Self::GEOMETRY_PRECISION); - let geometry = decode_polyline(&otp.leg_geometry.points, Self::OTP_GEOMETRY_PRECISION)?; + let line = decode_polyline(&otp.leg_geometry.points, Self::OTP_GEOMETRY_PRECISION)?; + let geometry = polyline::encode_coordinates(line, Self::GEOMETRY_PRECISION)?; let from_place: Place = (&otp.from).into(); let to_place: Place = (&otp.to).into(); - let mut distance_so_far = 0.0; - let mut segmenter = HaversineSegmenter::new(geometry.clone()); let mode_leg = match otp.mode { otp_api::TransitMode::Walk | otp_api::TransitMode::Bicycle @@ -507,13 +450,7 @@ impl Leg { .steps .iter() .cloned() - .map(|otp_step| { - // compute step geometry by distance along leg geometry - let step_geometry = - segmenter.next_segment(otp_step.distance).expect("TODO"); - distance_so_far += otp_step.distance; - Maneuver::from_otp(otp_step, step_geometry, otp, distance_unit) - }) + .map(|otp_step| Maneuver::from_otp(otp_step, otp.mode, distance_unit)) .collect(); // OTP doesn't include an arrival step like valhalla, so we synthesize one @@ -522,11 +459,9 @@ impl Leg { instruction: Some("Arrive at your destination.".to_string()), distance: 0.0, street_names: None, - duration_seconds: 0.0, r#type: ManeuverType::Destination, verbal_post_transition_instruction: None, start_point: to_place.location, - geometry: LineString::new(vec![to_place.location.into()]), }; maneuvers.push(maneuver); } @@ -546,8 +481,6 @@ impl Leg { end_time: system_time_from_millis(otp.end_time), geometry, mode: otp.mode.into(), - distance: convert_from_meters(otp.distance, distance_unit), - duration_seconds: otp.duration_seconds(), mode_leg, }) } @@ -561,8 +494,7 @@ impl Leg { to_place: LonLat, ) -> Self { let geometry = - polyline::decode_polyline(&valhalla.shape, Self::VALHALLA_GEOMETRY_PRECISION) - .expect("valid polyline from valhalla"); + polyline::decode_polyline(&valhalla.shape, Self::VALHALLA_GEOMETRY_PRECISION).unwrap(); let maneuvers = valhalla .maneuvers .iter() @@ -570,20 +502,53 @@ impl Leg { .map(|valhalla_maneuver| Maneuver::from_valhalla(valhalla_maneuver, &geometry)) .collect(); let leg = NonTransitLeg::new(maneuvers); + debug_assert_eq!(Self::VALHALLA_GEOMETRY_PRECISION, Self::GEOMETRY_PRECISION); Self { start_time, end_time, from_place: from_place.into(), to_place: to_place.into(), - geometry, + geometry: valhalla.shape.clone(), mode: travel_mode, mode_leg: ModeLeg::NonTransit(Box::new(leg)), - distance: valhalla.summary.length, - duration_seconds: valhalla.summary.time, } } } +// Comma separated list of travel modes +#[derive(Debug, Serialize, PartialEq, Eq, Clone)] +struct TravelModes(Vec); + +impl<'de> Deserialize<'de> for TravelModes { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + struct CommaSeparatedVecVisitor; + + impl<'de> Visitor<'de> for CommaSeparatedVecVisitor { + type Value = TravelModes; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a comma-separated string") + } + + fn visit_str(self, value: &str) -> std::result::Result + where + E: de::Error, + { + let modes = value + .split(',') + .map(|s| TravelMode::deserialize(s.into_deserializer())) + .collect::>()?; + Ok(TravelModes(modes)) + } + } + + deserializer.deserialize_str(CommaSeparatedVecVisitor) + } +} + impl actix_web::Responder for PlanResponseOk { type Body = actix_web::body::BoxBody; @@ -600,15 +565,7 @@ pub async fn get_plan( req: HttpRequest, app_state: web::Data, ) -> std::result::Result { - _get_plan(query, req, app_state).await -} - -pub async fn _get_plan( - query: web::Query, - req: HttpRequest, - app_state: web::Data, -) -> std::result::Result { - let Some(primary_mode) = query.mode.first() else { + let Some(primary_mode) = query.mode.0.first() else { return Err(PlanResponseErr::from(Error::user("mode is required"))); }; @@ -621,7 +578,7 @@ 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"); + debug_assert!(query.mode.0.len() == 1, "valhalla only supports one mode"); let mode = match other { TravelMode::Transit => unreachable!("handled above"), @@ -796,8 +753,9 @@ mod tests { // legs assert_eq!(first_itinerary.legs.len(), 1); let first_leg = &first_itinerary.legs[0]; + let geometry = decode_polyline(&first_leg.geometry, 6).unwrap(); assert_relative_eq!( - first_leg.geometry.0[0], + geometry.0[0], geo::coord!(x: -122.33922, y: 47.57583), epsilon = 1e-4 ); @@ -835,14 +793,15 @@ mod tests { // itineraries let first_itinerary = &itineraries[0]; assert_eq!(first_itinerary.mode, TravelMode::Transit); - assert_relative_eq!(first_itinerary.distance, 6.311692992158277); + assert_relative_eq!(first_itinerary.distance, 10.157660000000002); assert_relative_eq!(first_itinerary.duration, 2347.0); // legs assert_eq!(first_itinerary.legs.len(), 4); let first_leg = &first_itinerary.legs[0]; + let geometry = polyline::decode_polyline(&first_leg.geometry, 6).unwrap(); assert_relative_eq!( - first_leg.geometry.0[0], + geometry.0[0], geo::coord!(x: -122.33922, y: 47.57583), epsilon = 1e-4 ); @@ -939,7 +898,7 @@ mod tests { .unwrap(); let first_maneuver = maneuvers.first().unwrap(); let expected_maneuver = json!({ - "distance": 0.0118992879068438, // TODO: truncate precision in serializer + "distance": 11.8993, "instruction": "Walk south on East Marginal Way South.", "startPoint": { "lat": 47.5758346, @@ -1047,7 +1006,7 @@ mod tests { { "begin_shape_index": 0, "cost": 246.056, - "end_shape_index": 1, + "end_shape_index": 69, "highway": true, "instruction": "Drive northeast on Fauntleroy Way Southwest.", "length": 2.218, diff --git a/services/travelmux/src/api/v5/directions.rs b/services/travelmux/src/api/v6/directions.rs similarity index 98% rename from services/travelmux/src/api/v5/directions.rs rename to services/travelmux/src/api/v6/directions.rs index b52368619..c5c0c52ad 100644 --- a/services/travelmux/src/api/v5/directions.rs +++ b/services/travelmux/src/api/v6/directions.rs @@ -5,7 +5,7 @@ use crate::api::AppState; use actix_web::{get, web, HttpRequest, HttpResponseBuilder}; use serde::Serialize; -#[get("/v5/directions")] +#[get("/v6/directions")] pub async fn get_directions( query: web::Query, req: HttpRequest, @@ -45,9 +45,9 @@ impl From for DirectionsResponseOk { #[cfg(test)] mod tests { + use super::osrm_api; + use super::PlanResponseOk; use super::*; - use crate::api::v5::error::PlanResponseOk; - use crate::api::v5::osrm_api; use crate::otp::otp_api; use crate::valhalla::valhalla_api; use crate::{DistanceUnit, TravelMode}; diff --git a/services/travelmux/src/api/v6/error.rs b/services/travelmux/src/api/v6/error.rs new file mode 100644 index 000000000..8e33623e8 --- /dev/null +++ b/services/travelmux/src/api/v6/error.rs @@ -0,0 +1,208 @@ +use crate::otp::otp_api; +use crate::valhalla::valhalla_api; +use crate::{DistanceUnit, Error, TravelMode}; +use actix_web::HttpResponseBuilder; +use serde::Serialize; + +use super::{Itinerary, 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, + + // The raw response from the upstream Valhalla /route service + #[serde(rename = "_valhalla")] + _valhalla: Option, +} +pub type PlanResponseErr = Box; + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlanError { + pub status_code: u16, + pub error_code: u32, + pub message: String, +} + +#[derive(Debug, 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, + + // The raw response from the upstream Valhalla /route service + #[serde(rename = "_valhalla")] + _valhalla: Option, +} + +impl From for PlanResponseErr { + fn from(value: valhalla_api::RouteResponseError) -> Self { + Self::new(UnboxedPlanResponseErr { + error: (&value).into(), + _valhalla: Some(value), + _otp: None, + }) + } +} + +impl From 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 for PlanResponseErr { + fn from(value: Error) -> Self { + Self::new(UnboxedPlanResponseErr { + error: value.into(), + _valhalla: None, + _otp: None, + }) + } +} + +impl From for PlanError { + fn from(value: Error) -> Self { + let error_code = value.error_type as u32; + match value.error_type { + ErrorType::NoCoverageForArea => 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 { + HttpResponseBuilder::new(self.status_code()) + .content_type("application/json") + .json(self) + } +} + +impl PlanResponseOk { + pub fn from_otp( + mode: TravelMode, + mut otp: otp_api::PlanResponse, + distance_unit: DistanceUnit, + ) -> Result { + 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> = otp + .plan + .itineraries + .iter() + .map(|itinerary: &otp_api::Itinerary| { + Itinerary::from_otp(itinerary, mode, distance_unit) + }) + .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 { + let valhalla = match valhalla { + valhalla_api::ValhallaRouteResponseResult::Ok(valhalla) => valhalla, + valhalla_api::ValhallaRouteResponseResult::Err(err) => return Err(err), + }; + + let mut itineraries = vec![Itinerary::from_valhalla(&valhalla.trip, mode)]; + if let Some(alternates) = &valhalla.alternates { + for alternate in alternates { + itineraries.push(Itinerary::from_valhalla(&alternate.trip, mode)); + } + } + + Ok(PlanResponseOk { + plan: Plan { itineraries }, + _otp: None, + _valhalla: Some(valhalla), + }) + } +} diff --git a/services/travelmux/src/api/v6/mod.rs b/services/travelmux/src/api/v6/mod.rs new file mode 100644 index 000000000..45cd1f61b --- /dev/null +++ b/services/travelmux/src/api/v6/mod.rs @@ -0,0 +1,9 @@ +pub mod directions; +mod error; +mod osrm_api; +pub mod plan; +mod travel_modes; + +pub use travel_modes::TravelModes; + +pub use plan::{Itinerary, Plan}; diff --git a/services/travelmux/src/api/v5/osrm_api.rs b/services/travelmux/src/api/v6/osrm_api.rs similarity index 100% rename from services/travelmux/src/api/v5/osrm_api.rs rename to services/travelmux/src/api/v6/osrm_api.rs diff --git a/services/travelmux/src/api/v6/plan.rs b/services/travelmux/src/api/v6/plan.rs new file mode 100644 index 000000000..3e504e462 --- /dev/null +++ b/services/travelmux/src/api/v6/plan.rs @@ -0,0 +1,1110 @@ +use actix_web::{get, web, HttpRequest, HttpResponseBuilder}; +use geo::algorithm::BoundingRect; +use geo::geometry::{LineString, Point, Rect}; +use polyline::decode_polyline; +use reqwest::header::{HeaderName, HeaderValue}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::time::{Duration, SystemTime}; + +use super::error::{PlanResponseErr, PlanResponseOk}; +use super::TravelModes; + +use crate::api::AppState; +use crate::error::ErrorType; +use crate::otp::otp_api; +use crate::otp::otp_api::{AbsoluteDirection, RelativeDirection}; +use crate::util::format::format_meters; +use crate::util::haversine_segmenter::HaversineSegmenter; +use crate::util::{ + deserialize_point_from_lat_lon, extend_bounds, serialize_line_string_as_polyline6, + serialize_rect_to_lng_lat, serialize_system_time_as_millis, system_time_from_millis, +}; +use crate::valhalla::valhalla_api; +use crate::valhalla::valhalla_api::{LonLat, ManeuverType}; +use crate::{DistanceUnit, Error, TravelMode}; + +const METERS_PER_MILE: f64 = 1609.34; + +fn convert_from_meters(meters: f64, output_units: DistanceUnit) -> f64 { + match output_units { + DistanceUnit::Kilometers => meters / 1000.0, + DistanceUnit::Miles => meters / METERS_PER_MILE, + } +} + +fn convert_to_meters(distance: f64, input_units: DistanceUnit) -> f64 { + match input_units { + DistanceUnit::Kilometers => distance * 1000.0, + DistanceUnit::Miles => distance * METERS_PER_MILE, + } +} + +#[derive(Debug, Deserialize, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PlanQuery { + #[serde(deserialize_with = "deserialize_point_from_lat_lon")] + to_place: Point, + + #[serde(deserialize_with = "deserialize_point_from_lat_lon")] + from_place: Point, + + num_itineraries: u32, + + mode: TravelModes, + + /// Ignored by OTP - transit trips will always be metric. + /// Examine the `distance_units` in the response `Itinerary` to correctly interpret the response. + preferred_distance_units: Option, +} + +#[derive(Debug, Serialize, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Plan { + pub(crate) itineraries: Vec, +} + +#[derive(Debug, Serialize, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Itinerary { + mode: TravelMode, + /// seconds + pub(crate) duration: f64, + /// unix millis, UTC + #[serde(serialize_with = "serialize_system_time_as_millis")] + start_time: SystemTime, + /// unix millis, UTC + #[serde(serialize_with = "serialize_system_time_as_millis")] + end_time: SystemTime, + /// Units are in `distance_units` + distance: f64, + /// FIXME: I think we're returning meters even though distance unit is "Kilometers" + /// Probably we should rename DistanceUnit::Kilometers to DistanceUnit::Meters + /// This is passed as a parameter though, so it'd be a breaking change. + pub(crate) distance_units: DistanceUnit, + #[serde(serialize_with = "serialize_rect_to_lng_lat")] + bounds: Rect, + pub(crate) legs: Vec, +} + +impl Itinerary { + pub fn distance_meters(&self) -> f64 { + match self.distance_units { + DistanceUnit::Kilometers => self.distance * 1000.0, + DistanceUnit::Miles => self.distance * METERS_PER_MILE, + } + } + + pub fn combined_geometry(&self) -> LineString { + let mut combined_geometry = LineString::new(vec![]); + for leg in &self.legs { + combined_geometry.0.extend(&leg.geometry.0); + } + combined_geometry + } + + pub fn from_valhalla(valhalla: &valhalla_api::Trip, mode: TravelMode) -> Self { + let bounds = Rect::new( + geo::coord!(x: valhalla.summary.min_lon, y: valhalla.summary.min_lat), + geo::coord!(x: valhalla.summary.max_lon, y: valhalla.summary.max_lat), + ); + + let start_time = SystemTime::now(); + let end_time = start_time + Duration::from_millis((valhalla.summary.time * 1000.0) as u64); + debug_assert!( + valhalla.locations.len() == valhalla.legs.len() + 1, + "assuming each leg has a start and end location" + ); + + let mut start_time = SystemTime::now(); + let legs = valhalla + .legs + .iter() + .zip(valhalla.locations.windows(2)) + .map(|(v_leg, locations)| { + let leg_start_time = start_time; + let leg_end_time = + start_time + Duration::from_millis((v_leg.summary.time * 1000.0) as u64); + start_time = leg_end_time; + Leg::from_valhalla( + v_leg, + mode, + leg_start_time, + leg_end_time, + locations[0], + locations[1], + ) + }) + .collect(); + + Self { + mode, + start_time, + end_time, + duration: valhalla.summary.time, + distance: valhalla.summary.length, + bounds, + distance_units: valhalla.units, + legs, + } + } + + pub fn from_otp( + itinerary: &otp_api::Itinerary, + mode: TravelMode, + distance_unit: DistanceUnit, + ) -> crate::Result { + // OTP responses are always in meters + let distance_meters: f64 = itinerary.legs.iter().map(|l| l.distance).sum(); + let Ok(legs): std::result::Result, _> = itinerary + .legs + .iter() + .enumerate() + .map(|(idx, leg)| { + let is_destination_leg = idx == itinerary.legs.len() - 1; + Leg::from_otp(leg, is_destination_leg, distance_unit) + }) + .collect() + else { + return Err(Error::server("failed to parse legs")); + }; + + let mut legs_iter = legs.iter(); + let Some(first_leg) = legs_iter.next() else { + return Err(Error::server("itinerary had no legs")); + }; + let Some(mut itinerary_bounds) = first_leg.bounding_rect() else { + return Err(Error::server("first leg has no bounding_rect")); + }; + for leg in legs_iter { + let Some(leg_bounds) = leg.bounding_rect() else { + return Err(Error::server("leg has no bounding_rect")); + }; + extend_bounds(&mut itinerary_bounds, &leg_bounds); + } + + Ok(Self { + duration: itinerary.duration as f64, + start_time: system_time_from_millis(itinerary.start_time), + end_time: system_time_from_millis(itinerary.end_time), + mode, + distance: convert_from_meters(distance_meters, distance_unit), + distance_units: distance_unit, + bounds: itinerary_bounds, + legs, + }) + } +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +struct Place { + #[serde(flatten)] + location: LonLat, + name: Option, +} + +impl From<&otp_api::Place> for Place { + fn from(value: &otp_api::Place) -> Self { + Self { + location: value.location.into(), + name: value.name.clone(), + } + } +} + +impl From for Place { + fn from(value: LonLat) -> Self { + Self { + location: value, + name: None, + } + } +} + +#[derive(Debug, Serialize, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct Leg { + /// encoded polyline. 1e-6 scale, (lat, lon) + #[serde(serialize_with = "serialize_line_string_as_polyline6")] + geometry: LineString, + + /// Which mode is this leg of the journey? + pub(crate) mode: TravelMode, + + #[serde(flatten)] + pub(crate) mode_leg: ModeLeg, + + /// Beginning of the leg + from_place: Place, + + /// End of the Leg + to_place: Place, + + // This is mostly OTP specific. We can synthesize a value from the valhalla response, but we + // don't currently use it. + /// Start time of the leg + #[serde(serialize_with = "serialize_system_time_as_millis")] + start_time: SystemTime, + + // This is mostly OTP specific. We can synthesize a value from the valhalla response, but we + // don't currently use it. + /// Start time of the leg + #[serde(serialize_with = "serialize_system_time_as_millis")] + end_time: SystemTime, + + /// Length of this leg, in units of the `distance_unit` of the Itinerary + distance: f64, + + /// Duration of this leg + pub(crate) duration_seconds: f64, +} + +// Should we just pass the entire OTP leg? +type TransitLeg = otp_api::Leg; + +#[derive(Debug, Serialize, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) enum ModeLeg { + // REVIEW: rename? There is a boolean field for OTP called TransitLeg + #[serde(rename = "transitLeg")] + Transit(Box), + + #[serde(rename = "nonTransitLeg")] + NonTransit(Box), +} + +#[derive(Debug, Serialize, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct NonTransitLeg { + pub(crate) maneuvers: Vec, + + /// The substantial road names along the route + pub(crate) substantial_street_names: Vec, +} + +impl NonTransitLeg { + fn new(maneuvers: Vec) -> Self { + let mut street_distances = HashMap::new(); + for maneuver in &maneuvers { + if let Some(street_names) = &maneuver.street_names { + for street_name in street_names { + *street_distances.entry(street_name).or_insert(0.0) += maneuver.distance; + } + } + } + let mut scores: Vec<_> = street_distances.into_iter().collect(); + scores.sort_by(|(_, a), (_, b)| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)); + + let limit = 3; + // Don't include tiny segments in the description of the route + let mut inclusion_threshold = None; + + let substantial_street_names = scores + .into_iter() + .take(limit) + .flat_map(|(street_name, distance)| { + let Some(inclusion_threshold) = inclusion_threshold else { + // don't consider streets that are much smaller than this one + inclusion_threshold = Some(distance * 0.5); + return Some(street_name.clone()); + }; + if distance > inclusion_threshold { + Some(street_name.clone()) + } else { + None + } + }) + .collect(); + + Self { + maneuvers, + substantial_street_names, + } + } +} + +// Eventually we might want to coalesce this into something not valhalla specific +// but for now we only use it for valhalla trips +#[derive(Debug, Serialize, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Maneuver { + pub instruction: Option, + // pub cost: f64, + // pub begin_shape_index: u64, + // pub end_shape_index: u64, + #[serde(skip_serializing)] + pub geometry: LineString, + // pub highway: Option, + /// In units of the `distance_unit` of the trip leg + pub distance: f64, + pub street_names: Option>, + #[serde(skip_serializing)] + pub duration_seconds: f64, + // pub travel_mode: String, + // pub travel_type: String, + pub r#type: ManeuverType, + pub verbal_post_transition_instruction: Option, + pub start_point: LonLat, + // pub verbal_pre_transition_instruction: Option, + // pub verbal_succinct_transition_instruction: Option, +} + +impl Maneuver { + fn from_valhalla(valhalla: valhalla_api::Maneuver, leg_geometry: &LineString) -> Self { + let coords = leg_geometry.0 + [valhalla.begin_shape_index as usize..=valhalla.end_shape_index as usize] + .to_owned(); + let geometry = LineString::from(coords); + Self { + instruction: Some(valhalla.instruction), + street_names: valhalla.street_names, + duration_seconds: valhalla.time, + r#type: valhalla.r#type, + start_point: Point(leg_geometry[valhalla.begin_shape_index as usize]).into(), + verbal_post_transition_instruction: valhalla.verbal_post_transition_instruction, + distance: valhalla.length, + geometry, + } + } + + fn from_otp( + otp: otp_api::Step, + geometry: LineString, + leg: &otp_api::Leg, + distance_unit: DistanceUnit, + ) -> Self { + let instruction = build_instruction( + leg.mode, + otp.relative_direction, + otp.absolute_direction, + &otp.street_name, + ); + + let verbal_post_transition_instruction = + build_verbal_post_transition_instruction(otp.distance, distance_unit); + + let street_names = if let Some(true) = otp.bogus_name { + None + } else { + Some(vec![otp.street_name]) + }; + + let duration_seconds = otp.distance / leg.distance * leg.duration_seconds(); + Self { + instruction, + r#type: otp.relative_direction.into(), + street_names, + verbal_post_transition_instruction, + distance: convert_from_meters(otp.distance, distance_unit), + duration_seconds, + start_point: Point::new(otp.lon, otp.lat).into(), + geometry, + } + } + + pub(crate) fn distance_meters(&self, distance_unit: DistanceUnit) -> f64 { + convert_to_meters(self.distance, distance_unit) + } +} + +// We could do so much better. Look at Valhalla's Odin. +// +// e.g. take context of previous maneuver. "Bear right to stay on Main Street" +fn build_instruction( + mode: otp_api::TransitMode, + maneuver_type: otp_api::RelativeDirection, + absolute_direction: Option, + street_name: &str, +) -> Option { + match maneuver_type { + RelativeDirection::Depart => { + if let Some(absolute_direction) = absolute_direction { + let direction = match absolute_direction { + AbsoluteDirection::North => "north", + AbsoluteDirection::Northeast => "northeast", + AbsoluteDirection::East => "east", + AbsoluteDirection::Southeast => "southeast", + AbsoluteDirection::South => "south", + AbsoluteDirection::Southwest => "southwest", + AbsoluteDirection::West => "west", + AbsoluteDirection::Northwest => "northwest", + }; + let mode = match mode { + otp_api::TransitMode::Walk => "Walk", + otp_api::TransitMode::Bicycle => "Bike", + otp_api::TransitMode::Car => "Drive", + _ => "Transit", + }; + Some(format!("{mode} {direction} on {street_name}.")) + } else { + Some("Depart.".to_string()) + } + } + RelativeDirection::HardLeft => Some(format!("Turn left onto {street_name}.")), + RelativeDirection::Left => Some(format!("Turn left onto {street_name}.")), + RelativeDirection::SlightlyLeft => Some(format!("Turn slightly left onto {street_name}.")), + RelativeDirection::Continue => Some(format!("Continue onto {street_name}.")), + RelativeDirection::SlightlyRight => { + Some(format!("Turn slightly right onto {street_name}.")) + } + RelativeDirection::Right => Some(format!("Turn right onto {street_name}.")), + RelativeDirection::HardRight => Some(format!("Turn right onto {street_name}.")), + RelativeDirection::CircleClockwise | RelativeDirection::CircleCounterclockwise => { + Some("Enter the roundabout.".to_string()) + } + RelativeDirection::Elevator => Some("Enter the elevator.".to_string()), + RelativeDirection::UturnLeft | RelativeDirection::UturnRight => { + Some("Make a U-turn.".to_string()) + } + } +} + +fn build_verbal_post_transition_instruction( + distance: f64, + distance_unit: DistanceUnit, +) -> Option { + if distance == 0.0 { + None + } else { + Some(format!( + "Continue for {}.", + format_meters(distance, distance_unit) + )) + } +} + +impl Leg { + const GEOMETRY_PRECISION: u32 = 6; + const VALHALLA_GEOMETRY_PRECISION: u32 = 6; + const OTP_GEOMETRY_PRECISION: u32 = 5; + + fn bounding_rect(&self) -> Option { + self.geometry.bounding_rect() + } + + pub(crate) fn distance_meters(&self, itinerary_units: DistanceUnit) -> f64 { + convert_to_meters(self.distance, itinerary_units) + } + + fn from_otp( + otp: &otp_api::Leg, + is_destination_leg: bool, + distance_unit: DistanceUnit, + ) -> std::result::Result { + debug_assert_ne!(Self::OTP_GEOMETRY_PRECISION, Self::GEOMETRY_PRECISION); + let geometry = decode_polyline(&otp.leg_geometry.points, Self::OTP_GEOMETRY_PRECISION)?; + let from_place: Place = (&otp.from).into(); + let to_place: Place = (&otp.to).into(); + + let mut distance_so_far = 0.0; + let mut segmenter = HaversineSegmenter::new(geometry.clone()); + let mode_leg = match otp.mode { + otp_api::TransitMode::Walk + | otp_api::TransitMode::Bicycle + | otp_api::TransitMode::Car => { + let mut maneuvers: Vec<_> = otp + .steps + .iter() + .cloned() + .map(|otp_step| { + // compute step geometry by distance along leg geometry + let step_geometry = + segmenter.next_segment(otp_step.distance).expect("TODO"); + distance_so_far += otp_step.distance; + Maneuver::from_otp(otp_step, step_geometry, otp, distance_unit) + }) + .collect(); + + // OTP doesn't include an arrival step like valhalla, so we synthesize one + if is_destination_leg { + let maneuver = Maneuver { + instruction: Some("Arrive at your destination.".to_string()), + distance: 0.0, + street_names: None, + duration_seconds: 0.0, + r#type: ManeuverType::Destination, + verbal_post_transition_instruction: None, + start_point: to_place.location, + geometry: LineString::new(vec![to_place.location.into()]), + }; + maneuvers.push(maneuver); + } + let leg = NonTransitLeg::new(maneuvers); + ModeLeg::NonTransit(Box::new(leg)) + } + _ => { + // assume everything else is transit + ModeLeg::Transit(Box::new(otp.clone())) + } + }; + + Ok(Self { + from_place, + to_place, + start_time: system_time_from_millis(otp.start_time), + end_time: system_time_from_millis(otp.end_time), + geometry, + mode: otp.mode.into(), + distance: convert_from_meters(otp.distance, distance_unit), + duration_seconds: otp.duration_seconds(), + mode_leg, + }) + } + + fn from_valhalla( + valhalla: &valhalla_api::Leg, + travel_mode: TravelMode, + start_time: SystemTime, + end_time: SystemTime, + from_place: LonLat, + to_place: LonLat, + ) -> Self { + let geometry = + polyline::decode_polyline(&valhalla.shape, Self::VALHALLA_GEOMETRY_PRECISION) + .expect("valid polyline from valhalla"); + let maneuvers = valhalla + .maneuvers + .iter() + .cloned() + .map(|valhalla_maneuver| Maneuver::from_valhalla(valhalla_maneuver, &geometry)) + .collect(); + let leg = NonTransitLeg::new(maneuvers); + Self { + start_time, + end_time, + from_place: from_place.into(), + to_place: to_place.into(), + geometry, + mode: travel_mode, + mode_leg: ModeLeg::NonTransit(Box::new(leg)), + distance: valhalla.summary.length, + duration_seconds: valhalla.summary.time, + } + } +} + +impl actix_web::Responder for PlanResponseOk { + type Body = actix_web::body::BoxBody; + + fn respond_to(self, _req: &HttpRequest) -> actix_web::HttpResponse { + let mut response = HttpResponseBuilder::new(actix_web::http::StatusCode::OK); + response.content_type("application/json"); + response.json(self) + } +} + +#[get("/v6/plan")] +pub async fn get_plan( + query: web::Query, + req: HttpRequest, + app_state: web::Data, +) -> std::result::Result { + _get_plan(query, req, app_state).await +} + +pub async fn _get_plan( + query: web::Query, + req: HttpRequest, + app_state: web::Data, +) -> std::result::Result { + let Some(primary_mode) = query.mode.first() else { + return Err(PlanResponseErr::from(Error::user("mode is required"))); + }; + + let distance_units = query + .preferred_distance_units + .unwrap_or(DistanceUnit::Kilometers); + + // TODO: Handle bus+bike if bike is first, for now all our clients are responsible for enforcing that + // the "primary" mode appears first. + 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) => { + 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); + } + } + Err(e) => { + // match error_code to raw value of ErrorType enum + match ErrorType::try_from(e.error.error_code) { + Ok(ErrorType::NoCoverageForArea) => { + log::debug!("No OTP coverage for route"); + } + other => { + debug_assert!(other.is_ok(), "unexpected error code: {e:?}"); + // We're mixing with results from Valhalla anyway, so dont' 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) + } + } +} + +async fn otp_plan( + query: &web::Query, + req: HttpRequest, + app_state: &web::Data, + primary_mode: &TravelMode, +) -> Result { + let Some(mut router_url) = app_state + .otp_cluster() + .find_router_url(query.from_place, query.to_place) + else { + Err( + Error::user("Transit directions not available for this area.") + .error_type(ErrorType::NoCoverageForArea), + )? + }; + + // if we end up building this manually rather than passing it through, we'll need to be sure + // to handle the bike+bus case + router_url.set_query(Some(req.query_string())); + log::debug!( + "found matching router. Forwarding request to: {}", + router_url + ); + + let otp_response: reqwest::Response = reqwest::get(router_url).await.map_err(|e| { + log::error!("error while fetching from otp service: {e}"); + PlanResponseErr::from(Error::server(e)) + })?; + if !otp_response.status().is_success() { + log::warn!( + "upstream HTTP Error from otp service: {}", + otp_response.status() + ) + } + + let mut response = HttpResponseBuilder::new(otp_response.status()); + debug_assert_eq!( + otp_response + .headers() + .get(HeaderName::from_static("content-type")), + Some(&HeaderValue::from_str("application/json").unwrap()) + ); + response.content_type("application/json"); + + let otp_plan_response: otp_api::PlanResponse = otp_response.json().await.map_err(|e| { + log::error!("error while parsing otp response: {e}"); + PlanResponseErr::from(Error::server(e)) + })?; + PlanResponseOk::from_otp( + *primary_mode, + otp_plan_response, + query + .preferred_distance_units + .unwrap_or(DistanceUnit::Kilometers), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use approx::assert_relative_eq; + use serde_json::{json, Value}; + use std::fs::File; + use std::io::BufReader; + + #[test] + fn parse_from_valhalla() { + let stubbed_response = + File::open("tests/fixtures/requests/valhalla_pedestrian_route.json").unwrap(); + let valhalla: valhalla_api::RouteResponse = + serde_json::from_reader(BufReader::new(stubbed_response)).unwrap(); + + let valhalla_response_result = valhalla_api::ValhallaRouteResponseResult::Ok(valhalla); + let plan_response = + PlanResponseOk::from_valhalla(TravelMode::Walk, valhalla_response_result).unwrap(); + assert_eq!(plan_response.plan.itineraries.len(), 3); + + // itineraries + let first_itinerary = &plan_response.plan.itineraries[0]; + assert_eq!(first_itinerary.mode, TravelMode::Walk); + assert_relative_eq!(first_itinerary.distance, 5.684); + assert_relative_eq!(first_itinerary.duration, 6488.443); + assert_relative_eq!( + first_itinerary.bounds, + Rect::new( + geo::coord!(x: -122.347201, y: 47.575663), + geo::coord!(x: -122.335618, y: 47.651047) + ) + ); + + // legs + assert_eq!(first_itinerary.legs.len(), 1); + let first_leg = &first_itinerary.legs[0]; + assert_relative_eq!( + first_leg.geometry.0[0], + geo::coord!(x: -122.33922, y: 47.57583), + epsilon = 1e-4 + ); + + assert_relative_eq!( + geo::Point::from(first_leg.from_place.location), + geo::point!(x: -122.339414, y: 47.575837) + ); + assert_relative_eq!( + geo::Point::from(first_leg.to_place.location), + geo::point!(x:-122.347234, y: 47.651048) + ); + assert!(first_leg.to_place.name.is_none()); + + let ModeLeg::NonTransit(non_transit_leg) = &first_leg.mode_leg else { + panic!("unexpected non-transit leg") + }; + + assert_eq!(first_leg.mode, TravelMode::Walk); + assert_eq!(non_transit_leg.maneuvers.len(), 21); + } + + #[test] + fn parse_from_otp() { + let stubbed_response = + File::open("tests/fixtures/requests/opentripplanner_transit_plan.json").unwrap(); + let otp: otp_api::PlanResponse = + serde_json::from_reader(BufReader::new(stubbed_response)).unwrap(); + let plan_response = + PlanResponseOk::from_otp(TravelMode::Transit, otp, DistanceUnit::Miles).unwrap(); + + let itineraries = plan_response.plan.itineraries; + assert_eq!(itineraries.len(), 6); + + // itineraries + let first_itinerary = &itineraries[0]; + assert_eq!(first_itinerary.mode, TravelMode::Transit); + assert_relative_eq!(first_itinerary.distance, 6.311692992158277); + assert_relative_eq!(first_itinerary.duration, 2347.0); + + // legs + assert_eq!(first_itinerary.legs.len(), 4); + let first_leg = &first_itinerary.legs[0]; + assert_relative_eq!( + first_leg.geometry.0[0], + geo::coord!(x: -122.33922, y: 47.57583), + epsilon = 1e-4 + ); + + assert_relative_eq!( + geo::Point::from(first_leg.from_place.location), + geo::point!(x: -122.339414, y: 47.575837) + ); + assert_relative_eq!( + geo::Point::from(first_leg.to_place.location), + geo::point!(x: -122.334106, y: 47.575924) + ); + assert_eq!( + first_leg.to_place.name.as_ref().unwrap(), + "1st Ave S & S Hanford St" + ); + + assert_eq!(first_leg.mode, TravelMode::Walk); + let ModeLeg::NonTransit(non_transit_leg) = &first_leg.mode_leg else { + panic!("expected non-transit leg") + }; + let maneuvers = &non_transit_leg.maneuvers; + assert_eq!(maneuvers.len(), 2); + assert_eq!(maneuvers[0].r#type, ManeuverType::Start); + assert_eq!(maneuvers[1].r#type, ManeuverType::Left); + + let transit_leg = &first_itinerary.legs[2]; + assert_eq!(transit_leg.mode, TravelMode::Transit); + let ModeLeg::Transit(transit_leg) = &transit_leg.mode_leg else { + panic!("expected transit leg") + }; + assert!(transit_leg.route_color.is_none()); + } + + #[test] + fn serialize_response_from_otp() { + let stubbed_response = + File::open("tests/fixtures/requests/opentripplanner_transit_plan.json").unwrap(); + let otp: otp_api::PlanResponse = + serde_json::from_reader(BufReader::new(stubbed_response)).unwrap(); + let plan_response = + PlanResponseOk::from_otp(TravelMode::Transit, otp, DistanceUnit::Miles).unwrap(); + let response = serde_json::to_string(&plan_response).unwrap(); + let parsed_response: serde_json::Value = serde_json::from_str(&response).unwrap(); + let response_object = parsed_response.as_object().expect("expected Object"); + let plan = response_object + .get("plan") + .unwrap() + .as_object() + .expect("expected Object"); + let first_itinerary = plan + .get("itineraries") + .unwrap() + .as_array() + .unwrap() + .first() + .unwrap(); + let legs = first_itinerary.get("legs").unwrap().as_array().unwrap(); + + // Verify walking leg + let first_leg = legs.first().unwrap().as_object().unwrap(); + let mode = first_leg.get("mode").unwrap().as_str().unwrap(); + assert_eq!(mode, "WALK"); + + let mode = first_leg + .get("startTime") + .expect("field missing") + .as_u64() + .expect("unexpected type. expected u64"); + assert_eq!(mode, 1715974501000); + + let mode = first_leg + .get("endTime") + .expect("field missing") + .as_u64() + .expect("unexpected type. expected u64"); + assert_eq!(mode, 1715974870000); + + assert!(first_leg.get("transitLeg").is_none()); + let non_transit_leg = first_leg.get("nonTransitLeg").unwrap().as_object().unwrap(); + + let substantial_street_names = non_transit_leg + .get("substantialStreetNames") + .unwrap() + .as_array() + .unwrap(); + let expected_names = vec!["East Marginal Way South"]; + assert_eq!(substantial_street_names, &expected_names); + + let maneuvers = non_transit_leg + .get("maneuvers") + .unwrap() + .as_array() + .unwrap(); + let first_maneuver = maneuvers.first().unwrap(); + let expected_maneuver = json!({ + "distance": 0.0118992879068438, // TODO: truncate precision in serializer + "instruction": "Walk south on East Marginal Way South.", + "startPoint": { + "lat": 47.5758346, + "lon": -122.3392181 + }, + "streetNames": ["East Marginal Way South"], + "type": 1, + "verbalPostTransitionInstruction": "Continue for 60 feet." + }); + assert_eq!(first_maneuver, &expected_maneuver); + + // Verify Transit leg + let transit_leg = legs.get(2).unwrap().as_object().unwrap(); + let mode = transit_leg.get("mode").unwrap().as_str().unwrap(); + assert_eq!(mode, "TRANSIT"); + assert!(transit_leg.get("maneuvers").is_none()); + let transit_leg = transit_leg + .get("transitLeg") + .unwrap() + .as_object() + .expect("json object"); + + // Brittle: If the fixtures are updated, these values might change due to time of day or whatever. + assert_eq!( + transit_leg.get("agencyName").unwrap().as_str().unwrap(), + "Metro Transit" + ); + + assert!(transit_leg.get("route").is_none()); + } + + #[test] + fn serialize_response_from_valhalla() { + let stubbed_response = + File::open("tests/fixtures/requests/valhalla_pedestrian_route.json").unwrap(); + let valhalla: valhalla_api::RouteResponse = + serde_json::from_reader(BufReader::new(stubbed_response)).unwrap(); + + let valhalla_response_result = valhalla_api::ValhallaRouteResponseResult::Ok(valhalla); + let plan_response = + PlanResponseOk::from_valhalla(TravelMode::Walk, valhalla_response_result).unwrap(); + + let response = serde_json::to_string(&plan_response).unwrap(); + let parsed_response: serde_json::Value = serde_json::from_str(&response).unwrap(); + let response_object = parsed_response.as_object().expect("expected Object"); + let plan = response_object + .get("plan") + .unwrap() + .as_object() + .expect("expected Object"); + let first_itinerary = plan + .get("itineraries") + .unwrap() + .as_array() + .unwrap() + .first() + .unwrap(); + let legs = first_itinerary.get("legs").unwrap().as_array().unwrap(); + + // Verify walking leg + let first_leg = legs.first().unwrap().as_object().unwrap(); + let mode = first_leg.get("mode").unwrap().as_str().unwrap(); + assert_eq!(mode, "WALK"); + assert!(first_leg.get("transitLeg").is_none()); + let non_transit_leg = first_leg.get("nonTransitLeg").unwrap().as_object().unwrap(); + + let substantial_street_names = non_transit_leg + .get("substantialStreetNames") + .unwrap() + .as_array() + .unwrap(); + assert_eq!( + substantial_street_names, + &[ + "Dexter Avenue", + "East Marginal Way South", + "Alaskan Way South" + ] + ); + + let maneuvers = non_transit_leg + .get("maneuvers") + .unwrap() + .as_array() + .unwrap(); + let first_maneuver = maneuvers.first().unwrap(); + let expected_maneuver = json!({ + "type": 2, + "instruction": "Walk south on East Marginal Way South.", + "verbalPostTransitionInstruction": "Continue for 60 feet.", + "distance": 0.011, + "startPoint": { + "lat": 47.575836, + "lon": -122.339216 + }, + "streetNames": ["East Marginal Way South"], + }); + assert_eq!(first_maneuver, &expected_maneuver); + } + + #[test] + fn parse_maneuver_from_valhalla_json() { + // deserialize a maneuver from a JSON string + let json = r#" + { + "begin_shape_index": 0, + "cost": 246.056, + "end_shape_index": 1, + "highway": true, + "instruction": "Drive northeast on Fauntleroy Way Southwest.", + "length": 2.218, + "street_names": [ + "Fauntleroy Way Southwest" + ], + "time": 198.858, + "travel_mode": "drive", + "travel_type": "car", + "type": 2, + "verbal_post_transition_instruction": "Continue for 2 miles.", + "verbal_pre_transition_instruction": "Drive northeast on Fauntleroy Way Southwest.", + "verbal_succinct_transition_instruction": "Drive northeast." + }"#; + + let valhalla_maneuver: valhalla_api::Maneuver = serde_json::from_str(json).unwrap(); + assert_eq!(valhalla_maneuver.r#type, ManeuverType::StartRight); + assert_eq!( + valhalla_maneuver.instruction, + "Drive northeast on Fauntleroy Way Southwest." + ); + + // fake geometry + let geometry = LineString::from(vec![ + geo::coord!(x: -122.398, y: 47.564), + geo::coord!(x: -122.396, y: 47.566), + ]); + let maneuver = Maneuver::from_valhalla(valhalla_maneuver, &geometry); + let actual = serde_json::to_string(&maneuver).unwrap(); + // parse the JSON string back into an Object Value + let actual_object: Value = serde_json::from_str(&actual).unwrap(); + + let expected_object = serde_json::json!({ + "instruction": "Drive northeast on Fauntleroy Way Southwest.", + "type": 2, + "distance": 2.218, + "startPoint": { "lon": -122.398, "lat": 47.564}, + "streetNames": ["Fauntleroy Way Southwest"], + "verbalPostTransitionInstruction": "Continue for 2 miles.", + }); + + assert_eq!(actual_object, expected_object); + } + + #[test] + fn parse_error_from_valhalla() { + let json = serde_json::json!({ + "error_code": 154, + "error": "Path distance exceeds the max distance limit: 200000 meters", + "status_code": 400, + "status": "Bad Request" + }) + .to_string(); + + let valhalla_error: valhalla_api::RouteResponseError = serde_json::from_str(&json).unwrap(); + let plan_error = PlanResponseErr::from(valhalla_error); + assert_eq!(plan_error.error.status_code, 400); + assert_eq!(plan_error.error.error_code, 2154); + } +} diff --git a/services/travelmux/src/api/v5/travel_modes.rs b/services/travelmux/src/api/v6/travel_modes.rs similarity index 100% rename from services/travelmux/src/api/v5/travel_modes.rs rename to services/travelmux/src/api/v6/travel_modes.rs diff --git a/services/travelmux/src/bin/travelmux-server/main.rs b/services/travelmux/src/bin/travelmux-server/main.rs index 44d50bb69..be44f5f70 100644 --- a/services/travelmux/src/bin/travelmux-server/main.rs +++ b/services/travelmux/src/bin/travelmux-server/main.rs @@ -50,7 +50,8 @@ async fn main() -> Result<()> { .app_data(web::Data::new(app_state.clone())) .service(api::v4::plan::get_plan) .service(api::v5::plan::get_plan) - .service(api::v5::directions::get_directions) + .service(api::v6::plan::get_plan) + .service(api::v6::directions::get_directions) .service(api::health::get_ready) .service(api::health::get_alive) }) diff --git a/services/travelmux/src/api/v5/haversine_segmenter.rs b/services/travelmux/src/util/haversine_segmenter.rs similarity index 97% rename from services/travelmux/src/api/v5/haversine_segmenter.rs rename to services/travelmux/src/util/haversine_segmenter.rs index d66d99fc2..996cd87e2 100644 --- a/services/travelmux/src/api/v5/haversine_segmenter.rs +++ b/services/travelmux/src/util/haversine_segmenter.rs @@ -58,7 +58,7 @@ impl HaversineSegmenter { mod test { use super::*; use approx::assert_relative_eq; - use geo::{point, wkt, HaversineDestination}; + use geo::{point, HaversineLength}; #[test] fn test_segmenter() { @@ -83,7 +83,7 @@ mod test { // next one should pick up where the last one left off let segment_2 = segmenter.next_segment(10_000.0).unwrap(); - assert_eq!(segment_1.0.last(), segment_2.0.first()); + assert_eq!(segment_1.0.last().unwrap(), segment_2.0.first().unwrap()); let east_of_berlin = point!(x: 13.482210264987538, y: 52.34640526357316); let segment_3 = segmenter.next_segment(paris_to_berlin_distance).unwrap(); diff --git a/services/travelmux/src/util/mod.rs b/services/travelmux/src/util/mod.rs index 53d2a6536..fb4928bcf 100644 --- a/services/travelmux/src/util/mod.rs +++ b/services/travelmux/src/util/mod.rs @@ -1,4 +1,5 @@ pub mod format; +pub mod haversine_segmenter; use geo::{Point, Rect}; use serde::{ From 598b8b96882f9c255f05d002027fc2c93ec01da8 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Thu, 23 May 2024 15:22:29 -0700 Subject: [PATCH 14/24] flesh out maneuvers --- services/travelmux/src/api/v6/osrm_api.rs | 103 +++++++++++++++------- 1 file changed, 70 insertions(+), 33 deletions(-) diff --git a/services/travelmux/src/api/v6/osrm_api.rs b/services/travelmux/src/api/v6/osrm_api.rs index ca10d0cd3..b40bc4de6 100644 --- a/services/travelmux/src/api/v6/osrm_api.rs +++ b/services/travelmux/src/api/v6/osrm_api.rs @@ -214,17 +214,13 @@ impl BannerInstruction { ManeuverType::Destination => (Arrive, None), ManeuverType::DestinationRight => (Arrive, Some(Right)), ManeuverType::DestinationLeft => (Arrive, Some(Left)), - /* - ManeuverType::Becomes => {} - */ - ManeuverType::Continue => (Fork, None), // Or maybe just return None? + ManeuverType::Becomes => (NewName, None), + ManeuverType::Continue => (Continue, None), ManeuverType::SlightRight => (Turn, Some(SlightRight)), ManeuverType::Right => (Turn, Some(Right)), ManeuverType::SharpRight => (Turn, Some(SharpRight)), - /* - ManeuverType::UturnRight => {} - ManeuverType::UturnLeft => {} - */ + ManeuverType::UturnRight => (Turn, Some(Uturn)), + ManeuverType::UturnLeft => (Turn, Some(Uturn)), ManeuverType::SharpLeft => (Turn, Some(SharpLeft)), ManeuverType::Left => (Turn, Some(Left)), ManeuverType::SlightLeft => (Turn, Some(SlightLeft)), @@ -233,33 +229,28 @@ impl BannerInstruction { ManeuverType::RampLeft => (OnRamp, Some(Left)), ManeuverType::ExitRight => (OffRamp, Some(Right)), ManeuverType::ExitLeft => (OffRamp, Some(Left)), - ManeuverType::StayStraight => (Fork, None), // Or maybe just return None? + ManeuverType::StayStraight => (Fork, Some(Straight)), ManeuverType::StayRight => (Fork, Some(Right)), ManeuverType::StayLeft => (Fork, Some(Left)), - /* - ManeuverType::Merge => {} - */ - ManeuverType::RoundaboutEnter => (RoundaboutEnter, None), // Enter/Exit? - ManeuverType::RoundaboutExit => (RoundaboutExit, None), // Enter/Exit? - /* - ManeuverType::FerryEnter => {} - ManeuverType::FerryExit => {} - ManeuverType::Transit => {} - ManeuverType::TransitTransfer => {} - ManeuverType::TransitRemainOn => {} - ManeuverType::TransitConnectionStart => {} - ManeuverType::TransitConnectionTransfer => {} - ManeuverType::TransitConnectionDestination => {} - ManeuverType::PostTransitConnectionDestination => {} - ManeuverType::MergeRight => {} - ManeuverType::MergeLeft => {} - ManeuverType::ElevatorEnter => {} - ManeuverType::StepsEnter => {} - ManeuverType::EscalatorEnter => {} - ManeuverType::BuildingEnter => {} - ManeuverType::BuildingExit => {} - */ - other => todo!("implement maneuver type: {other:?}"), + ManeuverType::Merge => (Merge, None), + ManeuverType::RoundaboutEnter => (RoundaboutEnter, None), + ManeuverType::RoundaboutExit => (RoundaboutExit, None), + ManeuverType::FerryEnter => (Notification, None), + ManeuverType::FerryExit => (Notification, None), + ManeuverType::Transit => (Notification, None), + ManeuverType::TransitTransfer => (Notification, None), + ManeuverType::TransitRemainOn => (Notification, None), + ManeuverType::TransitConnectionStart => (Notification, None), + ManeuverType::TransitConnectionTransfer => (Notification, None), + ManeuverType::TransitConnectionDestination => (Notification, None), + ManeuverType::PostTransitConnectionDestination => (Notification, None), + ManeuverType::MergeRight => (Merge, Some(Right)), + ManeuverType::MergeLeft => (Merge, Some(Left)), + ManeuverType::ElevatorEnter => (Notification, None), + ManeuverType::StepsEnter => (Notification, None), + ManeuverType::EscalatorEnter => (Notification, None), + ManeuverType::BuildingEnter => (Notification, None), + ManeuverType::BuildingExit => (Notification, None), }; Some(BannerManeuver { r#type: banner_type, @@ -320,19 +311,65 @@ pub struct BannerManeuver { #[derive(Debug, Serialize, PartialEq, Clone)] #[serde(rename_all = "lowercase")] pub enum BannerManeuverType { + /// A turn in the direction of the modifier. Turn, + + /// The road name changes (after a mandatory turn). + #[serde(rename = "new name")] + NewName, + + /// Merge onto a street. Merge, + + /// Indicates departure from a leg. The modifier value indicates the position of the departure point compared to the current direction of travel. Depart, + + /// Indicates arrival to a destination of a leg. The modifier value indicates the position of the arrival point compared to the current direction of travel. Arrive, + + /// Keep left or right side at a bifurcation, or left/right/straight at a trifurcation. Fork, + + /// Take a ramp to enter a highway. #[serde(rename = "on ramp")] OnRamp, + + /// Take a ramp to exit a highway. #[serde(rename = "off ramp")] OffRamp, + + /// Road ends in a T intersection. + #[allow(unused)] + #[serde(rename = "end of road")] + EndOfRoad, + + /// Continue on a street after a turn. + Continue, + + /// Traverse roundabout. Has an additional property exit in the route step that contains the exit number. The modifier specifies the direction of entering the roundabout. #[serde(rename = "roundabout")] RoundaboutEnter, + + /// Indicates the exit maneuver from a roundabout. Will not appear in results unless you supply the roundabout_exits=true query parameter in the request. #[serde(rename = "exit roundabout")] RoundaboutExit, + + /// A traffic circle. While like a larger version of a roundabout, it does not necessarily follow roundabout rules for right of way. It can offer rotary_name parameters, rotary_pronunciation parameters, or both, located in the route step object. It also contains the exit property. + #[allow(unused)] + #[serde(rename = "rotary")] + RotaryEnter, + + #[allow(unused)] + #[serde(rename = "exit rotary")] + RotaryExit, + + /// A small roundabout that is treated as an intersection. + #[allow(unused)] + #[serde(rename = "roundabout turn")] + RoundaboutTurn, + + /// Indicates a change of driving conditions, for example changing the mode from driving to ferry. + Notification, } #[derive(Debug, Serialize, PartialEq, Clone)] From 9cde92b3b9fcb7792c38d3e17a0358716c63d411 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Tue, 28 May 2024 11:49:39 -0400 Subject: [PATCH 15/24] components with dividers --- services/travelmux/src/api/v6/directions.rs | 8 --- services/travelmux/src/api/v6/osrm_api.rs | 51 +++++++++++++------ .../tests/fixtures/requests/refresh.sh | 11 ++-- 3 files changed, 39 insertions(+), 31 deletions(-) diff --git a/services/travelmux/src/api/v6/directions.rs b/services/travelmux/src/api/v6/directions.rs index c5c0c52ad..0f7a5e844 100644 --- a/services/travelmux/src/api/v6/directions.rs +++ b/services/travelmux/src/api/v6/directions.rs @@ -124,10 +124,6 @@ mod tests { step_maneuver.location, geo::point!(x: -122.3392181, y: 47.5758346) ); - // assert_eq!(step_maneuver.r#type, "my type"); - // assert_eq!(step_maneuver.modifier, "my modifier"); - // TODO: step_maneuver stuff - // etc... } #[test] @@ -199,9 +195,5 @@ mod tests { step_maneuver.location, geo::point!(x: -122.339216, y: 47.575836) ); - // assert_eq!(step_maneuver.r#type, "my type"); - // assert_eq!(step_maneuver.modifier, "my modifier"); - // TODO: step_maneuver stuff - // etc... } } diff --git a/services/travelmux/src/api/v6/osrm_api.rs b/services/travelmux/src/api/v6/osrm_api.rs index b40bc4de6..feee4a87b 100644 --- a/services/travelmux/src/api/v6/osrm_api.rs +++ b/services/travelmux/src/api/v6/osrm_api.rs @@ -1,3 +1,6 @@ +//! "osrm_api" is a bit of a misnomer. It's intended to work with maplibre's "Directions" library. +//! which happens to be strongly influenced by OSRM. + use super::plan::{Itinerary, Leg, Maneuver, ModeLeg}; use crate::util::{serialize_line_string_as_polyline6, serialize_point_as_lon_lat_pair}; use crate::valhalla::valhalla_api::ManeuverType; @@ -187,12 +190,12 @@ impl BannerInstruction { next_maneuver: Option<&Maneuver>, from_distance_unit: DistanceUnit, ) -> Option> { - let text = if let Some(next_maneuver) = next_maneuver { + let text_components = if let Some(next_maneuver) = next_maneuver { next_maneuver .street_names .as_ref() - .map(|names| names.join(", ")) - .or(next_maneuver.instruction.clone()) + .cloned() + .or(next_maneuver.instruction.as_ref().map(|s| vec![s.clone()])) } else { assert!(matches!( maneuver.r#type, @@ -200,7 +203,7 @@ impl BannerInstruction { | ManeuverType::DestinationRight | ManeuverType::DestinationLeft )); - maneuver.instruction.to_owned() + maneuver.instruction.as_ref().map(|s| vec![s.clone()]) }; let banner_maneuver = (|| { @@ -258,20 +261,36 @@ impl BannerInstruction { }) })(); - let text_component = - BannerComponent::Text(VisualInstructionComponent { text: text.clone() }); - // if let Some(banner_maneuver) = banner_maneuver { - // BannerComponent::Text(VisualInstructionComponent { - // text, - // }) - // } else { - // panic!("no banner_maneuver") - // } - // }; + let text = text_components.as_ref().map(|t| t.join("/")); + + let components: Vec = (|text_components: Option>| { + let text_components = text_components?; + let mut text_iter = text_components.into_iter(); + let first_text = text_iter.next()?; + + let mut output = vec![BannerComponent::Text(VisualInstructionComponent { + text: Some(first_text), + })]; + for next_text in text_iter { + output.push(BannerComponent::Delimiter(VisualInstructionComponent { + text: Some("/".to_string()), + })); + output.push(BannerComponent::Text(VisualInstructionComponent { + text: Some(next_text), + })); + } + Some(output) + })(text_components) + .unwrap_or( + // REVIEW: not sure if we need this default or if an empty component list is OK. + vec![BannerComponent::Text(VisualInstructionComponent { + text: None, + })], + ); let primary = BannerInstructionContent { text: text.unwrap_or_default(), - components: vec![text_component], // TODO + components, banner_maneuver, degrees: None, driving_side: None, @@ -397,7 +416,7 @@ pub enum BannerManeuverModifier { pub enum BannerComponent { Text(VisualInstructionComponent), // Icon(VisualInstructionComponent), - // Delimiter(VisualInstructionComponent), + Delimiter(VisualInstructionComponent), // #[serde(rename="exit-number")] // ExitNumber(VisualInstructionComponent), // Exit(VisualInstructionComponent), diff --git a/services/travelmux/tests/fixtures/requests/refresh.sh b/services/travelmux/tests/fixtures/requests/refresh.sh index 244db06c8..a28d75ed2 100755 --- a/services/travelmux/tests/fixtures/requests/refresh.sh +++ b/services/travelmux/tests/fixtures/requests/refresh.sh @@ -23,16 +23,13 @@ fetch_valhalla pedestrian fetch_valhalla bicycle fetch_valhalla auto # car +realFine="47.575837,-122.339414" +zeitgeist="47.651048,-122.347234" + function fetch_opentripplanner { mode=$1 output_prefix="opentripplanner_$(echo "$mode" | tr '[:upper:]' '[:lower:]' | sed 's/,/_with_/')" - - start_lat=47.575837 - start_lon=-122.339414 - end_lat=47.651048 - end_lon=-122.347234 - - request_url="http://localhost:9002/otp/routers/default/plan?fromPlace=$start_lat,$start_lon&toPlace=$end_lat,$end_lon&mode=${mode}" + request_url="http://localhost:9002/otp/routers/default/plan?fromPlace=${realFine}&toPlace=${zeitgeist}&mode=${mode}" # Make the request and save the output to a file curl "$request_url" | jq -S . > "${output_prefix}_plan.json" From 1203bb74bbd49d72c7e83e62e426166f8d6a5cba Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 29 May 2024 11:35:14 -0700 Subject: [PATCH 16/24] rm unused --- services/travelmux/src/api/v6/osrm_api.rs | 37 ++++------------------- 1 file changed, 6 insertions(+), 31 deletions(-) diff --git a/services/travelmux/src/api/v6/osrm_api.rs b/services/travelmux/src/api/v6/osrm_api.rs index feee4a87b..cf49d3c87 100644 --- a/services/travelmux/src/api/v6/osrm_api.rs +++ b/services/travelmux/src/api/v6/osrm_api.rs @@ -414,42 +414,17 @@ pub enum BannerManeuverModifier { #[serde(rename_all = "camelCase", tag = "type")] #[non_exhaustive] pub enum BannerComponent { + /// The component bears the name of a place or street. Text(VisualInstructionComponent), - // Icon(VisualInstructionComponent), - Delimiter(VisualInstructionComponent), - // #[serde(rename="exit-number")] - // ExitNumber(VisualInstructionComponent), - // Exit(VisualInstructionComponent), - Lane(LaneInstructionComponent), -} -#[derive(Debug, Serialize, PartialEq, Clone)] -#[serde(rename_all = "camelCase")] -pub struct LaneInstructionComponent {} - -// Maybe we won't use this? Because it'll need to be implicit in the containing BannerComponent enum variant of -#[derive(Debug, Serialize, PartialEq, Clone)] -#[serde(rename_all = "lowercase")] -pub enum VisualInstructionComponentType { /// The component separates two other destination components. /// /// If the two adjacent components are both displayed as images, you can hide this delimiter component. - Delimiter, - - /// The component bears the name of a place or street. - Text, - - /// Component contains an image that should be rendered. - Image, - - /// The component contains the localized word for "exit". - /// - /// This component may appear before or after an `.ExitCode` component, depending on the language. - Exit, - - /// A component contains an exit number. - #[serde(rename = "exit-number")] - ExitCode, + Delimiter(VisualInstructionComponent), + // #[serde(rename="exit-number")] + // ExitNumber(VisualInstructionComponent), + // Exit(VisualInstructionComponent), + // Lane(LaneInstructionComponent), } #[derive(Debug, Serialize, PartialEq, Clone)] From 68d504b7a0651e0aca3e9a1046688f4e5012aed0 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 29 May 2024 11:48:31 -0700 Subject: [PATCH 17/24] re-organize utils --- services/travelmux/src/api/v4/plan.rs | 6 +- services/travelmux/src/api/v5/plan.rs | 6 +- services/travelmux/src/api/v6/osrm_api.rs | 4 +- services/travelmux/src/api/v6/plan.rs | 28 +----- services/travelmux/src/util/mod.rs | 114 ++++------------------ services/travelmux/src/util/serde_util.rs | 77 +++++++++++++++ 6 files changed, 109 insertions(+), 126 deletions(-) create mode 100644 services/travelmux/src/util/serde_util.rs diff --git a/services/travelmux/src/api/v4/plan.rs b/services/travelmux/src/api/v4/plan.rs index 465bdae2d..65eae8e9d 100644 --- a/services/travelmux/src/api/v4/plan.rs +++ b/services/travelmux/src/api/v4/plan.rs @@ -11,10 +11,10 @@ use super::error::{PlanResponseErr, PlanResponseOk}; use crate::api::AppState; use crate::error::ErrorType; use crate::otp::otp_api; -use crate::util::{ - deserialize_point_from_lat_lon, extend_bounds, serialize_rect_to_lng_lat, - serialize_system_time_as_millis, system_time_from_millis, +use crate::util::serde_util::{ + deserialize_point_from_lat_lon, serialize_rect_to_lng_lat, serialize_system_time_as_millis, }; +use crate::util::{extend_bounds, system_time_from_millis}; use crate::valhalla::valhalla_api; use crate::valhalla::valhalla_api::{LonLat, ManeuverType}; use crate::{DistanceUnit, Error, TravelMode}; diff --git a/services/travelmux/src/api/v5/plan.rs b/services/travelmux/src/api/v5/plan.rs index 80746b1f3..28bae2e9c 100644 --- a/services/travelmux/src/api/v5/plan.rs +++ b/services/travelmux/src/api/v5/plan.rs @@ -14,10 +14,10 @@ use crate::error::ErrorType; use crate::otp::otp_api; use crate::otp::otp_api::{AbsoluteDirection, RelativeDirection}; use crate::util::format::format_meters; -use crate::util::{ - deserialize_point_from_lat_lon, extend_bounds, serialize_rect_to_lng_lat, - serialize_system_time_as_millis, system_time_from_millis, +use crate::util::serde_util::{ + deserialize_point_from_lat_lon, serialize_rect_to_lng_lat, serialize_system_time_as_millis, }; +use crate::util::{extend_bounds, system_time_from_millis}; use crate::valhalla::valhalla_api; use crate::valhalla::valhalla_api::{LonLat, ManeuverType}; use crate::{DistanceUnit, Error, TravelMode}; diff --git a/services/travelmux/src/api/v6/osrm_api.rs b/services/travelmux/src/api/v6/osrm_api.rs index cf49d3c87..47d86ec94 100644 --- a/services/travelmux/src/api/v6/osrm_api.rs +++ b/services/travelmux/src/api/v6/osrm_api.rs @@ -2,7 +2,9 @@ //! which happens to be strongly influenced by OSRM. use super::plan::{Itinerary, Leg, Maneuver, ModeLeg}; -use crate::util::{serialize_line_string_as_polyline6, serialize_point_as_lon_lat_pair}; +use crate::util::serde_util::{ + serialize_line_string_as_polyline6, serialize_point_as_lon_lat_pair, +}; use crate::valhalla::valhalla_api::ManeuverType; use crate::{DistanceUnit, TravelMode}; use geo::{LineString, Point}; diff --git a/services/travelmux/src/api/v6/plan.rs b/services/travelmux/src/api/v6/plan.rs index 3e504e462..0c7fa58e6 100644 --- a/services/travelmux/src/api/v6/plan.rs +++ b/services/travelmux/src/api/v6/plan.rs @@ -16,30 +16,15 @@ use crate::otp::otp_api; use crate::otp::otp_api::{AbsoluteDirection, RelativeDirection}; use crate::util::format::format_meters; use crate::util::haversine_segmenter::HaversineSegmenter; -use crate::util::{ - deserialize_point_from_lat_lon, extend_bounds, serialize_line_string_as_polyline6, - serialize_rect_to_lng_lat, serialize_system_time_as_millis, system_time_from_millis, +use crate::util::serde_util::{ + deserialize_point_from_lat_lon, serialize_line_string_as_polyline6, serialize_rect_to_lng_lat, + serialize_system_time_as_millis, }; +use crate::util::{convert_from_meters, convert_to_meters, extend_bounds, system_time_from_millis}; use crate::valhalla::valhalla_api; use crate::valhalla::valhalla_api::{LonLat, ManeuverType}; use crate::{DistanceUnit, Error, TravelMode}; -const METERS_PER_MILE: f64 = 1609.34; - -fn convert_from_meters(meters: f64, output_units: DistanceUnit) -> f64 { - match output_units { - DistanceUnit::Kilometers => meters / 1000.0, - DistanceUnit::Miles => meters / METERS_PER_MILE, - } -} - -fn convert_to_meters(distance: f64, input_units: DistanceUnit) -> f64 { - match input_units { - DistanceUnit::Kilometers => distance * 1000.0, - DistanceUnit::Miles => distance * METERS_PER_MILE, - } -} - #[derive(Debug, Deserialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub struct PlanQuery { @@ -89,10 +74,7 @@ pub struct Itinerary { impl Itinerary { pub fn distance_meters(&self) -> f64 { - match self.distance_units { - DistanceUnit::Kilometers => self.distance * 1000.0, - DistanceUnit::Miles => self.distance * METERS_PER_MILE, - } + convert_to_meters(self.distance, self.distance_units) } pub fn combined_geometry(&self) -> LineString { diff --git a/services/travelmux/src/util/mod.rs b/services/travelmux/src/util/mod.rs index fb4928bcf..d313fdd80 100644 --- a/services/travelmux/src/util/mod.rs +++ b/services/travelmux/src/util/mod.rs @@ -1,116 +1,38 @@ pub mod format; pub mod haversine_segmenter; +pub(crate) mod serde_util; -use geo::{Point, Rect}; -use serde::{ - ser::{Error, SerializeStruct, SerializeTuple}, - Deserialize, Deserializer, Serializer, -}; +use crate::DistanceUnit; use std::time::{Duration, SystemTime}; -pub fn deserialize_point_from_lat_lon<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - use serde::de::Error; - - let s: String = Deserialize::deserialize(deserializer)?; - use std::str::FromStr; - let mut iter = s.split(',').map(f64::from_str); - - let Some(lat_res) = iter.next() else { - return Err(D::Error::custom("missing lat")); - }; - let lat = lat_res.map_err(|e| D::Error::custom(format!("invalid lat: {e}")))?; - - let Some(lon_res) = iter.next() else { - return Err(D::Error::custom("missing lon")); - }; - let lon = lon_res.map_err(|e| D::Error::custom(format!("invalid lon: {e}")))?; - - if let Some(next) = iter.next() { - return Err(D::Error::custom(format!( - "found an extra param in lat,lon,???: {next:?}" - ))); - } - - Ok(Point::new(lon, lat)) -} -pub fn serialize_point_as_lon_lat_pair(point: &Point, serializer: S) -> Result -where - S: Serializer, -{ - let mut tuple_serializer = serializer.serialize_tuple(2)?; - tuple_serializer.serialize_element(&point.x())?; - tuple_serializer.serialize_element(&point.y())?; - tuple_serializer.end() -} - -pub fn serialize_line_string_as_polyline6( - line_string: &geo::LineString, - serializer: S, -) -> Result -where - S: Serializer, -{ - let string = - polyline::encode_coordinates(line_string.0.iter().copied(), 6).map_err(S::Error::custom)?; - serializer.serialize_str(&string) -} - -pub fn deserialize_duration_from_seconds<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - let seconds = u64::deserialize(deserializer)?; - Ok(Duration::from_secs(seconds)) -} - -pub fn serialize_duration_as_seconds( - duration: &Duration, - serializer: S, -) -> Result -where - S: Serializer, -{ - serializer.serialize_u64(duration.as_secs()) -} - -pub fn extend_bounds(bounds: &mut Rect, extension: &Rect) { +pub fn extend_bounds(bounds: &mut geo::Rect, extension: &geo::Rect) { let min_x = f64::min(bounds.min().x, extension.min().x); let min_y = f64::min(bounds.min().y, extension.min().y); let max_x = f64::max(bounds.max().x, extension.max().x); let max_y = f64::max(bounds.max().y, extension.max().y); - let mut new_bounds = Rect::new( + let mut new_bounds = geo::Rect::new( geo::coord! { x: min_x, y: min_y}, geo::coord! { x: max_x, y: max_y }, ); std::mem::swap(bounds, &mut new_bounds); } -pub fn serialize_rect_to_lng_lat( - rect: &Rect, - serializer: S, -) -> Result { - let mut struct_serializer = serializer.serialize_struct("BBox", 2)?; - struct_serializer.serialize_field("min", &[rect.min().x, rect.min().y])?; - struct_serializer.serialize_field("max", &[rect.max().x, rect.max().y])?; - struct_serializer.end() +pub fn system_time_from_millis(millis: u64) -> SystemTime { + std::time::UNIX_EPOCH + Duration::from_millis(millis) } -pub fn serialize_system_time_as_millis( - time: &SystemTime, - serializer: S, -) -> Result -where - S: Serializer, -{ - let since_epoch = time - .duration_since(std::time::UNIX_EPOCH) - .map_err(|_e| S::Error::custom("time is before epoch"))?; - serializer.serialize_u64(since_epoch.as_millis() as u64) +const METERS_PER_MILE: f64 = 1609.34; + +pub fn convert_from_meters(meters: f64, output_units: DistanceUnit) -> f64 { + match output_units { + DistanceUnit::Kilometers => meters / 1000.0, + DistanceUnit::Miles => meters / METERS_PER_MILE, + } } -pub fn system_time_from_millis(millis: u64) -> SystemTime { - std::time::UNIX_EPOCH + Duration::from_millis(millis) +pub fn convert_to_meters(distance: f64, input_units: DistanceUnit) -> f64 { + match input_units { + DistanceUnit::Kilometers => distance * 1000.0, + DistanceUnit::Miles => distance * METERS_PER_MILE, + } } diff --git a/services/travelmux/src/util/serde_util.rs b/services/travelmux/src/util/serde_util.rs new file mode 100644 index 000000000..b5c3bd1bb --- /dev/null +++ b/services/travelmux/src/util/serde_util.rs @@ -0,0 +1,77 @@ +use geo::{Point, Rect}; +use serde::ser::{Error, SerializeStruct, SerializeTuple}; +use serde::{Deserialize, Deserializer, Serializer}; +use std::time::SystemTime; + +pub fn deserialize_point_from_lat_lon<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + use serde::de::Error; + + let s: String = Deserialize::deserialize(deserializer)?; + use std::str::FromStr; + let mut iter = s.split(',').map(f64::from_str); + + let Some(lat_res) = iter.next() else { + return Err(D::Error::custom("missing lat")); + }; + let lat = lat_res.map_err(|e| D::Error::custom(format!("invalid lat: {e}")))?; + + let Some(lon_res) = iter.next() else { + return Err(D::Error::custom("missing lon")); + }; + let lon = lon_res.map_err(|e| D::Error::custom(format!("invalid lon: {e}")))?; + + if let Some(next) = iter.next() { + return Err(D::Error::custom(format!( + "found an extra param in lat,lon,???: {next:?}" + ))); + } + + Ok(Point::new(lon, lat)) +} +pub fn serialize_point_as_lon_lat_pair(point: &Point, serializer: S) -> Result +where + S: Serializer, +{ + let mut tuple_serializer = serializer.serialize_tuple(2)?; + tuple_serializer.serialize_element(&point.x())?; + tuple_serializer.serialize_element(&point.y())?; + tuple_serializer.end() +} + +pub fn serialize_line_string_as_polyline6( + line_string: &geo::LineString, + serializer: S, +) -> Result +where + S: Serializer, +{ + let string = + polyline::encode_coordinates(line_string.0.iter().copied(), 6).map_err(S::Error::custom)?; + serializer.serialize_str(&string) +} + +pub fn serialize_rect_to_lng_lat( + rect: &Rect, + serializer: S, +) -> Result { + let mut struct_serializer = serializer.serialize_struct("BBox", 2)?; + struct_serializer.serialize_field("min", &[rect.min().x, rect.min().y])?; + struct_serializer.serialize_field("max", &[rect.max().x, rect.max().y])?; + struct_serializer.end() +} + +pub fn serialize_system_time_as_millis( + time: &SystemTime, + serializer: S, +) -> Result +where + S: Serializer, +{ + let since_epoch = time + .duration_since(std::time::UNIX_EPOCH) + .map_err(|_e| S::Error::custom("time is before epoch"))?; + serializer.serialize_u64(since_epoch.as_millis() as u64) +} From a3d661b2e4e45000b95fc978356b785518622f56 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 29 May 2024 14:15:57 -0700 Subject: [PATCH 18/24] orient around maplibre naming --- services/travelmux/src/api/v6/osrm_api.rs | 128 ++++++++++++++-------- 1 file changed, 81 insertions(+), 47 deletions(-) diff --git a/services/travelmux/src/api/v6/osrm_api.rs b/services/travelmux/src/api/v6/osrm_api.rs index 47d86ec94..b7856a033 100644 --- a/services/travelmux/src/api/v6/osrm_api.rs +++ b/services/travelmux/src/api/v6/osrm_api.rs @@ -141,7 +141,7 @@ pub struct RouteStep { pub maneuver: StepManeuver, /// A list of `BannerInstruction` objects that represent all signs on the step. - pub banner_instructions: Option>, + pub banner_instructions: Option>, /// A list of `Intersection` objects that are passed along the segment, the very first belonging to the `StepManeuver` pub intersections: Option>, @@ -154,8 +154,11 @@ impl RouteStep { mode: TravelMode, from_distance_unit: DistanceUnit, ) -> Self { - let banner_instructions = - BannerInstruction::from_maneuver(&maneuver, next_maneuver.as_ref(), from_distance_unit); + let banner_instructions = VisualInstructionBanner::from_maneuver( + &maneuver, + next_maneuver.as_ref(), + from_distance_unit, + ); RouteStep { distance: maneuver.distance_meters(from_distance_unit), duration: maneuver.duration_seconds, @@ -179,14 +182,14 @@ impl RouteStep { #[derive(Debug, Serialize, PartialEq, Clone)] #[serde(rename_all = "camelCase")] -pub struct BannerInstruction { +pub struct VisualInstructionBanner { pub distance_along_geometry: f64, - pub primary: BannerInstructionContent, + pub primary: VisualInstruction, // secondary: Option, // sub: Option, } -impl BannerInstruction { +impl VisualInstructionBanner { fn from_maneuver( maneuver: &Maneuver, next_maneuver: Option<&Maneuver>, @@ -208,11 +211,15 @@ impl BannerInstruction { maneuver.instruction.as_ref().map(|s| vec![s.clone()]) }; - let banner_maneuver = (|| { - use BannerManeuverModifier::*; - use BannerManeuverType::*; - let (banner_type, modifier) = match next_maneuver.unwrap_or(maneuver).r#type { - ManeuverType::None => return None, + let (maneuver_type, maneuver_direction): ( + Option, + Option, + ) = (|| { + use ManeuverDirection::*; + use OSRMManeuverType::*; + let (maneuver_type, maneuver_direction) = match next_maneuver.unwrap_or(maneuver).r#type + { + ManeuverType::None => return (None, None), ManeuverType::Start => (Depart, None), ManeuverType::StartRight => (Depart, Some(Right)), ManeuverType::StartLeft => (Depart, Some(Left)), @@ -257,10 +264,7 @@ impl BannerInstruction { ManeuverType::BuildingEnter => (Notification, None), ManeuverType::BuildingExit => (Notification, None), }; - Some(BannerManeuver { - r#type: banner_type, - modifier, - }) + (Some(maneuver_type), maneuver_direction) })(); let text = text_components.as_ref().map(|t| t.join("/")); @@ -290,14 +294,15 @@ impl BannerInstruction { })], ); - let primary = BannerInstructionContent { + let primary = VisualInstruction { text: text.unwrap_or_default(), components, - banner_maneuver, + maneuver_type, + maneuver_direction, degrees: None, driving_side: None, }; - let instruction = BannerInstruction { + let instruction = VisualInstructionBanner { distance_along_geometry: maneuver.distance_meters(from_distance_unit), primary, }; @@ -305,44 +310,46 @@ impl BannerInstruction { } } -// REVIEW: Rename to VisualInstructionBanner? // How do audible instructions fit into this? #[derive(Debug, Serialize, PartialEq, Clone)] #[serde(rename_all = "camelCase")] -pub struct BannerInstructionContent { +pub struct VisualInstruction { pub text: String, - // components: Vec, - #[serde(flatten)] - pub banner_maneuver: Option, + #[serde(rename = "type")] + pub maneuver_type: Option, + #[serde(rename = "modifier")] + pub maneuver_direction: Option, pub degrees: Option, pub driving_side: Option, pub components: Vec, } -#[derive(Debug, Serialize, PartialEq, Clone)] -#[serde(rename_all = "lowercase")] -pub struct BannerManeuver { - pub r#type: BannerManeuverType, - pub modifier: Option, -} - // This is for `banner.primary(et. al).type` // There is a lot of overlap between this and `step_maneuver.type`, // but the docs imply they are different. #[derive(Debug, Serialize, PartialEq, Clone)] #[serde(rename_all = "lowercase")] -pub enum BannerManeuverType { - /// A turn in the direction of the modifier. +pub enum OSRMManeuverType { + /// The step requires the user to turn. + /// + /// The maneuver direction indicates the direction in which the user must turn relative to the current direction of travel. + /// The exit index indicates the number of intersections, large or small, from the previous maneuver up to and including the intersection at which the user must turn. Turn, - /// The road name changes (after a mandatory turn). + /// The step requires the user to continue on the current road as it changes names. + /// + /// The step’s name contains the road’s new name. To get the road’s old name, use the previous step’s name. #[serde(rename = "new name")] NewName, - /// Merge onto a street. + /// The step requires the user to merge onto another road. + /// + /// The maneuver direction indicates the side from which the other road approaches the intersection relative to the user. Merge, - /// Indicates departure from a leg. The modifier value indicates the position of the departure point compared to the current direction of travel. + /// The step requires the user to depart from a waypoint. + /// + /// If the waypoint is some distance away from the nearest road, the maneuver direction indicates the direction the user must turn upon reaching the road. Depart, /// Indicates arrival to a destination of a leg. The modifier value indicates the position of the arrival point compared to the current direction of travel. @@ -351,19 +358,32 @@ pub enum BannerManeuverType { /// Keep left or right side at a bifurcation, or left/right/straight at a trifurcation. Fork, - /// Take a ramp to enter a highway. + /// The step requires the user to take a entrance ramp (slip road) onto a highway. #[serde(rename = "on ramp")] OnRamp, - /// Take a ramp to exit a highway. + /// The step requires the user to take an exit ramp (slip road) off a highway. + /// + /// The maneuver direction indicates the side of the highway from which the user must exit. + /// The exit index indicates the number of highway exits from the previous maneuver up to and including the exit that the user must take. #[serde(rename = "off ramp")] OffRamp, - /// Road ends in a T intersection. + /// The step requires the user to turn at either a T-shaped three-way intersection or a sharp bend in the road where the road also changes names. + /// + /// This maneuver type is called out separately so that the user may be able to proceed more confidently, without fear of having overshot the turn. If this distinction is unimportant to you, you may treat the maneuver as an ordinary `turn`. #[allow(unused)] #[serde(rename = "end of road")] EndOfRoad, + /// The step requires the user to get into a specific lane in order to continue along the current road. + /// The maneuver direction is set to `straightAhead`. Each of the first intersection’s usable approach lanes also has an indication of `straightAhead`. A maneuver in a different direction would instead have a maneuver type of `turn`. + // + /// This maneuver type is called out separately so that the application can present the user with lane guidance based on the first element in the `intersections` property. If lane guidance is unimportant to you, you may treat the maneuver as an ordinary `continue` or ignore it. + #[serde(rename = "use lane")] + #[allow(unused)] + UseLane, + /// Continue on a street after a turn. Continue, @@ -384,18 +404,28 @@ pub enum BannerManeuverType { #[serde(rename = "exit rotary")] RotaryExit, - /// A small roundabout that is treated as an intersection. + /// The step requires the user to enter and exit a roundabout (traffic circle or rotary) that is compact enough to constitute a single intersection. + /// + /// The step’s name is the name of the road to take after exiting the roundabout. + /// This maneuver type is called out separately because the user may perceive the roundabout as an ordinary intersection with an island in the middle. + /// If this distinction is unimportant to you, you may treat the maneuver as either an ordinary `turn` or as a `takeRoundabout`. #[allow(unused)] #[serde(rename = "roundabout turn")] RoundaboutTurn, - /// Indicates a change of driving conditions, for example changing the mode from driving to ferry. + /// The step requires the user to respond to a change in travel conditions. + /// + /// This maneuver type may occur for example when driving directions require the user to board a ferry, or when cycling directions require the user to dismount. + /// The step’s transport type and instructions contains important contextual details that should be presented to the user at the maneuver location. + /// + /// Similar changes can occur simultaneously with other maneuvers, such as when the road changes its name at the site of a movable bridge. + /// In such cases, `notification` is suppressed in favor of another maneuver type. Notification, } #[derive(Debug, Serialize, PartialEq, Clone)] #[serde(rename_all = "lowercase")] -pub enum BannerManeuverModifier { +pub enum ManeuverDirection { Uturn, #[serde(rename = "sharp right")] SharpRight, @@ -410,8 +440,10 @@ pub enum BannerManeuverModifier { SharpLeft, } -// REVIEW: Rename to VisualInstruction? -// REVIEW: convert to inner enum of Lane or VisualInstructionComponent +//`BannerComponent` is kind of a corollary to ComponentRepresentable protocol from maplibre which +// has a `type: VisualInstructionComponentType` field, whereas here we have a variant for each type, +// with the associated value of the VisualInstructionComponent. +// Plus we have enum variant for the other implementors of ComponentRepresentable #[derive(Debug, Serialize, PartialEq, Clone)] #[serde(rename_all = "camelCase", tag = "type")] #[non_exhaustive] @@ -423,12 +455,14 @@ pub enum BannerComponent { /// /// If the two adjacent components are both displayed as images, you can hide this delimiter component. Delimiter(VisualInstructionComponent), - // #[serde(rename="exit-number")] - // ExitNumber(VisualInstructionComponent), - // Exit(VisualInstructionComponent), - // Lane(LaneInstructionComponent), + + #[allow(unused)] + Lane(LaneIndicationComponent), } +#[derive(Debug, Serialize, PartialEq, Clone)] +pub struct LaneIndicationComponent {} + #[derive(Debug, Serialize, PartialEq, Clone)] #[serde(rename_all = "camelCase")] pub struct VisualInstructionComponent { From 7cb53f04425a121f0f9d31a8be591fe01465e13e Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 29 May 2024 14:40:35 -0700 Subject: [PATCH 19/24] Separate MeasurementSystem from DistanceUnit --- services/travelmux/src/api/v5/plan.rs | 6 +- services/travelmux/src/api/v6/plan.rs | 5 +- services/travelmux/src/lib.rs | 17 +++++ services/travelmux/src/util/format.rs | 105 ++++++++++++++++++-------- services/travelmux/src/util/mod.rs | 2 + 5 files changed, 97 insertions(+), 38 deletions(-) diff --git a/services/travelmux/src/api/v5/plan.rs b/services/travelmux/src/api/v5/plan.rs index 28bae2e9c..9d85abbd5 100644 --- a/services/travelmux/src/api/v5/plan.rs +++ b/services/travelmux/src/api/v5/plan.rs @@ -337,6 +337,10 @@ impl Maneuver { }; let localized_distance = match distance_unit { DistanceUnit::Kilometers => otp.distance, + DistanceUnit::Meters => { + debug_assert!(false, "v5 API doesn't expect Meters as distance_unit. prior to v6, kilometers was used as if it were meters."); + otp.distance + } // round to the nearest ten-thousandth DistanceUnit::Miles => (otp.distance * 0.621371 * 10_000.0).round() / 10_000.0, }; @@ -412,7 +416,7 @@ fn build_verbal_post_transition_instruction( } else { Some(format!( "Continue for {}.", - format_meters(distance, distance_unit) + format_meters(distance, distance_unit.measurement_system()) )) } } diff --git a/services/travelmux/src/api/v6/plan.rs b/services/travelmux/src/api/v6/plan.rs index 0c7fa58e6..2aa7eb11b 100644 --- a/services/travelmux/src/api/v6/plan.rs +++ b/services/travelmux/src/api/v6/plan.rs @@ -63,9 +63,6 @@ pub struct Itinerary { end_time: SystemTime, /// Units are in `distance_units` distance: f64, - /// FIXME: I think we're returning meters even though distance unit is "Kilometers" - /// Probably we should rename DistanceUnit::Kilometers to DistanceUnit::Meters - /// This is passed as a parameter though, so it'd be a breaking change. pub(crate) distance_units: DistanceUnit, #[serde(serialize_with = "serialize_rect_to_lng_lat")] bounds: Rect, @@ -451,7 +448,7 @@ fn build_verbal_post_transition_instruction( } else { Some(format!( "Continue for {}.", - format_meters(distance, distance_unit) + format_meters(distance, distance_unit.measurement_system()) )) } } diff --git a/services/travelmux/src/lib.rs b/services/travelmux/src/lib.rs index bf9889ec6..30e9a4dff 100644 --- a/services/travelmux/src/lib.rs +++ b/services/travelmux/src/lib.rs @@ -19,6 +19,23 @@ pub enum TravelMode { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Copy)] #[serde(rename_all = "lowercase")] pub enum DistanceUnit { + Meters, Kilometers, Miles, } + +impl DistanceUnit { + fn measurement_system(&self) -> MeasurementSystem { + match self { + DistanceUnit::Meters | DistanceUnit::Kilometers => MeasurementSystem::Metric, + DistanceUnit::Miles => MeasurementSystem::Imperial, + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Copy)] +#[serde(rename_all = "lowercase")] +pub enum MeasurementSystem { + Metric, + Imperial, +} diff --git a/services/travelmux/src/util/format.rs b/services/travelmux/src/util/format.rs index e959f1205..cd08cc2ea 100644 --- a/services/travelmux/src/util/format.rs +++ b/services/travelmux/src/util/format.rs @@ -1,8 +1,8 @@ -use crate::DistanceUnit; +use crate::MeasurementSystem; -pub fn format_meters(meters: f64, unit: DistanceUnit) -> String { - match unit { - DistanceUnit::Kilometers => { +pub fn format_meters(meters: f64, output_system: MeasurementSystem) -> String { + match output_system { + MeasurementSystem::Metric => { if meters < 1.5 { "1 meter".to_string() } else if meters < 10.0 { @@ -24,7 +24,7 @@ pub fn format_meters(meters: f64, unit: DistanceUnit) -> String { } } } - DistanceUnit::Miles => { + MeasurementSystem::Imperial => { const METERS_PER_MILE: f64 = 1609.34; const METERS_PER_FOOT: f64 = 0.3048; if meters < METERS_PER_MILE * 0.125 { @@ -59,57 +59,96 @@ mod tests { #[test] fn meter_formatting() { - assert_eq!(format_meters(1.0, DistanceUnit::Kilometers), "1 meter"); - assert_eq!(format_meters(2.6, DistanceUnit::Kilometers), "3 meters"); - assert_eq!(format_meters(99.0, DistanceUnit::Kilometers), "100 meters"); - assert_eq!(format_meters(599.0, DistanceUnit::Kilometers), "600 meters"); - assert_eq!(format_meters(600.0, DistanceUnit::Kilometers), "600 meters"); - assert_eq!(format_meters(900.0, DistanceUnit::Kilometers), "900 meters"); - assert_eq!( - format_meters(960.0, DistanceUnit::Kilometers), + assert_eq!(format_meters(1.0, MeasurementSystem::Metric), "1 meter"); + assert_eq!(format_meters(2.6, MeasurementSystem::Metric), "3 meters"); + assert_eq!(format_meters(99.0, MeasurementSystem::Metric), "100 meters"); + assert_eq!( + format_meters(599.0, MeasurementSystem::Metric), + "600 meters" + ); + assert_eq!( + format_meters(600.0, MeasurementSystem::Metric), + "600 meters" + ); + assert_eq!( + format_meters(900.0, MeasurementSystem::Metric), + "900 meters" + ); + assert_eq!( + format_meters(960.0, MeasurementSystem::Metric), "1 kilometer" ); assert_eq!( - format_meters(1049.0, DistanceUnit::Kilometers), + format_meters(1049.0, MeasurementSystem::Metric), "1 kilometer" ); assert_eq!( - format_meters(1100.0, DistanceUnit::Kilometers), + format_meters(1100.0, MeasurementSystem::Metric), "1.1 kilometers" ); assert_eq!( - format_meters(9940.0, DistanceUnit::Kilometers), + format_meters(9940.0, MeasurementSystem::Metric), "9.9 kilometers" ); assert_eq!( - format_meters(9999.0, DistanceUnit::Kilometers), + format_meters(9999.0, MeasurementSystem::Metric), "10 kilometers" ); assert_eq!( - format_meters(10000.0, DistanceUnit::Kilometers), + format_meters(10000.0, MeasurementSystem::Metric), "10 kilometers" ); assert_eq!( - format_meters(100000.0, DistanceUnit::Kilometers), + format_meters(100000.0, MeasurementSystem::Metric), "100 kilometers" ); } #[test] fn format_miles_from_meters() { - assert_eq!(format_meters(1.0, DistanceUnit::Miles), "10 feet"); - assert_eq!(format_meters(10.0, DistanceUnit::Miles), "30 feet"); - assert_eq!(format_meters(50.0, DistanceUnit::Miles), "160 feet"); - assert_eq!(format_meters(100.0, DistanceUnit::Miles), "300 feet"); - assert_eq!(format_meters(500.0, DistanceUnit::Miles), "a quarter mile"); - assert_eq!(format_meters(1000.0, DistanceUnit::Miles), "a half mile"); - assert_eq!(format_meters(1100.0, DistanceUnit::Miles), "0.7 miles"); - assert_eq!(format_meters(1300.0, DistanceUnit::Miles), "0.8 miles"); - assert_eq!(format_meters(1700.0, DistanceUnit::Miles), "1 mile"); - assert_eq!(format_meters(1800.0, DistanceUnit::Miles), "1.1 miles"); - assert_eq!(format_meters(2000.0, DistanceUnit::Miles), "1.2 miles"); - assert_eq!(format_meters(16000.0, DistanceUnit::Miles), "9.9 miles"); - assert_eq!(format_meters(16500.0, DistanceUnit::Miles), "10 miles"); - assert_eq!(format_meters(20000.0, DistanceUnit::Miles), "12 miles"); + assert_eq!(format_meters(1.0, MeasurementSystem::Imperial), "10 feet"); + assert_eq!(format_meters(10.0, MeasurementSystem::Imperial), "30 feet"); + assert_eq!(format_meters(50.0, MeasurementSystem::Imperial), "160 feet"); + assert_eq!( + format_meters(100.0, MeasurementSystem::Imperial), + "300 feet" + ); + assert_eq!( + format_meters(500.0, MeasurementSystem::Imperial), + "a quarter mile" + ); + assert_eq!( + format_meters(1000.0, MeasurementSystem::Imperial), + "a half mile" + ); + assert_eq!( + format_meters(1100.0, MeasurementSystem::Imperial), + "0.7 miles" + ); + assert_eq!( + format_meters(1300.0, MeasurementSystem::Imperial), + "0.8 miles" + ); + assert_eq!(format_meters(1700.0, MeasurementSystem::Imperial), "1 mile"); + assert_eq!( + format_meters(1800.0, MeasurementSystem::Imperial), + "1.1 miles" + ); + assert_eq!( + format_meters(2000.0, MeasurementSystem::Imperial), + "1.2 miles" + ); + assert_eq!( + format_meters(16000.0, MeasurementSystem::Imperial), + "9.9 miles" + ); + assert_eq!( + format_meters(16500.0, MeasurementSystem::Imperial), + "10 miles" + ); + assert_eq!( + format_meters(20000.0, MeasurementSystem::Imperial), + "12 miles" + ); } } diff --git a/services/travelmux/src/util/mod.rs b/services/travelmux/src/util/mod.rs index d313fdd80..83c8fb1cf 100644 --- a/services/travelmux/src/util/mod.rs +++ b/services/travelmux/src/util/mod.rs @@ -25,6 +25,7 @@ const METERS_PER_MILE: f64 = 1609.34; pub fn convert_from_meters(meters: f64, output_units: DistanceUnit) -> f64 { match output_units { + DistanceUnit::Meters => meters, DistanceUnit::Kilometers => meters / 1000.0, DistanceUnit::Miles => meters / METERS_PER_MILE, } @@ -32,6 +33,7 @@ pub fn convert_from_meters(meters: f64, output_units: DistanceUnit) -> f64 { pub fn convert_to_meters(distance: f64, input_units: DistanceUnit) -> f64 { match input_units { + DistanceUnit::Meters => distance, DistanceUnit::Kilometers => distance * 1000.0, DistanceUnit::Miles => distance * METERS_PER_MILE, } From 6b9c3171ea78234cb78ab7e988f62f70cccfb0a3 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 29 May 2024 15:13:17 -0700 Subject: [PATCH 20/24] www uses travelmux/v6 --- services/frontend/www-app/src/services/TravelmuxClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/frontend/www-app/src/services/TravelmuxClient.ts b/services/frontend/www-app/src/services/TravelmuxClient.ts index b537d85f1..894c7d5b1 100644 --- a/services/frontend/www-app/src/services/TravelmuxClient.ts +++ b/services/frontend/www-app/src/services/TravelmuxClient.ts @@ -129,7 +129,7 @@ export class TravelmuxClient { const query = new URLSearchParams(params).toString(); - const response = await fetch('/travelmux/v5/plan?' + query); + const response = await fetch('/travelmux/v6/plan?' + query); if (response.ok) { const travelmuxResponseJson: TravelmuxPlanResponse = From f5e499462370df9aa4573c628ee1b3a9f5f55df7 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 29 May 2024 15:30:29 -0700 Subject: [PATCH 21/24] handle missing test cases --- .../travelmux/src/util/haversine_segmenter.rs | 53 +++++++++++++------ 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/services/travelmux/src/util/haversine_segmenter.rs b/services/travelmux/src/util/haversine_segmenter.rs index 996cd87e2..057ddeb02 100644 --- a/services/travelmux/src/util/haversine_segmenter.rs +++ b/services/travelmux/src/util/haversine_segmenter.rs @@ -16,7 +16,10 @@ impl HaversineSegmenter { } } pub fn next_segment(&mut self, distance_meters: f64) -> Option { - // REVIEW: Handle case with linestring of 1 point? + if self.geometry.0.is_empty() { + return None; + } + if self.next_index == self.geometry.0.len() - 1 { return None; } @@ -58,47 +61,63 @@ impl HaversineSegmenter { mod test { use super::*; use approx::assert_relative_eq; - use geo::{point, HaversineLength}; + use geo::{coord, HaversineLength}; #[test] - fn test_segmenter() { + fn test_happy_path() { // paris to berlin (878km) to prague - let paris = point!(x: 2.3514, y: 48.8575); - let berlin = point!(x: 13.4050, y: 52.5200); - let prague = point!(x: 14.4378, y: 50.0755); + let paris = coord!(x: 2.3514, y: 48.8575); + let berlin = coord!(x: 13.4050, y: 52.5200); + let prague = coord!(x: 14.4378, y: 50.0755); - let paris_to_berlin_distance = LineString::new(vec![paris.0, berlin.0]).haversine_length(); + let paris_to_berlin_distance = LineString::new(vec![paris, berlin]).haversine_length(); assert_relative_eq!(paris_to_berlin_distance, 877461.0, epsilon = 1.0); - let line_string = LineString::new(vec![paris.0, berlin.0, prague.0]); + let line_string = LineString::new(vec![paris, berlin, prague]); let total_distance = line_string.haversine_length(); assert_relative_eq!(total_distance, 1_158_595.0, epsilon = 1.0); let mut segmenter = HaversineSegmenter::new(line_string); - let east_of_paris = point!(x: 2.467660089582291, y: 48.90485360250366); + let east_of_paris = coord!(x: 2.467660089582291, y: 48.90485360250366); let segment_1 = segmenter.next_segment(10_000.0).unwrap(); assert_relative_eq!(segment_1.haversine_length(), 10_000.0, epsilon = 1e-9); - assert_relative_eq!(segment_1, LineString::new(vec![paris.0, east_of_paris.0])); + assert_relative_eq!(segment_1, LineString::new(vec![paris, east_of_paris])); // next one should pick up where the last one left off let segment_2 = segmenter.next_segment(10_000.0).unwrap(); assert_eq!(segment_1.0.last().unwrap(), segment_2.0.first().unwrap()); - let east_of_berlin = point!(x: 13.482210264987538, y: 52.34640526357316); + let east_of_berlin = coord!(x: 13.482210264987538, y: 52.34640526357316); let segment_3 = segmenter.next_segment(paris_to_berlin_distance).unwrap(); - let expected = LineString::new(vec![ - *segment_2.0.last().unwrap(), - berlin.0, - east_of_berlin.0, - ]); + let expected = LineString::new(vec![*segment_2.0.last().unwrap(), berlin, east_of_berlin]); assert_relative_eq!(segment_3, expected); // overshoot it let next = segmenter.next_segment(total_distance).unwrap(); - assert_relative_eq!(next, LineString::new(vec![east_of_berlin.0, prague.0])); + assert_relative_eq!(next, LineString::new(vec![east_of_berlin, prague])); let next = segmenter.next_segment(4.0); assert!(next.is_none()); } + + #[test] + fn degenerate_empty_line_string() { + let empty = LineString::new(vec![]); + assert_eq!(0.0, empty.haversine_length()); + + let mut segmenter = HaversineSegmenter::new(empty); + let first = segmenter.next_segment(10.0); + assert!(first.is_none()); + } + + #[test] + fn degenerate_line_string_with_1_pt() { + let empty = LineString::new(vec![coord!(x: 1.0, y: 2.0)]); + assert_eq!(0.0, empty.haversine_length()); + + let mut segmenter = HaversineSegmenter::new(empty); + let first = segmenter.next_segment(10.0); + assert!(first.is_none()); + } } From 68140bf4b08bb92428600c4907d04405ec2a9309 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 29 May 2024 15:43:47 -0700 Subject: [PATCH 22/24] improve docs --- services/travelmux/src/api/v6/directions.rs | 1 + services/travelmux/src/api/v6/osrm_api.rs | 18 +++---- services/travelmux/src/api/v6/plan.rs | 54 +++++++++++---------- 3 files changed, 38 insertions(+), 35 deletions(-) diff --git a/services/travelmux/src/api/v6/directions.rs b/services/travelmux/src/api/v6/directions.rs index 0f7a5e844..ef30006a0 100644 --- a/services/travelmux/src/api/v6/directions.rs +++ b/services/travelmux/src/api/v6/directions.rs @@ -5,6 +5,7 @@ use crate::api::AppState; use actix_web::{get, web, HttpRequest, HttpResponseBuilder}; use serde::Serialize; +/// Returns directions between locations in the format of the OSRM-ish API used by maplibre-directions. #[get("/v6/directions")] pub async fn get_directions( query: web::Query, diff --git a/services/travelmux/src/api/v6/osrm_api.rs b/services/travelmux/src/api/v6/osrm_api.rs index b7856a033..8386fb714 100644 --- a/services/travelmux/src/api/v6/osrm_api.rs +++ b/services/travelmux/src/api/v6/osrm_api.rs @@ -1,5 +1,5 @@ //! "osrm_api" is a bit of a misnomer. It's intended to work with maplibre's "Directions" library. -//! which happens to be strongly influenced by OSRM. +//! which is strongly influenced by OSRM. use super::plan::{Itinerary, Leg, Maneuver, ModeLeg}; use crate::util::serde_util::{ @@ -10,6 +10,7 @@ use crate::{DistanceUnit, TravelMode}; use geo::{LineString, Point}; use serde::Serialize; +/// The route between waypoints. #[derive(Debug, Serialize, PartialEq, Clone)] #[serde(rename_all = "camelCase")] pub struct Route { @@ -19,7 +20,7 @@ pub struct Route { /// The estimated travel time, in float number of seconds. pub duration: f64, - // todo: simplify? + // TODO: simplify? /// The entire geometry of the route #[serde(serialize_with = "serialize_line_string_as_polyline6")] pub geometry: LineString, @@ -310,7 +311,6 @@ impl VisualInstructionBanner { } } -// How do audible instructions fit into this? #[derive(Debug, Serialize, PartialEq, Clone)] #[serde(rename_all = "camelCase")] pub struct VisualInstruction { @@ -324,9 +324,6 @@ pub struct VisualInstruction { pub components: Vec, } -// This is for `banner.primary(et. al).type` -// There is a lot of overlap between this and `step_maneuver.type`, -// but the docs imply they are different. #[derive(Debug, Serialize, PartialEq, Clone)] #[serde(rename_all = "lowercase")] pub enum OSRMManeuverType { @@ -440,10 +437,11 @@ pub enum ManeuverDirection { SharpLeft, } -//`BannerComponent` is kind of a corollary to ComponentRepresentable protocol from maplibre which -// has a `type: VisualInstructionComponentType` field, whereas here we have a variant for each type, -// with the associated value of the VisualInstructionComponent. -// Plus we have enum variant for the other implementors of ComponentRepresentable +/// `BannerComponent` is kind of a corollary to maplibre-navigation-ios's `ComponentRepresentable` +/// protocol, which has a `type: VisualInstructionComponentType` field. Here however, we have a +/// variant for each `type` with the associated value of the `VisualInstructionComponent`. +/// Plus we have enum variant for the other implementors of `ComponentRepresentable` +/// (really, that's only `Lane`, so far) #[derive(Debug, Serialize, PartialEq, Clone)] #[serde(rename_all = "camelCase", tag = "type")] #[non_exhaustive] diff --git a/services/travelmux/src/api/v6/plan.rs b/services/travelmux/src/api/v6/plan.rs index 2aa7eb11b..728d32d52 100644 --- a/services/travelmux/src/api/v6/plan.rs +++ b/services/travelmux/src/api/v6/plan.rs @@ -13,7 +13,6 @@ use super::TravelModes; use crate::api::AppState; use crate::error::ErrorType; use crate::otp::otp_api; -use crate::otp::otp_api::{AbsoluteDirection, RelativeDirection}; use crate::util::format::format_meters; use crate::util::haversine_segmenter::HaversineSegmenter; use crate::util::serde_util::{ @@ -245,7 +244,6 @@ type TransitLeg = otp_api::Leg; #[derive(Debug, Serialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub(crate) enum ModeLeg { - // REVIEW: rename? There is a boolean field for OTP called TransitLeg #[serde(rename = "transitLeg")] Transit(Box), @@ -303,8 +301,9 @@ impl NonTransitLeg { } } -// Eventually we might want to coalesce this into something not valhalla specific -// but for now we only use it for valhalla trips +/// One action taken by the user - like a turn or taking an exit. +/// This was originally based on the schema of a valhall_api::Maneuver, but it can be built from +/// either OTP or Valhalla data. #[derive(Debug, Serialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub struct Maneuver { @@ -353,7 +352,7 @@ impl Maneuver { leg: &otp_api::Leg, distance_unit: DistanceUnit, ) -> Self { - let instruction = build_instruction( + let instruction = maneuver_instruction( leg.mode, otp.relative_direction, otp.absolute_direction, @@ -387,27 +386,29 @@ impl Maneuver { } } +/// Returns the natural language description of the maneuver. // We could do so much better. Look at Valhalla's Odin. // // e.g. take context of previous maneuver. "Bear right to stay on Main Street" -fn build_instruction( +// TODO: localize +fn maneuver_instruction( mode: otp_api::TransitMode, maneuver_type: otp_api::RelativeDirection, absolute_direction: Option, street_name: &str, ) -> Option { match maneuver_type { - RelativeDirection::Depart => { + otp_api::RelativeDirection::Depart => { if let Some(absolute_direction) = absolute_direction { let direction = match absolute_direction { - AbsoluteDirection::North => "north", - AbsoluteDirection::Northeast => "northeast", - AbsoluteDirection::East => "east", - AbsoluteDirection::Southeast => "southeast", - AbsoluteDirection::South => "south", - AbsoluteDirection::Southwest => "southwest", - AbsoluteDirection::West => "west", - AbsoluteDirection::Northwest => "northwest", + otp_api::AbsoluteDirection::North => "north", + otp_api::AbsoluteDirection::Northeast => "northeast", + otp_api::AbsoluteDirection::East => "east", + otp_api::AbsoluteDirection::Southeast => "southeast", + otp_api::AbsoluteDirection::South => "south", + otp_api::AbsoluteDirection::Southwest => "southwest", + otp_api::AbsoluteDirection::West => "west", + otp_api::AbsoluteDirection::Northwest => "northwest", }; let mode = match mode { otp_api::TransitMode::Walk => "Walk", @@ -420,20 +421,23 @@ fn build_instruction( Some("Depart.".to_string()) } } - RelativeDirection::HardLeft => Some(format!("Turn left onto {street_name}.")), - RelativeDirection::Left => Some(format!("Turn left onto {street_name}.")), - RelativeDirection::SlightlyLeft => Some(format!("Turn slightly left onto {street_name}.")), - RelativeDirection::Continue => Some(format!("Continue onto {street_name}.")), - RelativeDirection::SlightlyRight => { + otp_api::RelativeDirection::HardLeft => Some(format!("Turn left onto {street_name}.")), + otp_api::RelativeDirection::Left => Some(format!("Turn left onto {street_name}.")), + otp_api::RelativeDirection::SlightlyLeft => { + Some(format!("Turn slightly left onto {street_name}.")) + } + otp_api::RelativeDirection::Continue => Some(format!("Continue onto {street_name}.")), + otp_api::RelativeDirection::SlightlyRight => { Some(format!("Turn slightly right onto {street_name}.")) } - RelativeDirection::Right => Some(format!("Turn right onto {street_name}.")), - RelativeDirection::HardRight => Some(format!("Turn right onto {street_name}.")), - RelativeDirection::CircleClockwise | RelativeDirection::CircleCounterclockwise => { + otp_api::RelativeDirection::Right => Some(format!("Turn right onto {street_name}.")), + otp_api::RelativeDirection::HardRight => Some(format!("Turn right onto {street_name}.")), + otp_api::RelativeDirection::CircleClockwise + | otp_api::RelativeDirection::CircleCounterclockwise => { Some("Enter the roundabout.".to_string()) } - RelativeDirection::Elevator => Some("Enter the elevator.".to_string()), - RelativeDirection::UturnLeft | RelativeDirection::UturnRight => { + otp_api::RelativeDirection::Elevator => Some("Enter the elevator.".to_string()), + otp_api::RelativeDirection::UturnLeft | otp_api::RelativeDirection::UturnRight => { Some("Make a U-turn.".to_string()) } } From 514c5546f9051e2dda4a7308b17af3e778ed329d Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 29 May 2024 16:41:00 -0700 Subject: [PATCH 23/24] remove unnecessary clone --- services/travelmux/src/api/v6/osrm_api.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/services/travelmux/src/api/v6/osrm_api.rs b/services/travelmux/src/api/v6/osrm_api.rs index 8386fb714..aae3cae33 100644 --- a/services/travelmux/src/api/v6/osrm_api.rs +++ b/services/travelmux/src/api/v6/osrm_api.rs @@ -82,8 +82,8 @@ impl RouteLeg { let maneuver = this_and_next[0].clone(); let next_maneuver = this_and_next.get(1); RouteStep::from_maneuver( - maneuver.clone(), - next_maneuver.cloned(), + maneuver, + next_maneuver, value.mode, distance_unit, ) @@ -151,13 +151,13 @@ pub struct RouteStep { impl RouteStep { fn from_maneuver( maneuver: Maneuver, - next_maneuver: Option, + next_maneuver: Option<&Maneuver>, mode: TravelMode, from_distance_unit: DistanceUnit, ) -> Self { let banner_instructions = VisualInstructionBanner::from_maneuver( &maneuver, - next_maneuver.as_ref(), + next_maneuver, from_distance_unit, ); RouteStep { From d556deb5cb46fb12068397d13575a09d72037238 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 29 May 2024 16:42:44 -0700 Subject: [PATCH 24/24] TODOs and format --- services/travelmux/src/api/v6/osrm_api.rs | 28 ++++++++--------------- services/travelmux/src/api/v6/plan.rs | 23 +++++++++++-------- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/services/travelmux/src/api/v6/osrm_api.rs b/services/travelmux/src/api/v6/osrm_api.rs index aae3cae33..918bf0631 100644 --- a/services/travelmux/src/api/v6/osrm_api.rs +++ b/services/travelmux/src/api/v6/osrm_api.rs @@ -81,12 +81,7 @@ impl RouteLeg { .map(|this_and_next| { let maneuver = this_and_next[0].clone(); let next_maneuver = this_and_next.get(1); - RouteStep::from_maneuver( - maneuver, - next_maneuver, - value.mode, - distance_unit, - ) + RouteStep::from_maneuver(maneuver, next_maneuver, value.mode, distance_unit) }) .collect(); if let Some(final_maneuver) = non_transit_leg.maneuvers.last() { @@ -155,11 +150,8 @@ impl RouteStep { mode: TravelMode, from_distance_unit: DistanceUnit, ) -> Self { - let banner_instructions = VisualInstructionBanner::from_maneuver( - &maneuver, - next_maneuver, - from_distance_unit, - ); + let banner_instructions = + VisualInstructionBanner::from_maneuver(&maneuver, next_maneuver, from_distance_unit); RouteStep { distance: maneuver.distance_meters(from_distance_unit), duration: maneuver.duration_seconds, @@ -168,14 +160,14 @@ impl RouteStep { .street_names .unwrap_or(vec!["".to_string()]) .join(", "), - r#ref: None, - pronunciation: None, - destinations: None, + r#ref: None, // TODO + pronunciation: None, // TODO + destinations: None, // TODO mode, maneuver: StepManeuver { location: maneuver.start_point.into(), }, - intersections: None, //vec![], + intersections: None, // TODO banner_instructions, } } @@ -186,8 +178,8 @@ impl RouteStep { pub struct VisualInstructionBanner { pub distance_along_geometry: f64, pub primary: VisualInstruction, - // secondary: Option, - // sub: Option, + // secondary: Option, // TODO + // sub: Option, // TODO } impl VisualInstructionBanner { @@ -454,7 +446,7 @@ pub enum BannerComponent { /// If the two adjacent components are both displayed as images, you can hide this delimiter component. Delimiter(VisualInstructionComponent), - #[allow(unused)] + #[allow(unused)] // TODO Lane(LaneIndicationComponent), } diff --git a/services/travelmux/src/api/v6/plan.rs b/services/travelmux/src/api/v6/plan.rs index 728d32d52..365279807 100644 --- a/services/travelmux/src/api/v6/plan.rs +++ b/services/travelmux/src/api/v6/plan.rs @@ -219,14 +219,10 @@ pub(crate) struct Leg { /// End of the Leg to_place: Place, - // This is mostly OTP specific. We can synthesize a value from the valhalla response, but we - // don't currently use it. /// Start time of the leg #[serde(serialize_with = "serialize_system_time_as_millis")] start_time: SystemTime, - // This is mostly OTP specific. We can synthesize a value from the valhalla response, but we - // don't currently use it. /// Start time of the leg #[serde(serialize_with = "serialize_system_time_as_millis")] end_time: SystemTime, @@ -238,7 +234,7 @@ pub(crate) struct Leg { pub(crate) duration_seconds: f64, } -// Should we just pass the entire OTP leg? +// Currently we just pass the entire OTP leg type TransitLeg = otp_api::Leg; #[derive(Debug, Serialize, Clone, PartialEq)] @@ -421,7 +417,9 @@ fn maneuver_instruction( Some("Depart.".to_string()) } } - otp_api::RelativeDirection::HardLeft => Some(format!("Turn left onto {street_name}.")), + otp_api::RelativeDirection::HardLeft => { + Some(format!("Turn sharp left onto {street_name}.")) + } otp_api::RelativeDirection::Left => Some(format!("Turn left onto {street_name}.")), otp_api::RelativeDirection::SlightlyLeft => { Some(format!("Turn slightly left onto {street_name}.")) @@ -431,7 +429,9 @@ fn maneuver_instruction( Some(format!("Turn slightly right onto {street_name}.")) } otp_api::RelativeDirection::Right => Some(format!("Turn right onto {street_name}.")), - otp_api::RelativeDirection::HardRight => Some(format!("Turn right onto {street_name}.")), + otp_api::RelativeDirection::HardRight => { + Some(format!("Turn sharp right onto {street_name}.")) + } otp_api::RelativeDirection::CircleClockwise | otp_api::RelativeDirection::CircleCounterclockwise => { Some("Enter the roundabout.".to_string()) @@ -492,8 +492,13 @@ impl Leg { .cloned() .map(|otp_step| { // compute step geometry by distance along leg geometry - let step_geometry = - segmenter.next_segment(otp_step.distance).expect("TODO"); + let step_geometry = segmenter + .next_segment(otp_step.distance) + .unwrap_or_else(|| { + log::warn!("no geometry for step"); + debug_assert!(false, "no geometry for step"); + LineString::new(vec![]) + }); distance_so_far += otp_step.distance; Maneuver::from_otp(otp_step, step_geometry, otp, distance_unit) })