diff --git a/Cargo.toml b/Cargo.toml index be78d54..2f6da5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ version = "0.40.0" authors = ["Tristram Gräbener ", "Antoine Desbordes "] repository = "https://github.com/rust-transit/gtfs-structure" license = "MIT" -edition = "2018" +edition = "2021" [features] default = ["read-url"] @@ -15,6 +15,7 @@ read-url = ["reqwest", "futures"] bytes = "1" csv = "1.1" derivative = "2.1" +language-tags = {version = "0.3.2", features = ["serde"]} serde = {version = "1.0", features = ["rc"]} serde_derive = "1.0" chrono = "0.4" diff --git a/examples/reading.rs b/examples/reading.rs index 1869699..1e12516 100644 --- a/examples/reading.rs +++ b/examples/reading.rs @@ -12,6 +12,7 @@ fn main() { match gtfs { Ok(g) => { g.print_stats(); + let (_, stop) = g.stops.iter().next().unwrap(); } Err(e) => eprintln!("error: {e:?}"), } diff --git a/fixtures/basic/translations.txt b/fixtures/basic/translations.txt new file mode 100644 index 0000000..6251593 --- /dev/null +++ b/fixtures/basic/translations.txt @@ -0,0 +1,3 @@ +table_name,field_name,language,translation,field_value,record_id,record_sub_id +stops,stop_name,nl,Stop Gebied,,stop1, +stops,stop_name,fr,Arrêt Région,"Stop Area",, \ No newline at end of file diff --git a/src/gtfs.rs b/src/gtfs.rs index 1e0bef9..7832c55 100644 --- a/src/gtfs.rs +++ b/src/gtfs.rs @@ -1,6 +1,8 @@ -use crate::{objects::*, Error, RawGtfs}; +use crate::objects::*; +use crate::{Error, RawGtfs}; use chrono::prelude::NaiveDate; use chrono::Duration; +use language_tags::LanguageTag; use std::collections::{HashMap, HashSet}; use std::convert::TryFrom; use std::sync::Arc; @@ -43,6 +45,10 @@ pub struct Gtfs { pub fare_rules: HashMap>, /// All feed information. There is no identifier pub feed_info: Vec, + /// List of possible localisations from this file + pub avaliable_languages: Vec, + pub translations: HashMap, + pub possible_translations: Vec<(TranslatableField, LanguageTag)>, } impl TryFrom for Gtfs { @@ -51,13 +57,25 @@ 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( + let stops = Self::to_stop_map( raw.stops?, raw.transfers.unwrap_or_else(|| Ok(Vec::new()))?, raw.pathways.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)?; + let trips = Self::create_trips(raw.trips?, raw.stop_times?, frequencies, &stops)?; + + let translations = Self::to_translations( + raw.translations.unwrap_or_else(|| Ok(Vec::new()))?, + ); + + let mut avaliable_languages: HashSet = HashSet::new(); + + for summary_item in translations.1.iter() { + avaliable_languages.insert(summary_item.1.clone()); + } + + let avaliable_languages = avaliable_languages.into_iter().collect::>(); let mut fare_rules = HashMap::>::new(); for f in raw.fare_rules.unwrap_or_else(|| Ok(Vec::new()))? { @@ -66,17 +84,20 @@ impl TryFrom for Gtfs { Ok(Gtfs { stops, - routes: to_map(raw.routes?), + routes: Self::to_map(raw.routes?), trips, agencies: raw.agencies?, shapes: to_shape_map(raw.shapes.unwrap_or_else(|| Ok(Vec::new()))?), fare_attributes: to_map(raw.fare_attributes.unwrap_or_else(|| Ok(Vec::new()))?), fare_rules, feed_info: raw.feed_info.unwrap_or_else(|| Ok(Vec::new()))?, - calendar: to_map(raw.calendar.unwrap_or_else(|| Ok(Vec::new()))?), - calendar_dates: to_calendar_dates( + calendar: Self::to_map(raw.calendar.unwrap_or_else(|| Ok(Vec::new()))?), + calendar_dates: Self::to_calendar_dates( raw.calendar_dates.unwrap_or_else(|| Ok(Vec::new()))?, ), + avaliable_languages: avaliable_languages, + possible_translations: translations.1, + translations: translations.0, read_duration: raw.read_duration, }) } @@ -93,7 +114,9 @@ impl Gtfs { println!(" Agencies: {}", self.agencies.len()); println!(" Shapes: {}", self.shapes.len()); println!(" Fare attributes: {}", self.fare_attributes.len()); - println!(" Feed info: {}", self.feed_info.len()); + println!(" Feed info: {:?}", self.feed_info); + println!(" Translatable Items: {:?}", self.translations.len()); + println!(" Avaliable Languages: {:?}", self.avaliable_languages); } /// Reads from an url (if starts with `"http"`), or a local path (either a directory or zipped file) @@ -104,7 +127,7 @@ impl Gtfs { RawGtfs::new(gtfs).and_then(Gtfs::try_from) } - /// Reads the GTFS from a local zip archive or local directory + /// Reads the GTFS from a local zip archive or local directory pub fn from_path

(path: P) -> Result where P: AsRef + std::fmt::Display, @@ -231,124 +254,269 @@ impl Gtfs { .get(id) .ok_or_else(|| Error::ReferenceError(id.to_owned())) } -} -fn to_map(elements: impl IntoIterator) -> HashMap { - elements - .into_iter() - .map(|e| (e.id().to_owned(), e)) - .collect() -} + + pub fn translate(&self, obj: &T, field: T::Fields, lang: &LanguageTag) -> Option<&str> { + let record = obj.record_id(); -fn to_stop_map( - stops: Vec, - raw_transfers: Vec, - raw_pathways: 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_else(|| { - let stop_id = &transfer.to_stop_id; - Error::ReferenceError(format!("'{stop_id}' in transfers.txt")) - })?; - stop_map - .entry(transfer.from_stop_id.clone()) - .and_modify(|stop| stop.transfers.push(StopTransfer::from(transfer))); - } + let key:TranslationKey = match record { + RecordIdTypes::RecordSubId(sub_id) => TranslationKey::RecordSub((sub_id.0, sub_id.1)), + RecordIdTypes::RecordId(id) => TranslationKey::Record(id) + }; - for pathway in raw_pathways { - stop_map.get(&pathway.to_stop_id).ok_or_else(|| { - let stop_id = &pathway.to_stop_id; - Error::ReferenceError(format!("'{stop_id}' in pathways.txt")) - })?; - stop_map - .entry(pathway.from_stop_id.clone()) - .and_modify(|stop| stop.pathways.push(Pathway::from(pathway))); - } + let lookup_field: TranslatableField = field.clone().wrap_with_table(); - let res = stop_map - .into_iter() - .map(|(i, s)| (i, Arc::new(s))) - .collect(); - Ok(res) -} + //according to the GTFS docs, record based translations take priority over value based translations. + if let Some(translation) = self.translations.get(&TranslationLookup{ + language: lang.clone(), + field: lookup_field.clone(), + key: key + }) { + return Some(translation); + } + + let value = obj.field_value_lookup(field); + + if let Some(value) = value { + if let Some(translation) = self.translations.get(&TranslationLookup{ + language: lang.clone(), + field: lookup_field, + key: TranslationKey::Value(value.to_string()) + }) { + return Some(translation); + } + } + + None + } -fn to_shape_map(shapes: Vec) -> HashMap> { - let mut res = HashMap::default(); - for s in shapes { - let shape = res.entry(s.id.to_owned()).or_insert_with(Vec::new); - shape.push(s); + fn to_map(elements: impl IntoIterator) -> HashMap { + elements + .into_iter() + .map(|e| (e.id().to_owned(), e)) + .collect() } - // we sort the shape by it's pt_sequence - for shapes in res.values_mut() { - shapes.sort_by_key(|s| s.sequence); + + fn to_stop_map( + stops: Vec, + raw_transfers: Vec, + raw_pathways: 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_else(|| { + let stop_id = &transfer.to_stop_id; + Error::ReferenceError(format!("'{stop_id}' in transfers.txt")) + })?; + stop_map + .entry(transfer.from_stop_id.clone()) + .and_modify(|stop| stop.transfers.push(StopTransfer::from(transfer))); + } + + for pathway in raw_pathways { + stop_map.get(&pathway.to_stop_id).ok_or_else(|| { + let stop_id = &pathway.to_stop_id; + Error::ReferenceError(format!("'{stop_id}' in pathways.txt")) + })?; + stop_map + .entry(pathway.from_stop_id.clone()) + .and_modify(|stop| stop.pathways.push(Pathway::from(pathway))); + } + + let res = stop_map + .into_iter() + .map(|(i, s)| (i, Arc::new(s))) + .collect(); + Ok(res) } - res -} + fn to_shape_map(shapes: Vec) -> HashMap> { + let mut res = HashMap::default(); + for s in shapes { + let shape = res.entry(s.id.to_owned()).or_insert_with(Vec::new); + shape.push(s); + } + // we sort the shape by it's pt_sequence + for shapes in res.values_mut() { + shapes.sort_by_key(|s| s.sequence); + } -fn to_calendar_dates(cd: Vec) -> HashMap> { - let mut res = HashMap::default(); - for c in cd { - let cal = res.entry(c.service_id.to_owned()).or_insert_with(Vec::new); - cal.push(c); + res } - res -} -// Number of stoptimes to `pop` from the list before using shrink_to_fit to reduce the memory footprint -// 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 -const NB_STOP_TIMES_BEFORE_SHRINK: usize = 1_000_000; - -fn create_trips( - raw_trips: Vec, - mut raw_stop_times: Vec, - raw_frequencies: Vec, - stops: &HashMap>, -) -> Result, Error> { - let mut trips = to_map(raw_trips.into_iter().map(|rt| Trip { - id: rt.id, - service_id: rt.service_id, - route_id: rt.route_id, - stop_times: vec![], - shape_id: rt.shape_id, - trip_headsign: rt.trip_headsign, - trip_short_name: rt.trip_short_name, - direction_id: rt.direction_id, - block_id: rt.block_id, - wheelchair_accessible: rt.wheelchair_accessible, - bikes_allowed: rt.bikes_allowed, - frequencies: vec![], - })); - - let mut st_idx = 0; - while let Some(s) = raw_stop_times.pop() { - st_idx += 1; - let trip = &mut trips - .get_mut(&s.trip_id) - .ok_or_else(|| Error::ReferenceError(s.trip_id.to_string()))?; - let stop = stops - .get(&s.stop_id) - .ok_or_else(|| Error::ReferenceError(s.stop_id.to_string()))?; - trip.stop_times.push(StopTime::from(s, Arc::clone(stop))); - if st_idx % NB_STOP_TIMES_BEFORE_SHRINK == 0 { - raw_stop_times.shrink_to_fit(); + fn to_calendar_dates(cd: Vec) -> HashMap> { + let mut res = HashMap::default(); + for c in cd { + let cal = res.entry(c.service_id.to_owned()).or_insert_with(Vec::new); + cal.push(c); } + res } - for trip in &mut trips.values_mut() { - trip.stop_times - .sort_by(|a, b| a.stop_sequence.cmp(&b.stop_sequence)); + fn table_and_field_to_enum(table_name: &str, field_name: &str) -> Option { + match table_name { + "agency" => { + match field_name { + "agency_name" => Some(TranslatableField::Agency(AgencyFields::Name)), + "agency_url" => Some(TranslatableField::Agency(AgencyFields::Url)), + "agency_fare_url" => Some(TranslatableField::Agency(AgencyFields::FareUrl)), + _ => None + } + }, + "areas" => { + match field_name { + "area_name" => Some(TranslatableField::Areas(AreaFields::Name)), + _ => None + } + }, + "routes" => { + match field_name { + "route_long_name" => Some(TranslatableField::Routes(RouteFields::LongName)), + "route_short_name" => Some(TranslatableField::Routes(RouteFields::ShortName)), + "route_url" => Some(TranslatableField::Routes(RouteFields::Url)), + _ => None + } + }, + "stop_times" => { + match field_name { + "stop_headsign" => Some(TranslatableField::StopTimes(StopTimeFields::Headsign)), + _ => None + } + }, + "stops" => { + match field_name { + "stop_code" => Some(TranslatableField::Stops(StopFields::Code)), + "stop_name" => Some(TranslatableField::Stops(StopFields::Name)), + "tts_stop_name" => Some(TranslatableField::Stops(StopFields::TtsName)), + "stop_desc" => Some(TranslatableField::Stops(StopFields::Desc)), + "platform_code" => Some(TranslatableField::Stops(StopFields::PlatformCode)), + _ => None + } + }, + "trips" => { + match field_name { + "trip_headsign" => Some(TranslatableField::Trips(TripFields::Headsign)), + "trip_short_name" => Some(TranslatableField::Trips(TripFields::ShortName)), + _ => None + } + }, + "calendar" => { + match field_name { + "service_id" => Some(TranslatableField::Calendar(CalendarFields::ServiceId)), + _ => None + } + }, + "fare_products" => { + match field_name { + "fare_product_name" => Some(TranslatableField::FareProducts(FareProductFields::ProductName)), + _ => None + } + }, + "feed_info" => { + match field_name { + "feed_publisher_name" => Some(TranslatableField::FeedInfo(FeedInfoFields::PublisherName)), + _ => None + } + } + _ => None + } + } + + fn key_options_to_struct(record_id: Option, record_sub_id: Option, field_value: Option) -> Option { + //https://gtfs.org/schedule/reference/#translationstxt + //If both referencing methods (record_id, record_sub_id) and field_value are used to translate the same value in 2 different rows, the translation provided with (record_id, record_sub_id) takes precedence. + match (record_id, record_sub_id, field_value) { + (Some(record_id), Some(record_sub_id), _) => Some(TranslationKey::RecordSub((record_id, record_sub_id))), + (Some(record_id), _, _) => Some(TranslationKey::Record(record_id)), + (_, _, Some(field_value)) => Some(TranslationKey::Value(field_value)), + _ => None + } } - for f in raw_frequencies { - let trip = &mut trips - .get_mut(&f.trip_id) - .ok_or_else(|| Error::ReferenceError(f.trip_id.to_string()))?; - trip.frequencies.push(Frequency::from(&f)); + fn to_translations( + raw_translations: Vec, + ) -> ( + //The translation table itself + HashMap, + //This is the summary for the GTFS structure + Vec<(TranslatableField, LanguageTag)> + ) { + let mut res:HashMap = HashMap::new(); + let mut possible_translations:HashSet<(TranslatableField, LanguageTag)> = HashSet::new(); + + for row in raw_translations { + if let Ok(language_tag) = LanguageTag::parse(row.language.as_str()) { + if let Some(field) = Self::table_and_field_to_enum(row.table_name.as_str(), row.field_name.as_str()) { + if let Some(key) = Self::key_options_to_struct(row.record_id, row.record_sub_id, row.field_value) { + res.insert(TranslationLookup { + language: language_tag.clone(), + field: field.clone(), + key: key + }, row.translation); + possible_translations.insert((field, language_tag)); + } + } + + } + } + + (res, possible_translations.into_iter().collect::>()) } - Ok(trips) + // Number of stoptimes to `pop` from the list before using shrink_to_fit to reduce the memory footprint + // 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 + const NB_STOP_TIMES_BEFORE_SHRINK: usize = 1_000_000; + + fn create_trips( + raw_trips: Vec, + mut raw_stop_times: Vec, + raw_frequencies: Vec, + stops: &HashMap>, + ) -> Result, Error> { + let mut trips = Self::to_map(raw_trips.into_iter().map(|rt| Trip { + id: rt.id, + service_id: rt.service_id, + route_id: rt.route_id, + stop_times: vec![], + shape_id: rt.shape_id, + trip_headsign: rt.trip_headsign, + trip_short_name: rt.trip_short_name, + direction_id: rt.direction_id, + block_id: rt.block_id, + wheelchair_accessible: rt.wheelchair_accessible, + bikes_allowed: rt.bikes_allowed, + frequencies: vec![], + })); + + let mut st_idx = 0; + while let Some(s) = raw_stop_times.pop() { + st_idx += 1; + let trip = &mut trips + .get_mut(&s.trip_id) + .ok_or_else(|| Error::ReferenceError(s.trip_id.to_string()))?; + let stop = stops + .get(&s.stop_id) + .ok_or_else(|| Error::ReferenceError(s.stop_id.to_string()))?; + trip.stop_times.push(StopTime::from(s, Arc::clone(stop))); + if st_idx % Self::NB_STOP_TIMES_BEFORE_SHRINK == 0 { + raw_stop_times.shrink_to_fit(); + } + } + + for trip in &mut trips.values_mut() { + trip.stop_times + .sort_by(|a, b| a.stop_sequence.cmp(&b.stop_sequence)); + } + + for f in raw_frequencies { + let trip = &mut trips + .get_mut(&f.trip_id) + .ok_or_else(|| Error::ReferenceError(f.trip_id.to_string()))?; + trip.frequencies.push(Frequency::from(&f)); + } + + Ok(trips) + } } diff --git a/src/gtfs_reader.rs b/src/gtfs_reader.rs index f35f0fb..b51e28c 100644 --- a/src/gtfs_reader.rs +++ b/src/gtfs_reader.rs @@ -182,6 +182,7 @@ impl RawGtfsReader { pathways: self.read_objs_from_optional_path(p, "pathways.txt"), feed_info: self.read_objs_from_optional_path(p, "feed_info.txt"), read_duration: Utc::now().signed_duration_since(now).num_milliseconds(), + translations: self.read_objs_from_optional_path(p, "translations.txt"), files, source_format: crate::SourceFormat::Directory, sha256: None, @@ -269,6 +270,7 @@ impl RawGtfsReader { "pathways.txt", "feed_info.txt", "shapes.txt", + "translations.txt", ] { let path = std::path::Path::new(archive_file.name()); if path.file_name() == Some(std::ffi::OsStr::new(gtfs_file)) { @@ -309,6 +311,7 @@ impl RawGtfsReader { } else { Some(Ok(Vec::new())) }, + translations: self.read_optional_file(&file_mapping, &mut archive, "translations.txt"), read_duration: Utc::now().signed_duration_since(now).num_milliseconds(), files, source_format: crate::SourceFormat::Zip, diff --git a/src/objects.rs b/src/objects.rs index 00d59f7..40b84af 100644 --- a/src/objects.rs +++ b/src/objects.rs @@ -1,11 +1,25 @@ pub use crate::enums::*; use crate::serde_helpers::*; use chrono::{Datelike, NaiveDate, Weekday}; +use language_tags::LanguageTag; use rgb::RGB8; - use std::fmt; use std::sync::Arc; +/// Raw Translation used for RawGtfs +#[derive(Derivative)] +#[derivative(Default(bound = ""))] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct RawTranslation { + pub table_name: String, + pub field_name: String, + pub language: String, + pub translation: String, + pub record_id: Option, + pub record_sub_id: Option, + pub field_value: Option, +} + /// Objects that have an identifier implement this trait /// /// Those identifier are technical and should not be shown to travellers @@ -20,6 +34,42 @@ impl Id for Arc { } } +/// Objects that have a couple tuple as an identifier implement this trait +/// +/// Those identifier are technical and should not be shown to travellers +pub trait CoupleId { + fn couple_id(&self) -> (&str, &str); +} + +pub enum RecordIdTypes { + RecordSubId((String, String)), + RecordId(String) +} + +pub trait TranslateRecord { + fn record_id(&self) -> RecordIdTypes; +} + +impl TranslateRecord for dyn CoupleId { + fn record_id(&self) -> RecordIdTypes { + let couple_id = self.couple_id(); + RecordIdTypes::RecordSubId((couple_id.0.to_string(), couple_id.1.to_string())) + } +} + +impl TranslateRecord for dyn Id { + fn record_id(&self) -> RecordIdTypes { + let id = self.id(); + RecordIdTypes::RecordId(id.to_string()) + } +} + +/// Translatable allows an Option as the field, as long as the record exists, a string will be returned, even if the original table's field is empty. +pub trait Translatable { + type Fields : WrapFieldWithTable + Clone; + fn field_value_lookup(&self, field: Self::Fields) -> Option<&str>; +} + /// Trait to introspect what is the object’s type (stop, route…) pub trait Type { /// What is the type of the object @@ -32,6 +82,183 @@ impl Type for Arc { } } +#[derive(Debug, Deserialize, Serialize, Hash, Eq, PartialEq, Clone)] +pub enum TranslatableField { + Agency(AgencyFields), + Areas(AreaFields), + Calendar(CalendarFields), + FareProducts(FareProductFields), + FeedInfo(FeedInfoFields), + Routes(RouteFields), + StopTimes(StopTimeFields), + Stops(StopFields), + Trips(TripFields), +} + +#[derive(Debug, Deserialize, Serialize, Hash, Eq, PartialEq, Clone)] +pub enum TranslationKey { + Record(String), + RecordSub((String, String)), + Value(String), +} + +#[derive(Debug, Deserialize, Serialize, Hash, Eq, PartialEq, Clone)] +pub struct TranslationLookup { + pub language: LanguageTag, + pub field: TranslatableField, + pub key: TranslationKey, +} + +#[derive(Debug, Deserialize, Serialize, Hash, Eq, PartialEq, Clone)] +pub enum StopTimeFields { + Headsign, +} + +#[derive(Debug, Deserialize, Serialize, Hash, Eq, PartialEq, Clone)] +pub enum RouteFields { + Desc, + LongName, + ShortName, + Url, +} + +#[derive(Debug, Deserialize, Serialize, Hash, Eq, PartialEq, Clone)] +pub enum CalendarFields { + ServiceId, +} + +#[derive(Debug, Deserialize, Serialize, Hash, Eq, PartialEq, Clone)] +pub enum FeedInfoFields { + PublisherName, +} + +#[derive(Debug, Deserialize, Serialize, Hash, Eq, PartialEq, Clone)] +pub enum AreaFields { + Name, +} + +#[derive(Debug, Deserialize, Serialize, Hash, Eq, PartialEq, Clone)] +pub enum AgencyFields { + Name, + FareUrl, + Url, +} + +#[derive(Debug, Deserialize, Serialize, Hash, Eq, PartialEq, Clone)] +pub enum FareProductFields { + ProductName, +} + +#[derive(Debug, Deserialize, Serialize, Hash, Eq, PartialEq, Clone)] +pub enum TripFields { + Headsign, + ShortName +} + +#[derive(Debug, Deserialize, Serialize, Hash, Eq, PartialEq, Clone)] +pub enum StopFields { + Code, + Name, + TtsName, + PlatformCode, + Desc, +} + +pub trait WrapFieldWithTable { + fn wrap_with_table(self) -> TranslatableField; +} + + +impl WrapFieldWithTable for StopFields { + fn wrap_with_table(self) -> TranslatableField { + TranslatableField::Stops(self) + } +} + +impl WrapFieldWithTable for AgencyFields { + fn wrap_with_table(self) -> TranslatableField { + TranslatableField::Agency(self) + } +} + +impl WrapFieldWithTable for AreaFields { + fn wrap_with_table(self) -> TranslatableField { + TranslatableField::Areas(self) + } +} + +impl WrapFieldWithTable for CalendarFields { + fn wrap_with_table(self) -> TranslatableField { + TranslatableField::Calendar(self) + } +} + +impl WrapFieldWithTable for FareProductFields { + fn wrap_with_table(self) -> TranslatableField { + TranslatableField::FareProducts(self) + } +} + +impl WrapFieldWithTable for FeedInfoFields { + fn wrap_with_table(self) -> TranslatableField { + TranslatableField::FeedInfo(self) + } +} + +impl WrapFieldWithTable for RouteFields { + fn wrap_with_table(self) -> TranslatableField { + TranslatableField::Routes(self) + } +} + +impl WrapFieldWithTable for StopTimeFields { + fn wrap_with_table(self) -> TranslatableField { + TranslatableField::StopTimes(self) + } +} + +impl WrapFieldWithTable for TripFields { + fn wrap_with_table(self) -> TranslatableField { + TranslatableField::Trips(self) + } +} + +impl Translatable for Stop { + type Fields = StopFields; + fn field_value_lookup(&self, field: Self::Fields) -> Option<&str> { + match field { + StopFields::Name => Some(&self.name), + StopFields::Code => self.code.as_deref(), + StopFields::TtsName => self.tts_name.as_deref(), + StopFields::PlatformCode => self.platform_code.as_deref(), + StopFields::Desc => self.description.as_deref(), + } + } +} + +impl Translatable for Route { + type Fields = RouteFields; + fn field_value_lookup(&self, field: Self::Fields) -> Option<&str> { + match field { + RouteFields::Desc => self.desc.as_deref(), + RouteFields::LongName => self.long_name.as_deref(), + RouteFields::ShortName => self.short_name.as_deref(), + RouteFields::Url => self.url.as_deref(), + } + } +} + +impl Translatable for Agency { + type Fields = AgencyFields; + fn field_value_lookup(&self, field: Self::Fields) -> Option<&str> { + match field { + AgencyFields::Name => Some(&self.name), + AgencyFields::FareUrl => self.fare_url.as_deref(), + AgencyFields::Url => Some(&self.url), + } + } +} + /// A calender describes on which days the vehicle runs. See #[derive(Debug, Deserialize, Serialize)] pub struct Calendar { @@ -156,7 +383,7 @@ pub struct Stop { pub name: String, /// Description of the location that provides useful, quality information #[serde(default, rename = "stop_desc")] - pub description: String, + pub description: Option, /// Type of the location #[serde(default)] pub location_type: LocationType, @@ -193,6 +420,9 @@ pub struct Stop { /// Pathways from this stop #[serde(skip)] pub pathways: Vec, + /// Text to speech readable version of the stop_name + #[serde(rename = "tts_stop_name")] + pub tts_name: Option } impl Type for Stop { @@ -309,6 +539,16 @@ impl StopTime { } } +impl CoupleId for StopTime { + fn couple_id(&self) -> (&str, &str) { + todo!() + + //oh no... i need trip_id... but that's contained in the parent... + + // should I add trip_id to stop_times? + } +} + /// A route is a commercial line (there can be various stop sequences for a same line). See #[derive(Debug, Serialize, Deserialize, Default)] pub struct Route { @@ -317,10 +557,10 @@ pub struct Route { pub id: String, /// Short name of a route. This will often be a short, abstract identifier like "32", "100X", or "Green" that riders use to identify a route, but which doesn't give any indication of what places the route serves #[serde(rename = "route_short_name", default)] - pub short_name: String, + pub short_name: Option, /// Full name of a route. This name is generally more descriptive than the [Route::short_name]] and often includes the route's destination or stop #[serde(rename = "route_long_name", default)] - pub long_name: String, + pub long_name: Option, /// Description of a route that provides useful, quality information #[serde(rename = "route_desc")] pub desc: Option, @@ -372,10 +612,10 @@ impl Id for Route { impl fmt::Display for Route { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if !self.long_name.is_empty() { - write!(f, "{}", self.long_name) + if self.long_name.is_some() { + write!(f, "{:?}", self.long_name) } else { - write!(f, "{}", self.short_name) + write!(f, "{:?}", self.short_name) } } } diff --git a/src/raw_gtfs.rs b/src/raw_gtfs.rs index 12e6695..9562cf7 100644 --- a/src/raw_gtfs.rs +++ b/src/raw_gtfs.rs @@ -1,3 +1,4 @@ +use crate::enums::*; use crate::objects::*; use crate::Error; use crate::GtfsReader; @@ -39,6 +40,8 @@ pub struct RawGtfs { pub feed_info: Option, Error>>, /// All StopTimes pub stop_times: Result, Error>, + /// All Translations + pub translations: Option, Error>>, /// All files that are present in the feed pub files: Vec, /// Format of the data read @@ -66,6 +69,7 @@ impl RawGtfs { println!(" Transfers: {}", optional_file_summary(&self.transfers)); println!(" Pathways: {}", optional_file_summary(&self.pathways)); println!(" Feed info: {}", optional_file_summary(&self.feed_info)); + println!(" Translatable Items: {}", optional_file_summary(&self.translations)); } /// 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 f5d9fbe..a1f79db 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -342,7 +342,7 @@ fn display() { #[test] fn path_files() { let gtfs = RawGtfs::from_path("fixtures/basic").expect("impossible to read gtfs"); - assert_eq!(gtfs.files.len(), 13); + assert_eq!(gtfs.files.len(), 14); assert_eq!(gtfs.source_format, SourceFormat::Directory); assert!(gtfs.files.contains(&"agency.txt".to_owned())); }