use crate::models::{
BlockedLane, Congestion, Impact, IncidentType, ManeuverModifier, ManeuverType,
};
use alloc::{string::String, vec::Vec};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[cfg(feature = "alloc")]
use std::collections::HashMap;
#[cfg(feature = "wasm-bindgen")]
use tsify::Tsify;
#[derive(Deserialize, Debug)]
#[serde(transparent)]
pub struct Coordinate {
tuple: (f64, f64),
}
impl Coordinate {
pub fn latitude(&self) -> f64 {
self.tuple.1
}
pub fn longitude(&self) -> f64 {
self.tuple.0
}
}
#[derive(Deserialize, Debug)]
pub struct RouteResponse {
pub code: String,
#[serde(default)]
pub message: Option<String>,
#[serde(default)]
pub routes: Vec<Route>,
#[serde(default)]
pub waypoints: Vec<Waypoint>,
}
#[derive(Deserialize, Debug)]
pub struct Route {
pub duration: f64,
pub distance: f64,
pub geometry: String,
pub legs: Vec<RouteLeg>,
}
#[derive(Deserialize, Debug)]
pub struct RouteLeg {
pub annotation: Option<AnyAnnotation>,
pub duration: f64,
pub distance: f64,
pub steps: Vec<RouteStep>,
#[serde(default)]
pub via_waypoints: Vec<ViaWaypoint>,
#[serde(default)]
pub incidents: Vec<MapboxOsrmIncident>,
}
#[derive(Deserialize, Debug, Clone, PartialEq)]
pub struct AnyAnnotation {
#[serde(flatten)]
pub values: HashMap<String, Vec<Value>>,
}
#[cfg_attr(test, derive(Serialize))]
#[derive(Deserialize, Debug)]
pub struct MapboxOsrmIncident {
pub id: String,
#[serde(rename = "type")]
pub incident_type: IncidentType,
pub description: Option<String>,
pub long_description: Option<String>,
pub creation_time: Option<DateTime<Utc>>,
pub start_time: Option<DateTime<Utc>>,
pub end_time: Option<DateTime<Utc>>,
pub impact: Option<Impact>,
#[serde(default)]
pub lanes_blocked: Vec<BlockedLane>,
pub num_lanes_blocked: Option<u8>,
pub congestion: Option<Congestion>,
pub closed: Option<bool>,
pub geometry_index_start: u64,
pub geometry_index_end: Option<u64>,
pub sub_type: Option<String>,
pub sub_type_description: Option<String>,
pub iso_3166_1_alpha2: Option<String>,
pub iso_3166_1_alpha3: Option<String>,
#[serde(default)]
pub affected_road_names: Vec<String>,
pub south: Option<f64>,
pub west: Option<f64>,
pub north: Option<f64>,
pub east: Option<f64>,
}
#[derive(Deserialize, Debug)]
pub struct RouteStep {
pub distance: f64,
pub duration: f64,
pub geometry: String,
pub name: Option<String>,
#[serde(rename = "ref")]
pub reference: Option<String>,
pub pronunciation: Option<String>,
pub mode: Option<String>,
pub maneuver: StepManeuver,
pub intersections: Vec<Intersections>,
pub exits: Option<String>,
pub driving_side: Option<String>,
#[serde(default, rename = "bannerInstructions")]
pub banner_instructions: Vec<BannerInstruction>,
#[serde(default, rename = "voiceInstructions")]
pub voice_instructions: Vec<VoiceInstruction>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct BannerInstruction {
pub distance_along_geometry: f64,
pub primary: BannerContent,
pub secondary: Option<BannerContent>,
pub sub: Option<BannerContent>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct VoiceInstruction {
pub announcement: String,
pub ssml_announcement: Option<String>,
pub distance_along_geometry: f64,
}
#[derive(Deserialize, Debug)]
pub struct BannerContent {
pub text: String,
#[serde(rename = "type")]
pub maneuver_type: Option<ManeuverType>,
#[serde(rename = "modifier")]
pub maneuver_modifier: Option<ManeuverModifier>,
#[serde(rename = "degrees")]
pub roundabout_exit_degrees: Option<u16>,
pub components: Vec<BannerContentComponent>,
}
#[derive(Deserialize, Debug)]
pub struct BannerContentComponent {
#[serde(rename = "type")]
pub component_type: Option<String>,
pub directions: Option<Vec<String>>,
pub active: Option<bool>,
pub image_base_url: Option<String>,
pub abbr: Option<String>,
pub abbr_priority: Option<u8>,
pub active_direction: Option<String>,
pub text: Option<String>,
}
#[derive(Deserialize, Debug)]
pub struct StepManeuver {
pub location: Coordinate,
pub bearing_before: u16,
pub bearing_after: u16,
#[serde(rename = "type")]
pub maneuver_type: String,
pub modifier: Option<String>,
pub exit: Option<u8>,
instruction: Option<String>,
}
impl StepManeuver {
fn synthesize_instruction(&self, _locale: &str) -> String {
String::from("TODO: OSRM instruction synthesis")
}
pub fn get_instruction(&self) -> String {
self.instruction
.clone()
.unwrap_or_else(|| self.synthesize_instruction("en-US"))
}
}
#[derive(Deserialize, Debug)]
pub struct Intersections {
pub location: Coordinate,
pub bearings: Vec<u16>,
#[serde(default)]
pub classes: Vec<String>,
pub entry: Vec<bool>,
#[serde(default)]
#[serde(rename = "in")]
pub intersection_in: usize,
#[serde(default)]
#[serde(rename = "out")]
pub intersection_out: usize,
#[serde(default)]
pub lanes: Vec<Lane>,
}
#[derive(Deserialize, Debug)]
pub struct Lane {
pub indications: Vec<String>,
pub valid: bool,
}
#[derive(Deserialize, Debug)]
pub struct Waypoint {
pub name: Option<String>,
pub distance: Option<f64>,
pub location: Coordinate,
}
#[derive(Clone, PartialEq, PartialOrd, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
#[cfg_attr(feature = "wasm-bindgen", derive(Tsify))]
#[cfg_attr(feature = "wasm-bindgen", tsify(into_wasm_abi, from_wasm_abi))]
pub struct OsrmWaypointProperties {
pub name: Option<String>,
pub distance: Option<f64>,
}
#[derive(Deserialize, Debug)]
pub struct ViaWaypoint {
pub distance_from_start: f64,
pub geometry_index: f64,
pub waypoint_index: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deserialize_annotation() {
let data = r#"{
"distance": [
4.294596842089401,
5.051172053200946,
5.533254065167979,
6.576513793849532,
7.4449640160938015,
8.468757534990829,
15.202780313562714,
7.056346577326572
],
"duration": [
1,
1.2,
2,
1.6,
1.8,
2,
3.6,
1.7
],
"speed": [
4.3,
4.2,
2.8,
4.1,
4.1,
4.2,
4.2,
4.2
],
"congestion": [
"low",
"moderate",
"moderate",
"moderate",
"heavy",
"heavy",
"heavy",
"heavy"
],
"maxspeed": [
{
"speed": 56,
"unit": "km/h"
},
{
"speed": 56,
"unit": "km/h"
},
{
"speed": 56,
"unit": "km/h"
},
{
"speed": 56,
"unit": "km/h"
},
{
"speed": 56,
"unit": "km/h"
},
{
"speed": 56,
"unit": "km/h"
},
{
"speed": 56,
"unit": "km/h"
},
{
"speed": 56,
"unit": "km/h"
}
]
}"#;
let annotation: AnyAnnotation =
serde_json::from_str(data).expect("Failed to parse Annotation");
insta::with_settings!({sort_maps => true}, {
insta::assert_yaml_snapshot!(annotation.values);
});
}
#[test]
fn deserialize_banner_instruction() {
let data = r#"
{
"distanceAlongGeometry": 100,
"primary": {
"type": "turn",
"modifier": "left",
"text": "I 495 North / I 95",
"components": [
{
"text": "I 495",
"imageBaseURL": "https://s3.amazonaws.com/mapbox/shields/v3/i-495",
"type": "icon"
},
{
"text": "North",
"type": "text",
"abbr": "N",
"abbr_priority": 0
},
{
"text": "/",
"type": "delimiter"
},
{
"text": "I 95",
"imageBaseURL": "https://s3.amazonaws.com/mapbox/shields/v3/i-95",
"type": "icon"
}
]
},
"secondary": {
"type": "turn",
"modifier": "left",
"text": "Baltimore / Northern Virginia",
"components": [
{
"text": "Baltimore",
"type": "text"
},
{
"text": "/",
"type": "text"
},
{
"text": "Northern Virginia",
"type": "text"
}
]
},
"sub": {
"text": "",
"components": [
{
"text": "",
"type": "lane",
"directions": [
"left"
],
"active": true
},
{
"text": "",
"type": "lane",
"directions": [
"left",
"straight"
],
"active": true
},
{
"text": "",
"type": "lane",
"directions": [
"right"
],
"active": false
}
]
}
}
"#;
let instruction: BannerInstruction =
serde_json::from_str(data).expect("Failed to parse Annotation");
assert_eq!(instruction.distance_along_geometry, 100.0);
assert_eq!(instruction.primary.text, "I 495 North / I 95");
assert_eq!(instruction.primary.maneuver_type, Some(ManeuverType::Turn));
assert_eq!(
instruction.primary.maneuver_modifier,
Some(ManeuverModifier::Left)
);
let secondary = instruction.secondary.expect("Expected secondary content");
assert_eq!(secondary.text, "Baltimore / Northern Virginia");
assert_eq!(secondary.maneuver_type, Some(ManeuverType::Turn));
assert_eq!(secondary.maneuver_modifier, Some(ManeuverModifier::Left));
let submaneuver = instruction.sub.expect("Expected submaneuver content");
assert_eq!(submaneuver.components.len(), 3);
assert_eq!(
submaneuver.components[0].directions,
Some(vec!["left".to_string()])
);
assert_eq!(
submaneuver.components[1].directions,
Some(vec!["left".to_string(), "straight".to_string()])
);
assert_eq!(
submaneuver.components[2].directions,
Some(vec!["right".to_string()])
);
}
#[test]
fn deserialize_incidents() {
let data = r#"
{
"id": "13956787949218641",
"type": "construction",
"creation_time": "2024-11-13T16:39:17Z",
"start_time": "2023-04-03T22:00:00Z",
"end_time": "2024-11-26T04:59:00Z",
"iso_3166_1_alpha2": "US",
"iso_3166_1_alpha3": "USA",
"description": "I-84 W/B: intermittent lane closures from Exit 57 CT-15 to US-44 Connecticut Blvd",
"long_description": "Intermittent lane closures due to barrier repairs on I-84 Westbound from Exit 57 CT-15 to US-44 Connecticut Blvd.",
"impact": "major",
"sub_type": "CONSTRUCTION",
"alertc_codes": [
701,
703
],
"traffic_codes": {
"incident_primary_code": 701
},
"lanes_blocked": [],
"length": 2403,
"south": 41.763362,
"west": -72.661148,
"north": 41.769363,
"east": -72.633712,
"congestion": {
"value": 101
},
"geometry_index_start": 2932,
"geometry_index_end": 3017,
"affected_road_names": [
"Officer Brian A. Aselton Memorial Highway"
],
"affected_road_names_unknown": [
"I 84 West/US 6"
],
"affected_road_names_en": [
"Officer Brian A. Aselton Memorial Highway"
]
}
"#;
let incident: MapboxOsrmIncident =
serde_json::from_str(data).expect("Failed to parse Incident");
insta::assert_yaml_snapshot!(incident);
}
}