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