diff --git a/Cargo.toml b/Cargo.toml index 40958a3..29b682e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] description = "Read GTFS (public transit timetables) files" name = "gtfs-structures" -version = "0.47.0" +version = "0.48.0" authors = [ "Tristram Gräbener ", "Antoine Desbordes ", diff --git a/fixtures/ticketing-extension/agency.txt b/fixtures/ticketing-extension/agency.txt new file mode 100644 index 0000000..ab3cded --- /dev/null +++ b/fixtures/ticketing-extension/agency.txt @@ -0,0 +1,2 @@ +agency_id,agency_name,agency_url,agency_timezone,ticketing_deep_link_id +agency,Agency,https://example.com,Etc/UTC,ticket-shop diff --git a/fixtures/ticketing-extension/routes.txt b/fixtures/ticketing-extension/routes.txt new file mode 100644 index 0000000..75509ab --- /dev/null +++ b/fixtures/ticketing-extension/routes.txt @@ -0,0 +1,3 @@ +route_id,route_short_name,route_long_name,route_desc,route_type,route_url,agency_id,ticketing_deep_link_id +route1,667,,,2,,agency,ticket-shop +route2,689,,,2,,agency, diff --git a/fixtures/ticketing-extension/stop_times.txt b/fixtures/ticketing-extension/stop_times.txt new file mode 100644 index 0000000..5f42fc1 --- /dev/null +++ b/fixtures/ticketing-extension/stop_times.txt @@ -0,0 +1,7 @@ +trip_id,arrival_time,departure_time,stop_id,stop_sequence,ticketing_type +trip1,13:57:00,13:57:00,stop1,0,0 +trip1,14:09:00,14:10:00,stop2,1,0 +trip1,14:17:00,14:18:00,stop3,2,0 +trip2,14:24:00,14:25:00,stop1,0,0 +trip2,14:34:00,14:35:00,stop2,1,1 +trip2,14:41:00,14:42:00,stop3,2,0 diff --git a/fixtures/ticketing-extension/stops.txt b/fixtures/ticketing-extension/stops.txt new file mode 100644 index 0000000..62e88c2 --- /dev/null +++ b/fixtures/ticketing-extension/stops.txt @@ -0,0 +1,4 @@ +stop_id,stop_name,stop_lon,stop_lat +stop1,Stop 1,1.0,2.0 +stop2,Stop 2,2.0,1.0 +stop3,Stop 2,4.0,2.0 diff --git a/fixtures/ticketing-extension/ticketing_deep_links.txt b/fixtures/ticketing-extension/ticketing_deep_links.txt new file mode 100644 index 0000000..27869f4 --- /dev/null +++ b/fixtures/ticketing-extension/ticketing_deep_links.txt @@ -0,0 +1,2 @@ +ticketing_deep_link_id,web_url,android_intent_url,ios_universal_link_url +ticket-shop,https://example.com/tickets,, diff --git a/fixtures/ticketing-extension/ticketing_identifiers.txt b/fixtures/ticketing-extension/ticketing_identifiers.txt new file mode 100644 index 0000000..321c799 --- /dev/null +++ b/fixtures/ticketing-extension/ticketing_identifiers.txt @@ -0,0 +1,4 @@ +ticketing_stop_id,stop_id,agency_id +ticketing-stop1,stop1,agency +ticketing-stop2,stop2,agency +ticketing-stop3,stop3,agency diff --git a/fixtures/ticketing-extension/trips.txt b/fixtures/ticketing-extension/trips.txt new file mode 100644 index 0000000..19d0e4c --- /dev/null +++ b/fixtures/ticketing-extension/trips.txt @@ -0,0 +1,3 @@ +trip_id,service_id,route_id,shape_id,trip_headsign,trip_short_name,direction_id,block_id,wheelchair_accessible,bikes_allowed,ticketing_trip_id,ticketing_type +trip1,service1,route1,,,,,,0,1,test,0 +trip2,service1,route2,,,,,,0,1,,0 diff --git a/src/enums.rs b/src/enums.rs index ae869dc..533668d 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -657,3 +657,16 @@ impl<'de> Deserialize<'de> for DefaultFareCategory { }) } } + +/// Specifies whether tickets can be bought for this item +#[derive(Debug, Derivative, Deserialize, Serialize, Copy, Clone, PartialEq, Eq)] +#[derivative(Default)] +pub enum TicketingType { + /// If a ticketing_deep_link_id is set, tickets are available + #[derivative(Default)] + #[serde(rename = "0")] + Available, + /// Tickets are unavailable + #[serde(rename = "1")] + Unavailable, +} diff --git a/src/gtfs.rs b/src/gtfs.rs index cf2a812..adc296d 100644 --- a/src/gtfs.rs +++ b/src/gtfs.rs @@ -358,8 +358,7 @@ fn create_trips( } for trip in &mut trips.values_mut() { - trip.stop_times - .sort_by(|a, b| a.stop_sequence.cmp(&b.stop_sequence)); + trip.stop_times.sort_by_key(|st| st.stop_sequence); } for f in raw_frequencies { diff --git a/src/gtfs_reader.rs b/src/gtfs_reader.rs index f9e609d..9986f61 100644 --- a/src/gtfs_reader.rs +++ b/src/gtfs_reader.rs @@ -187,6 +187,9 @@ impl RawGtfsReader { feed_info: self.read_objs_from_optional_path(p, "feed_info.txt"), read_duration: start_of_read_instant.elapsed(), translations: self.read_objs_from_optional_path(p, "translations.txt"), + ticketing_deep_links: self.read_objs_from_optional_path(p, "ticketing_deep_links.txt"), + ticketing_identifiers: self + .read_objs_from_optional_path(p, "ticketing_identifiers.txt"), files, source_format: crate::SourceFormat::Directory, sha256: None, @@ -330,6 +333,16 @@ impl RawGtfsReader { Some(Ok(Vec::new())) }, translations: self.read_optional_file(&file_mapping, &mut archive, "translations.txt"), + ticketing_deep_links: self.read_optional_file( + &file_mapping, + &mut archive, + "ticketing_deep_links.txt", + ), + ticketing_identifiers: self.read_optional_file( + &file_mapping, + &mut archive, + "ticketing_identifiers.txt", + ), read_duration: start_of_read_instant.elapsed(), files, source_format: crate::SourceFormat::Zip, diff --git a/src/objects.rs b/src/objects.rs index 54d5b43..302ff39 100644 --- a/src/objects.rs +++ b/src/objects.rs @@ -261,6 +261,10 @@ pub struct RawStopTime { /// Indicates if arrival and departure times for a stop are strictly adhered to by the vehicle or if they are instead approximate and/or interpolated times #[serde(default)] pub timepoint: TimepointType, + /// This field is not part of the main GTFS specification, it is part of the Google Transit Ticketing extension + /// Enable or disable buying tickets via a deep link + #[serde(default)] + pub ticketing_type: TicketingType, } /// The moment where a vehicle, running on [Trip] stops at a [Stop]. See @@ -360,6 +364,9 @@ pub struct Route { /// Indicates whether a rider can alight from the transit vehicle at any point along the vehicle’s travel path #[serde(default)] pub continuous_drop_off: ContinuousPickupDropOff, + /// This field is not part of the main GTFS specification, it is part of the Google Transit Ticketing extension + /// Enable or disable buying tickets via a deep link + pub ticketing_deep_link_id: Option, } impl Route { @@ -445,6 +452,14 @@ pub struct RawTrip { /// Indicates whether bikes are allowed #[serde(default)] pub bikes_allowed: BikesAllowedType, + /// This field is not part of the main GTFS specification, it is part of the Google Transit Ticketing extension + /// Trip ID to pass to a ticket shop + #[serde(default)] + pub ticketing_trip_id: Option, + /// This field is not part of the main GTFS specification, it is part of the Google Transit Ticketing extension + /// Enable or disable buying tickets via a deep link + #[serde(default)] + pub ticketing_type: TicketingType, } impl Type for RawTrip { @@ -547,6 +562,9 @@ pub struct Agency { /// Email address actively monitored by the agency’s customer service department #[serde(rename = "agency_email")] pub email: Option, + /// This field is not part of the main GTFS specification, it is part of the Google Transit Ticketing extension + /// Trip ID to pass to a ticket shop + pub ticketing_deep_link_id: Option, } impl Type for Agency { @@ -907,6 +925,32 @@ impl Id for RawPathway { } } +/// This object is not part of the main GTFS specification, it is part of the Google Transit Ticketing extension +/// A mapping of the GTFS-identifiers to the ticket shop identifiers +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct TicketingIdentifier { + /// Stop ID used by the ticket shop + pub ticketing_stop_id: String, + /// GTFS-side stop id + pub stop_id: String, + /// GTFS-side agency id + pub agency_id: String, +} + +/// This object is not part of the main GTFS specification, it is part of the Google Transit Ticketing extension +/// The base url to a ticket shop without the trip specific parameters +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct TicketingDeepLink { + /// Unique identifier for this base link + pub ticketing_deep_link_id: String, + /// URL to be used for buying a ticket on the web + pub web_url: Option, + /// URI to be used for buying a ticket in an android app + pub android_intent_url: Option, + /// URL used to use on iOS + pub ios_universal_link_url: Option, +} + impl Type for RawPathway { fn object_type(&self) -> ObjectType { ObjectType::Pathway diff --git a/src/raw_gtfs.rs b/src/raw_gtfs.rs index 04a411b..eaf37f5 100644 --- a/src/raw_gtfs.rs +++ b/src/raw_gtfs.rs @@ -54,6 +54,10 @@ pub struct RawGtfs { pub sha256: Option, /// All translations, None if the file was absent as it is not mandatory pub translations: Option, Error>>, + /// Base urls to ticket shops + pub ticketing_deep_links: Option, Error>>, + /// Identifiers to pass to ticket shops + pub ticketing_identifiers: Option, Error>>, } impl RawGtfs { @@ -79,6 +83,14 @@ impl RawGtfs { " Translations: {}", optional_file_summary(&self.translations) ); + println!( + " Ticketing deep links: {}", + optional_file_summary(&self.ticketing_deep_links) + ); + println!( + " Ticketing identifiers: {}", + optional_file_summary(&self.ticketing_identifiers) + ); } /// Reads from an url (if starts with http), or a local path (either a directory or zipped file) diff --git a/src/tests.rs b/src/tests.rs index bfeecab..6639d3b 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -561,3 +561,39 @@ fn fares_v2() { assert_eq!(gtfs.rider_categories.len(), 2); assert_eq!(gtfs.rider_categories["concession"], expected); } + +#[test] +fn ticketing_extension() { + let gtfs = RawGtfs::from_path("fixtures/ticketing-extension").expect("impossible to read gtfs"); + + assert_eq!(gtfs.ticketing_deep_links.unwrap().unwrap().len(), 1); + assert_eq!(gtfs.ticketing_identifiers.unwrap().unwrap().len(), 3); + assert_eq!(gtfs.routes.as_ref().unwrap().len(), 2); + assert_eq!(gtfs.trips.unwrap().len(), 2); + assert_eq!(gtfs.agencies.as_ref().unwrap().len(), 1); + assert!( + gtfs.agencies + .unwrap() + .first() + .unwrap() + .ticketing_deep_link_id + .is_some() + ); + assert!( + gtfs.routes + .as_ref() + .unwrap() + .last() + .unwrap() + .ticketing_deep_link_id + .is_none() + ); + assert!( + gtfs.routes + .unwrap() + .first() + .unwrap() + .ticketing_deep_link_id + .is_some() + ); +}