Skip to main content

valhalla_client/costing/
transit.rs

1//! Transit-specific costing options
2use serde::Serialize;
3
4#[serde_with::skip_serializing_none]
5#[derive(Serialize, Debug, Clone, Default, PartialEq)]
6pub(crate) struct TransitCostingOptionsInner {
7    use_bus: Option<f32>,
8    use_rail: Option<f32>,
9    use_transfers: Option<f32>,
10    filters: Option<Filters>,
11}
12
13#[derive(Serialize, Debug, Clone, Default, PartialEq)]
14/// Transit costing options
15pub struct TransitCostingOptions {
16    pub(crate) transit: TransitCostingOptionsInner,
17}
18impl TransitCostingOptions {
19    #[must_use]
20    /// Builder for [`TransitCostingOptions`]
21    pub fn builder() -> Self {
22        Self::default()
23    }
24    /// User's desire to use buses.
25    ///
26    /// Range of values from
27    /// - `0` (try to avoid buses) to
28    /// - `1` (strong preference for riding buses).
29    pub fn use_bus(mut self, use_bus: f32) -> Self {
30        self.transit.use_bus = Some(use_bus);
31        self
32    }
33    /// User's desire to use rail/subway/metro.
34    ///
35    /// Range of values from
36    /// - `0` (try to avoid rail) to
37    /// - `1` (strong preference for riding rail).
38    pub fn use_rail(mut self, use_rail: f32) -> Self {
39        self.transit.use_rail = Some(use_rail);
40        self
41    }
42    /// User's desire to favor transfers.
43    ///
44    /// Range of values from
45    /// - `0` (try to avoid transfers) to
46    /// - `1` (totally comfortable with transfers).
47    pub fn use_transfers(mut self, use_transfers: f32) -> Self {
48        self.transit.use_transfers = Some(use_transfers);
49        self
50    }
51    /// Sets a filter for one or more ~~`stops`~~ (TODO: need to re-enable)
52    ///
53    /// Filters must contain a list of so-called Onestop IDs, which is (supposed to be) a
54    /// unique identifier for GTFS data, and an [`Action`].
55    /// The OneStop ID is simply the feeds's directory name and the object's GTFS ID separated
56    /// by an underscore.
57    ///
58    /// Example:
59    /// A route with `route_id: AUR` in `routes.txt` from the feed `NYC` would have
60    /// the OneStop ID `NYC_AUR`, similar with operators/agencies
61    ///
62    /// **Tip**: Can be combined with [`Self::filter_routes`] and/or [`Self::filter_operators`]
63    #[doc(hidden)] // TODO: enable once this works in valhalla
64    pub fn filter_stops<S>(mut self, ids: impl IntoIterator<Item = S>, action: Action) -> Self
65    where
66        S: Into<String>,
67    {
68        let new_filter = Filter {
69            ids: ids.into_iter().map(Into::into).collect(),
70            action,
71        };
72        if let Some(ref mut filters) = self.transit.filters {
73            filters.stops = Some(new_filter);
74        } else {
75            self.transit.filters = Some(Filters {
76                stops: Some(new_filter),
77                ..Default::default()
78            });
79        }
80        self
81    }
82    /// Sets a filter for one or more `routes`
83    ///
84    /// Filters must contain a list of so-called Onestop IDs, which is (supposed to be) a
85    /// unique identifier for GTFS data, and an [`Action`].
86    /// The OneStop ID is simply the feeds's directory name and the object's GTFS ID separated
87    /// by an underscore.
88    ///
89    /// Example:
90    /// A route with `route_id: AUR` in `routes.txt` from the feed `NYC` would have
91    /// the OneStop ID `NYC_AUR`, similar with operators/agencies
92    ///
93    /// **Tip**: Can be combined with [`Self::filter_stops`] and/or [`Self::filter_operators`]
94    pub fn filter_routes<S>(mut self, ids: impl IntoIterator<Item = S>, action: Action) -> Self
95    where
96        S: Into<String>,
97    {
98        let new_filter = Filter {
99            ids: ids.into_iter().map(Into::into).collect(),
100            action,
101        };
102        if let Some(ref mut filters) = self.transit.filters {
103            filters.routes = Some(new_filter);
104        } else {
105            self.transit.filters = Some(Filters {
106                routes: Some(new_filter),
107                ..Default::default()
108            });
109        }
110        self
111    }
112    /// Sets a filter for one or more `operators`.
113    ///
114    /// Filters must contain a list of so-called Onestop IDs, which is (supposed to be) a
115    /// unique identifier for GTFS data, and an [`Action`].
116    /// The OneStop ID is simply the feeds's directory name and the object's GTFS ID separated
117    /// by an underscore.
118    ///
119    /// Example:
120    /// A route with `route_id: AUR` in `routes.txt` from the feed `NYC` would have
121    /// the OneStop ID `NYC_AUR`, similar with operators/agencies
122    ///
123    /// **Tip**: Can be combined with [`Self::filter_stops`] and/or [`Self::filter_routes`]
124    pub fn filter_operators<S>(mut self, ids: impl IntoIterator<Item = S>, action: Action) -> Self
125    where
126        S: Into<String>,
127    {
128        let new_filter = Filter {
129            ids: ids.into_iter().map(Into::into).collect(),
130            action,
131        };
132        if let Some(ref mut filters) = self.transit.filters {
133            filters.operators = Some(new_filter);
134        } else {
135            self.transit.filters = Some(Filters {
136                operators: Some(new_filter),
137                ..Default::default()
138            });
139        }
140        self
141    }
142}
143
144#[derive(Serialize, Debug, Clone, Copy, Default, PartialEq, Eq)]
145/// Action to take when filtering
146pub enum Action {
147    /// Include only the `ids` listed in the filter
148    #[default]
149    Include,
150    /// Exclude all the `ids` listed in the filter
151    Exclude,
152}
153
154#[serde_with::skip_serializing_none]
155#[derive(Serialize, Debug, Clone, Default, PartialEq)]
156struct Filters {
157    routes: Option<Filter>,
158    operators: Option<Filter>,
159    stops: Option<Filter>,
160}
161
162#[derive(Serialize, Debug, Clone, Default, PartialEq, Eq)]
163struct Filter {
164    ids: Vec<String>,
165    action: Action,
166}
167
168#[cfg(test)]
169mod test {
170    use super::*;
171    #[test]
172    fn serialisation() {
173        assert_eq!(
174            serde_json::to_value(TransitCostingOptions::default()).unwrap(),
175            serde_json::json!({"transit":{}})
176        );
177    }
178
179    #[test]
180    fn builder_returns_default() {
181        assert_eq!(
182            TransitCostingOptions::builder(),
183            TransitCostingOptions::default()
184        );
185    }
186
187    #[test]
188    fn use_bus_sets_value() {
189        let opts = TransitCostingOptions::builder().use_bus(0.8);
190        assert_eq!(opts.transit.use_bus, Some(0.8));
191    }
192
193    #[test]
194    fn use_rail_sets_value() {
195        let opts = TransitCostingOptions::builder().use_rail(0.9);
196        assert_eq!(opts.transit.use_rail, Some(0.9));
197    }
198
199    #[test]
200    fn use_transfers_sets_value() {
201        let opts = TransitCostingOptions::builder().use_transfers(0.5);
202        assert_eq!(opts.transit.use_transfers, Some(0.5));
203    }
204
205    #[test]
206    fn filter_routes_sets_value() {
207        let opts = TransitCostingOptions::builder()
208            .filter_routes(vec!["NYC_AUR", "NYC_BX"], Action::Include);
209        assert!(opts.transit.filters.is_some());
210        let filters = opts.transit.filters.unwrap();
211        assert!(filters.routes.is_some());
212        let route_filter = filters.routes.unwrap();
213        assert_eq!(route_filter.ids, vec!["NYC_AUR", "NYC_BX"]);
214        assert_eq!(route_filter.action, Action::Include);
215    }
216
217    #[test]
218    fn filter_operators_sets_value() {
219        let opts =
220            TransitCostingOptions::builder().filter_operators(vec!["NYC_MTA"], Action::Exclude);
221        assert!(opts.transit.filters.is_some());
222        let filters = opts.transit.filters.unwrap();
223        assert!(filters.operators.is_some());
224        let op_filter = filters.operators.unwrap();
225        assert_eq!(op_filter.ids, vec!["NYC_MTA"]);
226        assert_eq!(op_filter.action, Action::Exclude);
227    }
228
229    #[test]
230    fn chaining_works() {
231        let opts = TransitCostingOptions::builder()
232            .use_bus(0.7)
233            .use_rail(0.9)
234            .use_transfers(0.4);
235        assert_eq!(opts.transit.use_bus, Some(0.7));
236        assert_eq!(opts.transit.use_rail, Some(0.9));
237        assert_eq!(opts.transit.use_transfers, Some(0.4));
238    }
239}