Skip to main content

valhalla_client/
trace_attributes.rs

1//! Models connected to the [`trace_attributes`] map-matching API
2//!
3//! See <https://valhalla.github.io/valhalla/api/map-matching/api-reference/> for details.
4
5use crate::costing;
6use crate::elevation::ShapeFormat;
7pub use crate::shapes::ShapePoint;
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11/// A shape point for the trace_attributes request.
12#[derive(Serialize, Debug, Clone)]
13pub struct TracePoint {
14    /// Latitude in degrees
15    pub lat: f64,
16    /// Longitude in degrees
17    pub lon: f64,
18}
19
20impl TracePoint {
21    /// Create a new trace point
22    pub fn new(lat: f64, lon: f64) -> Self {
23        Self { lat, lon }
24    }
25}
26
27/// How to match the shape to the road network.
28#[derive(Serialize, Debug, Clone, Default)]
29#[serde(rename_all = "snake_case")]
30pub enum ShapeMatch {
31    /// Try edge walking first, fall back to map matching
32    #[default]
33    WalkOrSnap,
34    /// Use map matching algorithm
35    MapSnap,
36    /// Use edge walking algorithm (requires very precise input)
37    EdgeWalk,
38}
39
40/// Filter to include or exclude specific attributes in the response.
41#[derive(Serialize, Debug, Clone)]
42pub struct Filter {
43    /// List of attribute names to include or exclude
44    pub attributes: Vec<String>,
45    /// Whether to include or exclude the listed attributes
46    pub action: FilterAction,
47}
48
49/// Whether to include or exclude filtered attributes.
50#[derive(Serialize, Debug, Clone)]
51#[serde(rename_all = "snake_case")]
52pub enum FilterAction {
53    /// Include only the listed attributes
54    Include,
55    /// Exclude the listed attributes
56    Exclude,
57}
58
59/// Options to fine-tune the GPS trace matching algorithm.
60#[serde_with::skip_serializing_none]
61#[derive(Serialize, Debug, Clone, Default)]
62pub struct TraceOptions {
63    /// Search radius in meters around each input point within which to search
64    /// for candidate edges.
65    ///
66    /// Default: `25`
67    pub search_radius: Option<f64>,
68    /// GPS accuracy in meters for the input points.
69    ///
70    /// Default: `5`
71    pub gps_accuracy: Option<f64>,
72    /// Distance in meters beyond which a new breakage will be created.
73    ///
74    /// Default: `2000`
75    pub breakage_distance: Option<f64>,
76    /// Distance in meters to interpolate between input points.
77    ///
78    /// Default: `10`
79    pub interpolation_distance: Option<f64>,
80}
81
82/// Request manifest for the trace_attributes API.
83#[serde_with::skip_serializing_none]
84#[derive(Serialize, Debug, Clone)]
85pub struct Manifest {
86    shape: Option<Vec<TracePoint>>,
87    encoded_polyline: Option<String>,
88    shape_format: Option<ShapeFormat>,
89    #[serde(flatten)]
90    costing: costing::Costing,
91    shape_match: ShapeMatch,
92    filters: Option<Filter>,
93    trace_options: Option<TraceOptions>,
94    units: Option<super::Units>,
95    id: Option<String>,
96    language: Option<String>,
97    durations: Option<Vec<f64>>,
98    use_timestamps: Option<bool>,
99    begin_time: Option<String>,
100}
101
102impl Manifest {
103    /// Create a builder with the given shape points and costing.
104    pub fn builder(shape: impl IntoIterator<Item = TracePoint>, costing: costing::Costing) -> Self {
105        Self {
106            shape: Some(shape.into_iter().collect()),
107            encoded_polyline: None,
108            shape_format: None,
109            costing,
110            shape_match: ShapeMatch::default(),
111            filters: None,
112            trace_options: None,
113            units: None,
114            id: None,
115            language: None,
116            durations: None,
117            use_timestamps: None,
118            begin_time: None,
119        }
120    }
121
122    /// Create a builder with an encoded polyline and costing.
123    ///
124    /// See [`Self::shape_format`] to set the precision of the polyline.
125    pub fn builder_encoded(encoded_polyline: impl ToString, costing: costing::Costing) -> Self {
126        Self {
127            shape: None,
128            encoded_polyline: Some(encoded_polyline.to_string()),
129            shape_format: None,
130            costing,
131            shape_match: ShapeMatch::default(),
132            filters: None,
133            trace_options: None,
134            units: None,
135            id: None,
136            language: None,
137            durations: None,
138            use_timestamps: None,
139            begin_time: None,
140        }
141    }
142
143    /// Set the shape matching mode.
144    pub fn shape_match(mut self, shape_match: ShapeMatch) -> Self {
145        self.shape_match = shape_match;
146        self
147    }
148
149    /// Specifies whether the polyline is encoded with
150    /// - 6 digit precision ([`ShapeFormat::Polyline6`]) or
151    /// - 5 digit precision ([`ShapeFormat::Polyline5`]).
152    ///
153    /// Default: [`ShapeFormat::Polyline6`]
154    pub fn shape_format(mut self, shape_format: ShapeFormat) -> Self {
155        debug_assert!(
156            self.shape.is_none(),
157            "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."
158        );
159        self.shape_format = Some(shape_format);
160        self
161    }
162
163    /// Set the attribute filter to include specific edge attributes.
164    pub fn include_attributes(
165        mut self,
166        attributes: impl IntoIterator<Item = impl Into<String>>,
167    ) -> Self {
168        self.filters = Some(Filter {
169            attributes: attributes.into_iter().map(|a| a.into()).collect(),
170            action: FilterAction::Include,
171        });
172        self
173    }
174
175    /// Set the attribute filter to exclude specific edge attributes.
176    pub fn exclude_attributes(
177        mut self,
178        attributes: impl IntoIterator<Item = impl Into<String>>,
179    ) -> Self {
180        self.filters = Some(Filter {
181            attributes: attributes.into_iter().map(|a| a.into()).collect(),
182            action: FilterAction::Exclude,
183        });
184        self
185    }
186
187    /// Set trace matching algorithm options.
188    pub fn trace_options(mut self, trace_options: TraceOptions) -> Self {
189        self.trace_options = Some(trace_options);
190        self
191    }
192
193    /// Sets the distance units for output.
194    ///
195    /// Default: [`super::Units::Metric`]
196    pub fn units(mut self, units: super::Units) -> Self {
197        self.units = Some(units);
198        self
199    }
200
201    /// Name of the request.
202    ///
203    /// If id is specified, the naming will be sent through to the response.
204    pub fn id(mut self, id: impl ToString) -> Self {
205        self.id = Some(id.to_string());
206        self
207    }
208
209    /// The language of the narration instructions based on the
210    /// [IETF BCP 47](https://en.wikipedia.org/wiki/IETF_language_tag) language tag string.
211    ///
212    /// Default: `en-US`
213    pub fn language(mut self, language: impl ToString) -> Self {
214        self.language = Some(language.to_string());
215        self
216    }
217
218    /// Set durations in seconds between successive input points.
219    ///
220    /// When provided along with [`Self::use_timestamps`], Valhalla can use timing
221    /// information to improve matching accuracy.
222    pub fn durations(mut self, durations: impl IntoIterator<Item = f64>) -> Self {
223        self.durations = Some(durations.into_iter().collect());
224        self
225    }
226
227    /// Whether to use timestamps/durations for the trace matching.
228    ///
229    /// Default: `false`
230    pub fn use_timestamps(mut self, use_timestamps: bool) -> Self {
231        self.use_timestamps = Some(use_timestamps);
232        self
233    }
234
235    /// Set the begin time for the trace in the format `YYYY-MM-DDTHH:MM`.
236    ///
237    /// Used together with [`Self::durations`] and [`Self::use_timestamps`].
238    pub fn begin_time(mut self, begin_time: impl ToString) -> Self {
239        self.begin_time = Some(begin_time.to_string());
240        self
241    }
242}
243
244/// Surface type of a road edge.
245#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq)]
246#[serde(rename_all = "snake_case")]
247pub enum Surface {
248    /// Smooth paved surface
249    PavedSmooth,
250    /// Paved surface
251    Paved,
252    /// Rough paved surface
253    PavedRough,
254    /// Compacted surface
255    Compacted,
256    /// Dirt surface
257    Dirt,
258    /// Gravel surface
259    Gravel,
260    /// Path surface
261    Path,
262    /// Impassable surface
263    Impassable,
264}
265
266/// Road classification of an edge.
267#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq)]
268#[serde(rename_all = "snake_case")]
269pub enum RoadClass {
270    /// Motorway
271    Motorway,
272    /// Trunk road
273    Trunk,
274    /// Primary road
275    Primary,
276    /// Secondary road
277    Secondary,
278    /// Tertiary road
279    Tertiary,
280    /// Unclassified road
281    Unclassified,
282    /// Residential road
283    Residential,
284    /// Service or other road
285    ServiceOther,
286}
287
288/// Use type of an edge.
289#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq)]
290#[serde(rename_all = "snake_case")]
291pub enum EdgeUse {
292    /// Standard road
293    Road,
294    /// Ramp (highway on/off)
295    Ramp,
296    /// Turn channel
297    TurnChannel,
298    /// Track
299    Track,
300    /// Driveway
301    Driveway,
302    /// Alley
303    Alley,
304    /// Parking aisle
305    ParkingAisle,
306    /// Emergency access
307    EmergencyAccess,
308    /// Drive through
309    DriveThrough,
310    /// Cul-de-sac
311    Culdesac,
312    /// Cycleway
313    Cycleway,
314    /// Mountain bike trail
315    MountainBike,
316    /// Sidewalk
317    Sidewalk,
318    /// Footway
319    Footway,
320    /// Steps/stairs
321    Steps,
322    /// Ferry
323    Ferry,
324    /// Rail ferry
325    #[serde(rename = "rail-ferry")]
326    RailFerry,
327    /// Service road
328    ServiceRoad,
329    /// Path
330    Path,
331    /// Living street
332    LivingStreet,
333    /// Pedestrian crossing
334    PedestrianCrossing,
335    /// Other use
336    Other,
337}
338
339/// A matched edge in the trace_attributes response.
340#[derive(Deserialize, Serialize, Debug, Clone)]
341pub struct Edge {
342    /// Road surface type
343    #[serde(default)]
344    pub surface: Option<Surface>,
345    /// Road classification
346    #[serde(default)]
347    pub road_class: Option<RoadClass>,
348    /// Edge use type
349    #[serde(default)]
350    pub r#use: Option<EdgeUse>,
351    /// Length of the edge in the response units (km or miles)
352    #[serde(default)]
353    pub length: Option<f64>,
354    /// Road names
355    #[serde(default)]
356    pub names: Option<Vec<String>>,
357    /// Index into the response shape where this edge begins
358    #[serde(default)]
359    pub begin_shape_index: Option<u32>,
360    /// Index into the response shape where this edge ends
361    #[serde(default)]
362    pub end_shape_index: Option<u32>,
363    /// OSM way ID
364    #[serde(default)]
365    pub way_id: Option<u64>,
366    /// Percentage along the edge where the source point lies (first edge only)
367    #[serde(default)]
368    pub source_percent_along: Option<f64>,
369    /// Percentage along the edge where the target point lies (last edge only)
370    #[serde(default)]
371    pub target_percent_along: Option<f64>,
372}
373
374/// A matched point in the trace_attributes response.
375#[derive(Deserialize, Serialize, Debug, Clone)]
376pub struct MatchedPoint {
377    /// Latitude of the matched point
378    pub lat: f64,
379    /// Longitude of the matched point
380    pub lon: f64,
381    /// Match type
382    #[serde(default)]
383    pub r#type: Option<String>,
384    /// Index of the edge this point was matched to
385    #[serde(default)]
386    pub edge_index: Option<u32>,
387    /// Distance along the edge
388    #[serde(default)]
389    pub distance_along_edge: Option<f64>,
390}
391
392/// Response from the trace_attributes API.
393#[derive(Deserialize, Serialize, Debug, Clone)]
394pub struct Response {
395    /// Matched edges with attributes
396    #[serde(default)]
397    pub edges: Vec<Edge>,
398    /// Matched input points
399    #[serde(default)]
400    pub matched_points: Vec<MatchedPoint>,
401    /// Encoded polyline of the matched path
402    #[serde(default)]
403    pub shape: Option<String>,
404    /// Units used in the response
405    #[serde(default)]
406    pub units: Option<String>,
407    /// Name of the request (echoed from the request)
408    #[serde(default)]
409    pub id: Option<String>,
410    /// This array may contain warning objects informing about deprecated
411    /// request parameters, clamped values, etc.
412    #[serde(default)]
413    pub warnings: Vec<Value>,
414}
415
416#[cfg(test)]
417mod tests {
418    use super::*;
419
420    #[test]
421    fn test_serialize_manifest() {
422        let manifest = Manifest::builder(
423            [TracePoint::new(48.1, 11.5), TracePoint::new(48.2, 11.6)],
424            costing::Costing::Auto(Default::default()),
425        );
426        let value = serde_json::to_value(&manifest).unwrap();
427        assert_eq!(
428            value,
429            serde_json::json!({
430                "shape": [{"lat": 48.1, "lon": 11.5}, {"lat": 48.2, "lon": 11.6}],
431                "costing": "auto",
432                "costing_options": {"auto": {}},
433                "shape_match": "walk_or_snap"
434            })
435        );
436    }
437
438    #[test]
439    fn test_serialize_manifest_encoded_polyline() {
440        let manifest =
441            Manifest::builder_encoded("some_polyline", costing::Costing::Auto(Default::default()))
442                .shape_format(ShapeFormat::Polyline5);
443        let value = serde_json::to_value(&manifest).unwrap();
444        assert_eq!(
445            value,
446            serde_json::json!({
447                "encoded_polyline": "some_polyline",
448                "shape_format": "polyline5",
449                "costing": "auto",
450                "costing_options": {"auto": {}},
451                "shape_match": "walk_or_snap"
452            })
453        );
454    }
455
456    #[test]
457    fn test_serialize_manifest_with_filter() {
458        let manifest = Manifest::builder(
459            [TracePoint::new(48.1, 11.5)],
460            costing::Costing::Pedestrian(Default::default()),
461        )
462        .shape_match(ShapeMatch::MapSnap)
463        .include_attributes(["edge.surface", "edge.road_class"]);
464        let value = serde_json::to_value(&manifest).unwrap();
465        assert_eq!(
466            value,
467            serde_json::json!({
468                "shape": [{"lat": 48.1, "lon": 11.5}],
469                "costing": "pedestrian",
470                "costing_options": {"pedestrian": {}},
471                "shape_match": "map_snap",
472                "filters": {
473                    "attributes": ["edge.surface", "edge.road_class"],
474                    "action": "include"
475                }
476            })
477        );
478    }
479
480    #[test]
481    fn test_serialize_manifest_exclude_attributes() {
482        let manifest = Manifest::builder(
483            [TracePoint::new(48.1, 11.5)],
484            costing::Costing::Auto(Default::default()),
485        )
486        .exclude_attributes(["edge.names"]);
487        let value = serde_json::to_value(&manifest).unwrap();
488        assert_eq!(
489            value["filters"],
490            serde_json::json!({
491                "attributes": ["edge.names"],
492                "action": "exclude"
493            })
494        );
495    }
496
497    #[test]
498    fn test_serialize_manifest_with_all_options() {
499        let manifest = Manifest::builder(
500            [TracePoint::new(48.1, 11.5)],
501            costing::Costing::Auto(Default::default()),
502        )
503        .units(super::super::Units::Imperial)
504        .id("my-trace")
505        .language("de-DE")
506        .trace_options(TraceOptions {
507            search_radius: Some(50.0),
508            gps_accuracy: Some(10.0),
509            breakage_distance: Some(3000.0),
510            interpolation_distance: Some(20.0),
511        })
512        .durations(vec![0.0, 5.0, 10.0])
513        .use_timestamps(true)
514        .begin_time("2025-01-15T08:30");
515        let value = serde_json::to_value(&manifest).unwrap();
516        assert_eq!(value["units"], serde_json::json!("miles"));
517        assert_eq!(value["id"], serde_json::json!("my-trace"));
518        assert_eq!(value["language"], serde_json::json!("de-DE"));
519        assert_eq!(
520            value["trace_options"]["search_radius"],
521            serde_json::json!(50.0)
522        );
523        assert_eq!(
524            value["trace_options"]["gps_accuracy"],
525            serde_json::json!(10.0)
526        );
527        assert_eq!(
528            value["trace_options"]["breakage_distance"],
529            serde_json::json!(3000.0)
530        );
531        assert_eq!(
532            value["trace_options"]["interpolation_distance"],
533            serde_json::json!(20.0)
534        );
535        assert_eq!(value["durations"], serde_json::json!([0.0, 5.0, 10.0]));
536        assert_eq!(value["use_timestamps"], serde_json::json!(true));
537        assert_eq!(value["begin_time"], serde_json::json!("2025-01-15T08:30"));
538    }
539
540    #[test]
541    fn test_serialize_trace_options_skips_none() {
542        let manifest = Manifest::builder(
543            [TracePoint::new(48.1, 11.5)],
544            costing::Costing::Auto(Default::default()),
545        )
546        .trace_options(TraceOptions {
547            search_radius: Some(50.0),
548            ..Default::default()
549        });
550        let value = serde_json::to_value(&manifest).unwrap();
551        assert_eq!(
552            value["trace_options"],
553            serde_json::json!({"search_radius": 50.0})
554        );
555    }
556
557    #[test]
558    fn test_deserialize_response() {
559        let json = serde_json::json!({
560            "edges": [{
561                "surface": "paved",
562                "road_class": "primary",
563                "use": "road",
564                "length": 0.123,
565                "names": ["Main Street"],
566                "begin_shape_index": 0,
567                "end_shape_index": 5,
568                "way_id": 12345,
569                "source_percent_along": 0.1,
570                "target_percent_along": 0.9
571            }],
572            "matched_points": [{
573                "lat": 48.1,
574                "lon": 11.5,
575                "type": "matched",
576                "edge_index": 0,
577                "distance_along_edge": 0.5
578            }],
579            "shape": "encoded_shape_string",
580            "units": "km",
581            "id": "my-trace",
582            "warnings": [{"message": "some warning"}]
583        });
584        let response: Response = serde_json::from_value(json).unwrap();
585        assert_eq!(response.edges.len(), 1);
586        assert_eq!(response.edges[0].surface, Some(Surface::Paved));
587        assert_eq!(response.edges[0].road_class, Some(RoadClass::Primary));
588        assert_eq!(response.edges[0].r#use, Some(EdgeUse::Road));
589        assert_eq!(response.edges[0].length, Some(0.123));
590        assert_eq!(
591            response.edges[0].names,
592            Some(vec!["Main Street".to_string()])
593        );
594        assert_eq!(response.edges[0].begin_shape_index, Some(0));
595        assert_eq!(response.edges[0].end_shape_index, Some(5));
596        assert_eq!(response.edges[0].way_id, Some(12345));
597        assert_eq!(response.edges[0].source_percent_along, Some(0.1));
598        assert_eq!(response.edges[0].target_percent_along, Some(0.9));
599        assert_eq!(response.matched_points.len(), 1);
600        assert_eq!(response.matched_points[0].lat, 48.1);
601        assert_eq!(response.matched_points[0].lon, 11.5);
602        assert_eq!(
603            response.matched_points[0].r#type,
604            Some("matched".to_string())
605        );
606        assert_eq!(response.matched_points[0].edge_index, Some(0));
607        assert_eq!(response.matched_points[0].distance_along_edge, Some(0.5));
608        assert_eq!(response.shape, Some("encoded_shape_string".to_string()));
609        assert_eq!(response.units, Some("km".to_string()));
610        assert_eq!(response.id, Some("my-trace".to_string()));
611        assert_eq!(response.warnings.len(), 1);
612    }
613
614    #[test]
615    fn test_deserialize_response_with_defaults() {
616        let json = serde_json::json!({});
617        let response: Response = serde_json::from_value(json).unwrap();
618        assert_eq!(response.edges.len(), 0);
619        assert_eq!(response.matched_points.len(), 0);
620        assert_eq!(response.shape, None);
621        assert_eq!(response.units, None);
622        assert_eq!(response.id, None);
623        assert_eq!(response.warnings.len(), 0);
624    }
625
626    #[test]
627    fn test_deserialize_edge_with_defaults() {
628        let json = serde_json::json!({});
629        let edge: Edge = serde_json::from_value(json).unwrap();
630        assert_eq!(edge.surface, None);
631        assert_eq!(edge.road_class, None);
632        assert_eq!(edge.r#use, None);
633        assert_eq!(edge.length, None);
634        assert_eq!(edge.names, None);
635        assert_eq!(edge.way_id, None);
636    }
637
638    #[test]
639    fn test_serialize_shape_match_variants() {
640        assert_eq!(
641            serde_json::to_value(ShapeMatch::WalkOrSnap).unwrap(),
642            serde_json::json!("walk_or_snap")
643        );
644        assert_eq!(
645            serde_json::to_value(ShapeMatch::MapSnap).unwrap(),
646            serde_json::json!("map_snap")
647        );
648        assert_eq!(
649            serde_json::to_value(ShapeMatch::EdgeWalk).unwrap(),
650            serde_json::json!("edge_walk")
651        );
652    }
653}