diff --git a/Cargo.toml b/Cargo.toml index 8039086..1c3acf6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] description = "Read GTFS (public transit timetables) files" name = "gtfs-structures" -version = "0.30.0" +version = "0.31.0" authors = ["Tristram Gräbener ", "Antoine Desbordes "] repository = "https://github.com/rust-transit/gtfs-structure" license = "MIT" diff --git a/fixtures/basic/transfers.txt b/fixtures/basic/transfers.txt new file mode 100644 index 0000000..92f0fac --- /dev/null +++ b/fixtures/basic/transfers.txt @@ -0,0 +1,3 @@ +from_stop_id,to_stop_id,transfer_type,min_transfer_time +stop3,stop5,0,60 +stop1,stop2,3, \ No newline at end of file diff --git a/src/enums.rs b/src/enums.rs index e22a4b4..48204f9 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -505,3 +505,21 @@ impl Serialize for Transfers { } } } +/// Defines the type of a [StopTransfer] +#[derive(Debug, Serialize, Deserialize, Derivative, Copy, Clone, PartialEq)] +#[derivative(Default)] +pub enum TransferType { + /// Recommended transfer point between routes + #[serde(rename = "0")] + #[derivative(Default)] + Recommended, + /// Departing vehicle waits for arriving one + #[serde(rename = "1")] + Timed, + /// Transfer requires a minimum amount of time between arrival and departure to ensure a connection. + #[serde(rename = "2")] + MinTime, + /// Transfer is not possible at this location + #[serde(rename = "3")] + Impossible, +} diff --git a/src/gtfs.rs b/src/gtfs.rs index 3dea6ba..80d8d38 100644 --- a/src/gtfs.rs +++ b/src/gtfs.rs @@ -49,7 +49,7 @@ impl TryFrom for Gtfs { /// /// It might fail if some mandatory files couldn’t be read or if there are references to other objects that are invalid. fn try_from(raw: RawGtfs) -> Result { - let stops = to_stop_map(raw.stops?); + let stops = to_stop_map(raw.stops?, raw.transfers.unwrap_or(Ok(Vec::new()))?)?; let frequencies = raw.frequencies.unwrap_or_else(|| Ok(Vec::new()))?; let trips = create_trips(raw.trips?, raw.stop_times?, frequencies, &stops)?; @@ -228,11 +228,27 @@ fn to_map(elements: impl IntoIterator) -> HashMap { .collect() } -fn to_stop_map(stops: Vec) -> HashMap> { - stops +fn to_stop_map( + stops: Vec, + raw_transfers: Vec, +) -> Result>, Error> { + let mut stop_map: HashMap = + stops.into_iter().map(|s| (s.id.clone(), s)).collect(); + + for transfer in raw_transfers { + stop_map + .get(&transfer.to_stop_id) + .ok_or(Error::ReferenceError(transfer.to_stop_id.to_string()))?; + stop_map + .entry(transfer.from_stop_id.clone()) + .and_modify(|stop| stop.transfers.push(StopTransfer::from(transfer))); + } + + let res = stop_map .into_iter() - .map(|s| (s.id.clone(), Arc::new(s))) - .collect() + .map(|(i, s)| (i, Arc::new(s))) + .collect(); + Ok(res) } fn to_shape_map(shapes: Vec) -> HashMap> { diff --git a/src/gtfs_reader.rs b/src/gtfs_reader.rs index f9f8e08..1211542 100644 --- a/src/gtfs_reader.rs +++ b/src/gtfs_reader.rs @@ -159,6 +159,7 @@ impl RawGtfsReader { shapes: self.read_objs_from_optional_path(p, "shapes.txt"), fare_attributes: self.read_objs_from_optional_path(p, "fare_attributes.txt"), frequencies: self.read_objs_from_optional_path(p, "frequencies.txt"), + transfers: self.read_objs_from_optional_path(p, "transfers.txt"), feed_info: self.read_objs_from_optional_path(p, "feed_info.txt"), read_duration: Utc::now().signed_duration_since(now).num_milliseconds(), files, @@ -242,6 +243,7 @@ impl RawGtfsReader { "trips.txt", "fare_attributes.txt", "frequencies.txt", + "transfers.txt", "feed_info.txt", "shapes.txt", ] { @@ -275,6 +277,7 @@ impl RawGtfsReader { "fare_attributes.txt", ), frequencies: self.read_optional_file(&file_mapping, &mut archive, "frequencies.txt"), + transfers: self.read_optional_file(&file_mapping, &mut archive, "transfers.txt"), feed_info: self.read_optional_file(&file_mapping, &mut archive, "feed_info.txt"), shapes: self.read_optional_file(&file_mapping, &mut archive, "shapes.txt"), read_duration: Utc::now().signed_duration_since(now).num_milliseconds(), diff --git a/src/objects.rs b/src/objects.rs index 15ff87f..75d76ba 100644 --- a/src/objects.rs +++ b/src/objects.rs @@ -185,6 +185,9 @@ pub struct Stop { pub level_id: Option, /// Platform identifier for a platform stop (a stop belonging to a station) pub platform_code: Option, + /// Transfers from this Stop + #[serde(skip)] + pub transfers: Vec, } impl Type for Stop { @@ -634,6 +637,41 @@ impl Frequency { } } +/// Transfer information between stops before merged into [Stop] +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct RawTransfer { + /// Stop from which to leave + pub from_stop_id: String, + /// Stop which to transfer to + pub to_stop_id: String, + /// Type of the transfer + pub transfer_type: TransferType, + /// Minimum time needed to make the transfer in seconds + pub min_transfer_time: Option, +} + +#[derive(Debug, Default, Clone)] +/// Transfer information between stops +pub struct StopTransfer { + /// Stop which to transfer to + pub to_stop_id: String, + /// Type of the transfer + pub transfer_type: TransferType, + /// Minimum time needed to make the transfer in seconds + pub min_transfer_time: Option, +} + +impl From for StopTransfer { + /// Converts from a [RawTransfer] to a [StopTransfer] + fn from(transfer: RawTransfer) -> Self { + Self { + to_stop_id: transfer.to_stop_id, + transfer_type: transfer.transfer_type, + min_transfer_time: transfer.min_transfer_time, + } + } +} + /// Meta-data about the feed. See #[derive(Debug, Serialize, Deserialize)] pub struct FeedInfo { diff --git a/src/raw_gtfs.rs b/src/raw_gtfs.rs index ccec343..9d9ca2e 100644 --- a/src/raw_gtfs.rs +++ b/src/raw_gtfs.rs @@ -29,6 +29,8 @@ pub struct RawGtfs { pub fare_attributes: Option, Error>>, /// All Frequencies, None if the file was absent as it is not mandatory pub frequencies: Option, Error>>, + /// All Transfers, None if the file was absent as it is not mandatory + pub transfers: Option, Error>>, /// All FeedInfo, None if the file was absent as it is not mandatory pub feed_info: Option, Error>>, /// All StopTimes @@ -55,6 +57,7 @@ impl RawGtfs { " Frequencies: {}", optional_file_summary(&self.frequencies) ); + println!(" Transfers {}", optional_file_summary(&self.transfers)); println!(" Feed info: {}", optional_file_summary(&self.feed_info)); } diff --git a/src/tests.rs b/src/tests.rs index 5e6d9de..14e89fb 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -143,6 +143,38 @@ fn read_fare_attributes() { ); } +#[test] +fn read_transfers() { + let gtfs = Gtfs::from_path("fixtures/basic").expect("impossible to read gtfs"); + assert_eq!(1, gtfs.get_stop("stop3").unwrap().transfers.len()); + assert_eq!(1, gtfs.get_stop("stop1").unwrap().transfers.len()); + + assert_eq!( + "stop5", + gtfs.get_stop("stop3").unwrap().transfers[0].to_stop_id + ); + assert_eq!( + "stop2", + gtfs.get_stop("stop1").unwrap().transfers[0].to_stop_id + ); + assert_eq!( + TransferType::Recommended, + gtfs.get_stop("stop3").unwrap().transfers[0].transfer_type + ); + assert_eq!( + TransferType::Impossible, + gtfs.get_stop("stop1").unwrap().transfers[0].transfer_type + ); + assert_eq!( + Some(60), + gtfs.get_stop("stop3").unwrap().transfers[0].min_transfer_time + ); + assert_eq!( + None, + gtfs.get_stop("stop1").unwrap().transfers[0].min_transfer_time + ); +} + #[test] fn read_feed_info() { let gtfs = Gtfs::from_path("fixtures/basic").expect("impossible to read gtfs"); @@ -241,7 +273,7 @@ fn display() { #[test] fn path_files() { let gtfs = RawGtfs::from_path("fixtures/basic").expect("impossible to read gtfs"); - assert_eq!(gtfs.files.len(), 11); + assert_eq!(gtfs.files.len(), 12); } #[test]