Skip to main content

gtfs_structures/
gtfs.rs

1use crate::{Error, RawGtfs, objects::*};
2use jiff::civil::Date;
3use std::collections::{HashMap, HashSet};
4use std::convert::TryFrom;
5use std::sync::Arc;
6use web_time::{Duration, Instant};
7
8/// Data structure with all the GTFS objects
9///
10/// This structure is easier to use than the [RawGtfs] structure as some relationships are parsed to be easier to use.
11///
12/// If you want to configure the behaviour (e.g. skipping : [StopTime] or [Shape]), see [crate::GtfsReader] for more personalisation
13///
14/// This is probably the entry point you want to use:
15/// ```
16/// let gtfs = gtfs_structures::Gtfs::new("fixtures/zips/gtfs.zip")?;
17/// assert_eq!(gtfs.stops.len(), 5);
18/// # Ok::<(), gtfs_structures::error::Error>(())
19/// ```
20///
21/// The [StopTime] are accessible from the [Trip]
22#[derive(Default)]
23pub struct Gtfs {
24    /// Time needed to read and parse the archive
25    pub read_duration: Duration,
26    /// All Calendar by `service_id`
27    pub calendar: HashMap<String, Calendar>,
28    /// All calendar dates grouped by service_id
29    pub calendar_dates: HashMap<String, Vec<CalendarDate>>,
30    /// All stop by `stop_id`. Stops are in an [Arc] because they are also referenced by each [StopTime]
31    pub stops: HashMap<String, Arc<Stop>>,
32    /// All routes by `route_id`
33    pub routes: HashMap<String, Route>,
34    /// All trips by `trip_id`
35    pub trips: HashMap<String, Trip>,
36    /// All agencies. They can not be read by `agency_id`, as it is not a required field
37    pub agencies: Vec<Agency>,
38    /// All shapes by shape_id
39    pub shapes: HashMap<String, Vec<Shape>>,
40    /// All fare attributes by `fare_id`
41    pub fare_attributes: HashMap<String, FareAttribute>,
42    /// All fare rules by `fare_id`
43    pub fare_rules: HashMap<String, Vec<FareRule>>,
44    /// All fare products by `fare_product_id`
45    pub fare_products: HashMap<String, Vec<FareProduct>>,
46    /// All fare media by `fare_media_id`
47    pub fare_media: HashMap<String, FareMedia>,
48    /// All rider categories by `rider_category_id`
49    pub rider_categories: HashMap<String, RiderCategory>,
50    /// All feed information. There is no identifier
51    pub feed_info: Vec<FeedInfo>,
52    /// All levels by `level_id`
53    pub levels: HashMap<String, Level>,
54}
55
56impl TryFrom<RawGtfs> for Gtfs {
57    type Error = Error;
58    /// Tries to build a [Gtfs] from a [RawGtfs]
59    ///
60    /// It might fail if some mandatory files couldn’t be read or if there are references to other objects that are invalid.
61    fn try_from(raw: RawGtfs) -> Result<Gtfs, Error> {
62        let start = Instant::now();
63
64        let stops = to_stop_map(
65            raw.stops?,
66            raw.transfers.unwrap_or_else(|| Ok(Vec::new()))?,
67            raw.pathways.unwrap_or(Ok(Vec::new()))?,
68        )?;
69        let frequencies = raw.frequencies.unwrap_or_else(|| Ok(Vec::new()))?;
70        let trips = create_trips(raw.trips?, raw.stop_times?, frequencies, &stops)?;
71
72        let mut fare_rules = HashMap::<String, Vec<FareRule>>::new();
73        for f in raw.fare_rules.unwrap_or_else(|| Ok(Vec::new()))? {
74            fare_rules.entry(f.fare_id.clone()).or_default().push(f);
75        }
76
77        let mut fare_products = HashMap::<String, Vec<FareProduct>>::new();
78        for f in raw.fare_products.unwrap_or_else(|| Ok(Vec::new()))? {
79            fare_products.entry(f.id.clone()).or_default().push(f);
80        }
81
82        Ok(Gtfs {
83            stops,
84            routes: to_map(raw.routes?),
85            trips,
86            agencies: raw.agencies?,
87            shapes: to_shape_map(raw.shapes.unwrap_or_else(|| Ok(Vec::new()))?),
88            fare_attributes: to_map(raw.fare_attributes.unwrap_or_else(|| Ok(Vec::new()))?),
89            fare_rules,
90            fare_products,
91            fare_media: to_map(raw.fare_media.unwrap_or_else(|| Ok(Vec::new()))?),
92            rider_categories: to_map(raw.rider_categories.unwrap_or_else(|| Ok(Vec::new()))?),
93            feed_info: raw.feed_info.unwrap_or_else(|| Ok(Vec::new()))?,
94            levels: to_map(raw.levels.unwrap_or_else(|| Ok(Vec::new()))?),
95            calendar: to_map(raw.calendar.unwrap_or_else(|| Ok(Vec::new()))?),
96            calendar_dates: to_calendar_dates(
97                raw.calendar_dates.unwrap_or_else(|| Ok(Vec::new()))?,
98            ),
99            read_duration: raw.read_duration + start.elapsed(),
100        })
101    }
102}
103
104impl Gtfs {
105    /// Prints on stdout some basic statistics about the GTFS file (numbers of elements for each object). Mostly to be sure that everything was read
106    pub fn print_stats(&self) {
107        println!("GTFS data:");
108        println!("  Read in {:?}", self.read_duration);
109        println!("  Stops: {}", self.stops.len());
110        println!("  Routes: {}", self.routes.len());
111        println!("  Trips: {}", self.trips.len());
112        println!("  Agencies: {}", self.agencies.len());
113        println!("  Shapes: {}", self.shapes.len());
114        println!("  Fare attributes: {}", self.fare_attributes.len());
115        println!("  Feed info: {}", self.feed_info.len());
116        println!("  Levels: {}", self.levels.len());
117    }
118
119    /// Reads from an url (if starts with `"http"`), or a local path (either a directory or zipped file)
120    ///
121    /// To read from an url, build with read-url feature
122    /// See also [Gtfs::from_url] and [Gtfs::from_path] if you don’t want the library to guess
123    #[cfg(not(target_arch = "wasm32"))]
124    pub fn new(gtfs: &str) -> Result<Gtfs, Error> {
125        RawGtfs::new(gtfs).and_then(Gtfs::try_from)
126    }
127
128    /// Reads the GTFS from a local zip archive or local directory
129    pub fn from_path<P>(path: P) -> Result<Gtfs, Error>
130    where
131        P: AsRef<std::path::Path>,
132    {
133        RawGtfs::from_path(path).and_then(Gtfs::try_from)
134    }
135
136    /// Reads the GTFS from a remote url
137    ///
138    /// The library must be built with the read-url feature. Not available on WASM targets.
139    #[cfg(all(feature = "read-url", not(target_arch = "wasm32")))]
140    pub fn from_url<U: reqwest::IntoUrl>(url: U) -> Result<Gtfs, Error> {
141        RawGtfs::from_url(url).and_then(Gtfs::try_from)
142    }
143
144    /// Asynchronously reads the GTFS from a remote url
145    ///
146    /// The library must be built with the read-url feature
147    #[cfg(feature = "read-url")]
148    pub async fn from_url_async<U: reqwest::IntoUrl>(url: U) -> Result<Gtfs, Error> {
149        RawGtfs::from_url_async(url).await.and_then(Gtfs::try_from)
150    }
151
152    /// Reads for any object implementing [std::io::Read] and [std::io::Seek]
153    ///
154    /// Mostly an internal function that abstracts reading from an url or local file
155    pub fn from_reader<T: std::io::Read + std::io::Seek>(reader: T) -> Result<Gtfs, Error> {
156        RawGtfs::from_reader(reader).and_then(Gtfs::try_from)
157    }
158
159    /// For a given a `service_id` and a starting date returns all the following day offset the vehicle runs
160    ///
161    /// For instance if the `start_date` is 2021-12-20, `[0, 4]` means that the vehicle will run the 20th and 24th
162    ///
163    /// It will consider use both [Calendar] and [CalendarDate] (both added and removed)
164    pub fn trip_days(&self, service_id: &str, start_date: Date) -> Vec<u16> {
165        let mut result = Vec::new();
166
167        // Handle services given by specific days and exceptions
168        let mut removed_days = HashSet::new();
169        for extra_day in self
170            .calendar_dates
171            .get(service_id)
172            .iter()
173            .flat_map(|e| e.iter())
174        {
175            if extra_day.date >= start_date {
176                if extra_day.exception_type == Exception::Added
177                    && let Ok(offset) = start_date.until(extra_day.date)
178                {
179                    result.push(offset.get_days() as u16);
180                } else if extra_day.exception_type == Exception::Deleted {
181                    removed_days.insert(extra_day.date);
182                }
183            }
184        }
185
186        if let Some(calendar) = self.calendar.get(service_id) {
187            for current_date in start_date.series(jiff::Span::new().days(1)) {
188                if current_date > calendar.end_date {
189                    break;
190                }
191
192                if let Ok(days_offset) = start_date.until(current_date)
193                    && calendar.start_date <= current_date
194                    && calendar.end_date >= current_date
195                    && calendar.valid_weekday(current_date)
196                    && !removed_days.contains(&current_date)
197                {
198                    result.push(days_offset.get_days() as u16);
199                }
200            }
201        }
202
203        result
204    }
205
206    /// Gets a [Stop] by its `stop_id`
207    pub fn get_stop<'a>(&'a self, id: &str) -> Result<&'a Stop, Error> {
208        match self.stops.get(id) {
209            Some(stop) => Ok(stop),
210            None => Err(Error::ReferenceError(id.to_owned())),
211        }
212    }
213
214    /// Gets a [Trip] by its `trip_id`
215    pub fn get_trip<'a>(&'a self, id: &str) -> Result<&'a Trip, Error> {
216        self.trips
217            .get(id)
218            .ok_or_else(|| Error::ReferenceError(id.to_owned()))
219    }
220
221    /// Gets a [Route] by its `route_id`
222    pub fn get_route<'a>(&'a self, id: &str) -> Result<&'a Route, Error> {
223        self.routes
224            .get(id)
225            .ok_or_else(|| Error::ReferenceError(id.to_owned()))
226    }
227
228    /// Gets a [Calendar] by its `service_id`
229    pub fn get_calendar<'a>(&'a self, id: &str) -> Result<&'a Calendar, Error> {
230        self.calendar
231            .get(id)
232            .ok_or_else(|| Error::ReferenceError(id.to_owned()))
233    }
234
235    /// Gets all [CalendarDate] of a `service_id`
236    pub fn get_calendar_date<'a>(&'a self, id: &str) -> Result<&'a Vec<CalendarDate>, Error> {
237        self.calendar_dates
238            .get(id)
239            .ok_or_else(|| Error::ReferenceError(id.to_owned()))
240    }
241
242    /// Gets all [Shape] points of a `shape_id`
243    pub fn get_shape<'a>(&'a self, id: &str) -> Result<&'a Vec<Shape>, Error> {
244        self.shapes
245            .get(id)
246            .ok_or_else(|| Error::ReferenceError(id.to_owned()))
247    }
248
249    /// Gets a [FareAttribute] by its `fare_id`
250    pub fn get_fare_attributes<'a>(&'a self, id: &str) -> Result<&'a FareAttribute, Error> {
251        self.fare_attributes
252            .get(id)
253            .ok_or_else(|| Error::ReferenceError(id.to_owned()))
254    }
255
256    /// Gets a [Level] by its `level_id`
257    pub fn get_level<'a>(&'a self, id: &str) -> Result<&'a Level, Error> {
258        self.levels
259            .get(id)
260            .ok_or_else(|| Error::ReferenceError(id.to_owned()))
261    }
262}
263
264fn to_map<O: Id>(elements: impl IntoIterator<Item = O>) -> HashMap<String, O> {
265    elements
266        .into_iter()
267        .map(|e| (e.id().to_owned(), e))
268        .collect()
269}
270
271fn to_stop_map(
272    stops: Vec<Stop>,
273    raw_transfers: Vec<RawTransfer>,
274    raw_pathways: Vec<RawPathway>,
275) -> Result<HashMap<String, Arc<Stop>>, Error> {
276    let mut stop_map: HashMap<String, Stop> =
277        stops.into_iter().map(|s| (s.id.clone(), s)).collect();
278
279    for transfer in raw_transfers {
280        stop_map.get(&transfer.to_stop_id).ok_or_else(|| {
281            let stop_id = &transfer.to_stop_id;
282            Error::ReferenceError(format!("'{stop_id}' in transfers.txt"))
283        })?;
284        stop_map
285            .entry(transfer.from_stop_id.clone())
286            .and_modify(|stop| stop.transfers.push(StopTransfer::from(transfer)));
287    }
288
289    for pathway in raw_pathways {
290        stop_map.get(&pathway.to_stop_id).ok_or_else(|| {
291            let stop_id = &pathway.to_stop_id;
292            Error::ReferenceError(format!("'{stop_id}' in pathways.txt"))
293        })?;
294        stop_map
295            .entry(pathway.from_stop_id.clone())
296            .and_modify(|stop| stop.pathways.push(Pathway::from(pathway)));
297    }
298
299    let res = stop_map
300        .into_iter()
301        .map(|(i, s)| (i, Arc::new(s)))
302        .collect();
303    Ok(res)
304}
305
306fn to_shape_map(shapes: Vec<Shape>) -> HashMap<String, Vec<Shape>> {
307    let mut res = HashMap::default();
308    for s in shapes {
309        let shape = res.entry(s.id.to_owned()).or_insert_with(Vec::new);
310        shape.push(s);
311    }
312    // we sort the shape by it's pt_sequence
313    for shapes in res.values_mut() {
314        shapes.sort_by_key(|s| s.sequence);
315    }
316
317    res
318}
319
320fn to_calendar_dates(cd: Vec<CalendarDate>) -> HashMap<String, Vec<CalendarDate>> {
321    let mut res = HashMap::default();
322    for c in cd {
323        let cal = res.entry(c.service_id.to_owned()).or_insert_with(Vec::new);
324        cal.push(c);
325    }
326    res
327}
328
329// Number of stoptimes to `pop` from the list before using shrink_to_fit to reduce the memory footprint
330// Hardcoded to what seems a sensible value, but if needed we could make this a parameter, feel free to open an issue if this could help
331const NB_STOP_TIMES_BEFORE_SHRINK: usize = 1_000_000;
332
333fn create_trips(
334    raw_trips: Vec<RawTrip>,
335    mut raw_stop_times: Vec<RawStopTime>,
336    raw_frequencies: Vec<RawFrequency>,
337    stops: &HashMap<String, Arc<Stop>>,
338) -> Result<HashMap<String, Trip>, Error> {
339    let mut trips = to_map(raw_trips.into_iter().map(|rt| Trip {
340        id: rt.id,
341        service_id: rt.service_id,
342        route_id: rt.route_id,
343        stop_times: vec![],
344        shape_id: rt.shape_id,
345        trip_headsign: rt.trip_headsign,
346        trip_short_name: rt.trip_short_name,
347        direction_id: rt.direction_id,
348        block_id: rt.block_id,
349        wheelchair_accessible: rt.wheelchair_accessible,
350        bikes_allowed: rt.bikes_allowed,
351        cars_allowed: rt.cars_allowed,
352        frequencies: vec![],
353    }));
354
355    let mut st_idx = 0;
356    while let Some(s) = raw_stop_times.pop() {
357        st_idx += 1;
358        let trip = &mut trips
359            .get_mut(&s.trip_id)
360            .ok_or_else(|| Error::ReferenceError(s.trip_id.to_string()))?;
361        let stop = stops
362            .get(&s.stop_id)
363            .ok_or_else(|| Error::ReferenceError(s.stop_id.to_string()))?;
364        trip.stop_times.push(StopTime::from(s, Arc::clone(stop)));
365        if st_idx % NB_STOP_TIMES_BEFORE_SHRINK == 0 {
366            raw_stop_times.shrink_to_fit();
367        }
368    }
369
370    for trip in &mut trips.values_mut() {
371        trip.stop_times.sort_by_key(|st| st.stop_sequence);
372    }
373
374    for f in raw_frequencies {
375        let trip = &mut trips
376            .get_mut(&f.trip_id)
377            .ok_or_else(|| Error::ReferenceError(f.trip_id.to_string()))?;
378        trip.frequencies.push(Frequency::from(&f));
379    }
380
381    Ok(trips)
382}