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    /// Heading at the start of the edge in degrees (0-360)
373    #[serde(default)]
374    pub begin_heading: Option<f64>,
375    /// Heading at the end of the edge in degrees (0-360)
376    #[serde(default)]
377    pub end_heading: Option<f64>,
378    /// Speed in kph along the edge
379    #[serde(default)]
380    pub speed: Option<f64>,
381    /// `true` if a toll booth is encountered on this edge
382    #[serde(default)]
383    pub toll: Option<bool>,
384    /// `true` if a tunnel is encountered on this edge
385    #[serde(default)]
386    pub tunnel: Option<bool>,
387    /// `true` if a bridge is encountered on this edge
388    #[serde(default)]
389    pub bridge: Option<bool>,
390    /// `true` if this edge is part of a roundabout
391    #[serde(default)]
392    pub roundabout: Option<bool>,
393    /// `true` if this edge is an internal intersection edge
394    #[serde(default)]
395    pub internal_intersection: Option<bool>,
396    /// The number of signs on the edge
397    #[serde(default)]
398    pub sign: Option<EdgeSign>,
399}
400
401/// Sign information associated with an edge.
402#[derive(Deserialize, Serialize, Debug, Clone)]
403pub struct EdgeSign {
404    /// Exit number elements
405    #[serde(default)]
406    pub exit_number_elements: Vec<SignElement>,
407    /// Exit branch elements
408    #[serde(default)]
409    pub exit_branch_elements: Vec<SignElement>,
410    /// Exit toward elements
411    #[serde(default)]
412    pub exit_toward_elements: Vec<SignElement>,
413    /// Exit name elements
414    #[serde(default)]
415    pub exit_name_elements: Vec<SignElement>,
416}
417
418/// A single element on a sign.
419#[derive(Deserialize, Serialize, Debug, Clone)]
420pub struct SignElement {
421    /// The text of the sign element
422    pub text: String,
423}
424
425/// A matched point in the trace_attributes response.
426#[derive(Deserialize, Serialize, Debug, Clone)]
427pub struct MatchedPoint {
428    /// Latitude of the matched point
429    pub lat: f64,
430    /// Longitude of the matched point
431    pub lon: f64,
432    /// Match type
433    #[serde(default)]
434    pub r#type: Option<String>,
435    /// Index of the edge this point was matched to
436    #[serde(default)]
437    pub edge_index: Option<u32>,
438    /// Distance along the edge
439    #[serde(default)]
440    pub distance_along_edge: Option<f64>,
441}
442
443/// Response from the trace_attributes API.
444#[derive(Deserialize, Serialize, Debug, Clone)]
445pub struct Response {
446    /// Matched edges with attributes
447    #[serde(default)]
448    pub edges: Vec<Edge>,
449    /// Matched input points
450    #[serde(default)]
451    pub matched_points: Vec<MatchedPoint>,
452    /// Encoded polyline of the matched path
453    #[serde(default)]
454    pub shape: Option<String>,
455    /// Units used in the response
456    #[serde(default)]
457    pub units: Option<String>,
458    /// Name of the request (echoed from the request)
459    #[serde(default)]
460    pub id: Option<String>,
461    /// This array may contain warning objects informing about deprecated
462    /// request parameters, clamped values, etc.
463    #[serde(default)]
464    pub warnings: Vec<Value>,
465}
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470
471    #[test]
472    fn test_serialize_manifest() {
473        let manifest = Manifest::builder(
474            [TracePoint::new(48.1, 11.5), TracePoint::new(48.2, 11.6)],
475            costing::Costing::Auto(Default::default()),
476        );
477        let value = serde_json::to_value(&manifest).unwrap();
478        assert_eq!(
479            value,
480            serde_json::json!({
481                "shape": [{"lat": 48.1, "lon": 11.5}, {"lat": 48.2, "lon": 11.6}],
482                "costing": "auto",
483                "costing_options": {"auto": {}},
484                "shape_match": "walk_or_snap"
485            })
486        );
487    }
488
489    #[test]
490    fn test_serialize_manifest_encoded_polyline() {
491        let manifest =
492            Manifest::builder_encoded("some_polyline", costing::Costing::Auto(Default::default()))
493                .shape_format(ShapeFormat::Polyline5);
494        let value = serde_json::to_value(&manifest).unwrap();
495        assert_eq!(
496            value,
497            serde_json::json!({
498                "encoded_polyline": "some_polyline",
499                "shape_format": "polyline5",
500                "costing": "auto",
501                "costing_options": {"auto": {}},
502                "shape_match": "walk_or_snap"
503            })
504        );
505    }
506
507    #[test]
508    fn test_serialize_manifest_with_filter() {
509        let manifest = Manifest::builder(
510            [TracePoint::new(48.1, 11.5)],
511            costing::Costing::Pedestrian(Default::default()),
512        )
513        .shape_match(ShapeMatch::MapSnap)
514        .include_attributes(["edge.surface", "edge.road_class"]);
515        let value = serde_json::to_value(&manifest).unwrap();
516        assert_eq!(
517            value,
518            serde_json::json!({
519                "shape": [{"lat": 48.1, "lon": 11.5}],
520                "costing": "pedestrian",
521                "costing_options": {"pedestrian": {}},
522                "shape_match": "map_snap",
523                "filters": {
524                    "attributes": ["edge.surface", "edge.road_class"],
525                    "action": "include"
526                }
527            })
528        );
529    }
530
531    #[test]
532    fn test_serialize_manifest_exclude_attributes() {
533        let manifest = Manifest::builder(
534            [TracePoint::new(48.1, 11.5)],
535            costing::Costing::Auto(Default::default()),
536        )
537        .exclude_attributes(["edge.names"]);
538        let value = serde_json::to_value(&manifest).unwrap();
539        assert_eq!(
540            value["filters"],
541            serde_json::json!({
542                "attributes": ["edge.names"],
543                "action": "exclude"
544            })
545        );
546    }
547
548    #[test]
549    fn test_serialize_manifest_with_all_options() {
550        let manifest = Manifest::builder(
551            [TracePoint::new(48.1, 11.5)],
552            costing::Costing::Auto(Default::default()),
553        )
554        .units(super::super::Units::Imperial)
555        .id("my-trace")
556        .language("de-DE")
557        .trace_options(TraceOptions {
558            search_radius: Some(50.0),
559            gps_accuracy: Some(10.0),
560            breakage_distance: Some(3000.0),
561            interpolation_distance: Some(20.0),
562        })
563        .durations(vec![0.0, 5.0, 10.0])
564        .use_timestamps(true)
565        .begin_time("2025-01-15T08:30");
566        let value = serde_json::to_value(&manifest).unwrap();
567        assert_eq!(value["units"], serde_json::json!("miles"));
568        assert_eq!(value["id"], serde_json::json!("my-trace"));
569        assert_eq!(value["language"], serde_json::json!("de-DE"));
570        assert_eq!(
571            value["trace_options"]["search_radius"],
572            serde_json::json!(50.0)
573        );
574        assert_eq!(
575            value["trace_options"]["gps_accuracy"],
576            serde_json::json!(10.0)
577        );
578        assert_eq!(
579            value["trace_options"]["breakage_distance"],
580            serde_json::json!(3000.0)
581        );
582        assert_eq!(
583            value["trace_options"]["interpolation_distance"],
584            serde_json::json!(20.0)
585        );
586        assert_eq!(value["durations"], serde_json::json!([0.0, 5.0, 10.0]));
587        assert_eq!(value["use_timestamps"], serde_json::json!(true));
588        assert_eq!(value["begin_time"], serde_json::json!("2025-01-15T08:30"));
589    }
590
591    #[test]
592    fn test_serialize_trace_options_skips_none() {
593        let manifest = Manifest::builder(
594            [TracePoint::new(48.1, 11.5)],
595            costing::Costing::Auto(Default::default()),
596        )
597        .trace_options(TraceOptions {
598            search_radius: Some(50.0),
599            ..Default::default()
600        });
601        let value = serde_json::to_value(&manifest).unwrap();
602        assert_eq!(
603            value["trace_options"],
604            serde_json::json!({"search_radius": 50.0})
605        );
606    }
607
608    #[test]
609    fn test_deserialize_response() {
610        let json = serde_json::json!({
611            "edges": [{
612                "surface": "paved",
613                "road_class": "primary",
614                "use": "road",
615                "length": 0.123,
616                "names": ["Main Street"],
617                "begin_shape_index": 0,
618                "end_shape_index": 5,
619                "way_id": 12345,
620                "source_percent_along": 0.1,
621                "target_percent_along": 0.9
622            }],
623            "matched_points": [{
624                "lat": 48.1,
625                "lon": 11.5,
626                "type": "matched",
627                "edge_index": 0,
628                "distance_along_edge": 0.5
629            }],
630            "shape": "encoded_shape_string",
631            "units": "km",
632            "id": "my-trace",
633            "warnings": [{"message": "some warning"}]
634        });
635        let response: Response = serde_json::from_value(json).unwrap();
636        assert_eq!(response.edges.len(), 1);
637        assert_eq!(response.edges[0].surface, Some(Surface::Paved));
638        assert_eq!(response.edges[0].road_class, Some(RoadClass::Primary));
639        assert_eq!(response.edges[0].r#use, Some(EdgeUse::Road));
640        assert_eq!(response.edges[0].length, Some(0.123));
641        assert_eq!(
642            response.edges[0].names,
643            Some(vec!["Main Street".to_string()])
644        );
645        assert_eq!(response.edges[0].begin_shape_index, Some(0));
646        assert_eq!(response.edges[0].end_shape_index, Some(5));
647        assert_eq!(response.edges[0].way_id, Some(12345));
648        assert_eq!(response.edges[0].source_percent_along, Some(0.1));
649        assert_eq!(response.edges[0].target_percent_along, Some(0.9));
650        assert_eq!(response.matched_points.len(), 1);
651        assert_eq!(response.matched_points[0].lat, 48.1);
652        assert_eq!(response.matched_points[0].lon, 11.5);
653        assert_eq!(
654            response.matched_points[0].r#type,
655            Some("matched".to_string())
656        );
657        assert_eq!(response.matched_points[0].edge_index, Some(0));
658        assert_eq!(response.matched_points[0].distance_along_edge, Some(0.5));
659        assert_eq!(response.shape, Some("encoded_shape_string".to_string()));
660        assert_eq!(response.units, Some("km".to_string()));
661        assert_eq!(response.id, Some("my-trace".to_string()));
662        assert_eq!(response.warnings.len(), 1);
663    }
664
665    #[test]
666    fn test_deserialize_response_with_defaults() {
667        let json = serde_json::json!({});
668        let response: Response = serde_json::from_value(json).unwrap();
669        assert_eq!(response.edges.len(), 0);
670        assert_eq!(response.matched_points.len(), 0);
671        assert_eq!(response.shape, None);
672        assert_eq!(response.units, None);
673        assert_eq!(response.id, None);
674        assert_eq!(response.warnings.len(), 0);
675    }
676
677    #[test]
678    fn test_deserialize_edge_with_defaults() {
679        let json = serde_json::json!({});
680        let edge: Edge = serde_json::from_value(json).unwrap();
681        assert_eq!(edge.surface, None);
682        assert_eq!(edge.road_class, None);
683        assert_eq!(edge.r#use, None);
684        assert_eq!(edge.length, None);
685        assert_eq!(edge.names, None);
686        assert_eq!(edge.way_id, None);
687    }
688
689    #[test]
690    fn test_serialize_shape_match_variants() {
691        assert_eq!(
692            serde_json::to_value(ShapeMatch::WalkOrSnap).unwrap(),
693            serde_json::json!("walk_or_snap")
694        );
695        assert_eq!(
696            serde_json::to_value(ShapeMatch::MapSnap).unwrap(),
697            serde_json::json!("map_snap")
698        );
699        assert_eq!(
700            serde_json::to_value(ShapeMatch::EdgeWalk).unwrap(),
701            serde_json::json!("edge_walk")
702        );
703    }
704}