use crate::costing;
use crate::elevation::ShapeFormat;
pub use crate::shapes::ShapePoint;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Serialize, Debug, Clone)]
pub struct TracePoint {
pub lat: f64,
pub lon: f64,
}
impl TracePoint {
pub fn new(lat: f64, lon: f64) -> Self {
Self { lat, lon }
}
}
#[derive(Serialize, Debug, Clone, Default)]
#[serde(rename_all = "snake_case")]
pub enum ShapeMatch {
#[default]
WalkOrSnap,
MapSnap,
EdgeWalk,
}
#[derive(Serialize, Debug, Clone)]
pub struct Filter {
pub attributes: Vec<String>,
pub action: FilterAction,
}
#[derive(Serialize, Debug, Clone)]
#[serde(rename_all = "snake_case")]
pub enum FilterAction {
Include,
Exclude,
}
#[serde_with::skip_serializing_none]
#[derive(Serialize, Debug, Clone, Default)]
pub struct TraceOptions {
pub search_radius: Option<f64>,
pub gps_accuracy: Option<f64>,
pub breakage_distance: Option<f64>,
pub interpolation_distance: Option<f64>,
}
#[serde_with::skip_serializing_none]
#[derive(Serialize, Debug, Clone)]
pub struct Manifest {
shape: Option<Vec<TracePoint>>,
encoded_polyline: Option<String>,
shape_format: Option<ShapeFormat>,
#[serde(flatten)]
costing: costing::Costing,
shape_match: ShapeMatch,
filters: Option<Filter>,
trace_options: Option<TraceOptions>,
units: Option<super::Units>,
id: Option<String>,
language: Option<String>,
durations: Option<Vec<f64>>,
use_timestamps: Option<bool>,
begin_time: Option<String>,
}
impl Manifest {
pub fn builder(shape: impl IntoIterator<Item = TracePoint>, costing: costing::Costing) -> Self {
Self {
shape: Some(shape.into_iter().collect()),
encoded_polyline: None,
shape_format: None,
costing,
shape_match: ShapeMatch::default(),
filters: None,
trace_options: None,
units: None,
id: None,
language: None,
durations: None,
use_timestamps: None,
begin_time: None,
}
}
pub fn builder_encoded(encoded_polyline: impl ToString, costing: costing::Costing) -> Self {
Self {
shape: None,
encoded_polyline: Some(encoded_polyline.to_string()),
shape_format: None,
costing,
shape_match: ShapeMatch::default(),
filters: None,
trace_options: None,
units: None,
id: None,
language: None,
durations: None,
use_timestamps: None,
begin_time: None,
}
}
pub fn shape_match(mut self, shape_match: ShapeMatch) -> Self {
self.shape_match = shape_match;
self
}
pub fn shape_format(mut self, shape_format: ShapeFormat) -> Self {
debug_assert!(
self.shape.is_none(),
"shape is set and setting the shape_format is requested. This combination does not make sense: shapes and encoded_polylines as input are mutually exclusive."
);
self.shape_format = Some(shape_format);
self
}
pub fn include_attributes(
mut self,
attributes: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
self.filters = Some(Filter {
attributes: attributes.into_iter().map(|a| a.into()).collect(),
action: FilterAction::Include,
});
self
}
pub fn exclude_attributes(
mut self,
attributes: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
self.filters = Some(Filter {
attributes: attributes.into_iter().map(|a| a.into()).collect(),
action: FilterAction::Exclude,
});
self
}
pub fn trace_options(mut self, trace_options: TraceOptions) -> Self {
self.trace_options = Some(trace_options);
self
}
pub fn units(mut self, units: super::Units) -> Self {
self.units = Some(units);
self
}
pub fn id(mut self, id: impl ToString) -> Self {
self.id = Some(id.to_string());
self
}
pub fn language(mut self, language: impl ToString) -> Self {
self.language = Some(language.to_string());
self
}
pub fn durations(mut self, durations: impl IntoIterator<Item = f64>) -> Self {
self.durations = Some(durations.into_iter().collect());
self
}
pub fn use_timestamps(mut self, use_timestamps: bool) -> Self {
self.use_timestamps = Some(use_timestamps);
self
}
pub fn begin_time(mut self, begin_time: impl ToString) -> Self {
self.begin_time = Some(begin_time.to_string());
self
}
}
#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum Surface {
PavedSmooth,
Paved,
PavedRough,
Compacted,
Dirt,
Gravel,
Path,
Impassable,
}
#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum RoadClass {
Motorway,
Trunk,
Primary,
Secondary,
Tertiary,
Unclassified,
Residential,
ServiceOther,
}
#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum EdgeUse {
Road,
Ramp,
TurnChannel,
Track,
Driveway,
Alley,
ParkingAisle,
EmergencyAccess,
DriveThrough,
Culdesac,
Cycleway,
MountainBike,
Sidewalk,
Footway,
Steps,
Ferry,
#[serde(rename = "rail-ferry")]
RailFerry,
ServiceRoad,
Path,
LivingStreet,
PedestrianCrossing,
Other,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Edge {
#[serde(default)]
pub surface: Option<Surface>,
#[serde(default)]
pub road_class: Option<RoadClass>,
#[serde(default)]
pub r#use: Option<EdgeUse>,
#[serde(default)]
pub length: Option<f64>,
#[serde(default)]
pub names: Option<Vec<String>>,
#[serde(default)]
pub begin_shape_index: Option<u32>,
#[serde(default)]
pub end_shape_index: Option<u32>,
#[serde(default)]
pub way_id: Option<u64>,
#[serde(default)]
pub source_percent_along: Option<f64>,
#[serde(default)]
pub target_percent_along: Option<f64>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct MatchedPoint {
pub lat: f64,
pub lon: f64,
#[serde(default)]
pub r#type: Option<String>,
#[serde(default)]
pub edge_index: Option<u32>,
#[serde(default)]
pub distance_along_edge: Option<f64>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Response {
#[serde(default)]
pub edges: Vec<Edge>,
#[serde(default)]
pub matched_points: Vec<MatchedPoint>,
#[serde(default)]
pub shape: Option<String>,
#[serde(default)]
pub units: Option<String>,
#[serde(default)]
pub id: Option<String>,
#[serde(default)]
pub warnings: Vec<Value>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_serialize_manifest() {
let manifest = Manifest::builder(
[TracePoint::new(48.1, 11.5), TracePoint::new(48.2, 11.6)],
costing::Costing::Auto(Default::default()),
);
let value = serde_json::to_value(&manifest).unwrap();
assert_eq!(
value,
serde_json::json!({
"shape": [{"lat": 48.1, "lon": 11.5}, {"lat": 48.2, "lon": 11.6}],
"costing": "auto",
"costing_options": {"auto": {}},
"shape_match": "walk_or_snap"
})
);
}
#[test]
fn test_serialize_manifest_encoded_polyline() {
let manifest =
Manifest::builder_encoded("some_polyline", costing::Costing::Auto(Default::default()))
.shape_format(ShapeFormat::Polyline5);
let value = serde_json::to_value(&manifest).unwrap();
assert_eq!(
value,
serde_json::json!({
"encoded_polyline": "some_polyline",
"shape_format": "polyline5",
"costing": "auto",
"costing_options": {"auto": {}},
"shape_match": "walk_or_snap"
})
);
}
#[test]
fn test_serialize_manifest_with_filter() {
let manifest = Manifest::builder(
[TracePoint::new(48.1, 11.5)],
costing::Costing::Pedestrian(Default::default()),
)
.shape_match(ShapeMatch::MapSnap)
.include_attributes(["edge.surface", "edge.road_class"]);
let value = serde_json::to_value(&manifest).unwrap();
assert_eq!(
value,
serde_json::json!({
"shape": [{"lat": 48.1, "lon": 11.5}],
"costing": "pedestrian",
"costing_options": {"pedestrian": {}},
"shape_match": "map_snap",
"filters": {
"attributes": ["edge.surface", "edge.road_class"],
"action": "include"
}
})
);
}
#[test]
fn test_serialize_manifest_exclude_attributes() {
let manifest = Manifest::builder(
[TracePoint::new(48.1, 11.5)],
costing::Costing::Auto(Default::default()),
)
.exclude_attributes(["edge.names"]);
let value = serde_json::to_value(&manifest).unwrap();
assert_eq!(
value["filters"],
serde_json::json!({
"attributes": ["edge.names"],
"action": "exclude"
})
);
}
#[test]
fn test_serialize_manifest_with_all_options() {
let manifest = Manifest::builder(
[TracePoint::new(48.1, 11.5)],
costing::Costing::Auto(Default::default()),
)
.units(super::super::Units::Imperial)
.id("my-trace")
.language("de-DE")
.trace_options(TraceOptions {
search_radius: Some(50.0),
gps_accuracy: Some(10.0),
breakage_distance: Some(3000.0),
interpolation_distance: Some(20.0),
})
.durations(vec![0.0, 5.0, 10.0])
.use_timestamps(true)
.begin_time("2025-01-15T08:30");
let value = serde_json::to_value(&manifest).unwrap();
assert_eq!(value["units"], serde_json::json!("miles"));
assert_eq!(value["id"], serde_json::json!("my-trace"));
assert_eq!(value["language"], serde_json::json!("de-DE"));
assert_eq!(
value["trace_options"]["search_radius"],
serde_json::json!(50.0)
);
assert_eq!(
value["trace_options"]["gps_accuracy"],
serde_json::json!(10.0)
);
assert_eq!(
value["trace_options"]["breakage_distance"],
serde_json::json!(3000.0)
);
assert_eq!(
value["trace_options"]["interpolation_distance"],
serde_json::json!(20.0)
);
assert_eq!(value["durations"], serde_json::json!([0.0, 5.0, 10.0]));
assert_eq!(value["use_timestamps"], serde_json::json!(true));
assert_eq!(value["begin_time"], serde_json::json!("2025-01-15T08:30"));
}
#[test]
fn test_serialize_trace_options_skips_none() {
let manifest = Manifest::builder(
[TracePoint::new(48.1, 11.5)],
costing::Costing::Auto(Default::default()),
)
.trace_options(TraceOptions {
search_radius: Some(50.0),
..Default::default()
});
let value = serde_json::to_value(&manifest).unwrap();
assert_eq!(
value["trace_options"],
serde_json::json!({"search_radius": 50.0})
);
}
#[test]
fn test_deserialize_response() {
let json = serde_json::json!({
"edges": [{
"surface": "paved",
"road_class": "primary",
"use": "road",
"length": 0.123,
"names": ["Main Street"],
"begin_shape_index": 0,
"end_shape_index": 5,
"way_id": 12345,
"source_percent_along": 0.1,
"target_percent_along": 0.9
}],
"matched_points": [{
"lat": 48.1,
"lon": 11.5,
"type": "matched",
"edge_index": 0,
"distance_along_edge": 0.5
}],
"shape": "encoded_shape_string",
"units": "km",
"id": "my-trace",
"warnings": [{"message": "some warning"}]
});
let response: Response = serde_json::from_value(json).unwrap();
assert_eq!(response.edges.len(), 1);
assert_eq!(response.edges[0].surface, Some(Surface::Paved));
assert_eq!(response.edges[0].road_class, Some(RoadClass::Primary));
assert_eq!(response.edges[0].r#use, Some(EdgeUse::Road));
assert_eq!(response.edges[0].length, Some(0.123));
assert_eq!(
response.edges[0].names,
Some(vec!["Main Street".to_string()])
);
assert_eq!(response.edges[0].begin_shape_index, Some(0));
assert_eq!(response.edges[0].end_shape_index, Some(5));
assert_eq!(response.edges[0].way_id, Some(12345));
assert_eq!(response.edges[0].source_percent_along, Some(0.1));
assert_eq!(response.edges[0].target_percent_along, Some(0.9));
assert_eq!(response.matched_points.len(), 1);
assert_eq!(response.matched_points[0].lat, 48.1);
assert_eq!(response.matched_points[0].lon, 11.5);
assert_eq!(
response.matched_points[0].r#type,
Some("matched".to_string())
);
assert_eq!(response.matched_points[0].edge_index, Some(0));
assert_eq!(response.matched_points[0].distance_along_edge, Some(0.5));
assert_eq!(response.shape, Some("encoded_shape_string".to_string()));
assert_eq!(response.units, Some("km".to_string()));
assert_eq!(response.id, Some("my-trace".to_string()));
assert_eq!(response.warnings.len(), 1);
}
#[test]
fn test_deserialize_response_with_defaults() {
let json = serde_json::json!({});
let response: Response = serde_json::from_value(json).unwrap();
assert_eq!(response.edges.len(), 0);
assert_eq!(response.matched_points.len(), 0);
assert_eq!(response.shape, None);
assert_eq!(response.units, None);
assert_eq!(response.id, None);
assert_eq!(response.warnings.len(), 0);
}
#[test]
fn test_deserialize_edge_with_defaults() {
let json = serde_json::json!({});
let edge: Edge = serde_json::from_value(json).unwrap();
assert_eq!(edge.surface, None);
assert_eq!(edge.road_class, None);
assert_eq!(edge.r#use, None);
assert_eq!(edge.length, None);
assert_eq!(edge.names, None);
assert_eq!(edge.way_id, None);
}
#[test]
fn test_serialize_shape_match_variants() {
assert_eq!(
serde_json::to_value(ShapeMatch::WalkOrSnap).unwrap(),
serde_json::json!("walk_or_snap")
);
assert_eq!(
serde_json::to_value(ShapeMatch::MapSnap).unwrap(),
serde_json::json!("map_snap")
);
assert_eq!(
serde_json::to_value(ShapeMatch::EdgeWalk).unwrap(),
serde_json::json!("edge_walk")
);
}
}