├── .github └── workflows │ ├── publish.yml │ └── tests.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── gtfs_raw_reader.rs ├── gtfs_reader.rs ├── gtfs_reading.rs ├── raw_gtfs_reading.rs └── reading.rs ├── fixtures ├── basic │ ├── agency.txt │ ├── calendar.txt │ ├── calendar_dates.txt │ ├── fare_attributes.txt │ ├── feed_info.txt │ ├── frequencies.txt │ ├── pathways.txt │ ├── routes.txt │ ├── shapes.txt │ ├── stop_times.txt │ ├── stops.txt │ ├── transfers.txt │ ├── translations.txt │ └── trips.txt ├── fares_v1 │ ├── agency.txt │ ├── fare_attributes.txt │ ├── fare_rules.txt │ ├── readme.md │ ├── routes.txt │ ├── stop_times.txt │ ├── stops.txt │ ├── transfers.txt │ └── trips.txt ├── interpolated_stop_times │ ├── agency.txt │ ├── calendar.txt │ ├── calendar_dates.txt │ ├── fare_attributes.txt │ ├── feed_info.txt │ ├── routes.txt │ ├── shapes.txt │ ├── stop_times.txt │ ├── stops.txt │ └── trips.txt ├── missing_feed_date │ ├── agency.txt │ ├── calendar.txt │ ├── calendar_dates.txt │ ├── fare_attributes.txt │ ├── feed_info.txt │ ├── readme.md │ ├── routes.txt │ ├── shapes.txt │ ├── stop_times.txt │ ├── stops.txt │ └── trips.txt ├── only_required_fields │ ├── agency.txt │ ├── calendar.txt │ ├── calendar_dates.txt │ ├── fare_attributes.txt │ ├── feed_info.txt │ ├── routes.txt │ ├── shapes.txt │ ├── stop_times.txt │ ├── stops.txt │ └── trips.txt ├── subdirectory │ └── gtfs │ │ ├── agency.txt │ │ ├── calendar.txt │ │ ├── calendar_dates.txt │ │ ├── fare_attributes.txt │ │ ├── feed_info.txt │ │ ├── frequencies.txt │ │ ├── pathways.txt │ │ ├── routes.txt │ │ ├── shapes.txt │ │ ├── stop_times.txt │ │ ├── stops.txt │ │ ├── transfers.txt │ │ └── trips.txt └── zips │ ├── gtfs.zip │ ├── gtfs_with_bom.zip │ ├── macosx.zip │ ├── metra.zip │ └── subdirectory.zip └── src ├── enums.rs ├── error.rs ├── gtfs.rs ├── gtfs_reader.rs ├── lib.rs ├── objects.rs ├── raw_gtfs.rs ├── serde_helpers.rs └── tests.rs /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | 6 | jobs: 7 | publish: 8 | name: Publish on crates.io 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: katyo/publish-crates@v2 13 | with: 14 | registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} 15 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Tests and linting 4 | 5 | jobs: 6 | test: 7 | name: Test Suite 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: actions/cache@v3 12 | with: 13 | path: | 14 | ~/.cargo/bin/ 15 | ~/.cargo/registry/index/ 16 | ~/.cargo/registry/cache/ 17 | ~/.cargo/git/db/ 18 | target/ 19 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }} 20 | - run: test --all-features 21 | - run: cargo test --no-default-features 22 | 23 | lints: 24 | name: Lints 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Checkout sources 28 | uses: actions/checkout@v4 29 | - name: Run cargo fmt 30 | run: cargo fmt --all -- --check 31 | - name: Run cargo clippy 32 | run: cargo clippy -- -D warnings 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | target 3 | .vscode 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | description = "Read GTFS (public transit timetables) files" 3 | name = "gtfs-structures" 4 | version = "0.43.0" 5 | authors = [ 6 | "Tristram Gräbener ", 7 | "Antoine Desbordes ", 8 | ] 9 | repository = "https://github.com/rust-transit/gtfs-structure" 10 | license = "MIT" 11 | edition = "2018" 12 | 13 | [features] 14 | default = ["read-url"] 15 | read-url = ["reqwest", "futures"] 16 | 17 | [dependencies] 18 | bytes = "1" 19 | csv = "1.1" 20 | derivative = "2.1" 21 | serde = { version = "1.0", features = ["rc"] } 22 | serde_derive = "1.0" 23 | chrono = "0.4.38" 24 | itertools = "0.13" 25 | sha2 = "0.10" 26 | zip = "2.2" 27 | thiserror = "1" 28 | rgb = "0.8" 29 | 30 | futures = { version = "0.3", optional = true } 31 | reqwest = { version = "0.12", optional = true, features = ["blocking"] } 32 | 33 | [dev-dependencies] 34 | serde_json = "1.0" 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Tristram Gräbener 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [GTFS](https://gtfs.org/) Model ![crates.io](https://img.shields.io/crates/v/gtfs-structures.svg) [![](https://docs.rs/gtfs-structures/badge.svg)](https://docs.rs/gtfs-structures) 2 | 3 | The [General Transit Feed Specification](https://gtfs.org/) (GTFS) is a commonly used model to represent public transit data. 4 | 5 | This crates brings [serde](https://serde.rs) structures of this model and helpers to read GTFS archives. 6 | 7 | ## Using 8 | 9 | This crates has 2 main entry-points. 10 | 11 | ### Gtfs 12 | The most common one is to create a `gtfs_structures::Gtfs`: 13 | 14 | ```rust 15 | // Gtfs::new will try to guess if you provide a path, a local zip file or a remote zip file. 16 | // You can also use Gtfs::from_path or Gtfs::from_url 17 | let gtfs = gtfs_structures::Gtfs::new("path_of_a_zip_or_directory_or_url")?; 18 | println!("there are {} stops in the gtfs", gtfs.stops.len()); 19 | 20 | // This structure is the easiest to use as the collections are `HashMap`, 21 | // thus you can access an object by its id. 22 | let route_1 = gtfs.routes.get("1").expect("no route 1"); 23 | println!("{}: {:?}", route_1.short_name, route_1); 24 | ``` 25 | 26 | ### RawGtfs 27 | 28 | If you want a lower level model, you can use `gtfs_structures::RawGtfs`: 29 | 30 | ```rust 31 | let raw_gtfs = RawGtfs::new("fixtures/basic").expect("impossible to read gtfs"); 32 | for stop in raw_gtfs.stops.expect("impossible to read stops.txt") { 33 | println!("stop: {}", stop.name); 34 | } 35 | ``` 36 | 37 | Instead of easy to use `HashMap`, each collection is a `Result` with an error if something went wrong during the reading. 38 | 39 | This makes it possible for example for a [GTFS validator](https://github.com/etalab/transport-validator/) to display better error messages. 40 | 41 | ### Feature 'read-url' 42 | 43 | By default the feature 'read-url' is activated. It makes it possible to read a Gtfs from an url. 44 | 45 | ```rust 46 | let gtfs = gtfs_structures::Gtfs::new("http://www.metromobilite.fr/data/Horaires/SEM-GTFS.zip")?; 47 | ``` 48 | 49 | Or you can use the explicit constructor: 50 | ```rust 51 | let gtfs = gtfs_structures::Gtfs::from_url("http://www.metromobilite.fr/data/Horaires/SEM-GTFS.zip")?; 52 | ``` 53 | 54 | If you don't want the dependency to `reqwest`, you can remove this feature. 55 | 56 | ## Building 57 | 58 | You need an up to date rust tool-chain (commonly installed with [rustup](https://rustup.rs/)). 59 | 60 | Building is done with: 61 | 62 | `cargo build` 63 | 64 | You can also run the unit tests: 65 | 66 | `cargo test` 67 | 68 | And run the examples by giving their names: 69 | 70 | `cargo run --example gtfs_reading` 71 | 72 | # Alternative 73 | 74 | If you are interested in transit data, you can also use the really nice crate [transit_model](https://github.com/CanalTP/transit_model) that can also handle GTFS data. 75 | 76 | The price to pay is a steeper learning curve (and a documentation that could be improved :roll_eyes:), but this crate provides very nice ergonomics to handle transit data and lots of utilities like data format conversions, datasets merge, ... 77 | -------------------------------------------------------------------------------- /examples/gtfs_raw_reader.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | /* Gtfs::new will try to guess if you provide a path, a local zip file or a remote zip file. 3 | You can also use Gtfs::from_path, Gtfs::from_url 4 | */ 5 | let gtfs = gtfs_structures::GtfsReader::default() 6 | .read_stop_times(false) 7 | .raw() 8 | .read("fixtures/basic") 9 | .expect("impossible to read gtfs"); 10 | gtfs.print_stats(); 11 | 12 | let routes = gtfs.routes.expect("impossible to read routes"); 13 | let route_1 = routes.first().expect("no route"); 14 | println!("{}: {:?}", route_1.short_name.as_ref().unwrap(), route_1); 15 | } 16 | -------------------------------------------------------------------------------- /examples/gtfs_reader.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | /* Gtfs::new will try to guess if you provide a path, a local zip file or a remote zip file. 3 | You can also use Gtfs::from_path, Gtfs::from_url 4 | */ 5 | let gtfs = gtfs_structures::GtfsReader::default() 6 | .read_stop_times(false) 7 | .read("fixtures/basic") 8 | .expect("impossible to read gtfs"); 9 | gtfs.print_stats(); 10 | 11 | println!("there are {} stops in the gtfs", gtfs.stops.len()); 12 | 13 | let route_1 = gtfs.routes.get("1").expect("no route 1"); 14 | println!("{}: {:?}", route_1.short_name.as_ref().unwrap(), route_1); 15 | } 16 | -------------------------------------------------------------------------------- /examples/gtfs_reading.rs: -------------------------------------------------------------------------------- 1 | use gtfs_structures::Gtfs; 2 | 3 | fn main() { 4 | /* Gtfs::new will try to guess if you provide a path, a local zip file or a remote zip file. 5 | You can also use Gtfs::from_path, Gtfs::from_url 6 | */ 7 | let gtfs = Gtfs::new("fixtures/basic").expect("impossible to read gtfs"); 8 | 9 | gtfs.print_stats(); 10 | 11 | println!("there are {} stops in the gtfs", gtfs.stops.len()); 12 | 13 | let route_1 = gtfs.routes.get("1").expect("no route 1"); 14 | println!("{}: {:?}", route_1.short_name.as_ref().unwrap(), route_1); 15 | } 16 | -------------------------------------------------------------------------------- /examples/raw_gtfs_reading.rs: -------------------------------------------------------------------------------- 1 | use gtfs_structures::RawGtfs; 2 | 3 | fn main() { 4 | /* Gtfs::new will try to guess if you provide a path, a local zip file or a remote zip file. 5 | You can also use RawGtfs::from_path, RawGtfs::from_url 6 | */ 7 | let raw_gtfs = RawGtfs::new("fixtures/basic").expect("impossible to read gtfs"); 8 | 9 | raw_gtfs.print_stats(); 10 | 11 | for stop in raw_gtfs.stops.expect("impossible to read stops.txt") { 12 | println!("stop: {}", stop.name.unwrap_or(String::from(""))); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/reading.rs: -------------------------------------------------------------------------------- 1 | use gtfs_structures::Gtfs; 2 | 3 | /// prints some stats about the GTFS given as a cli argument 4 | fn main() { 5 | let file_path = std::env::args() 6 | .nth(1) 7 | .expect("you should put the path of the file to load"); 8 | 9 | println!("reading file {}", &file_path); 10 | let gtfs = Gtfs::new(&file_path); 11 | 12 | match gtfs { 13 | Ok(g) => { 14 | g.print_stats(); 15 | } 16 | Err(e) => eprintln!("error: {e:?}"), 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /fixtures/basic/agency.txt: -------------------------------------------------------------------------------- 1 | agency_name,agency_url,agency_timezone,agency_lang 2 | "BIBUS",http://www.bibus.fr,Europe/Paris,fr 3 | "Ter",http://www.sncf.com,Europe/Paris,fr -------------------------------------------------------------------------------- /fixtures/basic/calendar.txt: -------------------------------------------------------------------------------- 1 | service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date 2 | service1,0,0,0,0,0,1,1,20170101,20170115 3 | -------------------------------------------------------------------------------- /fixtures/basic/calendar_dates.txt: -------------------------------------------------------------------------------- 1 | service_id,date,exception_type 2 | service1,20170101,2 3 | service1,20170102,2 4 | service2,20170101,1 5 | -------------------------------------------------------------------------------- /fixtures/basic/fare_attributes.txt: -------------------------------------------------------------------------------- 1 | fare_id,price,currency_type,payment_method,transfers,agency_id,transfer_duration 2 | "50","1.50","EUR","0","","1","3600" 3 | -------------------------------------------------------------------------------- /fixtures/basic/feed_info.txt: -------------------------------------------------------------------------------- 1 | feed_publisher_name,feed_publisher_url,feed_lang,feed_start_date,feed_end_date,feed_version 2 | SNCF,http://www.sncf.com,fr,20180709,20180927,0.3 3 | -------------------------------------------------------------------------------- /fixtures/basic/frequencies.txt: -------------------------------------------------------------------------------- 1 | trip_id,start_time,end_time,headway_secs 2 | trip1,05:30:00,05:38:00,480 3 | -------------------------------------------------------------------------------- /fixtures/basic/pathways.txt: -------------------------------------------------------------------------------- 1 | pathway_id,from_stop_id,to_stop_id,pathway_mode,is_bidirectional,length,traversal_time,stair_count,max_slope,min_width,signposted_as,reversed_signposted_as 2 | pathway1,stop1,stop3,1,0,,,,,,, 3 | -------------------------------------------------------------------------------- /fixtures/basic/routes.txt: -------------------------------------------------------------------------------- 1 | route_id,agency_id,route_short_name,route_long_name,route_desc,route_type,route_url,route_color,route_text_color,route_sort_order 2 | 1,848,"100","100","",3,,000000,FFFFFF,1 3 | invalid_type,848,"100","100","",42,,000000,FFFFFF, 4 | default_colors,848,"default_colors","route with default colors","",3,,,,1 5 | -------------------------------------------------------------------------------- /fixtures/basic/shapes.txt: -------------------------------------------------------------------------------- 1 | shape_id,shape_pt_lat,shape_pt_lon,shape_pt_sequence,shape_dist_traveled 2 | A_shp,37.61956,-122.48161,0,0 3 | A_shp,37.64430,-122.41070,6,6.8310 4 | A_shp,37.65863,-122.30839,11,15.8765 5 | Unordered_shp,37.64430,-122.41070,6,6.8310 6 | Unordered_shp,37.61956,-122.48161,0,0 7 | Unordered_shp,37.65863,-122.30839,11,15.8765 -------------------------------------------------------------------------------- /fixtures/basic/stop_times.txt: -------------------------------------------------------------------------------- 1 | trip_id,arrival_time,departure_time,stop_id,stop_sequence,stop_time_desc,pickup_type,drop_off_type,timepoint 2 | trip1,14:00:00,14:00:00,stop2,0,"",0,1,"" 3 | trip1,15:00:00,15:00:00,stop3,1,"",2,,"0" 4 | trip1,16:00:00,16:00:00,stop4,2,"",2,-999, -------------------------------------------------------------------------------- /fixtures/basic/stops.txt: -------------------------------------------------------------------------------- 1 | stop_id,stop_name,stop_desc,stop_lat,stop_lon,zone_id,stop_url,location_type,parent_station,wheelchair_boarding 2 | stop1,"Stop Area",, 48.796058 ,2.449386,,,1,, 3 | stop2,"StopPoint",,48.796058,2.449386,,,,, 4 | stop3,"Stop Point child of 1",,48.796058,2.449386,,,0,1, 5 | stop4,"StopPoint2",,48.796058,2.449386,,,,, 6 | stop5,"Stop Point child of 1 bis",,48.796058,2.449386,,,0,1, 7 | stop6,"Generic node",,,,,,3,1, 8 | -------------------------------------------------------------------------------- /fixtures/basic/transfers.txt: -------------------------------------------------------------------------------- 1 | from_stop_id,to_stop_id,transfer_type,min_transfer_time 2 | stop3,stop5,0,60 3 | stop1,stop2,3, 4 | stop2,stop3,, 5 | stop5,stop2,4, 6 | stop5,stop3,5, -------------------------------------------------------------------------------- /fixtures/basic/translations.txt: -------------------------------------------------------------------------------- 1 | table_name,field_name,language,translation,field_value,record_id,record_sub_id 2 | stops,stop_name,nl,Stop Gebied,,stop1, 3 | stops,stop_name,fr,Arrêt Région,"Stop Area",, -------------------------------------------------------------------------------- /fixtures/basic/trips.txt: -------------------------------------------------------------------------------- 1 | route_id,service_id,trip_id,trip_headsign,trip_short_name,direction_id,block_id,wheelchair_accessible,bikes_allowed,trip_desc,shape_id 2 | route1,service1,trip1,"85088452",,0,,0,0,, 3 | -------------------------------------------------------------------------------- /fixtures/fares_v1/agency.txt: -------------------------------------------------------------------------------- 1 | agency_name,agency_url,agency_timezone,agency_lang -------------------------------------------------------------------------------- /fixtures/fares_v1/fare_attributes.txt: -------------------------------------------------------------------------------- 1 | fare_id,price,currency_type,payment_method,transfers,transfer_duration 2 | presto_fare,3.2,CAD,1,,7200 -------------------------------------------------------------------------------- /fixtures/fares_v1/fare_rules.txt: -------------------------------------------------------------------------------- 1 | fare_id,route_id,origin_id,destination_id 2 | presto_fare,line1,ttc_subway_stations,ttc_subway_stations 3 | presto_fare,line2,ttc_subway_stations,ttc_subway_stations -------------------------------------------------------------------------------- /fixtures/fares_v1/readme.md: -------------------------------------------------------------------------------- 1 | Dataset constructed from the [GTFS fares v1 example](https://gtfs.org/schedule/examples/fares-v1/). 2 | -------------------------------------------------------------------------------- /fixtures/fares_v1/routes.txt: -------------------------------------------------------------------------------- 1 | agency_id,route_id,route_type 2 | TTC,Line1,1 3 | TTC,Line2,1 -------------------------------------------------------------------------------- /fixtures/fares_v1/stop_times.txt: -------------------------------------------------------------------------------- 1 | trip_id,arrival_time,departure_time,stop_id,stop_sequence,stop_time_desc,pickup_type,drop_off_type,timepoint -------------------------------------------------------------------------------- /fixtures/fares_v1/stops.txt: -------------------------------------------------------------------------------- 1 | stop_id,stop_name,stop_lat,stop_lon,zone_id 2 | Bloor,Bloor Station,,43.670049,-79.385389,ttc_subway_stations 3 | Yonge,Yonge Station,,43.671049,-79.386789,ttc_subway_stations -------------------------------------------------------------------------------- /fixtures/fares_v1/transfers.txt: -------------------------------------------------------------------------------- 1 | from_stop_id,to_stop_id,from_route_id,to_route_id,transfer_type 2 | Bloor,Yonge,line1,line2,0 3 | Yonge,Bloor,line2,line1,0 -------------------------------------------------------------------------------- /fixtures/fares_v1/trips.txt: -------------------------------------------------------------------------------- 1 | route_id,service_id,trip_id,trip_headsign,trip_short_name,direction_id,block_id,wheelchair_accessible,bikes_allowed,trip_desc,shape_id 2 | -------------------------------------------------------------------------------- /fixtures/interpolated_stop_times/agency.txt: -------------------------------------------------------------------------------- 1 | agency_name,agency_url,agency_timezone,agency_lang 2 | "BIBUS",http://www.bibus.fr,Europe/Paris,fr 3 | "Ter",http://www.sncf.com,Europe/Paris,fr 4 | -------------------------------------------------------------------------------- /fixtures/interpolated_stop_times/calendar.txt: -------------------------------------------------------------------------------- 1 | service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date 2 | service1,0,0,0,0,0,1,1,20170101,20170115 3 | -------------------------------------------------------------------------------- /fixtures/interpolated_stop_times/calendar_dates.txt: -------------------------------------------------------------------------------- 1 | service_id,date,exception_type 2 | service1,20170101,2 3 | service1,20170102,2 4 | service2,20170101,1 5 | -------------------------------------------------------------------------------- /fixtures/interpolated_stop_times/fare_attributes.txt: -------------------------------------------------------------------------------- 1 | fare_id,price,currency_type,payment_method,transfers,agency_id,transfer_duration 2 | "50","1.50","EUR","0","","1","3600" 3 | -------------------------------------------------------------------------------- /fixtures/interpolated_stop_times/feed_info.txt: -------------------------------------------------------------------------------- 1 | feed_publisher_name,feed_publisher_url,feed_lang,feed_start_date,feed_end_date,feed_version 2 | SNCF,http://www.sncf.com,fr,20180709,20180927,0.3 3 | -------------------------------------------------------------------------------- /fixtures/interpolated_stop_times/routes.txt: -------------------------------------------------------------------------------- 1 | route_id,agency_id,route_short_name,route_long_name,route_desc,route_type,route_url,route_color,route_text_color 2 | 1,848,"100","100","",3,,000000,FFFFFF 3 | invalid_type,848,"100","100","",42,,000000,FFFFFF 4 | -------------------------------------------------------------------------------- /fixtures/interpolated_stop_times/shapes.txt: -------------------------------------------------------------------------------- 1 | shape_id,shape_pt_lat,shape_pt_lon,shape_pt_sequence,shape_dist_traveled 2 | A_shp,37.61956,-122.48161,0,0 3 | A_shp,37.64430,-122.41070,6,6.8310 4 | A_shp,37.65863,-122.30839,11,15.8765 -------------------------------------------------------------------------------- /fixtures/interpolated_stop_times/stop_times.txt: -------------------------------------------------------------------------------- 1 | trip_id,arrival_time,departure_time,stop_id,stop_sequence,stop_time_desc,pickup_type,drop_off_type 2 | trip1,14:00:00,14:00:00,stop2,0,"",0,1 3 | trip1,,,stop3,1,"",2, 4 | trip1,15:00:00,15:00:00,stop4,2,"",2, 5 | -------------------------------------------------------------------------------- /fixtures/interpolated_stop_times/stops.txt: -------------------------------------------------------------------------------- 1 | stop_id,stop_name,stop_desc,stop_lat,stop_lon,zone_id,stop_url,location_type,parent_station,wheelchair_boarding 2 | stop1,"Stop Area",, 48.796058 ,2.449386,,,1,, 3 | stop2,"StopPoint",,48.796058,2.449386,,,,, 4 | stop3,"Stop Point child of 1",,48.796058,2.449386,,,0,1, 5 | stop4,"StopPoint2",,48.796058,2.449386,,,,, 6 | stop5,"Stop Point child of 1 bis",,48.796058,2.449386,,,0,1, 7 | -------------------------------------------------------------------------------- /fixtures/interpolated_stop_times/trips.txt: -------------------------------------------------------------------------------- 1 | route_id,service_id,trip_id,trip_headsign,trip_short_name,direction_id,block_id,wheelchair_accessible,bikes_allowed,trip_desc,shape_id 2 | route1,service1,trip1,"85088452",,0,,0,0,, 3 | -------------------------------------------------------------------------------- /fixtures/missing_feed_date/agency.txt: -------------------------------------------------------------------------------- 1 | agency_name,agency_url,agency_timezone,agency_lang 2 | "BIBUS",http://www.bibus.fr,Europe/Paris,fr 3 | "Ter",http://www.sncf.com,Europe/Paris,fr 4 | -------------------------------------------------------------------------------- /fixtures/missing_feed_date/calendar.txt: -------------------------------------------------------------------------------- 1 | service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date 2 | service1,0,0,0,0,0,1,1,20170101,20170115 3 | -------------------------------------------------------------------------------- /fixtures/missing_feed_date/calendar_dates.txt: -------------------------------------------------------------------------------- 1 | service_id,date,exception_type 2 | service1,20170101,2 3 | service1,20170102,2 4 | service2,20170101,1 5 | -------------------------------------------------------------------------------- /fixtures/missing_feed_date/fare_attributes.txt: -------------------------------------------------------------------------------- 1 | fare_id,price,currency_type,payment_method,transfers,agency_id,transfer_duration 2 | "50","1.50","EUR","0","","1","3600" 3 | -------------------------------------------------------------------------------- /fixtures/missing_feed_date/feed_info.txt: -------------------------------------------------------------------------------- 1 | feed_publisher_name,feed_publisher_url,feed_lang,feed_version 2 | SNCF,http://www.sncf.com,fr,0.3 3 | -------------------------------------------------------------------------------- /fixtures/missing_feed_date/readme.md: -------------------------------------------------------------------------------- 1 | Same as the main dataset, but with no field `feed_start_date`, and `feed_end_date` in feed_info.txt (that should be optional). 2 | -------------------------------------------------------------------------------- /fixtures/missing_feed_date/routes.txt: -------------------------------------------------------------------------------- 1 | route_id,agency_id,route_short_name,route_long_name,route_desc,route_type,route_url,route_color,route_text_color 2 | 1,848,"100","100","",3,,000000,FFFFFF 3 | invalid_type,848,"100","100","",42,,000000,FFFFFF 4 | -------------------------------------------------------------------------------- /fixtures/missing_feed_date/shapes.txt: -------------------------------------------------------------------------------- 1 | shape_id,shape_pt_lat,shape_pt_lon,shape_pt_sequence,shape_dist_traveled 2 | A_shp,37.61956,-122.48161,0,0 3 | A_shp,37.64430,-122.41070,6,6.8310 4 | A_shp,37.65863,-122.30839,11,15.8765 -------------------------------------------------------------------------------- /fixtures/missing_feed_date/stop_times.txt: -------------------------------------------------------------------------------- 1 | trip_id,arrival_time,departure_time,stop_id,stop_sequence,stop_time_desc,pickup_type,drop_off_type 2 | trip1,14:00:00,14:00:00,stop2,0,"",0,1 3 | trip1,15:00:00,15:00:00,stop3,0,"",2, 4 | -------------------------------------------------------------------------------- /fixtures/missing_feed_date/stops.txt: -------------------------------------------------------------------------------- 1 | stop_id,stop_name,stop_desc,stop_lat,stop_lon,zone_id,stop_url,location_type,parent_station,wheelchair_boarding 2 | stop1,"Stop Area",, 48.796058 ,2.449386,,,1,, 3 | stop2,"StopPoint",,48.796058,2.449386,,,,, 4 | stop3,"Stop Point child of 1",,48.796058,2.449386,,,0,1, 5 | stop4,"StopPoint2",,48.796058,2.449386,,,,, 6 | stop5,"Stop Point child of 1 bis",,48.796058,2.449386,,,0,1, 7 | -------------------------------------------------------------------------------- /fixtures/missing_feed_date/trips.txt: -------------------------------------------------------------------------------- 1 | route_id,service_id,trip_id,trip_headsign,trip_short_name,direction_id,block_id,wheelchair_accessible,bikes_allowed,trip_desc,shape_id 2 | route1,service1,trip1,"85088452",,0,,0,0,, 3 | -------------------------------------------------------------------------------- /fixtures/only_required_fields/agency.txt: -------------------------------------------------------------------------------- 1 | agency_name,agency_url,agency_timezone 2 | "BIBUS",http://www.bibus.fr,Europe/Paris 3 | "Ter",http://www.sncf.com,Europe/Paris 4 | -------------------------------------------------------------------------------- /fixtures/only_required_fields/calendar.txt: -------------------------------------------------------------------------------- 1 | service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date 2 | service1,0,0,0,0,0,1,1,20170101,20170115 3 | -------------------------------------------------------------------------------- /fixtures/only_required_fields/calendar_dates.txt: -------------------------------------------------------------------------------- 1 | service_id,date,exception_type 2 | service1,20170101,2 3 | service1,20170102,2 4 | service2,20170101,1 5 | -------------------------------------------------------------------------------- /fixtures/only_required_fields/fare_attributes.txt: -------------------------------------------------------------------------------- 1 | fare_id,price,currency_type,payment_method,transfers,agency_id 2 | "50","1.50","EUR","0","","1" 3 | -------------------------------------------------------------------------------- /fixtures/only_required_fields/feed_info.txt: -------------------------------------------------------------------------------- 1 | feed_publisher_name,feed_publisher_url,feed_lang 2 | SNCF,http://www.sncf.com,fr 3 | -------------------------------------------------------------------------------- /fixtures/only_required_fields/routes.txt: -------------------------------------------------------------------------------- 1 | route_id,agency_id,route_short_name,route_long_name,route_type 2 | 1,848,"100","100",3 3 | -------------------------------------------------------------------------------- /fixtures/only_required_fields/shapes.txt: -------------------------------------------------------------------------------- 1 | shape_id,shape_pt_lat,shape_pt_lon,shape_pt_sequence 2 | A_shp,37.61956,-122.48161,0 3 | B_shp,37.64430,-122.41070,6 4 | C_shp,37.65863,-122.30839,11 -------------------------------------------------------------------------------- /fixtures/only_required_fields/stop_times.txt: -------------------------------------------------------------------------------- 1 | trip_id,arrival_time,departure_time,stop_id,stop_sequence,stop_time_desc 2 | trip1,14:00:00,14:00:00,stop2,0,"" 3 | -------------------------------------------------------------------------------- /fixtures/only_required_fields/stops.txt: -------------------------------------------------------------------------------- 1 | stop_id,stop_name,stop_lat,stop_lon 2 | stop1,"Stop Area", 48.796058 ,2.449386 3 | stop2,"StopPoint",48.796058,2.449386 4 | stop3,"Stop Point child of 1",48.796058,2.449386 5 | stop4,"StopPoint2",48.796058,2.449386,,, 6 | stop5,"Stop Point child of 1 bis",48.796058,2.449386 7 | stop6,"Generic node",,,3, 8 | -------------------------------------------------------------------------------- /fixtures/only_required_fields/trips.txt: -------------------------------------------------------------------------------- 1 | route_id,service_id,trip_id 2 | route1,service1,trip1 3 | -------------------------------------------------------------------------------- /fixtures/subdirectory/gtfs/agency.txt: -------------------------------------------------------------------------------- 1 | agency_name,agency_url,agency_timezone,agency_lang 2 | "BIBUS",http://www.bibus.fr,Europe/Paris,fr 3 | "Ter",http://www.sncf.com,Europe/Paris,fr -------------------------------------------------------------------------------- /fixtures/subdirectory/gtfs/calendar.txt: -------------------------------------------------------------------------------- 1 | service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date 2 | service1,0,0,0,0,0,1,1,20170101,20170115 3 | -------------------------------------------------------------------------------- /fixtures/subdirectory/gtfs/calendar_dates.txt: -------------------------------------------------------------------------------- 1 | service_id,date,exception_type 2 | service1,20170101,2 3 | service1,20170102,2 4 | service2,20170101,1 5 | -------------------------------------------------------------------------------- /fixtures/subdirectory/gtfs/fare_attributes.txt: -------------------------------------------------------------------------------- 1 | fare_id,price,currency_type,payment_method,transfers,agency_id,transfer_duration 2 | "50","1.50","EUR","0","","1","3600" 3 | -------------------------------------------------------------------------------- /fixtures/subdirectory/gtfs/feed_info.txt: -------------------------------------------------------------------------------- 1 | feed_publisher_name,feed_publisher_url,feed_lang,feed_start_date,feed_end_date,feed_version 2 | SNCF,http://www.sncf.com,fr,20180709,20180927,0.3 3 | -------------------------------------------------------------------------------- /fixtures/subdirectory/gtfs/frequencies.txt: -------------------------------------------------------------------------------- 1 | trip_id,start_time,end_time,headway_secs 2 | trip1,05:30:00,05:38:00,480 3 | -------------------------------------------------------------------------------- /fixtures/subdirectory/gtfs/pathways.txt: -------------------------------------------------------------------------------- 1 | pathway_id,from_stop_id,to_stop_id,pathway_mode,is_bidirectional,length,traversal_time,stair_count,max_slope,min_width,signposted_as,reversed_signposted_as 2 | pathway1,stop1,stop3,1,0,,,,,,, 3 | -------------------------------------------------------------------------------- /fixtures/subdirectory/gtfs/routes.txt: -------------------------------------------------------------------------------- 1 | route_id,agency_id,route_short_name,route_long_name,route_desc,route_type,route_url,route_color,route_text_color,route_sort_order 2 | 1,848,"100","100","",3,,000000,FFFFFF,1 3 | invalid_type,848,"100","100","",42,,000000,FFFFFF, 4 | default_colors,848,"default_colors","route with default colors","",3,,,,1 5 | -------------------------------------------------------------------------------- /fixtures/subdirectory/gtfs/shapes.txt: -------------------------------------------------------------------------------- 1 | shape_id,shape_pt_lat,shape_pt_lon,shape_pt_sequence,shape_dist_traveled 2 | A_shp,37.61956,-122.48161,0,0 3 | A_shp,37.64430,-122.41070,6,6.8310 4 | A_shp,37.65863,-122.30839,11,15.8765 5 | Unordered_shp,37.64430,-122.41070,6,6.8310 6 | Unordered_shp,37.61956,-122.48161,0,0 7 | Unordered_shp,37.65863,-122.30839,11,15.8765 -------------------------------------------------------------------------------- /fixtures/subdirectory/gtfs/stop_times.txt: -------------------------------------------------------------------------------- 1 | trip_id,arrival_time,departure_time,stop_id,stop_sequence,stop_time_desc,pickup_type,drop_off_type,timepoint 2 | trip1,14:00:00,14:00:00,stop2,0,"",0,1,"" 3 | trip1,15:00:00,15:00:00,stop3,1,"",2,,"0" 4 | trip1,16:00:00,16:00:00,stop4,2,"",2,-999, -------------------------------------------------------------------------------- /fixtures/subdirectory/gtfs/stops.txt: -------------------------------------------------------------------------------- 1 | stop_id,stop_name,stop_desc,stop_lat,stop_lon,zone_id,stop_url,location_type,parent_station,wheelchair_boarding 2 | stop1,"Stop Area",, 48.796058 ,2.449386,,,1,, 3 | stop2,"StopPoint",,48.796058,2.449386,,,,, 4 | stop3,"Stop Point child of 1",,48.796058,2.449386,,,0,1, 5 | stop4,"StopPoint2",,48.796058,2.449386,,,,, 6 | stop5,"Stop Point child of 1 bis",,48.796058,2.449386,,,0,1, 7 | stop6,"Generic node",,,,,,3,1, 8 | -------------------------------------------------------------------------------- /fixtures/subdirectory/gtfs/transfers.txt: -------------------------------------------------------------------------------- 1 | from_stop_id,to_stop_id,transfer_type,min_transfer_time 2 | stop3,stop5,0,60 3 | stop1,stop2,3, 4 | stop2,stop3,, 5 | stop5,stop2,4, 6 | stop5,stop3,5, -------------------------------------------------------------------------------- /fixtures/subdirectory/gtfs/trips.txt: -------------------------------------------------------------------------------- 1 | route_id,service_id,trip_id,trip_headsign,trip_short_name,direction_id,block_id,wheelchair_accessible,bikes_allowed,trip_desc,shape_id 2 | route1,service1,trip1,"85088452",,0,,0,0,, 3 | -------------------------------------------------------------------------------- /fixtures/zips/gtfs.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-transit/gtfs-structure/03cd946b99494df1a81cfae346ad2012d00b49b6/fixtures/zips/gtfs.zip -------------------------------------------------------------------------------- /fixtures/zips/gtfs_with_bom.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-transit/gtfs-structure/03cd946b99494df1a81cfae346ad2012d00b49b6/fixtures/zips/gtfs_with_bom.zip -------------------------------------------------------------------------------- /fixtures/zips/macosx.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-transit/gtfs-structure/03cd946b99494df1a81cfae346ad2012d00b49b6/fixtures/zips/macosx.zip -------------------------------------------------------------------------------- /fixtures/zips/metra.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-transit/gtfs-structure/03cd946b99494df1a81cfae346ad2012d00b49b6/fixtures/zips/metra.zip -------------------------------------------------------------------------------- /fixtures/zips/subdirectory.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-transit/gtfs-structure/03cd946b99494df1a81cfae346ad2012d00b49b6/fixtures/zips/subdirectory.zip -------------------------------------------------------------------------------- /src/enums.rs: -------------------------------------------------------------------------------- 1 | use serde::de::{Deserialize, Deserializer}; 2 | use serde::ser::{Serialize, Serializer}; 3 | 4 | /// All the objects type from the GTFS specification that this library reads 5 | #[derive(Debug, Serialize, Eq, PartialEq, Hash)] 6 | pub enum ObjectType { 7 | /// [Agency] 8 | Agency, 9 | /// [Stop] 10 | Stop, 11 | /// [Route] 12 | Route, 13 | /// [Trip] 14 | Trip, 15 | /// [Calendar] 16 | Calendar, 17 | /// [Shape] 18 | Shape, 19 | /// [FareAttribute] 20 | Fare, 21 | /// [Pathway] 22 | Pathway, 23 | } 24 | 25 | /// Describes the kind of [Stop]. See `location_type` 26 | #[derive(Derivative, Debug, Copy, Clone, PartialEq, Eq, Hash)] 27 | #[derivative(Default(bound = ""))] 28 | pub enum LocationType { 29 | /// Stop (or Platform). A location where passengers board or disembark from a transit vehicle. Is called a platform when defined within a parent_station 30 | #[derivative(Default)] 31 | StopPoint, 32 | /// Station. A physical structure or area that contains one or more platform 33 | StopArea, 34 | /// A location where passengers can enter or exit a station from the street. If an entrance/exit belongs to multiple stations, it can be linked by pathways to both, but the data provider must pick one of them as parent 35 | StationEntrance, 36 | /// A location within a station, not matching any other [Stop::location_type], which can be used to link together pathways define in pathways.txt. 37 | GenericNode, 38 | /// A specific location on a platform, where passengers can board and/or alight vehicles 39 | BoardingArea, 40 | /// An unknown value 41 | Unknown(i16), 42 | } 43 | 44 | fn serialize_i16_as_str(s: S, value: i16) -> Result { 45 | s.serialize_str(&value.to_string()) 46 | } 47 | impl<'de> Deserialize<'de> for LocationType { 48 | fn deserialize(deserializer: D) -> Result 49 | where 50 | D: Deserializer<'de>, 51 | { 52 | let s = <&str>::deserialize(deserializer)?; 53 | Ok(match s { 54 | "" | "0" => LocationType::StopPoint, 55 | "1" => LocationType::StopArea, 56 | "2" => LocationType::StationEntrance, 57 | "3" => LocationType::GenericNode, 58 | "4" => LocationType::BoardingArea, 59 | s => LocationType::Unknown(s.parse().map_err(|_| { 60 | serde::de::Error::custom(format!( 61 | "invalid value for LocationType, must be an integer: {s}" 62 | )) 63 | })?), 64 | }) 65 | } 66 | } 67 | 68 | impl Serialize for LocationType { 69 | fn serialize(&self, serializer: S) -> Result 70 | where 71 | S: Serializer, 72 | { 73 | // Note: for extended route type, we might loose the initial precise route type 74 | serialize_i16_as_str( 75 | serializer, 76 | match self { 77 | LocationType::StopPoint => 0, 78 | LocationType::StopArea => 1, 79 | LocationType::StationEntrance => 2, 80 | LocationType::GenericNode => 3, 81 | LocationType::BoardingArea => 4, 82 | LocationType::Unknown(i) => *i, 83 | }, 84 | ) 85 | } 86 | } 87 | 88 | /// Describes the kind of [Route]. See `route_type` 89 | /// 90 | /// -ome route types are extended GTFS ( 91 | #[derive(Debug, Derivative, Copy, Clone, PartialEq, Eq, Hash)] 92 | #[derivative(Default(bound = ""))] 93 | pub enum RouteType { 94 | /// Tram, Streetcar, Light rail. Any light rail or street level system within a metropolitan area 95 | Tramway, 96 | /// Tram, Streetcar, Light rail. Any light rail or street level system within a metropolitan area 97 | Subway, 98 | /// Used for intercity or long-distance travel 99 | Rail, 100 | /// Used for short- and long-distance bus routes 101 | #[derivative(Default)] 102 | Bus, 103 | /// Used for short- and long-distance boat service 104 | Ferry, 105 | /// Used for street-level rail cars where the cable runs beneath the vehicle, e.g., cable car in San Francisco 106 | CableCar, 107 | /// Aerial lift, suspended cable car (e.g., gondola lift, aerial tramway). Cable transport where cabins, cars, gondolas or open chairs are suspended by means of one or more cables 108 | Gondola, 109 | /// Any rail system designed for steep inclines 110 | Funicular, 111 | /// (extended) Used for intercity bus services 112 | Coach, 113 | /// (extended) Airplanes 114 | Air, 115 | /// (extended) Taxi, Cab 116 | Taxi, 117 | /// (extended) any other value 118 | Other(i16), 119 | } 120 | 121 | impl<'de> Deserialize<'de> for RouteType { 122 | fn deserialize(deserializer: D) -> Result 123 | where 124 | D: Deserializer<'de>, 125 | { 126 | let i = i16::deserialize(deserializer)?; 127 | 128 | let hundreds = i / 100; 129 | Ok(match (i, hundreds) { 130 | (0, _) | (_, 9) => RouteType::Tramway, 131 | (1, _) | (_, 4) => RouteType::Subway, 132 | (2, _) | (_, 1) => RouteType::Rail, 133 | (3, _) | (_, 7) | (_, 8) => RouteType::Bus, 134 | (4, _) | (_, 10) | (_, 12) => RouteType::Ferry, 135 | (5, _) => RouteType::CableCar, 136 | (6, _) | (_, 13) => RouteType::Gondola, 137 | (7, _) | (_, 14) => RouteType::Funicular, 138 | (_, 2) => RouteType::Coach, 139 | (_, 11) => RouteType::Air, 140 | (_, 15) => RouteType::Taxi, 141 | _ => RouteType::Other(i), 142 | }) 143 | } 144 | } 145 | 146 | impl Serialize for RouteType { 147 | fn serialize(&self, serializer: S) -> Result 148 | where 149 | S: Serializer, 150 | { 151 | // Note: for extended route type, we might loose the initial precise route type 152 | serializer.serialize_i16(match self { 153 | RouteType::Tramway => 0, 154 | RouteType::Subway => 1, 155 | RouteType::Rail => 2, 156 | RouteType::Bus => 3, 157 | RouteType::Ferry => 4, 158 | RouteType::CableCar => 5, 159 | RouteType::Gondola => 6, 160 | RouteType::Funicular => 7, 161 | RouteType::Coach => 200, 162 | RouteType::Air => 1100, 163 | RouteType::Taxi => 1500, 164 | RouteType::Other(i) => *i, 165 | }) 166 | } 167 | } 168 | 169 | /// Describes if and how a traveller can board or alight the vehicle. See `pickup_type` and `dropoff_type` 170 | #[derive(Debug, Derivative, Copy, Clone, PartialEq, Eq, Hash)] 171 | #[derivative(Default(bound = ""))] 172 | pub enum PickupDropOffType { 173 | /// Regularly scheduled pickup or drop off (default when empty). 174 | #[derivative(Default)] 175 | Regular, 176 | /// No pickup or drop off available. 177 | NotAvailable, 178 | /// Must phone agency to arrange pickup or drop off. 179 | ArrangeByPhone, 180 | /// Must coordinate with driver to arrange pickup or drop off. 181 | CoordinateWithDriver, 182 | /// An unknown value not in the specification 183 | Unknown(i16), 184 | } 185 | 186 | impl<'de> Deserialize<'de> for PickupDropOffType { 187 | fn deserialize(deserializer: D) -> Result 188 | where 189 | D: Deserializer<'de>, 190 | { 191 | let s = <&str>::deserialize(deserializer)?; 192 | Ok(match s { 193 | "" | "0" => PickupDropOffType::Regular, 194 | "1" => PickupDropOffType::NotAvailable, 195 | "2" => PickupDropOffType::ArrangeByPhone, 196 | "3" => PickupDropOffType::CoordinateWithDriver, 197 | s => PickupDropOffType::Unknown(s.parse().map_err(|_| { 198 | serde::de::Error::custom(format!( 199 | "invalid value for PickupDropOffType, must be an integer: {s}" 200 | )) 201 | })?), 202 | }) 203 | } 204 | } 205 | 206 | impl Serialize for PickupDropOffType { 207 | fn serialize(&self, serializer: S) -> Result 208 | where 209 | S: Serializer, 210 | { 211 | // Note: for extended route type, we might loose the initial precise route type 212 | serialize_i16_as_str( 213 | serializer, 214 | match self { 215 | PickupDropOffType::Regular => 0, 216 | PickupDropOffType::NotAvailable => 1, 217 | PickupDropOffType::ArrangeByPhone => 2, 218 | PickupDropOffType::CoordinateWithDriver => 3, 219 | PickupDropOffType::Unknown(i) => *i, 220 | }, 221 | ) 222 | } 223 | } 224 | 225 | /// Indicates whether a rider can board the transit vehicle anywhere along the vehicle’s travel path 226 | /// 227 | /// Those values are only defined on not on 228 | #[derive(Debug, Derivative, Copy, Clone, PartialEq, Eq, Hash)] 229 | #[derivative(Default(bound = ""))] 230 | pub enum ContinuousPickupDropOff { 231 | /// Continuous stopping pickup or drop off. 232 | Continuous, 233 | /// No continuous stopping pickup or drop off (default when empty). 234 | #[derivative(Default)] 235 | NotAvailable, 236 | /// Must phone agency to arrange continuous stopping pickup or drop off. 237 | ArrangeByPhone, 238 | /// Must coordinate with driver to arrange continuous stopping pickup or drop off. 239 | CoordinateWithDriver, 240 | /// An unknown value not in the specification 241 | Unknown(i16), 242 | } 243 | 244 | impl Serialize for ContinuousPickupDropOff { 245 | fn serialize(&self, serializer: S) -> Result 246 | where 247 | S: Serializer, 248 | { 249 | // Note: for extended route type, we might loose the initial precise route type 250 | serialize_i16_as_str( 251 | serializer, 252 | match self { 253 | ContinuousPickupDropOff::Continuous => 0, 254 | ContinuousPickupDropOff::NotAvailable => 1, 255 | ContinuousPickupDropOff::ArrangeByPhone => 2, 256 | ContinuousPickupDropOff::CoordinateWithDriver => 3, 257 | ContinuousPickupDropOff::Unknown(i) => *i, 258 | }, 259 | ) 260 | } 261 | } 262 | 263 | impl<'de> Deserialize<'de> for ContinuousPickupDropOff { 264 | fn deserialize(deserializer: D) -> Result 265 | where 266 | D: Deserializer<'de>, 267 | { 268 | let s = <&str>::deserialize(deserializer)?; 269 | Ok(match s { 270 | "0" => ContinuousPickupDropOff::Continuous, 271 | "" | "1" => ContinuousPickupDropOff::NotAvailable, 272 | "2" => ContinuousPickupDropOff::ArrangeByPhone, 273 | "3" => ContinuousPickupDropOff::CoordinateWithDriver, 274 | s => ContinuousPickupDropOff::Unknown(s.parse().map_err(|_| { 275 | serde::de::Error::custom(format!( 276 | "invalid value for ContinuousPickupDropOff, must be an integer: {s}" 277 | )) 278 | })?), 279 | }) 280 | } 281 | } 282 | 283 | /// Describes if the stop time is exact or not. See `timepoint` 284 | #[derive(Debug, Derivative, Serialize, Copy, Clone, PartialEq, Eq, Hash)] 285 | #[derivative(Default)] 286 | pub enum TimepointType { 287 | /// Times are considered approximate 288 | #[serde(rename = "0")] 289 | Approximate = 0, 290 | /// Times are considered exact 291 | #[derivative(Default)] 292 | #[serde(rename = "1")] 293 | Exact = 1, 294 | } 295 | 296 | impl<'de> Deserialize<'de> for TimepointType { 297 | fn deserialize(deserializer: D) -> Result 298 | where 299 | D: Deserializer<'de>, 300 | { 301 | let s = <&str>::deserialize(deserializer)?; 302 | match s { 303 | "" | "1" => Ok(Self::Exact), 304 | "0" => Ok(Self::Approximate), 305 | v => Err(serde::de::Error::custom(format!( 306 | "invalid value for timepoint: {v}" 307 | ))), 308 | } 309 | } 310 | } 311 | 312 | /// Generic enum to define if a service (like wheelchair boarding) is available 313 | #[derive(Debug, Derivative, PartialEq, Eq, Hash, Clone, Copy)] 314 | #[derivative(Default)] 315 | pub enum Availability { 316 | /// No information if the service is available 317 | #[derivative(Default)] 318 | InformationNotAvailable, 319 | /// The service is available 320 | Available, 321 | /// The service is not available 322 | NotAvailable, 323 | /// An unknown value not in the specification 324 | Unknown(i16), 325 | } 326 | 327 | impl<'de> Deserialize<'de> for Availability { 328 | fn deserialize(deserializer: D) -> Result 329 | where 330 | D: Deserializer<'de>, 331 | { 332 | let s = <&str>::deserialize(deserializer)?; 333 | Ok(match s { 334 | "" | "0" => Availability::InformationNotAvailable, 335 | "1" => Availability::Available, 336 | "2" => Availability::NotAvailable, 337 | s => Availability::Unknown(s.parse().map_err(|_| { 338 | serde::de::Error::custom(format!( 339 | "invalid value for Availability, must be an integer: {s}" 340 | )) 341 | })?), 342 | }) 343 | } 344 | } 345 | 346 | impl Serialize for Availability { 347 | fn serialize(&self, serializer: S) -> Result 348 | where 349 | S: Serializer, 350 | { 351 | // Note: for extended route type, we might loose the initial precise route type 352 | serialize_i16_as_str( 353 | serializer, 354 | match self { 355 | Availability::InformationNotAvailable => 0, 356 | Availability::Available => 1, 357 | Availability::NotAvailable => 2, 358 | Availability::Unknown(i) => *i, 359 | }, 360 | ) 361 | } 362 | } 363 | 364 | /// Defines if a [CalendarDate] is added or deleted from a [Calendar] 365 | #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash, Clone, Copy)] 366 | pub enum Exception { 367 | /// There will be a service on that day 368 | #[serde(rename = "1")] 369 | Added, 370 | /// There won’t be a service on that day 371 | #[serde(rename = "2")] 372 | Deleted, 373 | } 374 | 375 | /// Defines the direction of a [Trip], only for display, not for routing. See `direction_id` 376 | #[derive(Debug, Deserialize, Serialize, Copy, Clone, PartialEq, Eq, Hash)] 377 | pub enum DirectionType { 378 | /// Travel in one direction (e.g. outbound travel). 379 | #[serde(rename = "0")] 380 | Outbound, 381 | /// Travel in the opposite direction (e.g. inbound travel). 382 | #[serde(rename = "1")] 383 | Inbound, 384 | } 385 | 386 | /// Is the [Trip] accessible with a bike. See `bikes_allowed` 387 | #[derive(Debug, Derivative, Copy, Clone, PartialEq, Eq, Hash)] 388 | #[derivative(Default())] 389 | pub enum BikesAllowedType { 390 | /// No bike information for the trip 391 | #[derivative(Default)] 392 | NoBikeInfo, 393 | /// Vehicle being used on this particular trip can accommodate at least one bicycle 394 | AtLeastOneBike, 395 | /// No bicycles are allowed on this trip 396 | NoBikesAllowed, 397 | /// An unknown value not in the specification 398 | Unknown(i16), 399 | } 400 | 401 | impl<'de> Deserialize<'de> for BikesAllowedType { 402 | fn deserialize(deserializer: D) -> Result 403 | where 404 | D: Deserializer<'de>, 405 | { 406 | let s = <&str>::deserialize(deserializer)?; 407 | Ok(match s { 408 | "" | "0" => BikesAllowedType::NoBikeInfo, 409 | "1" => BikesAllowedType::AtLeastOneBike, 410 | "2" => BikesAllowedType::NoBikesAllowed, 411 | s => BikesAllowedType::Unknown(s.parse().map_err(|_| { 412 | serde::de::Error::custom(format!( 413 | "invalid value for BikeAllowedType, must be an integer: {s}" 414 | )) 415 | })?), 416 | }) 417 | } 418 | } 419 | 420 | impl Serialize for BikesAllowedType { 421 | fn serialize(&self, serializer: S) -> Result 422 | where 423 | S: Serializer, 424 | { 425 | // Note: for extended route type, we might loose the initial precise route type 426 | serialize_i16_as_str( 427 | serializer, 428 | match self { 429 | BikesAllowedType::NoBikeInfo => 0, 430 | BikesAllowedType::AtLeastOneBike => 1, 431 | BikesAllowedType::NoBikesAllowed => 2, 432 | BikesAllowedType::Unknown(i) => *i, 433 | }, 434 | ) 435 | } 436 | } 437 | 438 | /// Defines where a [FareAttribute] can be paid 439 | #[derive(Debug, Deserialize, Serialize, Copy, Clone, PartialEq, Eq)] 440 | pub enum PaymentMethod { 441 | /// Fare is paid on board 442 | #[serde(rename = "0")] 443 | Aboard, 444 | /// Fare must be paid before boarding 445 | #[serde(rename = "1")] 446 | PreBoarding, 447 | } 448 | 449 | /// Defines if the [Frequency] is exact (the vehicle runs exactly every n minutes) or not 450 | #[derive(Debug, Serialize, Copy, Clone, PartialEq, Eq, Hash)] 451 | pub enum ExactTimes { 452 | /// Frequency-based trips 453 | FrequencyBased = 0, 454 | /// Schedule-based trips with the exact same headway throughout the day. 455 | ScheduleBased = 1, 456 | } 457 | 458 | impl<'de> Deserialize<'de> for ExactTimes { 459 | fn deserialize(deserializer: D) -> Result 460 | where 461 | D: Deserializer<'de>, 462 | { 463 | let s = <&str>::deserialize(deserializer)?; 464 | Ok(match s { 465 | "" | "0" => ExactTimes::FrequencyBased, 466 | "1" => ExactTimes::ScheduleBased, 467 | &_ => { 468 | return Err(serde::de::Error::custom(format!( 469 | "Invalid value `{s}`, expected 0 or 1" 470 | ))) 471 | } 472 | }) 473 | } 474 | } 475 | 476 | /// Defines how many transfers can be done with on [FareAttribute] 477 | #[derive(Debug, Derivative, Copy, Clone, PartialEq, Eq, Hash)] 478 | #[derivative(Default(bound = ""))] 479 | pub enum Transfers { 480 | /// Unlimited transfers are permitted 481 | #[derivative(Default)] 482 | Unlimited, 483 | /// No transfers permitted on this fare 484 | NoTransfer, 485 | /// Riders may transfer once 486 | UniqueTransfer, 487 | ///Riders may transfer twice 488 | TwoTransfers, 489 | /// Other transfer values 490 | Other(i16), 491 | } 492 | 493 | impl<'de> Deserialize<'de> for Transfers { 494 | fn deserialize(deserializer: D) -> Result 495 | where 496 | D: Deserializer<'de>, 497 | { 498 | let i = Option::::deserialize(deserializer)?; 499 | Ok(match i { 500 | Some(0) => Transfers::NoTransfer, 501 | Some(1) => Transfers::UniqueTransfer, 502 | Some(2) => Transfers::TwoTransfers, 503 | Some(a) => Transfers::Other(a), 504 | None => Transfers::default(), 505 | }) 506 | } 507 | } 508 | 509 | impl Serialize for Transfers { 510 | fn serialize(&self, serializer: S) -> Result 511 | where 512 | S: Serializer, 513 | { 514 | match self { 515 | Transfers::NoTransfer => serialize_i16_as_str(serializer, 0), 516 | Transfers::UniqueTransfer => serialize_i16_as_str(serializer, 1), 517 | Transfers::TwoTransfers => serialize_i16_as_str(serializer, 2), 518 | Transfers::Other(a) => serialize_i16_as_str(serializer, *a), 519 | Transfers::Unlimited => serializer.serialize_none(), 520 | } 521 | } 522 | } 523 | /// Defines the type of a [StopTransfer] 524 | #[derive(Debug, Serialize, Derivative, Copy, Clone, PartialEq, Eq, Hash)] 525 | #[derivative(Default)] 526 | pub enum TransferType { 527 | /// Recommended transfer point between routes 528 | #[serde(rename = "0")] 529 | #[derivative(Default)] 530 | Recommended, 531 | /// Departing vehicle waits for arriving one 532 | #[serde(rename = "1")] 533 | Timed, 534 | /// Transfer requires a minimum amount of time between arrival and departure to ensure a connection. 535 | #[serde(rename = "2")] 536 | MinTime, 537 | /// Transfer is not possible at this location 538 | #[serde(rename = "3")] 539 | Impossible, 540 | /// Passengers can stay onboard the same vehicle to transfer from one trip to another 541 | #[serde(rename = "4")] 542 | StayOnBoard, 543 | /// In-seat transfers aren't allowed between sequential trips. 544 | /// The passenger must alight from the vehicle and re-board. 545 | #[serde(rename = "5")] 546 | MustAlight, 547 | } 548 | 549 | impl<'de> Deserialize<'de> for TransferType { 550 | fn deserialize(deserializer: D) -> Result 551 | where 552 | D: Deserializer<'de>, 553 | { 554 | let s = <&str>::deserialize(deserializer)?; 555 | Ok(match s { 556 | "" | "0" => TransferType::Recommended, 557 | "1" => TransferType::Timed, 558 | "2" => TransferType::MinTime, 559 | "3" => TransferType::Impossible, 560 | "4" => TransferType::StayOnBoard, 561 | "5" => TransferType::MustAlight, 562 | s => { 563 | return Err(serde::de::Error::custom(format!( 564 | "Invalid value `{s}`, expected 0, 1, 2, 3, 4, 5" 565 | ))) 566 | } 567 | }) 568 | } 569 | } 570 | 571 | /// Type of pathway between [from_stop] and [to_stop] 572 | #[derive(Debug, Serialize, Deserialize, Derivative, Copy, Clone, PartialEq, Eq, Hash)] 573 | #[derivative(Default)] 574 | pub enum PathwayMode { 575 | /// A walkway 576 | #[serde(rename = "1")] 577 | #[derivative(Default)] 578 | Walkway, 579 | /// Stairs 580 | #[serde(rename = "2")] 581 | Stairs, 582 | /// Moving sidewalk / travelator 583 | #[serde(rename = "3")] 584 | MovingSidewalk, 585 | /// Escalator 586 | #[serde(rename = "4")] 587 | Escalator, 588 | /// Elevator 589 | #[serde(rename = "5")] 590 | Elevator, 591 | /// A pathway that crosses into an area of the station where a 592 | /// proof of payment is required (usually via a physical payment gate) 593 | #[serde(rename = "6")] 594 | FareGate, 595 | /// Indicates a pathway exiting an area where proof-of-payment is required 596 | /// into an area where proof-of-payment is no longer required. 597 | #[serde(rename = "7")] 598 | ExitGate, 599 | } 600 | 601 | /// Indicates in which direction the pathway can be used 602 | #[derive(Debug, Serialize, Deserialize, Derivative, Copy, Clone, PartialEq, Eq, Hash)] 603 | #[derivative(Default)] 604 | pub enum PathwayDirectionType { 605 | /// Unidirectional pathway, it can only be used from [from_stop_id] to [to_stop_id]. 606 | #[serde(rename = "0")] 607 | #[derivative(Default)] 608 | Unidirectional, 609 | /// Bidirectional pathway, it can be used in the two directions. 610 | #[serde(rename = "1")] 611 | Bidirectional, 612 | } 613 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! Module for the error management 2 | use thiserror::Error; 3 | 4 | /// Specific line from a CSV file that could not be read 5 | #[derive(Debug)] 6 | pub struct LineError { 7 | /// Headers of the CSV file 8 | pub headers: Vec, 9 | /// Values of the line that could not be parsed 10 | pub values: Vec, 11 | } 12 | 13 | /// An error that can occur when processing GTFS data. 14 | #[derive(Error, Debug)] 15 | pub enum Error { 16 | /// A mandatory file is not present in the archive 17 | #[error("Cound not find file {0}")] 18 | MissingFile(String), 19 | /// A file references an Id that is not present 20 | #[error("The id {0} is not known")] 21 | ReferenceError(String), 22 | /// The given path to the GTFS is neither a file nor a directory 23 | #[error("Could not read GTFS: {0} is neither a file nor a directory")] 24 | NotFileNorDirectory(String), 25 | /// The time is not given in the HH:MM:SS format 26 | #[error("'{0}' is not a valid time; HH:MM:SS format is expected.")] 27 | InvalidTime(String), 28 | /// The color is not given in the RRGGBB format, without a leading `#` 29 | #[error("'{0}' is not a valid color; RRGGBB format is expected, without a leading `#`")] 30 | InvalidColor(String), 31 | /// Generic Input/Output error while reading a file 32 | #[error("impossible to read file")] 33 | IO(#[from] std::io::Error), 34 | /// Impossible to read a file 35 | #[error("impossible to read '{file_name}'")] 36 | NamedFileIO { 37 | /// The file name that could not be read 38 | file_name: String, 39 | /// The inital error that caused the unability to read the file 40 | #[source] 41 | source: Box, 42 | }, 43 | /// Impossible to fetch the remote archive by the URL 44 | #[cfg(feature = "read-url")] 45 | #[error("impossible to remotely access file")] 46 | Fetch(#[from] reqwest::Error), 47 | /// Impossible to read a CSV file 48 | #[error("impossible to read csv file '{file_name}'")] 49 | CSVError { 50 | /// File name that could not be parsed as CSV 51 | file_name: String, 52 | /// The initial error by the csv library 53 | #[source] 54 | source: csv::Error, 55 | /// The line that could not be parsed by the csv library 56 | line_in_error: Option, 57 | }, 58 | /// Error when trying to unzip the GTFS archive 59 | #[error(transparent)] 60 | Zip(#[from] zip::result::ZipError), 61 | } 62 | -------------------------------------------------------------------------------- /src/gtfs.rs: -------------------------------------------------------------------------------- 1 | use crate::{objects::*, Error, RawGtfs}; 2 | use chrono::prelude::NaiveDate; 3 | use std::collections::{HashMap, HashSet}; 4 | use std::convert::TryFrom; 5 | use std::sync::Arc; 6 | use std::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)] 23 | pub 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, 28 | /// All calendar dates grouped by service_id 29 | pub calendar_dates: HashMap>, 30 | /// All stop by `stop_id`. Stops are in an [Arc] because they are also referenced by each [StopTime] 31 | pub stops: HashMap>, 32 | /// All routes by `route_id` 33 | pub routes: HashMap, 34 | /// All trips by `trip_id` 35 | pub trips: HashMap, 36 | /// All agencies. They can not be read by `agency_id`, as it is not a required field 37 | pub agencies: Vec, 38 | /// All shapes by shape_id 39 | pub shapes: HashMap>, 40 | /// All fare attributes by `fare_id` 41 | pub fare_attributes: HashMap, 42 | /// All fare rules by `fare_id` 43 | pub fare_rules: HashMap>, 44 | /// All feed information. There is no identifier 45 | pub feed_info: Vec, 46 | } 47 | 48 | impl TryFrom for Gtfs { 49 | type Error = Error; 50 | /// Tries to build a [Gtfs] from a [RawGtfs] 51 | /// 52 | /// It might fail if some mandatory files couldn’t be read or if there are references to other objects that are invalid. 53 | fn try_from(raw: RawGtfs) -> Result { 54 | let start = Instant::now(); 55 | 56 | let stops = to_stop_map( 57 | raw.stops?, 58 | raw.transfers.unwrap_or_else(|| Ok(Vec::new()))?, 59 | raw.pathways.unwrap_or(Ok(Vec::new()))?, 60 | )?; 61 | let frequencies = raw.frequencies.unwrap_or_else(|| Ok(Vec::new()))?; 62 | let trips = create_trips(raw.trips?, raw.stop_times?, frequencies, &stops)?; 63 | 64 | let mut fare_rules = HashMap::>::new(); 65 | for f in raw.fare_rules.unwrap_or_else(|| Ok(Vec::new()))? { 66 | (*fare_rules.entry(f.fare_id.clone()).or_default()).push(f); 67 | } 68 | 69 | Ok(Gtfs { 70 | stops, 71 | routes: to_map(raw.routes?), 72 | trips, 73 | agencies: raw.agencies?, 74 | shapes: to_shape_map(raw.shapes.unwrap_or_else(|| Ok(Vec::new()))?), 75 | fare_attributes: to_map(raw.fare_attributes.unwrap_or_else(|| Ok(Vec::new()))?), 76 | fare_rules, 77 | feed_info: raw.feed_info.unwrap_or_else(|| Ok(Vec::new()))?, 78 | calendar: to_map(raw.calendar.unwrap_or_else(|| Ok(Vec::new()))?), 79 | calendar_dates: to_calendar_dates( 80 | raw.calendar_dates.unwrap_or_else(|| Ok(Vec::new()))?, 81 | ), 82 | read_duration: raw.read_duration + start.elapsed(), 83 | }) 84 | } 85 | } 86 | 87 | impl Gtfs { 88 | /// Prints on stdout some basic statistics about the GTFS file (numbers of elements for each object). Mostly to be sure that everything was read 89 | pub fn print_stats(&self) { 90 | println!("GTFS data:"); 91 | println!(" Read in {:?}", self.read_duration); 92 | println!(" Stops: {}", self.stops.len()); 93 | println!(" Routes: {}", self.routes.len()); 94 | println!(" Trips: {}", self.trips.len()); 95 | println!(" Agencies: {}", self.agencies.len()); 96 | println!(" Shapes: {}", self.shapes.len()); 97 | println!(" Fare attributes: {}", self.fare_attributes.len()); 98 | println!(" Feed info: {}", self.feed_info.len()); 99 | } 100 | 101 | /// Reads from an url (if starts with `"http"`), or a local path (either a directory or zipped file) 102 | /// 103 | /// To read from an url, build with read-url feature 104 | /// See also [Gtfs::from_url] and [Gtfs::from_path] if you don’t want the library to guess 105 | pub fn new(gtfs: &str) -> Result { 106 | RawGtfs::new(gtfs).and_then(Gtfs::try_from) 107 | } 108 | 109 | /// Reads the GTFS from a local zip archive or local directory 110 | pub fn from_path

(path: P) -> Result 111 | where 112 | P: AsRef, 113 | { 114 | RawGtfs::from_path(path).and_then(Gtfs::try_from) 115 | } 116 | 117 | /// Reads the GTFS from a remote url 118 | /// 119 | /// The library must be built with the read-url feature 120 | #[cfg(feature = "read-url")] 121 | pub fn from_url(url: U) -> Result { 122 | RawGtfs::from_url(url).and_then(Gtfs::try_from) 123 | } 124 | 125 | /// Asynchronously reads the GTFS from a remote url 126 | /// 127 | /// The library must be built with the read-url feature 128 | #[cfg(feature = "read-url")] 129 | pub async fn from_url_async(url: U) -> Result { 130 | RawGtfs::from_url_async(url).await.and_then(Gtfs::try_from) 131 | } 132 | 133 | /// Reads for any object implementing [std::io::Read] and [std::io::Seek] 134 | /// 135 | /// Mostly an internal function that abstracts reading from an url or local file 136 | pub fn from_reader(reader: T) -> Result { 137 | RawGtfs::from_reader(reader).and_then(Gtfs::try_from) 138 | } 139 | 140 | /// For a given a `service_id` and a starting date returns all the following day offset the vehicle runs 141 | /// 142 | /// For instance if the `start_date` is 2021-12-20, `[0, 4]` means that the vehicle will run the 20th and 24th 143 | /// 144 | /// It will consider use both [Calendar] and [CalendarDate] (both added and removed) 145 | pub fn trip_days(&self, service_id: &str, start_date: NaiveDate) -> Vec { 146 | let mut result = Vec::new(); 147 | 148 | // Handle services given by specific days and exceptions 149 | let mut removed_days = HashSet::new(); 150 | for extra_day in self 151 | .calendar_dates 152 | .get(service_id) 153 | .iter() 154 | .flat_map(|e| e.iter()) 155 | { 156 | let offset = extra_day.date.signed_duration_since(start_date).num_days(); 157 | if offset >= 0 { 158 | if extra_day.exception_type == Exception::Added { 159 | result.push(offset as u16); 160 | } else if extra_day.exception_type == Exception::Deleted { 161 | removed_days.insert(offset); 162 | } 163 | } 164 | } 165 | 166 | if let Some(calendar) = self.calendar.get(service_id) { 167 | let total_days = calendar 168 | .end_date 169 | .signed_duration_since(start_date) 170 | .num_days(); 171 | for days_offset in 0..=total_days { 172 | if let Some(days_offset_timedelta) = chrono::TimeDelta::try_days(days_offset) { 173 | let current_date = start_date + days_offset_timedelta; 174 | 175 | if calendar.start_date <= current_date 176 | && calendar.end_date >= current_date 177 | && calendar.valid_weekday(current_date) 178 | && !removed_days.contains(&days_offset) 179 | { 180 | result.push(days_offset as u16); 181 | } 182 | } 183 | } 184 | } 185 | 186 | result 187 | } 188 | 189 | /// Gets a [Stop] by its `stop_id` 190 | pub fn get_stop<'a>(&'a self, id: &str) -> Result<&'a Stop, Error> { 191 | match self.stops.get(id) { 192 | Some(stop) => Ok(stop), 193 | None => Err(Error::ReferenceError(id.to_owned())), 194 | } 195 | } 196 | 197 | /// Gets a [Trip] by its `trip_id` 198 | pub fn get_trip<'a>(&'a self, id: &str) -> Result<&'a Trip, Error> { 199 | self.trips 200 | .get(id) 201 | .ok_or_else(|| Error::ReferenceError(id.to_owned())) 202 | } 203 | 204 | /// Gets a [Route] by its `route_id` 205 | pub fn get_route<'a>(&'a self, id: &str) -> Result<&'a Route, Error> { 206 | self.routes 207 | .get(id) 208 | .ok_or_else(|| Error::ReferenceError(id.to_owned())) 209 | } 210 | 211 | /// Gets a [Calendar] by its `service_id` 212 | pub fn get_calendar<'a>(&'a self, id: &str) -> Result<&'a Calendar, Error> { 213 | self.calendar 214 | .get(id) 215 | .ok_or_else(|| Error::ReferenceError(id.to_owned())) 216 | } 217 | 218 | /// Gets all [CalendarDate] of a `service_id` 219 | pub fn get_calendar_date<'a>(&'a self, id: &str) -> Result<&'a Vec, Error> { 220 | self.calendar_dates 221 | .get(id) 222 | .ok_or_else(|| Error::ReferenceError(id.to_owned())) 223 | } 224 | 225 | /// Gets all [Shape] points of a `shape_id` 226 | pub fn get_shape<'a>(&'a self, id: &str) -> Result<&'a Vec, Error> { 227 | self.shapes 228 | .get(id) 229 | .ok_or_else(|| Error::ReferenceError(id.to_owned())) 230 | } 231 | 232 | /// Gets a [FareAttribute] by its `fare_id` 233 | pub fn get_fare_attributes<'a>(&'a self, id: &str) -> Result<&'a FareAttribute, Error> { 234 | self.fare_attributes 235 | .get(id) 236 | .ok_or_else(|| Error::ReferenceError(id.to_owned())) 237 | } 238 | } 239 | 240 | fn to_map(elements: impl IntoIterator) -> HashMap { 241 | elements 242 | .into_iter() 243 | .map(|e| (e.id().to_owned(), e)) 244 | .collect() 245 | } 246 | 247 | fn to_stop_map( 248 | stops: Vec, 249 | raw_transfers: Vec, 250 | raw_pathways: Vec, 251 | ) -> Result>, Error> { 252 | let mut stop_map: HashMap = 253 | stops.into_iter().map(|s| (s.id.clone(), s)).collect(); 254 | 255 | for transfer in raw_transfers { 256 | stop_map.get(&transfer.to_stop_id).ok_or_else(|| { 257 | let stop_id = &transfer.to_stop_id; 258 | Error::ReferenceError(format!("'{stop_id}' in transfers.txt")) 259 | })?; 260 | stop_map 261 | .entry(transfer.from_stop_id.clone()) 262 | .and_modify(|stop| stop.transfers.push(StopTransfer::from(transfer))); 263 | } 264 | 265 | for pathway in raw_pathways { 266 | stop_map.get(&pathway.to_stop_id).ok_or_else(|| { 267 | let stop_id = &pathway.to_stop_id; 268 | Error::ReferenceError(format!("'{stop_id}' in pathways.txt")) 269 | })?; 270 | stop_map 271 | .entry(pathway.from_stop_id.clone()) 272 | .and_modify(|stop| stop.pathways.push(Pathway::from(pathway))); 273 | } 274 | 275 | let res = stop_map 276 | .into_iter() 277 | .map(|(i, s)| (i, Arc::new(s))) 278 | .collect(); 279 | Ok(res) 280 | } 281 | 282 | fn to_shape_map(shapes: Vec) -> HashMap> { 283 | let mut res = HashMap::default(); 284 | for s in shapes { 285 | let shape = res.entry(s.id.to_owned()).or_insert_with(Vec::new); 286 | shape.push(s); 287 | } 288 | // we sort the shape by it's pt_sequence 289 | for shapes in res.values_mut() { 290 | shapes.sort_by_key(|s| s.sequence); 291 | } 292 | 293 | res 294 | } 295 | 296 | fn to_calendar_dates(cd: Vec) -> HashMap> { 297 | let mut res = HashMap::default(); 298 | for c in cd { 299 | let cal = res.entry(c.service_id.to_owned()).or_insert_with(Vec::new); 300 | cal.push(c); 301 | } 302 | res 303 | } 304 | 305 | // Number of stoptimes to `pop` from the list before using shrink_to_fit to reduce the memory footprint 306 | // 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 307 | const NB_STOP_TIMES_BEFORE_SHRINK: usize = 1_000_000; 308 | 309 | fn create_trips( 310 | raw_trips: Vec, 311 | mut raw_stop_times: Vec, 312 | raw_frequencies: Vec, 313 | stops: &HashMap>, 314 | ) -> Result, Error> { 315 | let mut trips = to_map(raw_trips.into_iter().map(|rt| Trip { 316 | id: rt.id, 317 | service_id: rt.service_id, 318 | route_id: rt.route_id, 319 | stop_times: vec![], 320 | shape_id: rt.shape_id, 321 | trip_headsign: rt.trip_headsign, 322 | trip_short_name: rt.trip_short_name, 323 | direction_id: rt.direction_id, 324 | block_id: rt.block_id, 325 | wheelchair_accessible: rt.wheelchair_accessible, 326 | bikes_allowed: rt.bikes_allowed, 327 | frequencies: vec![], 328 | })); 329 | 330 | let mut st_idx = 0; 331 | while let Some(s) = raw_stop_times.pop() { 332 | st_idx += 1; 333 | let trip = &mut trips 334 | .get_mut(&s.trip_id) 335 | .ok_or_else(|| Error::ReferenceError(s.trip_id.to_string()))?; 336 | let stop = stops 337 | .get(&s.stop_id) 338 | .ok_or_else(|| Error::ReferenceError(s.stop_id.to_string()))?; 339 | trip.stop_times.push(StopTime::from(s, Arc::clone(stop))); 340 | if st_idx % NB_STOP_TIMES_BEFORE_SHRINK == 0 { 341 | raw_stop_times.shrink_to_fit(); 342 | } 343 | } 344 | 345 | for trip in &mut trips.values_mut() { 346 | trip.stop_times 347 | .sort_by(|a, b| a.stop_sequence.cmp(&b.stop_sequence)); 348 | } 349 | 350 | for f in raw_frequencies { 351 | let trip = &mut trips 352 | .get_mut(&f.trip_id) 353 | .ok_or_else(|| Error::ReferenceError(f.trip_id.to_string()))?; 354 | trip.frequencies.push(Frequency::from(&f)); 355 | } 356 | 357 | Ok(trips) 358 | } 359 | -------------------------------------------------------------------------------- /src/gtfs_reader.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use sha2::{Digest, Sha256}; 3 | 4 | use crate::{Error, Gtfs, RawGtfs}; 5 | use std::collections::HashMap; 6 | use std::convert::TryFrom; 7 | use std::fs::File; 8 | use std::io::Read; 9 | use std::path::Path; 10 | use std::time::Instant; 11 | 12 | /// Allows to parameterize how the parsing library behaves 13 | /// 14 | /// ``` 15 | ///let gtfs = gtfs_structures::GtfsReader::default() 16 | /// .read_stop_times(false) // Won’t read the stop times to save time and memory 17 | /// .read_shapes(false) // Won’t read shapes to save time and memory 18 | /// .unkown_enum_as_default(false) // Won’t convert unknown enumerations into default (e.g. LocationType=42 considered as a stop point) 19 | /// .read("fixtures/zips/gtfs.zip")?; 20 | ///assert_eq!(0, gtfs.trips.get("trip1").unwrap().stop_times.len()); 21 | /// # Ok::<(), gtfs_structures::error::Error>(()) 22 | ///``` 23 | /// 24 | /// You can also get a [RawGtfs] by doing 25 | /// ``` 26 | ///let gtfs = gtfs_structures::GtfsReader::default() 27 | /// .read_stop_times(false) 28 | /// .raw() 29 | /// .read("fixtures/zips/gtfs.zip")?; 30 | ///assert_eq!(1, gtfs.trips?.len()); 31 | ///assert_eq!(0, gtfs.stop_times?.len()); 32 | /// # Ok::<(), gtfs_structures::error::Error>(()) 33 | ///``` 34 | #[derive(Derivative)] 35 | #[derivative(Default)] 36 | pub struct GtfsReader { 37 | /// [crate::objects::StopTime] are very large and not always needed. This allows to skip reading them 38 | #[derivative(Default(value = "true"))] 39 | pub read_stop_times: bool, 40 | /// [crate::objects::Shape] are very large and not always needed. This allows to skip reading them 41 | #[derivative(Default(value = "true"))] 42 | pub read_shapes: bool, 43 | /// If a an enumeration has an unknown value, should we use the default value 44 | #[derivative(Default(value = "false"))] 45 | pub unkown_enum_as_default: bool, 46 | /// Avoid trimming the fields 47 | /// 48 | /// It is quite time consuming 49 | /// If performance is an issue, and if your data is high quality, you can switch it off 50 | #[derivative(Default(value = "true"))] 51 | pub trim_fields: bool, 52 | } 53 | 54 | impl GtfsReader { 55 | /// Configures the reader to read or not the stop times (default: true) 56 | /// 57 | /// This can be useful to save time and memory with large datasets when the timetable are not needed 58 | /// Returns Self and can be chained 59 | pub fn read_stop_times(mut self, read_stop_times: bool) -> Self { 60 | self.read_stop_times = read_stop_times; 61 | self 62 | } 63 | 64 | /// This can be useful to save time and memory with large datasets when shapes are not needed 65 | /// Returns Self and can be chained 66 | pub fn read_shapes(mut self, read_shapes: bool) -> Self { 67 | self.read_shapes = read_shapes; 68 | self 69 | } 70 | 71 | /// If a an enumeration has un unknown value, should we use the default value (default: false) 72 | /// 73 | /// For instance, if [crate::objects::Stop] has a [crate::objects::LocationType] with a value 42 in the GTFS 74 | /// when true, we will parse it as StopPoint 75 | /// when false, we will parse it as Unknown(42) 76 | /// Returns Self and can be chained 77 | pub fn unkown_enum_as_default(mut self, unkown_enum_as_default: bool) -> Self { 78 | self.unkown_enum_as_default = unkown_enum_as_default; 79 | self 80 | } 81 | 82 | /// Should the fields be trimmed (default: true) 83 | /// 84 | /// It is quite time consumming 85 | /// If performance is an issue, and if your data is high quality, you can set it to false 86 | pub fn trim_fields(mut self, trim_fields: bool) -> Self { 87 | self.trim_fields = trim_fields; 88 | self 89 | } 90 | 91 | /// Reads from an url (if starts with `"http"`), or a local path (either a directory or zipped file) 92 | /// 93 | /// To read from an url, build with read-url feature 94 | /// See also [Gtfs::from_url] and [Gtfs::from_path] if you don’t want the library to guess 95 | pub fn read(self, gtfs: &str) -> Result { 96 | self.raw().read(gtfs).and_then(Gtfs::try_from) 97 | } 98 | 99 | /// Reads the raw GTFS from a local zip archive or local directory 100 | pub fn read_from_path

(self, path: P) -> Result 101 | where 102 | P: AsRef, 103 | { 104 | self.raw().read_from_path(path).and_then(Gtfs::try_from) 105 | } 106 | 107 | /// Reads the GTFS from a remote url 108 | /// 109 | /// The library must be built with the read-url feature 110 | #[cfg(feature = "read-url")] 111 | pub fn read_from_url(self, url: U) -> Result { 112 | self.raw().read_from_url(url).and_then(Gtfs::try_from) 113 | } 114 | 115 | /// Asynchronously reads the GTFS from a remote url 116 | /// 117 | /// The library must be built with the read-url feature 118 | #[cfg(feature = "read-url")] 119 | pub async fn read_from_url_async(self, url: U) -> Result { 120 | self.raw() 121 | .read_from_url_async(url) 122 | .await 123 | .and_then(Gtfs::try_from) 124 | } 125 | 126 | /// Read the Gtfs as a [RawGtfs]. 127 | /// 128 | /// ``` 129 | ///let gtfs = gtfs_structures::GtfsReader::default() 130 | /// .read_stop_times(false) 131 | /// .raw() 132 | /// .read("fixtures/zips/gtfs.zip")?; 133 | ///assert_eq!(1, gtfs.trips?.len()); 134 | ///assert_eq!(0, gtfs.stop_times?.len()); 135 | /// # Ok::<(), gtfs_structures::error::Error>(()) 136 | ///``` 137 | pub fn raw(self) -> RawGtfsReader { 138 | RawGtfsReader { reader: self } 139 | } 140 | } 141 | 142 | /// This reader generates [RawGtfs]. It must be built using [GtfsReader::raw] 143 | /// 144 | /// The methods to read a Gtfs are the same as for [GtfsReader] 145 | pub struct RawGtfsReader { 146 | reader: GtfsReader, 147 | } 148 | 149 | impl RawGtfsReader { 150 | fn read_from_directory(&self, p: &std::path::Path) -> Result { 151 | let start_of_read_instant = Instant::now(); 152 | // Thoses files are not mandatory 153 | // We use None if they don’t exist, not an Error 154 | let files = std::fs::read_dir(p)? 155 | .filter_map(|d| { 156 | d.ok().and_then(|e| { 157 | e.path() 158 | .strip_prefix(p) 159 | .ok() 160 | .and_then(|f| f.to_str().map(|s| s.to_owned())) 161 | }) 162 | }) 163 | .collect(); 164 | 165 | let mut result = RawGtfs { 166 | trips: self.read_objs_from_path(p.join("trips.txt")), 167 | calendar: self.read_objs_from_optional_path(p, "calendar.txt"), 168 | calendar_dates: self.read_objs_from_optional_path(p, "calendar_dates.txt"), 169 | stops: self.read_objs_from_path(p.join("stops.txt")), 170 | routes: self.read_objs_from_path(p.join("routes.txt")), 171 | stop_times: if self.reader.read_stop_times { 172 | self.read_objs_from_path(p.join("stop_times.txt")) 173 | } else { 174 | Ok(Vec::new()) 175 | }, 176 | agencies: self.read_objs_from_path(p.join("agency.txt")), 177 | shapes: self.read_objs_from_optional_path(p, "shapes.txt"), 178 | fare_attributes: self.read_objs_from_optional_path(p, "fare_attributes.txt"), 179 | fare_rules: self.read_objs_from_optional_path(p, "fare_rules.txt"), 180 | frequencies: self.read_objs_from_optional_path(p, "frequencies.txt"), 181 | transfers: self.read_objs_from_optional_path(p, "transfers.txt"), 182 | pathways: self.read_objs_from_optional_path(p, "pathways.txt"), 183 | feed_info: self.read_objs_from_optional_path(p, "feed_info.txt"), 184 | read_duration: start_of_read_instant.elapsed(), 185 | translations: self.read_objs_from_optional_path(p, "translations.txt"), 186 | files, 187 | source_format: crate::SourceFormat::Directory, 188 | sha256: None, 189 | }; 190 | 191 | if self.reader.unkown_enum_as_default { 192 | result.unknown_to_default(); 193 | } 194 | Ok(result) 195 | } 196 | 197 | /// Reads from an url (if starts with `"http"`) if the feature `read-url` is activated, 198 | /// or a local path (either a directory or zipped file) 199 | pub fn read(self, gtfs: &str) -> Result { 200 | #[cfg(feature = "read-url")] 201 | if gtfs.starts_with("http") { 202 | return self.read_from_url(gtfs); 203 | } 204 | self.read_from_path(gtfs) 205 | } 206 | 207 | /// Reads the GTFS from a remote url 208 | #[cfg(feature = "read-url")] 209 | pub fn read_from_url(self, url: U) -> Result { 210 | let mut res = reqwest::blocking::get(url)?; 211 | let mut body = Vec::new(); 212 | res.read_to_end(&mut body)?; 213 | let cursor = std::io::Cursor::new(body); 214 | self.read_from_reader(cursor) 215 | } 216 | 217 | /// Asynchronously reads the GTFS from a remote url 218 | #[cfg(feature = "read-url")] 219 | pub async fn read_from_url_async(self, url: U) -> Result { 220 | let res = reqwest::get(url).await?.bytes().await?; 221 | let reader = std::io::Cursor::new(res); 222 | self.read_from_reader(reader) 223 | } 224 | 225 | /// Reads the raw GTFS from a local zip archive or local directory 226 | pub fn read_from_path

(&self, path: P) -> Result 227 | where 228 | P: AsRef, 229 | { 230 | let p = path.as_ref(); 231 | if p.is_file() { 232 | let reader = File::open(p)?; 233 | self.read_from_reader(reader) 234 | } else if p.is_dir() { 235 | self.read_from_directory(p) 236 | } else { 237 | Err(Error::NotFileNorDirectory(format!("{}", p.display()))) 238 | } 239 | } 240 | 241 | pub fn read_from_reader( 242 | &self, 243 | reader: T, 244 | ) -> Result { 245 | let start_of_read_instant = Instant::now(); 246 | let mut hasher = Sha256::new(); 247 | let mut buf_reader = std::io::BufReader::new(reader); 248 | let _n = std::io::copy(&mut buf_reader, &mut hasher)?; 249 | let hash = hasher.finalize(); 250 | let mut archive = zip::ZipArchive::new(buf_reader)?; 251 | let mut file_mapping = HashMap::new(); 252 | let mut files = Vec::new(); 253 | 254 | for i in 0..archive.len() { 255 | let archive_file = archive.by_index(i)?; 256 | files.push(archive_file.name().to_owned()); 257 | 258 | for gtfs_file in &[ 259 | "agency.txt", 260 | "calendar.txt", 261 | "calendar_dates.txt", 262 | "routes.txt", 263 | "stops.txt", 264 | "stop_times.txt", 265 | "trips.txt", 266 | "fare_attributes.txt", 267 | "fare_rules.txt", 268 | "frequencies.txt", 269 | "transfers.txt", 270 | "pathways.txt", 271 | "feed_info.txt", 272 | "shapes.txt", 273 | ] { 274 | let path = std::path::Path::new(archive_file.name()); 275 | if path.file_name() == Some(std::ffi::OsStr::new(gtfs_file)) { 276 | file_mapping.insert(gtfs_file, i); 277 | break; 278 | } 279 | } 280 | } 281 | 282 | let mut result = RawGtfs { 283 | agencies: self.read_file(&file_mapping, &mut archive, "agency.txt"), 284 | calendar: self.read_optional_file(&file_mapping, &mut archive, "calendar.txt"), 285 | calendar_dates: self.read_optional_file( 286 | &file_mapping, 287 | &mut archive, 288 | "calendar_dates.txt", 289 | ), 290 | routes: self.read_file(&file_mapping, &mut archive, "routes.txt"), 291 | stops: self.read_file(&file_mapping, &mut archive, "stops.txt"), 292 | stop_times: if self.reader.read_stop_times { 293 | self.read_file(&file_mapping, &mut archive, "stop_times.txt") 294 | } else { 295 | Ok(Vec::new()) 296 | }, 297 | trips: self.read_file(&file_mapping, &mut archive, "trips.txt"), 298 | fare_attributes: self.read_optional_file( 299 | &file_mapping, 300 | &mut archive, 301 | "fare_attributes.txt", 302 | ), 303 | fare_rules: self.read_optional_file(&file_mapping, &mut archive, "fare_rules.txt"), 304 | frequencies: self.read_optional_file(&file_mapping, &mut archive, "frequencies.txt"), 305 | transfers: self.read_optional_file(&file_mapping, &mut archive, "transfers.txt"), 306 | pathways: self.read_optional_file(&file_mapping, &mut archive, "pathways.txt"), 307 | feed_info: self.read_optional_file(&file_mapping, &mut archive, "feed_info.txt"), 308 | shapes: if self.reader.read_shapes { 309 | self.read_optional_file(&file_mapping, &mut archive, "shapes.txt") 310 | } else { 311 | Some(Ok(Vec::new())) 312 | }, 313 | translations: self.read_optional_file(&file_mapping, &mut archive, "translations.txt"), 314 | read_duration: start_of_read_instant.elapsed(), 315 | files, 316 | source_format: crate::SourceFormat::Zip, 317 | sha256: Some(format!("{hash:x}")), 318 | }; 319 | 320 | if self.reader.unkown_enum_as_default { 321 | result.unknown_to_default(); 322 | } 323 | Ok(result) 324 | } 325 | 326 | fn read_objs(&self, mut reader: T, file_name: &str) -> Result, Error> 327 | where 328 | for<'de> O: Deserialize<'de>, 329 | T: std::io::Read, 330 | { 331 | let mut bom = [0; 3]; 332 | reader 333 | .read_exact(&mut bom) 334 | .map_err(|e| Error::NamedFileIO { 335 | file_name: file_name.to_owned(), 336 | source: Box::new(e), 337 | })?; 338 | 339 | let chained = if bom != [0xefu8, 0xbbu8, 0xbfu8] { 340 | bom.chain(reader) 341 | } else { 342 | [].chain(reader) 343 | }; 344 | 345 | let mut reader = csv::ReaderBuilder::new() 346 | .flexible(true) 347 | .trim(if self.reader.trim_fields { 348 | csv::Trim::Fields 349 | } else { 350 | csv::Trim::None 351 | }) 352 | .from_reader(chained); 353 | // We store the headers to be able to return them in case of errors 354 | let headers = reader 355 | .headers() 356 | .map_err(|e| Error::CSVError { 357 | file_name: file_name.to_owned(), 358 | source: e, 359 | line_in_error: None, 360 | })? 361 | .clone() 362 | .into_iter() 363 | .map(|x| x.trim()) 364 | .collect::(); 365 | 366 | // Pre-allocate a StringRecord for performance reasons 367 | let mut rec = csv::StringRecord::new(); 368 | let mut objs = Vec::new(); 369 | 370 | // Read each record into the pre-allocated StringRecord one at a time 371 | while reader.read_record(&mut rec).map_err(|e| Error::CSVError { 372 | file_name: file_name.to_owned(), 373 | source: e, 374 | line_in_error: None, 375 | })? { 376 | let obj = rec 377 | .deserialize(Some(&headers)) 378 | .map_err(|e| Error::CSVError { 379 | file_name: file_name.to_owned(), 380 | source: e, 381 | line_in_error: Some(crate::error::LineError { 382 | headers: headers.into_iter().map(String::from).collect(), 383 | values: rec.into_iter().map(String::from).collect(), 384 | }), 385 | })?; 386 | objs.push(obj); 387 | } 388 | Ok(objs) 389 | } 390 | 391 | fn read_objs_from_path(&self, path: std::path::PathBuf) -> Result, Error> 392 | where 393 | for<'de> O: Deserialize<'de>, 394 | { 395 | let file_name = path 396 | .file_name() 397 | .and_then(|f| f.to_str()) 398 | .unwrap_or("invalid_file_name") 399 | .to_string(); 400 | if path.exists() { 401 | File::open(path) 402 | .map_err(|e| Error::NamedFileIO { 403 | file_name: file_name.to_owned(), 404 | source: Box::new(e), 405 | }) 406 | .and_then(|r| self.read_objs(r, &file_name)) 407 | } else { 408 | Err(Error::MissingFile(file_name)) 409 | } 410 | } 411 | 412 | fn read_objs_from_optional_path( 413 | &self, 414 | dir_path: &std::path::Path, 415 | file_name: &str, 416 | ) -> Option, Error>> 417 | where 418 | for<'de> O: Deserialize<'de>, 419 | { 420 | File::open(dir_path.join(file_name)) 421 | .ok() 422 | .map(|r| self.read_objs(r, file_name)) 423 | } 424 | 425 | fn read_file( 426 | &self, 427 | file_mapping: &HashMap<&&str, usize>, 428 | archive: &mut zip::ZipArchive, 429 | file_name: &str, 430 | ) -> Result, Error> 431 | where 432 | for<'de> O: Deserialize<'de>, 433 | T: std::io::Read + std::io::Seek, 434 | { 435 | self.read_optional_file(file_mapping, archive, file_name) 436 | .unwrap_or_else(|| Err(Error::MissingFile(file_name.to_owned()))) 437 | } 438 | 439 | fn read_optional_file( 440 | &self, 441 | file_mapping: &HashMap<&&str, usize>, 442 | archive: &mut zip::ZipArchive, 443 | file_name: &str, 444 | ) -> Option, Error>> 445 | where 446 | for<'de> O: Deserialize<'de>, 447 | T: std::io::Read + std::io::Seek, 448 | { 449 | file_mapping.get(&file_name).map(|i| { 450 | self.read_objs( 451 | archive.by_index(*i).map_err(|e| Error::NamedFileIO { 452 | file_name: file_name.to_owned(), 453 | source: Box::new(e), 454 | })?, 455 | file_name, 456 | ) 457 | }) 458 | } 459 | } 460 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! The [General Transit Feed Specification](https://gtfs.org/) (GTFS) is a commonly used model to represent public transit data. 2 | 3 | This crates brings [serde](https://serde.rs) structures of this model and helpers to read GTFS files. 4 | 5 | To get started, see [Gtfs]. 6 | 7 | ## What is GTFS 8 | 9 | A Gtfs feed is a collection of CSV files (often bundled as a zip file). 10 | Each file represents a collection of one type (stops, lines, etc.) that have relationships through unique identifiers. 11 | 12 | This crate reads a feed, deserializes the objects into Rust structs and verifies the relationships. 13 | 14 | ## Design decisions 15 | 16 | ### Two representations 17 | 18 | The [RawGtfs] representation holds the objects as close as possible to their CSV representation. This allows to check invalid references. 19 | 20 | [Gtfs] re-organizes a bit the data. For instance all the [StopTime] are included within their corresponding [Trip] and cannot be accessed directly. 21 | If an object references a non existing Id it will be an error. 22 | 23 | ### Use of Enum 24 | 25 | Many values are integers that are actually enumerations of certain values. We always use Rust enums, like [LocationType] to represent them, and not the integer value. 26 | 27 | ### Reference 28 | 29 | We try to stick as closely as possible to the reference. Optional fields are [std::option], while missing mandatory elements will result in an error. 30 | If a default value is defined, we will use it. 31 | 32 | There are two references and . They are mostly the same, even if google’s specification has some extensions. 33 | 34 | ### Renaming 35 | 36 | We kept some names even if they can be confusing (a [Calendar] will be referenced by `service_id`), but we strip the object type (`route_short_name` is [Route::short_name]). 37 | 38 | */ 39 | #![warn(missing_docs)] 40 | 41 | #[macro_use] 42 | extern crate derivative; 43 | #[macro_use] 44 | extern crate serde_derive; 45 | 46 | mod enums; 47 | pub mod error; 48 | mod gtfs; 49 | mod gtfs_reader; 50 | pub(crate) mod objects; 51 | mod raw_gtfs; 52 | mod serde_helpers; 53 | 54 | #[cfg(test)] 55 | mod tests; 56 | 57 | pub use error::Error; 58 | pub use gtfs::Gtfs; 59 | pub use gtfs_reader::GtfsReader; 60 | pub use objects::*; 61 | pub use raw_gtfs::RawGtfs; 62 | -------------------------------------------------------------------------------- /src/objects.rs: -------------------------------------------------------------------------------- 1 | pub use crate::enums::*; 2 | use crate::serde_helpers::*; 3 | use chrono::{Datelike, NaiveDate, Weekday}; 4 | use rgb::RGB8; 5 | 6 | use std::fmt; 7 | use std::hash::Hash; 8 | use std::sync::Arc; 9 | 10 | /// Objects that have an identifier implement this trait 11 | /// 12 | /// Those identifier are technical and should not be shown to travellers 13 | pub trait Id { 14 | /// Identifier of the object 15 | fn id(&self) -> &str; 16 | } 17 | 18 | impl Id for Arc { 19 | fn id(&self) -> &str { 20 | self.as_ref().id() 21 | } 22 | } 23 | 24 | /// Trait to introspect what is the object’s type (stop, route…) 25 | pub trait Type { 26 | /// What is the type of the object 27 | fn object_type(&self) -> ObjectType; 28 | } 29 | 30 | impl Type for Arc { 31 | fn object_type(&self) -> ObjectType { 32 | self.as_ref().object_type() 33 | } 34 | } 35 | 36 | /// A calender describes on which days the vehicle runs. See 37 | #[derive(Debug, Clone, Deserialize, Serialize, Hash, PartialEq, Eq)] 38 | pub struct Calendar { 39 | /// Unique technical identifier (not for the traveller) of this calendar 40 | #[serde(rename = "service_id")] 41 | pub id: String, 42 | /// Does the service run on mondays 43 | #[serde( 44 | deserialize_with = "deserialize_bool", 45 | serialize_with = "serialize_bool" 46 | )] 47 | pub monday: bool, 48 | /// Does the service run on tuesdays 49 | #[serde( 50 | deserialize_with = "deserialize_bool", 51 | serialize_with = "serialize_bool" 52 | )] 53 | pub tuesday: bool, 54 | /// Does the service run on wednesdays 55 | #[serde( 56 | deserialize_with = "deserialize_bool", 57 | serialize_with = "serialize_bool" 58 | )] 59 | pub wednesday: bool, 60 | /// Does the service run on thursdays 61 | #[serde( 62 | deserialize_with = "deserialize_bool", 63 | serialize_with = "serialize_bool" 64 | )] 65 | pub thursday: bool, 66 | /// Does the service run on fridays 67 | #[serde( 68 | deserialize_with = "deserialize_bool", 69 | serialize_with = "serialize_bool" 70 | )] 71 | pub friday: bool, 72 | /// Does the service run on saturdays 73 | #[serde( 74 | deserialize_with = "deserialize_bool", 75 | serialize_with = "serialize_bool" 76 | )] 77 | pub saturday: bool, 78 | /// Does the service run on sundays 79 | #[serde( 80 | deserialize_with = "deserialize_bool", 81 | serialize_with = "serialize_bool" 82 | )] 83 | pub sunday: bool, 84 | /// Start service day for the service interval 85 | #[serde( 86 | deserialize_with = "deserialize_date", 87 | serialize_with = "serialize_date" 88 | )] 89 | pub start_date: NaiveDate, 90 | /// End service day for the service interval. This service day is included in the interval 91 | #[serde( 92 | deserialize_with = "deserialize_date", 93 | serialize_with = "serialize_date" 94 | )] 95 | pub end_date: NaiveDate, 96 | } 97 | 98 | impl Type for Calendar { 99 | fn object_type(&self) -> ObjectType { 100 | ObjectType::Calendar 101 | } 102 | } 103 | 104 | impl Id for Calendar { 105 | fn id(&self) -> &str { 106 | &self.id 107 | } 108 | } 109 | 110 | impl fmt::Display for Calendar { 111 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 112 | write!(f, "{}—{}", self.start_date, self.end_date) 113 | } 114 | } 115 | 116 | impl Calendar { 117 | /// Returns true if there is a service running on that day 118 | pub fn valid_weekday(&self, date: NaiveDate) -> bool { 119 | match date.weekday() { 120 | Weekday::Mon => self.monday, 121 | Weekday::Tue => self.tuesday, 122 | Weekday::Wed => self.wednesday, 123 | Weekday::Thu => self.thursday, 124 | Weekday::Fri => self.friday, 125 | Weekday::Sat => self.saturday, 126 | Weekday::Sun => self.sunday, 127 | } 128 | } 129 | } 130 | 131 | /// Defines a specific date that can be added or removed from a [Calendar]. See 132 | #[derive(Debug, Clone, Deserialize, Serialize)] 133 | pub struct CalendarDate { 134 | /// Identifier of the service that is modified at this date 135 | pub service_id: String, 136 | #[serde( 137 | deserialize_with = "deserialize_date", 138 | serialize_with = "serialize_date" 139 | )] 140 | /// Date where the service will be added or deleted 141 | pub date: NaiveDate, 142 | /// Is the service added or deleted 143 | pub exception_type: Exception, 144 | } 145 | 146 | /// A physical stop, station or area. See 147 | #[derive(Debug, Serialize, Deserialize, Clone, Default)] 148 | pub struct Stop { 149 | /// Unique technical identifier (not for the traveller) of the stop 150 | #[serde(rename = "stop_id")] 151 | pub id: String, 152 | /// Short text or a number that identifies the location for riders 153 | #[serde(rename = "stop_code")] 154 | pub code: Option, 155 | ///Name of the location. Use a name that people will understand in the local and tourist vernacular 156 | #[serde(rename = "stop_name")] 157 | pub name: Option, 158 | /// Description of the location that provides useful, quality information 159 | #[serde(default, rename = "stop_desc")] 160 | pub description: Option, 161 | /// Type of the location 162 | #[serde(default)] 163 | pub location_type: LocationType, 164 | /// Defines hierarchy between the different locations 165 | pub parent_station: Option, 166 | /// Identifies the fare zone for a stop 167 | pub zone_id: Option, 168 | /// URL of a web page about the location 169 | #[serde(rename = "stop_url")] 170 | pub url: Option, 171 | /// Longitude of the stop 172 | #[serde(deserialize_with = "de_with_optional_float")] 173 | #[serde(serialize_with = "serialize_float_as_str")] 174 | #[serde(rename = "stop_lon", default)] 175 | pub longitude: Option, 176 | /// Latitude of the stop 177 | #[serde(deserialize_with = "de_with_optional_float")] 178 | #[serde(serialize_with = "serialize_float_as_str")] 179 | #[serde(rename = "stop_lat", default)] 180 | pub latitude: Option, 181 | /// Timezone of the location 182 | #[serde(rename = "stop_timezone")] 183 | pub timezone: Option, 184 | /// Indicates whether wheelchair boardings are possible from the location 185 | #[serde(deserialize_with = "de_with_empty_default", default)] 186 | pub wheelchair_boarding: Availability, 187 | /// Level of the location. The same level can be used by multiple unlinked stations 188 | pub level_id: Option, 189 | /// Platform identifier for a platform stop (a stop belonging to a station) 190 | pub platform_code: Option, 191 | /// Transfers from this Stop 192 | #[serde(skip)] 193 | pub transfers: Vec, 194 | /// Pathways from this stop 195 | #[serde(skip)] 196 | pub pathways: Vec, 197 | /// Text to speech readable version of the stop_name 198 | #[serde(rename = "tts_stop_name")] 199 | pub tts_name: Option, 200 | } 201 | 202 | impl Type for Stop { 203 | fn object_type(&self) -> ObjectType { 204 | ObjectType::Stop 205 | } 206 | } 207 | 208 | impl Id for Stop { 209 | fn id(&self) -> &str { 210 | &self.id 211 | } 212 | } 213 | 214 | impl fmt::Display for Stop { 215 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 216 | write!(f, "{}", self.name.as_ref().unwrap_or(&String::from(""))) 217 | } 218 | } 219 | 220 | /// A [StopTime] where the relations with [Trip] and [Stop] have not been tested 221 | #[derive(Debug, Clone, Serialize, Deserialize, Default)] 222 | pub struct RawStopTime { 223 | /// [Trip] to which this stop time belongs to 224 | pub trip_id: String, 225 | /// Arrival time of the stop time. 226 | /// It's an option since the intermediate stops can have have no arrival 227 | /// and this arrival needs to be interpolated 228 | #[serde( 229 | deserialize_with = "deserialize_optional_time", 230 | serialize_with = "serialize_optional_time" 231 | )] 232 | pub arrival_time: Option, 233 | /// Departure time of the stop time. 234 | /// It's an option since the intermediate stops can have have no departure 235 | /// and this departure needs to be interpolated 236 | #[serde( 237 | deserialize_with = "deserialize_optional_time", 238 | serialize_with = "serialize_optional_time" 239 | )] 240 | pub departure_time: Option, 241 | /// Identifier of the [Stop] where the vehicle stops 242 | pub stop_id: String, 243 | /// Order of stops for a particular trip. The values must increase along the trip but do not need to be consecutive 244 | pub stop_sequence: u32, 245 | /// Text that appears on signage identifying the trip's destination to riders 246 | pub stop_headsign: Option, 247 | /// Indicates pickup method 248 | #[serde(default)] 249 | pub pickup_type: PickupDropOffType, 250 | /// Indicates drop off method 251 | #[serde(default)] 252 | pub drop_off_type: PickupDropOffType, 253 | /// Indicates whether a rider can board the transit vehicle anywhere along the vehicle’s travel path 254 | #[serde(default)] 255 | pub continuous_pickup: ContinuousPickupDropOff, 256 | /// Indicates whether a rider can alight from the transit vehicle at any point along the vehicle’s travel path 257 | #[serde(default)] 258 | pub continuous_drop_off: ContinuousPickupDropOff, 259 | /// Actual distance traveled along the associated shape, from the first stop to the stop specified in this record. This field specifies how much of the shape to draw between any two stops during a trip 260 | pub shape_dist_traveled: Option, 261 | /// 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 262 | #[serde(default)] 263 | pub timepoint: TimepointType, 264 | } 265 | 266 | /// The moment where a vehicle, running on [Trip] stops at a [Stop]. See 267 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] 268 | pub struct StopTime { 269 | /// Arrival time of the stop time. 270 | /// It's an option since the intermediate stops can have have no arrival 271 | /// and this arrival needs to be interpolated 272 | pub arrival_time: Option, 273 | /// [Stop] where the vehicle stops 274 | pub stop: Arc, 275 | /// Departure time of the stop time. 276 | /// It's an option since the intermediate stops can have have no departure 277 | /// and this departure needs to be interpolated 278 | pub departure_time: Option, 279 | /// Indicates pickup method 280 | pub pickup_type: PickupDropOffType, 281 | /// Indicates drop off method 282 | pub drop_off_type: PickupDropOffType, 283 | /// Order of stops for a particular trip. The values must increase along the trip but do not need to be consecutive 284 | pub stop_sequence: u32, 285 | /// Text that appears on signage identifying the trip's destination to riders 286 | pub stop_headsign: Option, 287 | /// Indicates whether a rider can board the transit vehicle anywhere along the vehicle’s travel path 288 | pub continuous_pickup: ContinuousPickupDropOff, 289 | /// Indicates whether a rider can alight from the transit vehicle at any point along the vehicle’s travel path 290 | pub continuous_drop_off: ContinuousPickupDropOff, 291 | /// Actual distance traveled along the associated shape, from the first stop to the stop specified in this record. This field specifies how much of the shape to draw between any two stops during a trip 292 | pub shape_dist_traveled: Option, 293 | /// 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 294 | pub timepoint: TimepointType, 295 | } 296 | 297 | impl StopTime { 298 | /// Creates [StopTime] by linking a [RawStopTime::stop_id] to the actual [Stop] 299 | pub fn from(stop_time_gtfs: RawStopTime, stop: Arc) -> Self { 300 | Self { 301 | arrival_time: stop_time_gtfs.arrival_time, 302 | departure_time: stop_time_gtfs.departure_time, 303 | stop, 304 | pickup_type: stop_time_gtfs.pickup_type, 305 | drop_off_type: stop_time_gtfs.drop_off_type, 306 | stop_sequence: stop_time_gtfs.stop_sequence, 307 | stop_headsign: stop_time_gtfs.stop_headsign, 308 | continuous_pickup: stop_time_gtfs.continuous_pickup, 309 | continuous_drop_off: stop_time_gtfs.continuous_drop_off, 310 | shape_dist_traveled: stop_time_gtfs.shape_dist_traveled, 311 | timepoint: stop_time_gtfs.timepoint, 312 | } 313 | } 314 | } 315 | 316 | /// A route is a commercial line (there can be various stop sequences for a same line). See 317 | #[derive(Debug, Clone, Serialize, Deserialize, Default)] 318 | pub struct Route { 319 | /// Unique technical (not for the traveller) identifier for the route 320 | #[serde(rename = "route_id")] 321 | pub id: String, 322 | /// 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 323 | #[serde(rename = "route_short_name", default)] 324 | pub short_name: Option, 325 | /// 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 326 | #[serde(rename = "route_long_name", default)] 327 | pub long_name: Option, 328 | /// Description of a route that provides useful, quality information 329 | #[serde(rename = "route_desc")] 330 | pub desc: Option, 331 | /// Indicates the type of transportation used on a route 332 | pub route_type: RouteType, 333 | /// URL of a web page about the particular route 334 | #[serde(rename = "route_url")] 335 | pub url: Option, 336 | /// Agency for the specified route 337 | pub agency_id: Option, 338 | /// Orders the routes in a way which is ideal for presentation to customers. Routes with smaller route_sort_order values should be displayed first. 339 | #[serde(rename = "route_sort_order")] 340 | pub order: Option, 341 | /// Route color designation that matches public facing material 342 | #[serde( 343 | deserialize_with = "deserialize_route_color", 344 | serialize_with = "serialize_color", 345 | rename = "route_color", 346 | default = "default_route_color" 347 | )] 348 | pub color: RGB8, 349 | /// Legible color to use for text drawn against a background of [Route::route_color] 350 | #[serde( 351 | deserialize_with = "deserialize_route_text_color", 352 | serialize_with = "serialize_color", 353 | rename = "route_text_color", 354 | default 355 | )] 356 | pub text_color: RGB8, 357 | /// Indicates whether a rider can board the transit vehicle anywhere along the vehicle’s travel path 358 | #[serde(default)] 359 | pub continuous_pickup: ContinuousPickupDropOff, 360 | /// Indicates whether a rider can alight from the transit vehicle at any point along the vehicle’s travel path 361 | #[serde(default)] 362 | pub continuous_drop_off: ContinuousPickupDropOff, 363 | } 364 | 365 | impl Type for Route { 366 | fn object_type(&self) -> ObjectType { 367 | ObjectType::Route 368 | } 369 | } 370 | 371 | impl Id for Route { 372 | fn id(&self) -> &str { 373 | &self.id 374 | } 375 | } 376 | 377 | impl fmt::Display for Route { 378 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 379 | if let Some(long_name) = self.long_name.as_ref() { 380 | write!(f, "{}", long_name) 381 | } else if let Some(short_name) = self.short_name.as_ref() { 382 | write!(f, "{}", short_name) 383 | } else { 384 | write!(f, "{}", self.id) 385 | } 386 | } 387 | } 388 | 389 | /// Raw structure to hold translations as defined in the GTFS file. See 390 | #[derive(Debug, Clone, Serialize, Deserialize, Default)] 391 | pub struct RawTranslation { 392 | /// To which table does the translation apply 393 | pub table_name: String, 394 | /// To which field does the translation apply 395 | pub field_name: String, 396 | /// Language of the translation 397 | pub language: String, 398 | /// Translated value 399 | pub translation: String, 400 | /// The record identifier to translate. For stop_times, it’s the trip_id 401 | pub record_id: Option, 402 | /// Only for stop_times: the stop_sequence 403 | pub record_sub_id: Option, 404 | /// Translate all values that match exactly, instead of specifying individual records 405 | pub field_value: Option, 406 | } 407 | 408 | /// A [Trip] where the relationships with other objects have not been checked 409 | #[derive(Debug, Clone, Serialize, Deserialize, Default)] 410 | pub struct RawTrip { 411 | /// Unique technical (not for the traveller) identifier for the Trip 412 | #[serde(rename = "trip_id")] 413 | pub id: String, 414 | /// References the [Calendar] on which this trip runs 415 | pub service_id: String, 416 | /// References along which [Route] this trip runs 417 | pub route_id: String, 418 | /// Shape of the trip 419 | pub shape_id: Option, 420 | /// Text that appears on signage identifying the trip's destination to riders 421 | pub trip_headsign: Option, 422 | /// Public facing text used to identify the trip to riders, for instance, to identify train numbers for commuter rail trips 423 | pub trip_short_name: Option, 424 | /// Indicates the direction of travel for a trip. This field is not used in routing; it provides a way to separate trips by direction when publishing time tables 425 | pub direction_id: Option, 426 | /// Identifies the block to which the trip belongs. A block consists of a single trip or many sequential trips made using the same vehicle, defined by shared service days and block_id. A block_id can have trips with different service days, making distinct blocks 427 | pub block_id: Option, 428 | /// Indicates wheelchair accessibility 429 | #[serde(default)] 430 | pub wheelchair_accessible: Availability, 431 | /// Indicates whether bikes are allowed 432 | #[serde(default)] 433 | pub bikes_allowed: BikesAllowedType, 434 | } 435 | 436 | impl Type for RawTrip { 437 | fn object_type(&self) -> ObjectType { 438 | ObjectType::Trip 439 | } 440 | } 441 | 442 | impl Id for RawTrip { 443 | fn id(&self) -> &str { 444 | &self.id 445 | } 446 | } 447 | 448 | impl fmt::Display for RawTrip { 449 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 450 | write!( 451 | f, 452 | "route id: {}, service id: {}", 453 | self.route_id, self.service_id 454 | ) 455 | } 456 | } 457 | 458 | /// A Trip is a vehicle that follows a sequence of [StopTime] on certain days. See 459 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] 460 | pub struct Trip { 461 | /// Unique technical identifier (not for the traveller) for the Trip 462 | pub id: String, 463 | /// References the [Calendar] on which this trip runs 464 | pub service_id: String, 465 | /// References along which [Route] this trip runs 466 | pub route_id: String, 467 | /// All the [StopTime] that define the trip 468 | pub stop_times: Vec, 469 | /// Unique technical (not for the traveller) identifier for the Shape 470 | pub shape_id: Option, 471 | /// Text that appears on signage identifying the trip's destination to riders 472 | pub trip_headsign: Option, 473 | /// Public facing text used to identify the trip to riders, for instance, to identify train numbers for commuter rail trips 474 | pub trip_short_name: Option, 475 | /// Indicates the direction of travel for a trip. This field is not used in routing; it provides a way to separate trips by direction when publishing time tables 476 | pub direction_id: Option, 477 | /// Identifies the block to which the trip belongs. A block consists of a single trip or many sequential trips made using the same vehicle, defined by shared service days and block_id. A block_id can have trips with different service days, making distinct blocks 478 | pub block_id: Option, 479 | /// Indicates wheelchair accessibility 480 | pub wheelchair_accessible: Availability, 481 | /// Indicates whether bikes are allowed 482 | pub bikes_allowed: BikesAllowedType, 483 | /// During which periods the trip runs by frequency and not by fixed timetable 484 | pub frequencies: Vec, 485 | } 486 | 487 | impl Type for Trip { 488 | fn object_type(&self) -> ObjectType { 489 | ObjectType::Trip 490 | } 491 | } 492 | 493 | impl Id for Trip { 494 | fn id(&self) -> &str { 495 | &self.id 496 | } 497 | } 498 | 499 | impl fmt::Display for Trip { 500 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 501 | write!( 502 | f, 503 | "route id: {}, service id: {}", 504 | self.route_id, self.service_id 505 | ) 506 | } 507 | } 508 | 509 | /// General informations about the agency running the network. See 510 | #[derive(Clone, Debug, Serialize, Deserialize, Default)] 511 | pub struct Agency { 512 | /// Unique technical (not for the traveller) identifier for the Agency 513 | #[serde(rename = "agency_id")] 514 | pub id: Option, 515 | ///Full name of the transit agency 516 | #[serde(rename = "agency_name")] 517 | pub name: String, 518 | /// Full url of the transit agency. 519 | #[serde(rename = "agency_url")] 520 | pub url: String, 521 | /// Timezone where the transit agency is located 522 | #[serde(rename = "agency_timezone")] 523 | pub timezone: String, 524 | /// Primary language used by this transit agency 525 | #[serde(rename = "agency_lang")] 526 | pub lang: Option, 527 | /// A voice telephone number for the specified agency 528 | #[serde(rename = "agency_phone")] 529 | pub phone: Option, 530 | /// URL of a web page that allows a rider to purchase tickets or other fare instruments for that agency online 531 | #[serde(rename = "agency_fare_url")] 532 | pub fare_url: Option, 533 | /// Email address actively monitored by the agency’s customer service department 534 | #[serde(rename = "agency_email")] 535 | pub email: Option, 536 | } 537 | 538 | impl Type for Agency { 539 | fn object_type(&self) -> ObjectType { 540 | ObjectType::Agency 541 | } 542 | } 543 | 544 | impl Id for Agency { 545 | fn id(&self) -> &str { 546 | match &self.id { 547 | None => "", 548 | Some(id) => id, 549 | } 550 | } 551 | } 552 | 553 | impl fmt::Display for Agency { 554 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 555 | write!(f, "{}", self.name) 556 | } 557 | } 558 | 559 | /// A single geographical point decribing the shape of a [Trip]. See 560 | #[derive(Clone, Debug, Serialize, Deserialize, Default)] 561 | pub struct Shape { 562 | /// Unique technical (not for the traveller) identifier for the Shape 563 | #[serde(rename = "shape_id")] 564 | pub id: String, 565 | #[serde(rename = "shape_pt_lat", default)] 566 | /// Latitude of a shape point 567 | pub latitude: f64, 568 | /// Longitude of a shape point 569 | #[serde(rename = "shape_pt_lon", default)] 570 | pub longitude: f64, 571 | /// Sequence in which the shape points connect to form the shape. Values increase along the trip but do not need to be consecutive. 572 | #[serde(rename = "shape_pt_sequence")] 573 | pub sequence: usize, 574 | /// Actual distance traveled along the shape from the first shape point to the point specified in this record. Used by trip planners to show the correct portion of the shape on a map 575 | #[serde(rename = "shape_dist_traveled")] 576 | pub dist_traveled: Option, 577 | } 578 | 579 | impl Type for Shape { 580 | fn object_type(&self) -> ObjectType { 581 | ObjectType::Shape 582 | } 583 | } 584 | 585 | impl Id for Shape { 586 | fn id(&self) -> &str { 587 | &self.id 588 | } 589 | } 590 | 591 | /// Defines one possible fare. See 592 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] 593 | pub struct FareAttribute { 594 | /// Unique technical (not for the traveller) identifier for the FareAttribute 595 | #[serde(rename = "fare_id")] 596 | pub id: String, 597 | /// Fare price, in the unit specified by [FareAttribute::currency] 598 | pub price: String, 599 | /// Currency used to pay the fare. 600 | #[serde(rename = "currency_type")] 601 | pub currency: String, 602 | ///Indicates when the fare must be paid 603 | pub payment_method: PaymentMethod, 604 | /// Indicates the number of transfers permitted on this fare 605 | pub transfers: Transfers, 606 | /// Identifies the relevant agency for a fare 607 | pub agency_id: Option, 608 | /// Length of time in seconds before a transfer expires 609 | pub transfer_duration: Option, 610 | } 611 | 612 | impl Id for FareAttribute { 613 | fn id(&self) -> &str { 614 | &self.id 615 | } 616 | } 617 | 618 | impl Type for FareAttribute { 619 | fn object_type(&self) -> ObjectType { 620 | ObjectType::Fare 621 | } 622 | } 623 | 624 | /// Defines one possible fare. See 625 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] 626 | pub struct FareRule { 627 | /// ID of the referenced FareAttribute. 628 | pub fare_id: String, 629 | /// ID of a [Route] associated with the fare class 630 | pub route_id: Option, 631 | /// Identifies an origin zone. References a [Stop].zone_id 632 | pub origin_id: Option, 633 | /// Identifies an destination zone. References a [Stop].zone_id 634 | pub destination_id: Option, 635 | /// Identifies the zones that a rider will enter while using a given fare class. References a [Stop].zone_id 636 | pub contains_id: Option, 637 | } 638 | 639 | /// A [Frequency] before being merged into the corresponding [Trip] 640 | #[derive(Clone, Debug, Serialize, Deserialize, Default)] 641 | pub struct RawFrequency { 642 | /// References the [Trip] that uses frequency 643 | pub trip_id: String, 644 | /// Time at which the first vehicle departs from the first stop of the trip 645 | #[serde( 646 | deserialize_with = "deserialize_time", 647 | serialize_with = "serialize_time" 648 | )] 649 | pub start_time: u32, 650 | /// Time at which service changes to a different headway (or ceases) at the first stop in the trip 651 | #[serde( 652 | deserialize_with = "deserialize_time", 653 | serialize_with = "serialize_time" 654 | )] 655 | pub end_time: u32, 656 | /// Time, in seconds, between departures from the same stop (headway) for the trip, during the time interval specified by start_time and end_time 657 | pub headway_secs: u32, 658 | /// Indicates the type of service for a trip 659 | pub exact_times: Option, 660 | } 661 | 662 | /// Timetables can be defined by the frequency of their vehicles. See <> 663 | #[derive(Clone, Debug, Default, Deserialize, Serialize)] 664 | pub struct Frequency { 665 | /// Time at which the first vehicle departs from the first stop of the trip 666 | pub start_time: u32, 667 | /// Time at which service changes to a different headway (or ceases) at the first stop in the trip 668 | pub end_time: u32, 669 | /// Time, in seconds, between departures from the same stop (headway) for the trip, during the time interval specified by start_time and end_time 670 | pub headway_secs: u32, 671 | /// Indicates the type of service for a trip 672 | pub exact_times: Option, 673 | } 674 | 675 | impl Frequency { 676 | /// Converts from a [RawFrequency] to a [Frequency] 677 | pub fn from(frequency: &RawFrequency) -> Self { 678 | Self { 679 | start_time: frequency.start_time, 680 | end_time: frequency.end_time, 681 | headway_secs: frequency.headway_secs, 682 | exact_times: frequency.exact_times, 683 | } 684 | } 685 | } 686 | 687 | /// Transfer information between stops before merged into [Stop] 688 | #[derive(Clone, Debug, Serialize, Deserialize, Default)] 689 | pub struct RawTransfer { 690 | /// Stop from which to leave 691 | pub from_stop_id: String, 692 | /// Stop which to transfer to 693 | pub to_stop_id: String, 694 | /// Type of the transfer 695 | pub transfer_type: TransferType, 696 | /// Minimum time needed to make the transfer in seconds 697 | pub min_transfer_time: Option, 698 | } 699 | 700 | #[derive(Clone, Debug, Default, Deserialize, Serialize)] 701 | /// Transfer information between stops 702 | pub struct StopTransfer { 703 | /// Stop which to transfer to 704 | pub to_stop_id: String, 705 | /// Type of the transfer 706 | pub transfer_type: TransferType, 707 | /// Minimum time needed to make the transfer in seconds 708 | pub min_transfer_time: Option, 709 | } 710 | 711 | impl From for StopTransfer { 712 | /// Converts from a [RawTransfer] to a [StopTransfer] 713 | fn from(transfer: RawTransfer) -> Self { 714 | Self { 715 | to_stop_id: transfer.to_stop_id, 716 | transfer_type: transfer.transfer_type, 717 | min_transfer_time: transfer.min_transfer_time, 718 | } 719 | } 720 | } 721 | 722 | /// Meta-data about the feed. See 723 | #[derive(Clone, Debug, Serialize, Deserialize)] 724 | pub struct FeedInfo { 725 | /// Full name of the organization that publishes the dataset. 726 | #[serde(rename = "feed_publisher_name")] 727 | pub name: String, 728 | /// URL of the dataset publishing organization's website 729 | #[serde(rename = "feed_publisher_url")] 730 | pub url: String, 731 | /// Default language used for the text in this dataset 732 | #[serde(rename = "feed_lang")] 733 | pub lang: String, 734 | /// Defines the language that should be used when the data consumer doesn’t know the language of the rider 735 | pub default_lang: Option, 736 | /// The dataset provides complete and reliable schedule information for service in the period from this date 737 | #[serde( 738 | deserialize_with = "deserialize_option_date", 739 | serialize_with = "serialize_option_date", 740 | rename = "feed_start_date", 741 | default 742 | )] 743 | pub start_date: Option, 744 | ///The dataset provides complete and reliable schedule information for service in the period until this date 745 | #[serde( 746 | deserialize_with = "deserialize_option_date", 747 | serialize_with = "serialize_option_date", 748 | rename = "feed_end_date", 749 | default 750 | )] 751 | pub end_date: Option, 752 | /// String that indicates the current version of their GTFS dataset 753 | #[serde(rename = "feed_version")] 754 | pub version: Option, 755 | /// Email address for communication regarding the GTFS dataset and data publishing practices 756 | #[serde(rename = "feed_contact_email")] 757 | pub contact_email: Option, 758 | /// URL for contact information, a web-form, support desk, or other tools for communication regarding the GTFS dataset and data publishing practices 759 | #[serde(rename = "feed_contact_url")] 760 | pub contact_url: Option, 761 | } 762 | 763 | impl fmt::Display for FeedInfo { 764 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 765 | write!(f, "{}", self.name) 766 | } 767 | } 768 | 769 | /// A graph representation to describe subway or train, with nodes (the locations) and edges (the pathways). 770 | #[derive(Clone, Debug, Serialize, Deserialize, Default)] 771 | pub struct RawPathway { 772 | /// Uniquely identifies the pathway 773 | #[serde(rename = "pathway_id")] 774 | pub id: String, 775 | /// Location at which the pathway begins 776 | pub from_stop_id: String, 777 | /// Location at which the pathway ends 778 | pub to_stop_id: String, 779 | /// Type of pathway between the specified (from_stop_id, to_stop_id) pair 780 | #[serde(rename = "pathway_mode")] 781 | pub mode: PathwayMode, 782 | /// Indicates in which direction the pathway can be used 783 | pub is_bidirectional: PathwayDirectionType, 784 | /// Horizontal length in meters of the pathway from the origin location to the destination location 785 | pub length: Option, 786 | /// Average time in seconds needed to walk through the pathway from the origin location to the destination location 787 | pub traversal_time: Option, 788 | /// Number of stairs of the pathway 789 | pub stair_count: Option, 790 | /// Maximum slope ratio of the pathway 791 | pub max_slope: Option, 792 | /// Minimum width of the pathway in meters 793 | pub min_width: Option, 794 | /// String of text from physical signage visible to transit riders 795 | pub signposted_as: Option, 796 | /// Same than the signposted_as field, but when the pathways is used backward 797 | pub reversed_signposted_as: Option, 798 | } 799 | 800 | impl Id for RawPathway { 801 | fn id(&self) -> &str { 802 | &self.id 803 | } 804 | } 805 | 806 | impl Type for RawPathway { 807 | fn object_type(&self) -> ObjectType { 808 | ObjectType::Pathway 809 | } 810 | } 811 | 812 | /// Pathway going from a stop to another. 813 | #[derive(Debug, Clone, Default, Serialize, Deserialize)] 814 | pub struct Pathway { 815 | /// Uniquely identifies the pathway 816 | pub id: String, 817 | /// Location at which the pathway ends 818 | pub to_stop_id: String, 819 | /// Type of pathway between the specified (from_stop_id, to_stop_id) pair 820 | pub mode: PathwayMode, 821 | /// Indicates in which direction the pathway can be used 822 | pub is_bidirectional: PathwayDirectionType, 823 | /// Horizontal length in meters of the pathway from the origin location to the destination location 824 | pub length: Option, 825 | /// Average time in seconds needed to walk through the pathway from the origin location to the destination location 826 | pub traversal_time: Option, 827 | /// Number of stairs of the pathway 828 | pub stair_count: Option, 829 | /// Maximum slope ratio of the pathway 830 | pub max_slope: Option, 831 | /// Minimum width of the pathway in meters 832 | pub min_width: Option, 833 | /// String of text from physical signage visible to transit riders 834 | pub signposted_as: Option, 835 | /// Same than the signposted_as field, but when the pathways is used backward 836 | pub reversed_signposted_as: Option, 837 | } 838 | 839 | impl Id for Pathway { 840 | fn id(&self) -> &str { 841 | &self.id 842 | } 843 | } 844 | 845 | impl Type for Pathway { 846 | fn object_type(&self) -> ObjectType { 847 | ObjectType::Pathway 848 | } 849 | } 850 | 851 | impl From for Pathway { 852 | /// Converts from a [RawPathway] to a [Pathway] 853 | fn from(raw: RawPathway) -> Self { 854 | Self { 855 | id: raw.id, 856 | to_stop_id: raw.to_stop_id, 857 | mode: raw.mode, 858 | is_bidirectional: raw.is_bidirectional, 859 | length: raw.length, 860 | max_slope: raw.max_slope, 861 | min_width: raw.min_width, 862 | reversed_signposted_as: raw.reversed_signposted_as, 863 | signposted_as: raw.signposted_as, 864 | stair_count: raw.stair_count, 865 | traversal_time: raw.traversal_time, 866 | } 867 | } 868 | } 869 | 870 | /// Format of the data 871 | #[derive(Clone, Debug, Serialize, PartialEq)] 872 | pub enum SourceFormat { 873 | /// `Directory` means the data comes from a directory 874 | Directory, 875 | /// `Zip` means the data were read from a zip 876 | Zip, 877 | } 878 | -------------------------------------------------------------------------------- /src/raw_gtfs.rs: -------------------------------------------------------------------------------- 1 | use crate::objects::*; 2 | use crate::Error; 3 | use crate::GtfsReader; 4 | use std::path::Path; 5 | use std::time::Duration; 6 | 7 | /// Data structure that map the GTFS csv with little intelligence 8 | /// 9 | /// This is used to analyze the GTFS and detect anomalies 10 | /// To manipulate the transit data, maybe [crate::Gtfs] will be more convienient 11 | #[derive(Debug)] 12 | pub struct RawGtfs { 13 | /// Time needed to read and parse the archive 14 | pub read_duration: Duration, 15 | /// All Calendar, None if the file was absent as it is not mandatory 16 | pub calendar: Option, Error>>, 17 | /// All Calendar dates, None if the file was absent as it is not mandatory 18 | pub calendar_dates: Option, Error>>, 19 | /// All Stops 20 | pub stops: Result, Error>, 21 | /// All Routes 22 | pub routes: Result, Error>, 23 | /// All Trips 24 | pub trips: Result, Error>, 25 | /// All Agencies 26 | pub agencies: Result, Error>, 27 | /// All shapes points, None if the file was absent as it is not mandatory 28 | pub shapes: Option, Error>>, 29 | /// All FareAttribates, None if the file was absent as it is not mandatory 30 | pub fare_attributes: Option, Error>>, 31 | /// All FareRules, None if the file was absent as it is not mandatory 32 | pub fare_rules: Option, Error>>, 33 | /// All Frequencies, None if the file was absent as it is not mandatory 34 | pub frequencies: Option, Error>>, 35 | /// All Transfers, None if the file was absent as it is not mandatory 36 | pub transfers: Option, Error>>, 37 | /// All Pathways, None if the file was absent as it is not mandatory 38 | pub pathways: Option, Error>>, 39 | /// All FeedInfo, None if the file was absent as it is not mandatory 40 | pub feed_info: Option, Error>>, 41 | /// All StopTimes 42 | pub stop_times: Result, Error>, 43 | /// All files that are present in the feed 44 | pub files: Vec, 45 | /// Format of the data read 46 | pub source_format: SourceFormat, 47 | /// sha256 sum of the feed 48 | pub sha256: Option, 49 | /// All translations, None if the file was absent as it is not mandatory 50 | pub translations: Option, Error>>, 51 | } 52 | 53 | impl RawGtfs { 54 | /// Prints on stdout some basic statistics about the GTFS file (numbers of elements for each object). Mostly to be sure that everything was read 55 | pub fn print_stats(&self) { 56 | println!("GTFS data:"); 57 | println!(" Read in {:?}", self.read_duration); 58 | println!(" Stops: {}", mandatory_file_summary(&self.stops)); 59 | println!(" Routes: {}", mandatory_file_summary(&self.routes)); 60 | println!(" Trips: {}", mandatory_file_summary(&self.trips)); 61 | println!(" Agencies: {}", mandatory_file_summary(&self.agencies)); 62 | println!(" Stop times: {}", mandatory_file_summary(&self.stop_times)); 63 | println!(" Shapes: {}", optional_file_summary(&self.shapes)); 64 | println!(" Fares: {}", optional_file_summary(&self.fare_attributes)); 65 | println!( 66 | " Frequencies: {}", 67 | optional_file_summary(&self.frequencies) 68 | ); 69 | println!(" Transfers: {}", optional_file_summary(&self.transfers)); 70 | println!(" Pathways: {}", optional_file_summary(&self.pathways)); 71 | println!(" Feed info: {}", optional_file_summary(&self.feed_info)); 72 | println!( 73 | " Translations: {}", 74 | optional_file_summary(&self.translations) 75 | ); 76 | } 77 | 78 | /// Reads from an url (if starts with http), or a local path (either a directory or zipped file) 79 | /// 80 | /// To read from an url, build with read-url feature 81 | /// See also [RawGtfs::from_url] and [RawGtfs::from_path] if you don’t want the library to guess 82 | pub fn new(gtfs: &str) -> Result { 83 | GtfsReader::default().raw().read(gtfs) 84 | } 85 | 86 | /// Reads the raw GTFS from a local zip archive or local directory 87 | pub fn from_path

(path: P) -> Result 88 | where 89 | P: AsRef, 90 | { 91 | GtfsReader::default().raw().read_from_path(path) 92 | } 93 | 94 | /// Reads the raw GTFS from a remote url 95 | /// 96 | /// The library must be built with the read-url feature 97 | #[cfg(feature = "read-url")] 98 | pub fn from_url(url: U) -> Result { 99 | GtfsReader::default().raw().read_from_url(url) 100 | } 101 | 102 | /// Non-blocking read the raw GTFS from a remote url 103 | /// 104 | /// The library must be built with the read-url feature 105 | #[cfg(feature = "read-url")] 106 | pub async fn from_url_async(url: U) -> Result { 107 | GtfsReader::default().raw().read_from_url_async(url).await 108 | } 109 | 110 | /// Reads for any object implementing [std::io::Read] and [std::io::Seek] 111 | /// 112 | /// Mostly an internal function that abstracts reading from an url or local file 113 | pub fn from_reader(reader: T) -> Result { 114 | GtfsReader::default().raw().read_from_reader(reader) 115 | } 116 | 117 | pub(crate) fn unknown_to_default(&mut self) { 118 | if let Ok(stops) = &mut self.stops { 119 | for stop in stops.iter_mut() { 120 | if let LocationType::Unknown(_) = stop.location_type { 121 | stop.location_type = LocationType::default(); 122 | } 123 | if let Availability::Unknown(_) = stop.wheelchair_boarding { 124 | stop.wheelchair_boarding = Availability::default(); 125 | } 126 | } 127 | } 128 | if let Ok(stop_times) = &mut self.stop_times { 129 | for stop_time in stop_times.iter_mut() { 130 | if let PickupDropOffType::Unknown(_) = stop_time.pickup_type { 131 | stop_time.pickup_type = PickupDropOffType::default(); 132 | } 133 | if let PickupDropOffType::Unknown(_) = stop_time.drop_off_type { 134 | stop_time.drop_off_type = PickupDropOffType::default(); 135 | } 136 | if let ContinuousPickupDropOff::Unknown(_) = stop_time.continuous_pickup { 137 | stop_time.continuous_pickup = ContinuousPickupDropOff::default(); 138 | } 139 | if let ContinuousPickupDropOff::Unknown(_) = stop_time.continuous_drop_off { 140 | stop_time.continuous_drop_off = ContinuousPickupDropOff::default(); 141 | } 142 | } 143 | } 144 | if let Ok(trips) = &mut self.trips { 145 | for trip in trips.iter_mut() { 146 | if let Availability::Unknown(_) = trip.wheelchair_accessible { 147 | trip.wheelchair_accessible = Availability::default(); 148 | } 149 | if let BikesAllowedType::Unknown(_) = trip.bikes_allowed { 150 | trip.bikes_allowed = BikesAllowedType::default(); 151 | } 152 | } 153 | } 154 | } 155 | } 156 | 157 | fn mandatory_file_summary(objs: &Result, Error>) -> String { 158 | match objs { 159 | Ok(vec) => format!("{} objects", vec.len()), 160 | Err(e) => format!("Could not read {e}"), 161 | } 162 | } 163 | 164 | fn optional_file_summary(objs: &Option, Error>>) -> String { 165 | match objs { 166 | Some(objs) => mandatory_file_summary(objs), 167 | None => "File not present".to_string(), 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/serde_helpers.rs: -------------------------------------------------------------------------------- 1 | use chrono::NaiveDate; 2 | use rgb::RGB8; 3 | use serde::de::{self, Deserialize, Deserializer}; 4 | use serde::ser::Serializer; 5 | 6 | pub fn deserialize_date<'de, D>(deserializer: D) -> Result 7 | where 8 | D: Deserializer<'de>, 9 | { 10 | let s: &str = Deserialize::deserialize(deserializer)?; 11 | NaiveDate::parse_from_str(s, "%Y%m%d").map_err(serde::de::Error::custom) 12 | } 13 | 14 | pub fn serialize_date(date: &NaiveDate, serializer: S) -> Result 15 | where 16 | S: Serializer, 17 | { 18 | serializer.serialize_str(&date.format("%Y%m%d").to_string()) 19 | } 20 | 21 | pub fn deserialize_option_date<'de, D>(deserializer: D) -> Result, D::Error> 22 | where 23 | D: Deserializer<'de>, 24 | { 25 | let s = Option::<&str>::deserialize(deserializer)? 26 | .map(|s| NaiveDate::parse_from_str(s, "%Y%m%d").map_err(serde::de::Error::custom)); 27 | match s { 28 | Some(Ok(s)) => Ok(Some(s)), 29 | Some(Err(e)) => Err(e), 30 | None => Ok(None), 31 | } 32 | } 33 | 34 | pub fn serialize_option_date(date: &Option, serializer: S) -> Result 35 | where 36 | S: Serializer, 37 | { 38 | match date { 39 | None => serializer.serialize_none(), 40 | Some(d) => serialize_date(d, serializer), 41 | } 42 | } 43 | 44 | pub fn parse_time_impl(h: &str, m: &str, s: &str) -> Result { 45 | let hours: u32 = h.parse()?; 46 | let minutes: u32 = m.parse()?; 47 | let seconds: u32 = s.parse()?; 48 | Ok(hours * 3600 + minutes * 60 + seconds) 49 | } 50 | 51 | pub fn parse_time(s: &str) -> Result { 52 | let mk_err = || crate::Error::InvalidTime(s.to_owned()); 53 | 54 | if s.len() < 7 { 55 | Err(mk_err()) 56 | } else { 57 | let mut parts = s.split(':'); 58 | 59 | let hour = parts.next().ok_or_else(mk_err)?; 60 | let min = parts.next().ok_or_else(mk_err)?; 61 | let sec = parts.next().ok_or_else(mk_err)?; 62 | if parts.next().is_some() { 63 | return Err(mk_err()); 64 | } 65 | 66 | if min.len() != 2 || sec.len() != 2 { 67 | return Err(mk_err()); 68 | } 69 | 70 | parse_time_impl(hour, min, sec).map_err(|_| mk_err()) 71 | } 72 | } 73 | 74 | pub fn deserialize_time<'de, D>(deserializer: D) -> Result 75 | where 76 | D: Deserializer<'de>, 77 | { 78 | let s: &str = Deserialize::deserialize(deserializer)?; 79 | parse_time(s).map_err(de::Error::custom) 80 | } 81 | 82 | pub fn serialize_time(time: &u32, serializer: S) -> Result 83 | where 84 | S: Serializer, 85 | { 86 | serializer.serialize_str( 87 | format!( 88 | "{:02}:{:02}:{:02}", 89 | time / 3600, 90 | time % 3600 / 60, 91 | time % 60 92 | ) 93 | .as_str(), 94 | ) 95 | } 96 | 97 | pub fn deserialize_optional_time<'de, D>(deserializer: D) -> Result, D::Error> 98 | where 99 | D: Deserializer<'de>, 100 | { 101 | let s: Option<&str> = Deserialize::deserialize(deserializer)?; 102 | 103 | match s { 104 | None => Ok(None), 105 | Some(t) => parse_time(t).map(Some).map_err(de::Error::custom), 106 | } 107 | } 108 | 109 | pub fn serialize_optional_time(time: &Option, serializer: S) -> Result 110 | where 111 | S: Serializer, 112 | { 113 | match time { 114 | None => serializer.serialize_none(), 115 | Some(t) => serialize_time(t, serializer), 116 | } 117 | } 118 | 119 | pub fn de_with_optional_float<'de, D>(de: D) -> Result, D::Error> 120 | where 121 | D: Deserializer<'de>, 122 | { 123 | String::deserialize(de).and_then(|s| { 124 | if s.is_empty() { 125 | Ok(None) 126 | } else { 127 | s.parse().map(Some).map_err(de::Error::custom) 128 | } 129 | }) 130 | } 131 | 132 | pub fn serialize_float_as_str(float: &Option, serializer: S) -> Result 133 | where 134 | S: Serializer, 135 | { 136 | match float { 137 | None => serializer.serialize_str(""), 138 | Some(f) => serializer.serialize_str(&f.to_string()), 139 | } 140 | } 141 | 142 | pub fn parse_color( 143 | s: &str, 144 | default: impl std::ops::FnOnce() -> RGB8, 145 | ) -> Result { 146 | if s.is_empty() { 147 | return Ok(default()); 148 | } 149 | if s.len() != 6 { 150 | return Err(crate::Error::InvalidColor(s.to_owned())); 151 | } 152 | let r = 153 | u8::from_str_radix(&s[0..2], 16).map_err(|_| crate::Error::InvalidColor(s.to_owned()))?; 154 | let g = 155 | u8::from_str_radix(&s[2..4], 16).map_err(|_| crate::Error::InvalidColor(s.to_owned()))?; 156 | let b = 157 | u8::from_str_radix(&s[4..6], 16).map_err(|_| crate::Error::InvalidColor(s.to_owned()))?; 158 | Ok(RGB8::new(r, g, b)) 159 | } 160 | 161 | pub fn deserialize_route_color<'de, D>(de: D) -> Result 162 | where 163 | D: Deserializer<'de>, 164 | { 165 | String::deserialize(de) 166 | .and_then(|s| parse_color(&s, default_route_color).map_err(de::Error::custom)) 167 | } 168 | 169 | pub fn deserialize_route_text_color<'de, D>(de: D) -> Result 170 | where 171 | D: Deserializer<'de>, 172 | { 173 | String::deserialize(de).and_then(|s| parse_color(&s, RGB8::default).map_err(de::Error::custom)) 174 | } 175 | 176 | pub fn serialize_color(color: &RGB8, serializer: S) -> Result 177 | where 178 | S: Serializer, 179 | { 180 | serializer.serialize_str(format!("{:02X}{:02X}{:02X}", color.r, color.g, color.b).as_str()) 181 | } 182 | 183 | pub fn default_route_color() -> RGB8 { 184 | RGB8::new(255, 255, 255) 185 | } 186 | 187 | pub fn de_with_empty_default<'de, T, D>(de: D) -> Result 188 | where 189 | D: Deserializer<'de>, 190 | T: Deserialize<'de> + Default, 191 | { 192 | Option::::deserialize(de).map(|opt| opt.unwrap_or_default()) 193 | } 194 | 195 | pub fn deserialize_bool<'de, D>(deserializer: D) -> Result 196 | where 197 | D: Deserializer<'de>, 198 | { 199 | let s: &str = Deserialize::deserialize(deserializer)?; 200 | match s { 201 | "0" => Ok(false), 202 | "1" => Ok(true), 203 | &_ => Err(serde::de::Error::custom(format!( 204 | "Invalid value `{s}`, expected 0 or 1" 205 | ))), 206 | } 207 | } 208 | 209 | pub fn serialize_bool(value: &bool, serializer: S) -> Result 210 | where 211 | S: Serializer, 212 | { 213 | serializer.serialize_u8(u8::from(*value)) 214 | } 215 | 216 | #[test] 217 | fn test_serialize_time() { 218 | #[derive(Serialize, Deserialize)] 219 | struct Test { 220 | #[serde( 221 | deserialize_with = "deserialize_time", 222 | serialize_with = "serialize_time" 223 | )] 224 | time: u32, 225 | } 226 | let data_in = "time\n01:01:01\n"; 227 | let parsed: Test = csv::Reader::from_reader(data_in.as_bytes()) 228 | .deserialize() 229 | .next() 230 | .unwrap() 231 | .unwrap(); 232 | assert_eq!(3600 + 60 + 1, parsed.time); 233 | 234 | let data_in_long_ride = "time\n172:35:42\n"; 235 | let parsed_long_ride: Test = csv::Reader::from_reader(data_in_long_ride.as_bytes()) 236 | .deserialize() 237 | .next() 238 | .unwrap() 239 | .unwrap(); 240 | assert_eq!((172 * 3600) + (35 * 60) + 42, parsed_long_ride.time); 241 | 242 | let mut wtr = csv::Writer::from_writer(vec![]); 243 | wtr.serialize(parsed).unwrap(); 244 | let data_out = String::from_utf8(wtr.into_inner().unwrap()).unwrap(); 245 | assert_eq!(data_in, data_out); 246 | } 247 | -------------------------------------------------------------------------------- /src/tests.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::objects::*; 4 | use crate::Gtfs; 5 | use crate::RawGtfs; 6 | use chrono::NaiveDate; 7 | use rgb::RGB8; 8 | 9 | #[test] 10 | fn serialization_deserialization() { 11 | // route, trip, stop, stop_times 12 | let gtfs = RawGtfs::from_path("fixtures/basic").expect("impossible to read gtfs"); 13 | 14 | let string = serde_json::to_string(>fs.routes.unwrap()).unwrap(); 15 | let _parsed: Vec = serde_json::from_str(&string).unwrap(); 16 | 17 | let string = serde_json::to_string(>fs.trips.unwrap()).unwrap(); 18 | let _parsed: Vec = serde_json::from_str(&string).unwrap(); 19 | 20 | let string = serde_json::to_string(>fs.stops.unwrap()).unwrap(); 21 | let _parsed: Vec = serde_json::from_str(&string).unwrap(); 22 | 23 | let string = serde_json::to_string(>fs.stop_times.unwrap()).unwrap(); 24 | let _parsed: Vec = serde_json::from_str(&string).unwrap(); 25 | 26 | let string = serde_json::to_string(>fs.frequencies.unwrap().unwrap()).unwrap(); 27 | let _parsed: Vec = serde_json::from_str(&string).unwrap(); 28 | 29 | let string = serde_json::to_string(>fs.pathways.unwrap().unwrap()).unwrap(); 30 | let _parsed: Vec = serde_json::from_str(&string).unwrap(); 31 | 32 | let string = serde_json::to_string(>fs.transfers.unwrap().unwrap()).unwrap(); 33 | let _parsed: Vec = serde_json::from_str(&string).unwrap(); 34 | } 35 | #[test] 36 | fn read_calendar() { 37 | let gtfs = Gtfs::from_path("fixtures/basic").expect("impossible to read gtfs"); 38 | assert_eq!(1, gtfs.calendar.len()); 39 | assert!(!gtfs.calendar["service1"].monday); 40 | assert!(gtfs.calendar["service1"].saturday); 41 | } 42 | 43 | #[test] 44 | fn read_calendar_dates() { 45 | let gtfs = Gtfs::from_path("fixtures/basic").expect("impossible to read gtfs"); 46 | assert_eq!(2, gtfs.calendar_dates.len()); 47 | assert_eq!(2, gtfs.calendar_dates["service1"].len()); 48 | assert_eq!( 49 | Exception::Deleted, 50 | gtfs.calendar_dates["service1"][0].exception_type 51 | ); 52 | assert_eq!( 53 | Exception::Added, 54 | gtfs.calendar_dates["service2"][0].exception_type 55 | ); 56 | } 57 | 58 | #[test] 59 | fn read_stop() { 60 | let gtfs = Gtfs::from_path("fixtures/basic").expect("impossible to read gtfs"); 61 | assert_eq!(6, gtfs.stops.len()); 62 | assert_eq!( 63 | LocationType::StopArea, 64 | gtfs.get_stop("stop1").unwrap().location_type 65 | ); 66 | assert_eq!( 67 | LocationType::StopPoint, 68 | gtfs.get_stop("stop2").unwrap().location_type 69 | ); 70 | assert_eq!(Some(48.796_058), gtfs.get_stop("stop2").unwrap().latitude); 71 | assert_eq!( 72 | Some("1".to_owned()), 73 | gtfs.get_stop("stop3").unwrap().parent_station 74 | ); 75 | assert_eq!( 76 | LocationType::GenericNode, 77 | gtfs.get_stop("stop6").unwrap().location_type 78 | ); 79 | assert_eq!(None, gtfs.get_stop("stop6").unwrap().latitude); 80 | } 81 | 82 | #[test] 83 | fn read_routes() { 84 | let gtfs = Gtfs::from_path("fixtures/basic").expect("impossible to read gtfs"); 85 | assert_eq!(3, gtfs.routes.len()); 86 | assert_eq!(RouteType::Bus, gtfs.get_route("1").unwrap().route_type); 87 | assert_eq!(RGB8::new(0, 0, 0), gtfs.get_route("1").unwrap().color); 88 | assert_eq!( 89 | RGB8::new(255, 255, 255), 90 | gtfs.get_route("1").unwrap().text_color 91 | ); 92 | assert_eq!(RGB8::new(0, 0, 0), gtfs.get_route("1").unwrap().color); 93 | assert_eq!( 94 | RGB8::new(0, 0, 0), 95 | gtfs.get_route("default_colors").unwrap().text_color 96 | ); 97 | assert_eq!( 98 | RGB8::new(255, 255, 255), 99 | gtfs.get_route("default_colors").unwrap().color 100 | ); 101 | assert_eq!(Some(1), gtfs.get_route("1").unwrap().order); 102 | assert_eq!( 103 | RouteType::Other(42), 104 | gtfs.get_route("invalid_type").unwrap().route_type 105 | ); 106 | } 107 | 108 | #[test] 109 | fn read_trips() { 110 | let gtfs = Gtfs::from_path("fixtures/basic").expect("impossible to read gtfs"); 111 | assert_eq!(1, gtfs.trips.len()); 112 | } 113 | 114 | #[test] 115 | fn read_stop_times() { 116 | let gtfs = Gtfs::from_path("fixtures/basic").expect("impossible to read gtfs"); 117 | let stop_times = >fs.trips.get("trip1").unwrap().stop_times; 118 | assert_eq!(3, stop_times.len()); 119 | assert_eq!(PickupDropOffType::Regular, stop_times[0].pickup_type); 120 | assert_eq!(PickupDropOffType::NotAvailable, stop_times[0].drop_off_type); 121 | assert_eq!(PickupDropOffType::ArrangeByPhone, stop_times[1].pickup_type); 122 | assert_eq!(PickupDropOffType::Regular, stop_times[1].drop_off_type); 123 | assert_eq!( 124 | PickupDropOffType::Unknown(-999), 125 | stop_times[2].drop_off_type 126 | ); 127 | assert_eq!(TimepointType::Exact, stop_times[0].timepoint); 128 | assert_eq!(TimepointType::Approximate, stop_times[1].timepoint); 129 | } 130 | 131 | #[test] 132 | fn read_frequencies() { 133 | let gtfs = Gtfs::from_path("fixtures/basic").expect("impossible to read gtfs"); 134 | let trip = >fs.trips.get("trip1").unwrap(); 135 | assert_eq!(1, trip.frequencies.len()); 136 | let frequency = &trip.frequencies[0]; 137 | assert_eq!(19800, frequency.start_time); 138 | } 139 | 140 | #[test] 141 | fn read_agencies() { 142 | let gtfs = Gtfs::from_path("fixtures/basic").expect("impossible to read gtfs"); 143 | let agencies = >fs.agencies; 144 | assert_eq!("BIBUS", agencies[0].name); 145 | assert_eq!("http://www.bibus.fr", agencies[0].url); 146 | assert_eq!("Europe/Paris", agencies[0].timezone); 147 | } 148 | 149 | #[test] 150 | fn read_shapes() { 151 | let gtfs = Gtfs::from_path("fixtures/basic").expect("impossible to read gtfs"); 152 | let shapes = >fs.shapes; 153 | assert_eq!(37.61956, shapes["A_shp"][0].latitude); 154 | assert_eq!(-122.48161, shapes["A_shp"][0].longitude); 155 | } 156 | 157 | #[test] 158 | fn read_fare_attributes() { 159 | let gtfs = Gtfs::from_path("fixtures/basic").expect("impossible to read gtfs"); 160 | assert_eq!(1, gtfs.fare_attributes.len()); 161 | assert_eq!("1.50", gtfs.get_fare_attributes("50").unwrap().price); 162 | assert_eq!("EUR", gtfs.get_fare_attributes("50").unwrap().currency); 163 | assert_eq!( 164 | PaymentMethod::Aboard, 165 | gtfs.get_fare_attributes("50").unwrap().payment_method 166 | ); 167 | assert_eq!( 168 | Transfers::Unlimited, 169 | gtfs.get_fare_attributes("50").unwrap().transfers 170 | ); 171 | assert_eq!( 172 | Some("1".to_string()), 173 | gtfs.get_fare_attributes("50").unwrap().agency_id 174 | ); 175 | assert_eq!( 176 | Some(3600), 177 | gtfs.get_fare_attributes("50").unwrap().transfer_duration 178 | ); 179 | } 180 | 181 | #[test] 182 | fn read_transfers() { 183 | let gtfs = Gtfs::from_path("fixtures/basic").expect("impossible to read gtfs"); 184 | assert_eq!(1, gtfs.get_stop("stop3").unwrap().transfers.len()); 185 | assert_eq!(1, gtfs.get_stop("stop1").unwrap().transfers.len()); 186 | 187 | assert_eq!( 188 | "stop5", 189 | gtfs.get_stop("stop3").unwrap().transfers[0].to_stop_id 190 | ); 191 | assert_eq!( 192 | "stop2", 193 | gtfs.get_stop("stop1").unwrap().transfers[0].to_stop_id 194 | ); 195 | assert_eq!( 196 | TransferType::Recommended, 197 | gtfs.get_stop("stop3").unwrap().transfers[0].transfer_type 198 | ); 199 | assert_eq!( 200 | TransferType::Impossible, 201 | gtfs.get_stop("stop1").unwrap().transfers[0].transfer_type 202 | ); 203 | assert_eq!( 204 | Some(60), 205 | gtfs.get_stop("stop3").unwrap().transfers[0].min_transfer_time 206 | ); 207 | assert_eq!( 208 | None, 209 | gtfs.get_stop("stop1").unwrap().transfers[0].min_transfer_time 210 | ); 211 | assert_eq!( 212 | TransferType::Recommended, 213 | gtfs.get_stop("stop2").unwrap().transfers[0].transfer_type 214 | ); 215 | assert_eq!( 216 | TransferType::StayOnBoard, 217 | gtfs.get_stop("stop5").unwrap().transfers[0].transfer_type 218 | ); 219 | assert_eq!( 220 | TransferType::MustAlight, 221 | gtfs.get_stop("stop5").unwrap().transfers[1].transfer_type 222 | ); 223 | } 224 | 225 | #[test] 226 | fn read_pathways() { 227 | let gtfs = Gtfs::from_path("fixtures/basic").expect("impossible to read gtfs"); 228 | 229 | let pathways = >fs.get_stop("stop1").unwrap().pathways; 230 | 231 | assert_eq!(1, pathways.len()); 232 | assert_eq!("stop3", pathways[0].to_stop_id); 233 | assert_eq!(PathwayMode::Walkway, pathways[0].mode); 234 | assert_eq!( 235 | PathwayDirectionType::Unidirectional, 236 | pathways[0].is_bidirectional 237 | ); 238 | assert_eq!(None, pathways[0].min_width); 239 | } 240 | 241 | #[test] 242 | fn read_translations() { 243 | let gtfs = RawGtfs::from_path("fixtures/basic").expect("impossible to read gtfs"); 244 | let translation = >fs.translations.unwrap().unwrap()[0]; 245 | assert_eq!(translation.table_name, "stops"); 246 | assert_eq!(translation.field_name, "stop_name"); 247 | assert_eq!(translation.language, "nl"); 248 | assert_eq!(translation.translation, "Stop Gebied"); 249 | assert_eq!(translation.field_value, None); 250 | } 251 | 252 | #[test] 253 | fn read_feed_info() { 254 | let gtfs = Gtfs::from_path("fixtures/basic").expect("impossible to read gtfs"); 255 | let feed = >fs.feed_info; 256 | assert_eq!(1, feed.len()); 257 | assert_eq!("SNCF", feed[0].name); 258 | assert_eq!("http://www.sncf.com", feed[0].url); 259 | assert_eq!("fr", feed[0].lang); 260 | assert_eq!(NaiveDate::from_ymd_opt(2018, 7, 9), feed[0].start_date); 261 | assert_eq!(NaiveDate::from_ymd_opt(2018, 9, 27), feed[0].end_date); 262 | assert_eq!(Some("0.3".to_string()), feed[0].version); 263 | } 264 | 265 | #[test] 266 | fn trip_days() { 267 | let gtfs = Gtfs::from_path("fixtures/basic/").unwrap(); 268 | let days = gtfs.trip_days("service1", NaiveDate::from_ymd_opt(2017, 1, 1).unwrap()); 269 | assert_eq!(vec![6, 7, 13, 14], days); 270 | 271 | let days2 = gtfs.trip_days("service2", NaiveDate::from_ymd_opt(2017, 1, 1).unwrap()); 272 | assert_eq!(vec![0], days2); 273 | } 274 | 275 | #[test] 276 | fn trip_clone() { 277 | let gtfs = Gtfs::from_path("fixtures/basic/").unwrap(); 278 | let _: Trip = gtfs.trips.get("trip1").unwrap().clone(); 279 | } 280 | 281 | #[test] 282 | fn read_from_gtfs() { 283 | let gtfs = Gtfs::from_path("fixtures/zips/gtfs.zip").unwrap(); 284 | assert_eq!(1, gtfs.calendar.len()); 285 | assert_eq!(2, gtfs.calendar_dates.len()); 286 | assert_eq!(5, gtfs.stops.len()); 287 | assert_eq!(1, gtfs.routes.len()); 288 | assert_eq!(1, gtfs.trips.len()); 289 | assert_eq!(1, gtfs.shapes.len()); 290 | assert_eq!(1, gtfs.fare_attributes.len()); 291 | assert_eq!(1, gtfs.feed_info.len()); 292 | assert_eq!(2, gtfs.get_trip("trip1").unwrap().stop_times.len()); 293 | 294 | assert!(gtfs.get_calendar("service1").is_ok()); 295 | assert!(gtfs.get_calendar_date("service1").is_ok()); 296 | assert!(gtfs.get_stop("stop1").is_ok()); 297 | assert!(gtfs.get_route("1").is_ok()); 298 | assert!(gtfs.get_trip("trip1").is_ok()); 299 | assert!(gtfs.get_fare_attributes("50").is_ok()); 300 | 301 | assert!(gtfs.get_stop("Utopia").is_err()); 302 | } 303 | 304 | #[test] 305 | fn read_from_subdirectory() { 306 | let gtfs = Gtfs::from_path("fixtures/zips/subdirectory.zip").unwrap(); 307 | assert_eq!(1, gtfs.calendar.len()); 308 | assert_eq!(2, gtfs.calendar_dates.len()); 309 | assert_eq!(5, gtfs.stops.len()); 310 | assert_eq!(1, gtfs.routes.len()); 311 | assert_eq!(1, gtfs.trips.len()); 312 | assert_eq!(1, gtfs.shapes.len()); 313 | assert_eq!(1, gtfs.fare_attributes.len()); 314 | assert_eq!(2, gtfs.get_trip("trip1").unwrap().stop_times.len()); 315 | } 316 | 317 | #[test] 318 | fn display() { 319 | assert_eq!( 320 | "Sorano".to_owned(), 321 | format!( 322 | "{}", 323 | Stop { 324 | name: Some("Sorano".to_owned()), 325 | ..Stop::default() 326 | } 327 | ) 328 | ); 329 | 330 | assert_eq!( 331 | "Long route name".to_owned(), 332 | format!( 333 | "{}", 334 | Route { 335 | long_name: Some("Long route name".to_owned()), 336 | short_name: None, 337 | ..Route::default() 338 | } 339 | ) 340 | ); 341 | 342 | assert_eq!( 343 | "Short route name".to_owned(), 344 | format!( 345 | "{}", 346 | Route { 347 | short_name: Some("Short route name".to_owned()), 348 | long_name: None, 349 | ..Route::default() 350 | } 351 | ) 352 | ); 353 | } 354 | 355 | #[test] 356 | fn path_files() { 357 | let gtfs = RawGtfs::from_path("fixtures/basic").expect("impossible to read gtfs"); 358 | assert_eq!(gtfs.files.len(), 14); 359 | assert_eq!(gtfs.source_format, SourceFormat::Directory); 360 | assert!(gtfs.files.contains(&"agency.txt".to_owned())); 361 | } 362 | 363 | #[test] 364 | fn subdirectory_files() { 365 | // reading subdirectory does not work when reading from a path (it's useless since the path can be given explicitly) 366 | // Note: if its needed, an issue can be opened to discuss it 367 | let gtfs = RawGtfs::from_path("fixtures/subdirectory").expect("impossible to read gtfs"); 368 | // no files can be read 369 | assert!(gtfs.stops.is_err()); 370 | assert!(gtfs.routes.is_err()); 371 | assert!(gtfs.agencies.is_err()); 372 | 373 | assert_eq!(gtfs.files, vec!["gtfs".to_string()]); 374 | } 375 | 376 | #[test] 377 | fn zip_files() { 378 | let gtfs = RawGtfs::from_path("fixtures/zips/gtfs.zip").expect("impossible to read gtfs"); 379 | assert_eq!(gtfs.files.len(), 10); 380 | assert_eq!(gtfs.source_format, SourceFormat::Zip); 381 | assert!(gtfs.files.contains(&"agency.txt".to_owned())); 382 | } 383 | 384 | #[test] 385 | fn zip_subdirectory_files() { 386 | let gtfs = 387 | RawGtfs::from_path("fixtures/zips/subdirectory.zip").expect("impossible to read gtfs"); 388 | assert_eq!(gtfs.files.len(), 11); 389 | assert_eq!(gtfs.source_format, SourceFormat::Zip); 390 | assert!(gtfs.files.contains(&"subdirectory/agency.txt".to_owned())); 391 | } 392 | 393 | #[test] 394 | fn compute_sha256() { 395 | let gtfs = RawGtfs::from_path("fixtures/zips/gtfs.zip").expect("impossible to read gtfs"); 396 | assert_eq!( 397 | gtfs.sha256, 398 | Some("4a262ae109101ffbd1629b67e080a2b074afdaa60d57684db0e1a31c0a1e75b0".to_owned()) 399 | ); 400 | } 401 | 402 | #[test] 403 | fn test_bom() { 404 | let gtfs = 405 | RawGtfs::from_path("fixtures/zips/gtfs_with_bom.zip").expect("impossible to read gtfs"); 406 | assert_eq!(gtfs.agencies.expect("agencies missing").len(), 2); 407 | } 408 | 409 | #[test] 410 | fn test_macosx() { 411 | let gtfs = RawGtfs::from_path("fixtures/zips/macosx.zip").expect("impossible to read gtfs"); 412 | assert_eq!(gtfs.agencies.expect("agencies missing").len(), 2); 413 | assert_eq!(gtfs.stops.expect("stops missing").len(), 5); 414 | } 415 | 416 | #[test] 417 | fn read_missing_feed_dates() { 418 | let gtfs = Gtfs::from_path("fixtures/missing_feed_date").expect("impossible to read gtfs"); 419 | assert_eq!(1, gtfs.feed_info.len()); 420 | assert!(gtfs.feed_info[0].start_date.is_none()); 421 | } 422 | 423 | #[test] 424 | fn read_interpolated_stops() { 425 | let gtfs = 426 | Gtfs::from_path("fixtures/interpolated_stop_times").expect("impossible to read gtfs"); 427 | assert_eq!(1, gtfs.feed_info.len()); 428 | // the second stop have no departure/arrival, it should not cause any problems 429 | assert_eq!( 430 | gtfs.trips["trip1"].stop_times[1].stop.name, 431 | Some("Stop Point child of 1".to_owned()) 432 | ); 433 | assert!(gtfs.trips["trip1"].stop_times[1].arrival_time.is_none()); 434 | } 435 | 436 | #[test] 437 | fn read_only_required_fields() { 438 | let gtfs = Gtfs::from_path("fixtures/only_required_fields").expect("impossible to read gtfs"); 439 | let route = gtfs.routes.get("1").unwrap(); 440 | let fare_attribute = gtfs.fare_attributes.get("50").unwrap(); 441 | let feed = >fs.feed_info[0]; 442 | let shape = >fs.shapes.get("A_shp").unwrap()[0]; 443 | assert_eq!(route.color, RGB8::new(255, 255, 255)); 444 | assert_eq!(route.text_color, RGB8::new(0, 0, 0)); 445 | assert_eq!(fare_attribute.transfer_duration, None); 446 | assert_eq!(feed.start_date, None); 447 | assert_eq!(feed.end_date, None); 448 | assert_eq!(shape.dist_traveled, None); 449 | assert_eq!( 450 | TimepointType::Exact, 451 | gtfs.trips["trip1"].stop_times[0].timepoint 452 | ) 453 | } 454 | 455 | #[test] 456 | fn metra_gtfs() { 457 | let gtfs = Gtfs::from_path("fixtures/zips/metra.zip"); 458 | 459 | if let Err(err) = >fs { 460 | eprintln!("{:#?}", err); 461 | } 462 | 463 | assert!(gtfs.is_ok()); 464 | } 465 | 466 | #[test] 467 | fn sorted_shapes() { 468 | let gtfs = Gtfs::from_path("fixtures/basic").expect("impossible to read gtfs"); 469 | let shape = >fs.shapes.get("Unordered_shp").unwrap(); 470 | 471 | let points = shape 472 | .iter() 473 | .map(|s| (s.sequence, s.latitude, s.longitude)) 474 | .collect::>(); 475 | 476 | assert_eq!( 477 | points, 478 | vec![ 479 | (0, 37.61956, -122.48161), 480 | (6, 37.64430, -122.41070), 481 | (11, 37.65863, -122.30839), 482 | ] 483 | ); 484 | } 485 | 486 | #[test] 487 | fn fare_v1() { 488 | let gtfs = Gtfs::from_path("fixtures/fares_v1").expect("impossible to read gtfs"); 489 | 490 | let mut expected_attributes = HashMap::new(); 491 | expected_attributes.insert( 492 | "presto_fare".to_string(), 493 | FareAttribute { 494 | id: "presto_fare".to_string(), 495 | currency: "CAD".to_string(), 496 | price: "3.2".to_string(), 497 | payment_method: PaymentMethod::PreBoarding, 498 | transfer_duration: Some(7200), 499 | agency_id: None, 500 | transfers: Transfers::Unlimited, 501 | }, 502 | ); 503 | assert_eq!(gtfs.fare_attributes, expected_attributes); 504 | 505 | let mut expected_rules = HashMap::new(); 506 | expected_rules.insert( 507 | "presto_fare".to_string(), 508 | vec![ 509 | FareRule { 510 | fare_id: "presto_fare".to_string(), 511 | route_id: Some("line1".to_string()), 512 | origin_id: Some("ttc_subway_stations".to_string()), 513 | destination_id: Some("ttc_subway_stations".to_string()), 514 | contains_id: None, 515 | }, 516 | FareRule { 517 | fare_id: "presto_fare".to_string(), 518 | route_id: Some("line2".to_string()), 519 | origin_id: Some("ttc_subway_stations".to_string()), 520 | destination_id: Some("ttc_subway_stations".to_string()), 521 | contains_id: None, 522 | }, 523 | ], 524 | ); 525 | assert_eq!(gtfs.fare_rules, expected_rules); 526 | } 527 | --------------------------------------------------------------------------------