From 359b8df8092fae8e2fa26b2ba03e4c4dcd457aa4 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Tue, 9 Apr 2024 14:13:15 -0700 Subject: [PATCH] [travelmux] add start/end to each trip leg --- services/travelmux/src/api/v4/plan.rs | 169 +++++++++++++++--- services/travelmux/src/otp/otp_api.rs | 37 +++- services/travelmux/src/util.rs | 24 ++- .../travelmux/src/valhalla/valhalla_api.rs | 30 +++- 4 files changed, 224 insertions(+), 36 deletions(-) diff --git a/services/travelmux/src/api/v4/plan.rs b/services/travelmux/src/api/v4/plan.rs index ae30ffde3..f67b37086 100644 --- a/services/travelmux/src/api/v4/plan.rs +++ b/services/travelmux/src/api/v4/plan.rs @@ -5,16 +5,19 @@ use polyline::decode_polyline; use reqwest::header::{HeaderName, HeaderValue}; use serde::{de, de::IntoDeserializer, de::Visitor, Deserialize, Deserializer, Serialize}; use std::fmt; +use std::time::{Duration, SystemTime}; use super::error::{PlanResponseErr, PlanResponseOk}; use crate::api::AppState; use crate::error::ErrorType; use crate::otp::otp_api; use crate::otp::otp_api::RelativeDirection; -use crate::util::serialize_rect_to_lng_lat; -use crate::util::{deserialize_point_from_lat_lon, extend_bounds}; +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::valhalla::valhalla_api; -use crate::valhalla::valhalla_api::ManeuverType; +use crate::valhalla::valhalla_api::{LonLat, ManeuverType}; use crate::{DistanceUnit, Error, TravelMode}; #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] @@ -48,9 +51,11 @@ pub struct Itinerary { /// seconds duration: f64, /// unix millis, UTC - start_time: u64, + #[serde(serialize_with = "serialize_system_time_as_millis")] + start_time: SystemTime, /// unix millis, UTC - end_time: u64, + #[serde(serialize_with = "serialize_system_time_as_millis")] + end_time: SystemTime, distance: f64, distance_units: DistanceUnit, #[serde(serialize_with = "serialize_rect_to_lng_lat")] @@ -65,16 +70,34 @@ impl Itinerary { geo::coord!(x: valhalla.summary.max_lon, y: valhalla.summary.max_lat), ); - use std::time::Duration; - fn time_since_epoch() -> Duration { - use std::time::{SystemTime, UNIX_EPOCH}; - SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system time is after unix epoch") - } + 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(); - let start_time = time_since_epoch().as_millis() as u64; - let end_time = start_time + (valhalla.summary.time * 1000.0) as u64; Self { mode, start_time, @@ -83,11 +106,7 @@ impl Itinerary { distance: valhalla.summary.length, bounds, distance_units: valhalla.units, - legs: valhalla - .legs - .iter() - .map(|v_leg| Leg::from_valhalla(v_leg, mode)) - .collect(), + legs, } } @@ -113,10 +132,11 @@ impl Itinerary { }; extend_bounds(&mut itinerary_bounds, &leg_bounds); } + Ok(Self { duration: itinerary.duration as f64, - start_time: itinerary.start_time, - end_time: itinerary.end_time, + 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, @@ -126,6 +146,32 @@ impl Itinerary { } } +#[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, Deserialize, Serialize, Clone, PartialEq)] #[serde(rename_all = "camelCase")] struct Leg { @@ -137,6 +183,24 @@ struct Leg { #[serde(flatten)] 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, } // Should we just pass the entire OTP leg? @@ -147,7 +211,7 @@ type TransitLeg = otp_api::Leg; enum ModeLeg { // REVIEW: rename? There is a boolean field for OTP called TransitLeg #[serde(rename = "transitLeg")] - Transit(TransitLeg), + Transit(Box), #[serde(rename = "maneuvers")] NonTransit(Vec), @@ -231,19 +295,36 @@ impl Leg { } _ => { // assume everything else is transit - ModeLeg::Transit(otp.clone()) + ModeLeg::Transit(Box::new(otp.clone())) } }; + let from_place = (&otp.from).into(); + let to_place = (&otp.to).into(); 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(), mode_leg, }) } - fn from_valhalla(valhalla: &valhalla_api::Leg, travel_mode: TravelMode) -> Self { + fn from_valhalla( + valhalla: &valhalla_api::Leg, + travel_mode: TravelMode, + start_time: SystemTime, + end_time: SystemTime, + from_place: LonLat, + to_place: LonLat, + ) -> Self { Self { + start_time, + end_time, + from_place: from_place.into(), + to_place: to_place.into(), geometry: valhalla.shape.clone(), mode: travel_mode, mode_leg: ModeLeg::NonTransit( @@ -462,6 +543,18 @@ mod tests { 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(maneuvers) = &first_leg.mode_leg else { panic!("unexpected transit leg") }; @@ -497,6 +590,19 @@ mod tests { 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(maneuvers) = &first_leg.mode_leg else { panic!("expected non-transit leg") @@ -541,6 +647,21 @@ mod tests { let first_leg = legs.get(0).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, 1708728373000); + + let mode = first_leg + .get("endTime") + .expect("field missing") + .as_u64() + .expect("unexpected type. expected u64"); + assert_eq!(mode, 1708728745000); + assert!(first_leg.get("transitLeg").is_none()); let maneuvers = first_leg.get("maneuvers").unwrap().as_array().unwrap(); let first_maneuver = maneuvers.get(0).unwrap(); diff --git a/services/travelmux/src/otp/otp_api.rs b/services/travelmux/src/otp/otp_api.rs index da94baf97..9b4b49c09 100644 --- a/services/travelmux/src/otp/otp_api.rs +++ b/services/travelmux/src/otp/otp_api.rs @@ -73,6 +73,16 @@ pub struct Leg { pub route_color: Option, // Present, but empty, for transit legs. Non-empty for non-transit legs. pub steps: Vec, + + pub from: Place, + pub to: Place, + + /// What time the leg starts, in millis since Unix epoch (UTC) + pub start_time: u64, + + /// What time the leg starts, in millis since Unix epoch (UTC) + pub end_time: u64, + #[serde(flatten)] pub extra: HashMap, } @@ -160,13 +170,36 @@ pub struct LegGeometry { pub points: String, } -#[derive(Debug, Deserialize, Serialize, PartialEq, Clone)] +#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy)] #[serde(rename_all = "camelCase")] -pub struct Location { +pub struct LonLat { pub lat: f64, pub lon: f64, } +#[derive(Debug, Deserialize, Serialize, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Place { + #[serde(flatten)] + pub location: LonLat, + + /// millis since Unix epoch + /// I think it's None iff it's the trip Origin + pub arrival: Option, + + /// millis since Unix epoch + /// I think it's None iff it's the trip Destination + pub departure: Option, + + /// "Civic Center / Grand Park Station" + /// Transit stops often have names. But this is often blank when + /// the place is some random lat/lon (e.g. the users destination) + pub name: Option, + + #[serde(flatten)] + pub extra: HashMap, +} + #[cfg(test)] mod tests { use super::*; diff --git a/services/travelmux/src/util.rs b/services/travelmux/src/util.rs index 0aa8df4ab..83ca88022 100644 --- a/services/travelmux/src/util.rs +++ b/services/travelmux/src/util.rs @@ -1,6 +1,9 @@ use geo::{Point, Rect}; -use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serializer}; -use std::time::Duration; +use serde::{ + ser::{Error, SerializeStruct}, + Deserialize, Deserializer, Serializer, +}; +use std::time::{Duration, SystemTime}; pub fn deserialize_point_from_lat_lon<'de, D>(deserializer: D) -> Result where @@ -70,3 +73,20 @@ pub fn serialize_rect_to_lng_lat( 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) +} + +pub fn system_time_from_millis(millis: u64) -> SystemTime { + std::time::UNIX_EPOCH + Duration::from_millis(millis) +} diff --git a/services/travelmux/src/valhalla/valhalla_api.rs b/services/travelmux/src/valhalla/valhalla_api.rs index 434969bd4..2db460b32 100644 --- a/services/travelmux/src/valhalla/valhalla_api.rs +++ b/services/travelmux/src/valhalla/valhalla_api.rs @@ -1,3 +1,4 @@ +use crate::otp::otp_api; use crate::DistanceUnit; use geo::Point; use serde::{Deserialize, Serialize}; @@ -16,7 +17,7 @@ pub enum ModeCosting { /// `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}` #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ValhallaRouteQuery { - pub locations: Vec, + pub locations: Vec, pub costing: ModeCosting, pub alternates: u32, pub units: DistanceUnit, @@ -47,7 +48,7 @@ pub enum ValhallaRouteResponseResult { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Trip { - pub locations: Vec, + pub locations: Vec, pub summary: Summary, pub units: DistanceUnit, // legs: Vec pub legs: Vec, @@ -74,11 +75,10 @@ pub struct Summary { pub extra: HashMap, } -// CLEANUP: rename to LonLat to match their field spelling? -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct LngLat { - pub lat: f64, +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct LonLat { pub lon: f64, + pub lat: f64, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -86,7 +86,6 @@ pub struct Leg { pub summary: Summary, pub maneuvers: Vec, pub shape: String, - // pub duration: f64, // pub length: f64, // pub steps: Vec, #[serde(flatten)] @@ -175,7 +174,7 @@ pub enum ManeuverType { BuildingExit = 43, } -impl From for LngLat { +impl From for LonLat { fn from(value: Point) -> Self { Self { lat: value.y(), @@ -184,6 +183,21 @@ impl From for LngLat { } } +impl From for Point { + fn from(value: LonLat) -> Self { + geo::point!(x: value.lon, y: value.lat) + } +} + +impl From for LonLat { + fn from(value: otp_api::LonLat) -> Self { + Self { + lon: value.lon, + lat: value.lat, + } + } +} + #[cfg(test)] mod tests { use super::*;