├── benchmarks └── CMakeLists.txt ├── docs ├── logo.jpeg └── CPP_STYLE.md ├── tests ├── data │ ├── sample_feed │ │ ├── calendar_dates.txt │ │ ├── levels.txt │ │ ├── fare_rules.txt │ │ ├── agency.txt │ │ ├── transfers.txt │ │ ├── feed_info.txt │ │ ├── translations.txt │ │ ├── attributions.txt │ │ ├── fare_attributes.txt │ │ ├── calendar.txt │ │ ├── pathways.txt │ │ ├── routes.txt │ │ ├── frequencies.txt │ │ ├── trips.txt │ │ ├── shapes.txt │ │ ├── stops.txt │ │ └── stop_times.txt │ └── output_feed │ │ ├── shapes.txt │ │ └── agency.txt ├── CMakeLists.txt └── unit_tests.cpp ├── .gitmodules ├── CMakeLists.txt ├── .gitignore ├── .github └── workflows │ └── ccpp.yml ├── .clang-format ├── LICENSE.MIT ├── README.md └── include └── just_gtfs └── just_gtfs.h /benchmarks/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/logo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mesozoic-drones/just_gtfs/HEAD/docs/logo.jpeg -------------------------------------------------------------------------------- /tests/data/sample_feed/calendar_dates.txt: -------------------------------------------------------------------------------- 1 | service_id,date,exception_type 2 | FULLW,20070604,2 -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "doctest"] 2 | path = doctest 3 | url = https://github.com/onqtam/doctest 4 | -------------------------------------------------------------------------------- /tests/data/sample_feed/levels.txt: -------------------------------------------------------------------------------- 1 | level_id,level_index,level_name 2 | U321L1,-1.5,"Vestibul" 3 | U321L2,-2,"Vestibul2" 4 | U321L0,0,"Povrch" -------------------------------------------------------------------------------- /tests/data/sample_feed/fare_rules.txt: -------------------------------------------------------------------------------- 1 | fare_id,route_id,origin_id,destination_id,contains_id 2 | p,AB,,, 3 | p,STBA,,, 4 | p,BFC,,, 5 | a,AAMV,,, -------------------------------------------------------------------------------- /tests/data/sample_feed/agency.txt: -------------------------------------------------------------------------------- 1 | agency_id,agency_name,agency_url,agency_timezone 2 | DTA,Demo Transit Authority,http://google.com,America/Los_Angeles -------------------------------------------------------------------------------- /tests/data/sample_feed/transfers.txt: -------------------------------------------------------------------------------- 1 | from_stop_id,to_stop_id,transfer_type,min_transfer_time 2 | 130,4,2,70 3 | 227,4,0,160 4 | 314,11,1, 5 | 385,11,2, -------------------------------------------------------------------------------- /tests/data/sample_feed/feed_info.txt: -------------------------------------------------------------------------------- 1 | feed_publisher_name,feed_publisher_url,feed_lang,feed_version,feed_license 2 | "Test Solutions, Inc.",http://test,en,, -------------------------------------------------------------------------------- /tests/data/sample_feed/translations.txt: -------------------------------------------------------------------------------- 1 | table_name,field_name,language,translation,record_id,record_sub_id,field_value 2 | stop_times,stop_headsign,en,"Downtown",,, -------------------------------------------------------------------------------- /tests/data/sample_feed/attributions.txt: -------------------------------------------------------------------------------- 1 | attribution_id,organization_name,is_producer,is_operator,is_authority,attribution_url 2 | 0,Test inc,1,0,0,"https://test.pl/gtfs/" -------------------------------------------------------------------------------- /tests/data/sample_feed/fare_attributes.txt: -------------------------------------------------------------------------------- 1 | fare_id,price,currency_type,payment_method,transfers,transfer_duration 2 | p,1.25,USD,0,0, 3 | a,5.25,USD,1,1, 4 | x,20,USD,0,,60 5 | -------------------------------------------------------------------------------- /tests/data/sample_feed/calendar.txt: -------------------------------------------------------------------------------- 1 | service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date 2 | FULLW,1,1,1,1,1,1,1,20070101,20101231 3 | WE,0,0,0,0,0,1,1,20070101,20101231 -------------------------------------------------------------------------------- /tests/data/output_feed/shapes.txt: -------------------------------------------------------------------------------- 1 | shape_id,shape_pt_lat,shape_pt_lon,shape_pt_sequence,shape_dist_traveled 2 | id1,61.197843,-149.867731,0,0.0 3 | id1,61.199419,-149.867680,1,178.0 4 | id2,61.199972,-149.867731,2,416.0 5 | -------------------------------------------------------------------------------- /tests/data/sample_feed/pathways.txt: -------------------------------------------------------------------------------- 1 | pathway_id,from_stop_id,to_stop_id,pathway_mode,signposted_as,reversed_signposted_as,is_bidirectional 2 | T-A01C01,1073S,1098E,2,"Sign1","Sign2",1 3 | T-A01D01,1075S,1118S,1,"Sign4",,0 4 | T-A01D01,1075N,1118N,1,,,1 -------------------------------------------------------------------------------- /tests/data/output_feed/agency.txt: -------------------------------------------------------------------------------- 1 | agency_id,agency_name,agency_url,agency_timezone,agency_lang,agency_phone,agency_fare_url,agency_email 2 | 0Id_b^3 Company,"Big Big ""Bus Company""",,,,,b3c.no,b3c@gtfs.com 3 | kwf,"""killer whale ferries""",,Asia/Tokyo,en,842,f@mail.com, 4 | -------------------------------------------------------------------------------- /tests/data/sample_feed/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 | AB,DTA,10,Airport - Bullfrog,,3,,, 3 | BFC,DTA,20,Bullfrog - Furnace Creek Resort,,3,,, 4 | STBA,DTA,30,Stagecoach - Airport Shuttle,,3,,, 5 | CITY,DTA,40,City,,3,,, 6 | AAMV,DTA,50,Airport - Amargosa Valley,,3,,, -------------------------------------------------------------------------------- /tests/data/sample_feed/frequencies.txt: -------------------------------------------------------------------------------- 1 | trip_id,start_time,end_time,headway_secs 2 | STBA,6:00:00,22:00:00,1800 3 | CITY1,6:00:00,7:59:59,1800 4 | CITY2,6:00:00,7:59:59,1800 5 | CITY1,8:00:00,9:59:59,600 6 | CITY2,8:00:00,9:59:59,600 7 | CITY1,10:00:00,15:59:59,1800 8 | CITY2,10:00:00,15:59:59,1800 9 | CITY1,16:00:00,18:59:59,600 10 | CITY2,16:00:00,18:59:59,600 11 | CITY1,19:00:00,22:00:00,1800 12 | CITY2,19:00:00,22:00:00,1800 -------------------------------------------------------------------------------- /docs/CPP_STYLE.md: -------------------------------------------------------------------------------- 1 | ## C++ Style Guide 2 | 3 | We use C++ code style similar to the [MAPS.ME project](https://github.com/mapsme/omim/blob/master/docs/CPP_STYLE.md) with some differences: 4 | - Use **CamelCase** for class names and **snake_case** for other entities like methods, variables, etc. 5 | - Use left-to-right order for variables/params: `const std::string & s` (reference to the const string). 6 | - Do not use prefixes like `m_` for member variables. 7 | -------------------------------------------------------------------------------- /tests/data/sample_feed/trips.txt: -------------------------------------------------------------------------------- 1 | route_id,service_id,trip_id,trip_headsign,direction_id,block_id,shape_id 2 | AB,FULLW,AB1,to Bullfrog,0,1, 3 | AB,FULLW,AB2,to Airport,1,2, 4 | STBA,FULLW,STBA,Shuttle,,, 5 | CITY,FULLW,CITY1,,0,, 6 | CITY,FULLW,CITY2,,1,, 7 | BFC,FULLW,BFC1,to Furnace Creek Resort,0,1, 8 | BFC,FULLW,BFC2,to Bullfrog,1,2, 9 | AAMV,WE,AAMV1,to Amargosa Valley,0,, 10 | AAMV,WE,AAMV2,to Airport,1,, 11 | AAMV,WE,AAMV3,to Amargosa Valley,0,, 12 | AAMV,WE,AAMV4,to Airport,1,, -------------------------------------------------------------------------------- /tests/data/sample_feed/shapes.txt: -------------------------------------------------------------------------------- 1 | shape_id,shape_pt_lat,shape_pt_lon,shape_pt_sequence,shape_dist_traveled 2 | 10237, 43.5176524709, -79.6906570431,50017,12669 3 | 10237, 43.5176982107, -79.6906412064,50018,12669 4 | 10237, 43.5177439788, -79.6906278437,50019,12669 5 | 10237, 43.5177457792, -79.6906278048,50020,12669 6 | 10243, 43.6448714082, -79.5249161004,10001,0 7 | 10243, 43.6448078510, -79.5252239093,10002,0 8 | 10243, 43.6446766156, -79.5251713255,10003,0 9 | 10243, 43.6445544452, -79.5251234796,10004,0 -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | file(GLOB TESTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} *.cpp) 2 | 3 | message(STATUS "CMAKE_CURRENT_BINARY_DIR=" ${CMAKE_CURRENT_BINARY_DIR}) 4 | 5 | foreach(TEST_SOURCE ${TESTS}) 6 | string(REPLACE ".cpp" "" TEST_TARGET "${TEST_SOURCE}") 7 | add_executable(${TEST_TARGET} ${TEST_SOURCE}) 8 | target_compile_features(${TEST_TARGET} PRIVATE cxx_std_17) 9 | add_test("${TEST_TARGET}" "${TEST_TARGET}" WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} --verbose) 10 | endforeach() 11 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.6) 2 | 3 | project(just_gtfs LANGUAGES CXX VERSION 0.1) 4 | 5 | include_directories(include) 6 | include_directories(doctest/doctest) 7 | 8 | set(CMAKE_CXX_STANDARD 17) 9 | set(CMAKE_CXX_STANDARD_REQUIRED on) 10 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Werror") 11 | 12 | enable_testing() 13 | 14 | add_library(just_gtfs INTERFACE) 15 | target_include_directories(just_gtfs INTERFACE ${CMAKE_CURRENT_LIST_DIR}/include) 16 | 17 | add_subdirectory(tests) 18 | add_subdirectory(benchmarks) 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | 34 | # Other 35 | .DS_Store 36 | .idea/ 37 | cmake-build-debug/ 38 | CMakeFiles/ 39 | Makefile 40 | *.cmake 41 | CMakeCache.txt 42 | -------------------------------------------------------------------------------- /tests/data/sample_feed/stops.txt: -------------------------------------------------------------------------------- 1 | stop_id,stop_name,stop_desc,stop_lat,stop_lon,zone_id,stop_url 2 | FUR_CREEK_RES,Furnace Creek Resort (Demo),,36.425288,-117.133162,, 3 | BEATTY_AIRPORT,Nye County Airport (Demo),,36.868446,-116.784582,, 4 | BULLFROG,Bullfrog (Demo),,36.88108,-116.81797,, 5 | STAGECOACH,Stagecoach Hotel & Casino (Demo),,36.915682,-116.751677,, 6 | NADAV,North Ave / D Ave N (Demo),,36.914893,-116.76821,, 7 | NANAA,North Ave / N A Ave (Demo),,36.914944,-116.761472,, 8 | DADAN,Doing Ave / D Ave N (Demo),,36.909489,-116.768242,, 9 | EMSI,E Main St / S Irving St (Demo),,36.905697,-116.76218,, 10 | AMV,Amargosa Valley (Demo),,36.641496,-116.40094,, -------------------------------------------------------------------------------- /.github/workflows/ccpp.yml: -------------------------------------------------------------------------------- 1 | name: C/C++ CI 2 | 3 | on: 4 | push: 5 | branches: [ master, add-*, fix-* ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: git_actions 17 | run: git submodule update --init --recursive 18 | - name: cmake 19 | run: | 20 | sudo apt update 21 | sudo apt install mm-common g++-9 22 | export CXX=g++-9 23 | cmake . 24 | - name: make 25 | run: | 26 | export CXX=g++-9 27 | make 28 | - name: run_tests 29 | run: | 30 | pwd 31 | ctest --output-on-failure 32 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | # Configuration file for clang-format, based on docs/CPP_STYLE.md. 2 | 3 | BasedOnStyle: Google 4 | IndentWidth: 2 5 | BreakBeforeBraces: Allman 6 | ColumnLimit: 100 7 | 8 | Language: Cpp 9 | AccessModifierOffset: -2 10 | AllowShortBlocksOnASingleLine: Never 11 | AllowShortCaseLabelsOnASingleLine: true 12 | AllowShortFunctionsOnASingleLine: All 13 | AllowShortIfStatementsOnASingleLine: Never 14 | AllowShortLoopsOnASingleLine: false 15 | BreakConstructorInitializersBeforeComma: true 16 | ConstructorInitializerIndentWidth: 4 17 | DerivePointerAlignment: false 18 | IndentCaseLabels: false 19 | NamespaceIndentation: None 20 | PointerAlignment: Middle 21 | SortIncludes: true 22 | Standard: c++17 23 | 24 | IncludeBlocks: Preserve 25 | IncludeCategories: 26 | - Regex: '^<.*\.h>' 27 | Priority: 1 28 | - Regex: '^<.*' 29 | Priority: 2 30 | - Regex: '.*' 31 | Priority: 3 32 | -------------------------------------------------------------------------------- /LICENSE.MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 mesozoic-drones 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 | -------------------------------------------------------------------------------- /tests/data/sample_feed/stop_times.txt: -------------------------------------------------------------------------------- 1 | trip_id,arrival_time,departure_time,stop_id,stop_sequence,stop_headsign,pickup_type,drop_off_time,shape_dist_traveled 2 | STBA,6:00:00,6:00:00,STAGECOACH,1,,,, 3 | STBA,6:20:00,6:20:00,BEATTY_AIRPORT,2,,,, 4 | CITY1,6:00:00,6:00:00,STAGECOACH,1,,,, 5 | CITY1,6:05:00,6:07:00,NANAA,2,,,, 6 | CITY1,6:12:00,6:14:00,NADAV,3,,,, 7 | CITY1,6:19:00,6:21:00,DADAN,4,,,, 8 | CITY1,6:26:00,6:28:00,EMSI,5,,,, 9 | CITY2,6:28:00,6:30:00,EMSI,1,,,, 10 | CITY2,6:35:00,6:37:00,DADAN,2,,,, 11 | CITY2,6:42:00,6:44:00,NADAV,3,,,, 12 | CITY2,6:49:00,6:51:00,NANAA,4,,,, 13 | CITY2,6:56:00,6:58:00,STAGECOACH,5,,,, 14 | AB1,8:00:00,8:00:00,BEATTY_AIRPORT,1,,,, 15 | AB1,8:10:00,8:15:00,BULLFROG,2,,,, 16 | AB2,12:05:00,12:05:00,BULLFROG,1,,,, 17 | AB2,12:15:00,12:15:00,BEATTY_AIRPORT,2 18 | BFC1,8:20:00,8:20:00,BULLFROG,1 19 | BFC1,9:20:00,9:20:00,FUR_CREEK_RES,2 20 | BFC2,11:00:00,11:00:00,FUR_CREEK_RES,1 21 | BFC2,12:00:00,12:00:00,BULLFROG,2 22 | AAMV1,8:00:00,8:00:00,BEATTY_AIRPORT,1 23 | AAMV1,9:00:00,9:00:00,AMV,2 24 | AAMV2,10:00:00,10:00:00,AMV,1 25 | AAMV2,11:00:00,11:00:00,BEATTY_AIRPORT,2 26 | AAMV3,13:00:00,13:00:00,BEATTY_AIRPORT,1 27 | AAMV3,14:00:00,14:00:00,AMV,2 28 | AAMV4,15:00:00,15:00:00,AMV,1 29 | AAMV4,16:00:00,16:00:00,BEATTY_AIRPORT,2 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # just_gtfs - header-only modern C++ library for reading and writing GTFS feeds 2 | 3 | [![GTFS reader and writer for C++](https://github.com/mapsme/just_gtfs/blob/add-the-most-important-readers/docs/logo.jpeg)](https://github.com/mapsme/just_gtfs) 4 | 5 | [![C++](https://img.shields.io/badge/c%2B%2B-17-informational.svg)](https://shields.io/) 6 | [![MIT license](https://img.shields.io/badge/License-MIT-blue.svg)](https://lbesson.mit-license.org/) 7 | ![](https://github.com/mapsme/just_gtfs/workflows/C%2FC%2B%2B%20CI/badge.svg) 8 | [![](https://github.com/sindresorhus/awesome/blob/main/media/mentioned-badge.svg)](https://github.com/CUTR-at-USF/awesome-transit) 9 | [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/mapsme/just_gtfs/issues) 10 | 11 | 12 | ## Table of Contents 13 | - [Description](#description) 14 | - [Reading and writing GTFS feeds](#reading-and-writing-gtfs-feeds) 15 | - [How to add library to your project](#how-to-add-library-to-your-project) 16 | - [Used third-party tools](#used-third-party-tools) 17 | - [Contributing](#contributing) 18 | - [Resources](#resources) 19 | 20 | ## Description 21 | The just_gtfs library implements reading and writing static transit data in GTFS - [General Transit Feed Specification](https://developers.google.com/transit/gtfs/reference). 22 | 23 | Its main features: 24 | - Fast reading and writing of GTFS feeds 25 | - Support for [extended GTFS route types](https://developers.google.com/transit/gtfs/reference/extended-route-types) 26 | - Simple working with GTFS `Date` and `Time` formats 27 | - Header-only 28 | - Written in C++17 29 | - Tested on GCC and Clang 30 | 31 | 32 | ## Reading and writing GTFS feeds 33 | Library provides main class for working with GTFS feeds: `gtfs::Feed`. It also provides classes for each of the 17 GTFS entities: `Route`, `Stop`, `Pathway`, `Translation` and others. 34 | GTFS csv files are mapped to the corresponding C++ classes. Every GTFS entity can be accessed through `gtfs::Feed` corresponding getters & setters. 35 | 36 | Method for reading GTFS feed. Path to the feed is specified in the `Feed` constructor: 37 | ```c++ 38 | Result read_feed(strict=true) 39 | ``` 40 | Flag `strict` is used to interrupt feed parsing in case of absence or errors in required GTFS files. If you want to read incomplete feed, set it to `false`. 41 | 42 | Method for getting reference to the `Agencies` - `std::vector` of all `Agency` objects of the feed: 43 | ```c++ 44 | const Agencies & get_agencies() 45 | ``` 46 | 47 | Method for finding agency by its id. Returns `Agency` so you should check if the result struct is empty: 48 | ```c++ 49 | const & Agency get_agency(const Id & agency_id) 50 | ``` 51 | 52 | Method for adding agency to the feed: 53 | ```c++ 54 | void add_agency(const Agency & agency) 55 | ``` 56 | 57 | Method for writing agencies to the `agency.txt` file to `gtfs_path`. 58 | ```c++ 59 | Result write_agencies(const std::string & gtfs_path) 60 | ``` 61 | 62 | Method for writing all GTFS entities (not only agencies, but stops, stop times, calendar etc): 63 | ```c++ 64 | Result write_feed(const std::string & gtfs_path) 65 | ``` 66 | 67 | :pushpin: **There are similar methods for all other GTFS entities** for getting the list of entities, finding and adding them. 68 | For some of them additional methods are provided. 69 | For example, you can find all the stop times for current stop by its id: 70 | ```c++ 71 | StopTimes get_stop_times_for_stop(const Id & stop_id) 72 | ``` 73 | 74 | Or you can find stop times for the particular trip: 75 | ```c++ 76 | StopTimesRange get_stop_times_for_trip(const Id & trip_id, bool sort_by_sequence = true) 77 | ``` 78 | 79 | ### Example of reading GTFS feed and working with its stops and routes 80 | :pushpin: Provide `gtfs::Feed` the feed path, read it and work with GTFS entities such as stops and routes: 81 | ```c++ 82 | Feed feed("~/data/SFMTA/"); 83 | if (feed.read_feed() == ResultCode::OK) 84 | { 85 | Stops stops = feed.get_stops(); 86 | std::cout << "Stops count in feed: " << stops.size() << std::endl; 87 | 88 | for (const Stop & stop: stops) 89 | { 90 | std::cout << stop.stop_id << std::endl; 91 | } 92 | 93 | Route route = feed.get_route("route_id_1009"); 94 | if (route) 95 | { 96 | std::cout << route->route_long_name << std::endl; 97 | } 98 | } 99 | ``` 100 | 101 | ### Example of parsing shapes.txt and working with its contents 102 | GTFS feed can be wholly read from directory as in the example above or you can read GTFS files separately. E.g., if you need only shapes data, you can avoid parsing all other files and just work with the shapes. 103 | 104 | :pushpin: Read only `shapes.txt` from the feed and work with shapes: 105 | ```c++ 106 | Feed feed("~/data/SFMTA/"); 107 | if (feed.read_feed() == ResultCode::OK) 108 | { 109 | Shapes all_shapes = feed.get_shapes(); 110 | Shape shape = feed.get_shape("9367"); 111 | 112 | for (const ShapePoint & point: shape) 113 | { 114 | std::cout << point.shape_pt_lat << " " << point.shape_pt_lon << std::endl; 115 | } 116 | } 117 | ``` 118 | 119 | ### Example of writing GTFS: 120 | :pushpin: If you already filled the `feed` object with data that suits you, you can write it to the corresponding path: 121 | ```c++ 122 | Feed feed; 123 | 124 | // Fill feed with agencies, stops, routes and other required data: 125 | 126 | feed.add_trip(some_trip); 127 | feed.add_attribution(attr); 128 | 129 | feed.write_feed("~/data/custom_feed/"); 130 | ``` 131 | 132 | ## How to add library to your project 133 | - For including just_gtfs to your own project **as a submodule:** use branch "for-usage-as-submodule" which consists of a single header. 134 | - Another way of including just_gtfs to your project: just_gtfs is completely contained inside a single header and therefore it is sufficient to copy include/just_gtfs/just_gtfs.h to your **include paths.** The library does not have to be explicitly build. 135 | - For building library and **running tests:** 136 | Clone just_gtfs with `git clone --recursive` or run `git submodule update --init --recursive --remote` after cloning. 137 | In the just_gtfs project directory build the project and run unit tests: 138 | ``` 139 | cmake . 140 | make 141 | ctest --output-on-failure --verbose 142 | ``` 143 | The library makes use of the C++17 features and therefore you have to use the appropriate compiler version. 144 | 145 | ## Used third-party tools 146 | - [**doctest**](https://github.com/onqtam/doctest) for unit testing. 147 | 148 | ## Contributing 149 | Please open a [Github issue](https://github.com/mapsme/just_gtfs/issues/new) with as much of the information as you're able to specify, or create a [pull request](https://github.com/mapsme/just_gtfs/pulls) according to our [guidelines](https://github.com/mapsme/just_gtfs/blob/master/docs/CPP_STYLE.md). 150 | 151 | ## Resources 152 | [GTFS reference in Google GitHub repository](https://github.com/google/transit/blob/master/gtfs/spec/en/reference.md) 153 | 154 | [GTFS reference on Google Transit API](https://developers.google.com/transit/gtfs/reference?csw=1) 155 | -------------------------------------------------------------------------------- /tests/unit_tests.cpp: -------------------------------------------------------------------------------- 1 | #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN 2 | #include "doctest.h" 3 | 4 | #include "just_gtfs/just_gtfs.h" 5 | 6 | #include 7 | 8 | using namespace gtfs; 9 | const std::string test_feed = "data/sample_feed"; 10 | const std::string test_output_feed = "data/output_feed"; 11 | 12 | TEST_SUITE_BEGIN("Handling time GTFS fields"); 13 | TEST_CASE("Time in H:MM:SS format") 14 | { 15 | Time stop_time("0:19:00"); 16 | REQUIRE(stop_time.is_provided()); 17 | CHECK_EQ(stop_time.get_hh_mm_ss(), std::make_tuple(0, 19, 0)); 18 | CHECK_EQ(stop_time.get_raw_time(), "0:19:00"); 19 | CHECK_EQ(stop_time.get_total_seconds(), 19 * 60); 20 | } 21 | 22 | TEST_CASE("Time in HH:MM:SS format") 23 | { 24 | Time stop_time("39:45:30"); 25 | CHECK_EQ(stop_time.get_hh_mm_ss(), std::make_tuple(39, 45, 30)); 26 | CHECK_EQ(stop_time.get_raw_time(), "39:45:30"); 27 | CHECK_EQ(stop_time.get_total_seconds(), 39 * 60 * 60 + 45 * 60 + 30); 28 | } 29 | 30 | TEST_CASE("Time in HHH:MM:SS format") 31 | { 32 | Time stop_time("103:05:21"); 33 | CHECK_EQ(stop_time.get_hh_mm_ss(), std::make_tuple(103, 5, 21)); 34 | CHECK_EQ(stop_time.get_raw_time(), "103:05:21"); 35 | CHECK_EQ(stop_time.get_total_seconds(), 103 * 60 * 60 + 5 * 60 + 21); 36 | } 37 | 38 | TEST_CASE("Time from integers 1") 39 | { 40 | Time stop_time(14, 30, 0); 41 | CHECK_EQ(stop_time.get_hh_mm_ss(), std::make_tuple(14, 30, 0)); 42 | CHECK_EQ(stop_time.get_raw_time(), "14:30:00"); 43 | CHECK_EQ(stop_time.get_total_seconds(), 14 * 60 * 60 + 30 * 60); 44 | } 45 | 46 | TEST_CASE("Time from integers 2") 47 | { 48 | Time stop_time(3, 0, 0); 49 | CHECK_EQ(stop_time.get_hh_mm_ss(), std::make_tuple(3, 0, 0)); 50 | CHECK_EQ(stop_time.get_raw_time(), "03:00:00"); 51 | CHECK_EQ(stop_time.get_total_seconds(), 3 * 60 * 60); 52 | } 53 | 54 | TEST_CASE("Invalid time format") 55 | { 56 | CHECK_THROWS_AS(Time("12/10/00"), const InvalidFieldFormat &); 57 | CHECK_THROWS_AS(Time("12:100:00"), const InvalidFieldFormat &); 58 | CHECK_THROWS_AS(Time("12:10:100"), const InvalidFieldFormat &); 59 | CHECK_THROWS_AS(Time("12:10/10"), const InvalidFieldFormat &); 60 | } 61 | 62 | TEST_CASE("Time not provided") 63 | { 64 | Time stop_time(""); 65 | CHECK(!stop_time.is_provided()); 66 | } 67 | 68 | TEST_CASE("Convert to Time with 24 hours max") 69 | { 70 | Time stop_time_near_midnight("24:05:00"); 71 | CHECK(stop_time_near_midnight.limit_hours_to_24max()); 72 | CHECK_EQ(stop_time_near_midnight.get_raw_time(), "00:05:00"); 73 | 74 | Time stop_time_morning("27:05:00"); 75 | stop_time_morning.limit_hours_to_24max(); 76 | CHECK_EQ(stop_time_morning.get_raw_time(), "03:05:00"); 77 | } 78 | 79 | TEST_SUITE_END(); 80 | 81 | TEST_SUITE_BEGIN("Handling date GTFS fields"); 82 | TEST_CASE("Date not provided") 83 | { 84 | Date date(""); 85 | CHECK(!date.is_provided()); 86 | } 87 | 88 | TEST_CASE("Invalid date format") 89 | { 90 | // Violation of the format YYYYMMDD: 91 | CHECK_THROWS_AS(Date("1999314"), const InvalidFieldFormat &); 92 | CHECK_THROWS_AS(Date("20081414"), const InvalidFieldFormat &); 93 | CHECK_THROWS_AS(Date("20170432"), const InvalidFieldFormat &); 94 | 95 | // Count of days in february (leap year): 96 | CHECK_THROWS_AS(Date("20200230"), const InvalidFieldFormat &); 97 | // Count of days in february (not leap year): 98 | CHECK_THROWS_AS(Date("20210229"), const InvalidFieldFormat &); 99 | 100 | // Count of days in months with 30 days: 101 | CHECK_THROWS_AS(Date("19980431"), const InvalidFieldFormat &); 102 | CHECK_THROWS_AS(Date("19980631"), const InvalidFieldFormat &); 103 | CHECK_THROWS_AS(Date("19980931"), const InvalidFieldFormat &); 104 | CHECK_THROWS_AS(Date("19981131"), const InvalidFieldFormat &); 105 | } 106 | 107 | TEST_CASE("Date from string 1") 108 | { 109 | Date date("20230903"); 110 | CHECK_EQ(date.get_yyyy_mm_dd(), std::make_tuple(2023, 9, 3)); 111 | CHECK_EQ(date.get_raw_date(), "20230903"); 112 | CHECK(date.is_provided()); 113 | } 114 | 115 | TEST_CASE("Date from string 2") 116 | { 117 | Date date("20161231"); 118 | CHECK_EQ(date.get_yyyy_mm_dd(), std::make_tuple(2016, 12, 31)); 119 | CHECK_EQ(date.get_raw_date(), "20161231"); 120 | CHECK(date.is_provided()); 121 | } 122 | 123 | TEST_CASE("Date from string 3") 124 | { 125 | Date date("20200229"); 126 | CHECK_EQ(date.get_yyyy_mm_dd(), std::make_tuple(2020, 2, 29)); 127 | CHECK_EQ(date.get_raw_date(), "20200229"); 128 | CHECK(date.is_provided()); 129 | } 130 | 131 | TEST_CASE("Date from integers") 132 | { 133 | Date date(2022, 8, 16); 134 | CHECK_EQ(date.get_yyyy_mm_dd(), std::make_tuple(2022, 8, 16)); 135 | 136 | CHECK_EQ(date.get_raw_date(), "20220816"); 137 | CHECK(date.is_provided()); 138 | } 139 | 140 | TEST_SUITE_END(); 141 | 142 | TEST_SUITE_BEGIN("Csv parsing"); 143 | TEST_CASE("Record with empty values") 144 | { 145 | const auto res = CsvParser::split_record(",, ,"); 146 | REQUIRE_EQ(res.size(), 4); 147 | for (const auto & token : res) 148 | CHECK(token.empty()); 149 | } 150 | 151 | TEST_CASE("Header with UTF BOM") 152 | { 153 | const auto res = CsvParser::split_record("\xef\xbb\xbfroute_id, agency_id", true); 154 | REQUIRE_EQ(res.size(), 2); 155 | CHECK_EQ(res[0], "route_id"); 156 | CHECK_EQ(res[1], "agency_id"); 157 | } 158 | 159 | TEST_CASE("Quotation marks") 160 | { 161 | const auto res = CsvParser::split_record(R"(27681 ,,"Sisters, OR",,"44.29124",1)"); 162 | REQUIRE_EQ(res.size(), 6); 163 | CHECK_EQ(res[2], "Sisters, OR"); 164 | CHECK_EQ(res[4], "44.29124"); 165 | CHECK_EQ(res[5], "1"); 166 | } 167 | 168 | TEST_CASE("Not wrapped quotation marks") 169 | { 170 | const auto res = CsvParser::split_record(R"(Contains "quotes", commas and text)"); 171 | REQUIRE_EQ(res.size(), 2); 172 | CHECK_EQ(res[0], R"(Contains "quotes")"); 173 | CHECK_EQ(res[1], "commas and text"); 174 | } 175 | 176 | TEST_CASE("Wrapped quotation marks") 177 | { 178 | const auto res = CsvParser::split_record(R"("Contains ""quotes"", commas and text")"); 179 | REQUIRE_EQ(res.size(), 1); 180 | CHECK_EQ(res[0], R"(Contains "quotes", commas and text)"); 181 | } 182 | 183 | TEST_CASE("Double wrapped quotation marks") 184 | { 185 | const auto res = CsvParser::split_record(R"(""Double quoted text"")"); 186 | REQUIRE_EQ(res.size(), 1); 187 | } 188 | 189 | TEST_CASE("Read quoted empty values") 190 | { 191 | const auto res = CsvParser::split_record(",\"\""); 192 | REQUIRE_EQ(res.size(), 2); 193 | CHECK_EQ(res[0], ""); 194 | CHECK_EQ(res[1], ""); 195 | } 196 | TEST_CASE("Read quoted quote") 197 | { 198 | const auto res = CsvParser::split_record(",\"\"\"\""); 199 | REQUIRE_EQ(res.size(), 2); 200 | CHECK_EQ(res[0], ""); 201 | CHECK_EQ(res[1], "\""); 202 | } 203 | 204 | TEST_CASE("Read quoted double quote") 205 | { 206 | const auto res = CsvParser::split_record(",\"\"\"\"\"\""); 207 | REQUIRE_EQ(res.size(), 2); 208 | CHECK_EQ(res[0], ""); 209 | CHECK_EQ(res[1], "\"\""); 210 | } 211 | 212 | TEST_CASE("Read quoted values with quotes in begin") 213 | { 214 | const auto res = CsvParser::split_record(",\"\"\"Name\"\" and some other\""); 215 | REQUIRE_EQ(res.size(), 2); 216 | CHECK_EQ(res[0], ""); 217 | CHECK_EQ(res[1], "\"Name\" and some other"); 218 | } 219 | 220 | TEST_CASE("Read quoted values with quotes at end") 221 | { 222 | const auto res = CsvParser::split_record(",\"Text and \"\"Name\"\"\""); 223 | REQUIRE_EQ(res.size(), 2); 224 | CHECK_EQ(res[0], ""); 225 | CHECK_EQ(res[1], "Text and \"Name\""); 226 | } 227 | TEST_SUITE_END(); 228 | 229 | TEST_SUITE_BEGIN("Read & write"); 230 | // Credits: 231 | // https://developers.google.com/transit/gtfs/examples/gtfs-feed 232 | TEST_CASE("Empty container before parsing") 233 | { 234 | Feed feed("data/non_existing_dir"); 235 | REQUIRE(feed.get_agencies().empty()); 236 | auto agency = feed.get_agency("agency_10"); 237 | const Agency non_existing_agency; 238 | CHECK_EQ(agency, non_existing_agency); 239 | } 240 | 241 | TEST_CASE("Non existing directory") 242 | { 243 | Feed feed("data/non_existing_dir"); 244 | REQUIRE_EQ(feed.read_feed(), ResultCode::ERROR_FILE_ABSENT); 245 | CHECK_EQ(feed.get_transfers().size(), 0); 246 | } 247 | 248 | TEST_CASE("Transfers") 249 | { 250 | Feed feed(test_feed); 251 | REQUIRE_EQ(feed.read_feed(), ResultCode::OK); 252 | const auto & transfers = feed.get_transfers(); 253 | CHECK_EQ(transfers.size(), 4); 254 | 255 | CHECK_EQ(transfers[0].from_stop_id, "130"); 256 | CHECK_EQ(transfers[0].to_stop_id, "4"); 257 | CHECK_EQ(transfers[0].transfer_type, TransferType::MinimumTime); 258 | CHECK_EQ(transfers[0].min_transfer_time, 70); 259 | 260 | const auto & transfer = feed.get_transfer("314", "11"); 261 | 262 | CHECK_EQ(transfer.transfer_type, TransferType::Timed); 263 | CHECK_EQ(transfer.min_transfer_time, 0); 264 | } 265 | 266 | TEST_CASE("Calendar") 267 | { 268 | Feed feed(test_feed); 269 | REQUIRE_EQ(feed.read_feed(), ResultCode::OK); 270 | const auto & calendar = feed.get_calendar(); 271 | REQUIRE_EQ(calendar.size(), 2); 272 | 273 | const auto & calendar_record = feed.get_calendar_item("WE"); 274 | 275 | CHECK_EQ(calendar_record.start_date, Date(2007, 01, 01)); 276 | CHECK_EQ(calendar_record.end_date, Date(2010, 12, 31)); 277 | 278 | CHECK_EQ(calendar_record.monday, CalendarAvailability::NotAvailable); 279 | CHECK_EQ(calendar_record.tuesday, CalendarAvailability::NotAvailable); 280 | CHECK_EQ(calendar_record.wednesday, CalendarAvailability::NotAvailable); 281 | CHECK_EQ(calendar_record.thursday, CalendarAvailability::NotAvailable); 282 | CHECK_EQ(calendar_record.friday, CalendarAvailability::NotAvailable); 283 | CHECK_EQ(calendar_record.saturday, CalendarAvailability::Available); 284 | CHECK_EQ(calendar_record.sunday, CalendarAvailability::Available); 285 | } 286 | 287 | TEST_CASE("Calendar dates") 288 | { 289 | Feed feed(test_feed); 290 | REQUIRE_EQ(feed.read_feed(), ResultCode::OK); 291 | const auto & calendar_dates = feed.get_calendar_dates(); 292 | REQUIRE_EQ(calendar_dates.size(), 1); 293 | 294 | const auto & calendar_records_range = feed.get_calendar_dates("FULLW"); 295 | 296 | CHECK_EQ(calendar_records_range.first->date, Date(2007, 06, 04)); 297 | CHECK_EQ(calendar_records_range.first->exception_type, CalendarDateException::Removed); 298 | } 299 | 300 | TEST_CASE("Read GTFS feed") 301 | { 302 | Feed feed(test_feed); 303 | REQUIRE_EQ(feed.read_feed(), ResultCode::OK); 304 | 305 | CHECK_EQ(feed.get_agencies().size(), 1); 306 | CHECK_EQ(feed.get_routes().size(), 5); 307 | CHECK_EQ(feed.get_trips().size(), 11); 308 | CHECK_EQ(feed.get_shapes().size(), 8); 309 | CHECK_EQ(feed.get_stops().size(), 9); 310 | CHECK_EQ(feed.get_stop_times().size(), 28); 311 | CHECK_EQ(feed.get_transfers().size(), 4); 312 | CHECK_EQ(feed.get_frequencies().size(), 11); 313 | CHECK_EQ(feed.get_attributions().size(), 1); 314 | CHECK_EQ(feed.get_calendar().size(), 2); 315 | CHECK_EQ(feed.get_calendar_dates().size(), 1); 316 | CHECK_EQ(feed.get_fare_attributes().size(), 3); 317 | CHECK_EQ(feed.get_fare_rules().size(), 4); 318 | CHECK(!feed.get_feed_info().feed_publisher_name.empty()); 319 | CHECK_EQ(feed.get_levels().size(), 3); 320 | CHECK_EQ(feed.get_pathways().size(), 3); 321 | CHECK_EQ(feed.get_translations().size(), 1); 322 | } 323 | 324 | TEST_CASE("Agency") 325 | { 326 | Feed feed(test_feed); 327 | REQUIRE_EQ(feed.read_feed(), ResultCode::OK); 328 | 329 | const auto & agencies = feed.get_agencies(); 330 | REQUIRE_EQ(agencies.size(), 1); 331 | CHECK_EQ(agencies[0].agency_id, "DTA"); 332 | CHECK_EQ(agencies[0].agency_name, "Demo Transit Authority"); 333 | CHECK_EQ(agencies[0].agency_url, "http://google.com"); 334 | CHECK(agencies[0].agency_lang.empty()); 335 | CHECK_EQ(agencies[0].agency_timezone, "America/Los_Angeles"); 336 | 337 | const auto agency = feed.get_agency("DTA"); 338 | CHECK_EQ(agency.agency_name, "Demo Transit Authority"); 339 | 340 | REQUIRE_EQ(feed.write_agencies(test_output_feed), ResultCode::OK); 341 | Feed feed_copy(test_output_feed); 342 | REQUIRE_EQ(feed_copy.read_feed(false), ResultCode::OK); 343 | CHECK_EQ(agencies, feed_copy.get_agencies()); 344 | } 345 | 346 | TEST_CASE("Routes") 347 | { 348 | Feed feed(test_feed); 349 | REQUIRE_EQ(feed.read_feed(), ResultCode::OK); 350 | 351 | const auto & routes = feed.get_routes(); 352 | REQUIRE_EQ(routes.size(), 5); 353 | const size_t i = 1; 354 | CHECK_EQ(routes[i].route_id, "AB"); 355 | CHECK_EQ(routes[i].agency_id, "DTA"); 356 | CHECK_EQ(routes[i].route_short_name, "10"); 357 | CHECK_EQ(routes[i].route_long_name, "Airport - Bullfrog"); 358 | CHECK_EQ(routes[i].route_type, RouteType::Bus); 359 | CHECK(routes[i].route_text_color.empty()); 360 | CHECK(routes[i].route_color.empty()); 361 | CHECK(routes[i].route_desc.empty()); 362 | 363 | const auto & route = feed.get_route("AB"); 364 | CHECK_EQ(route.agency_id, "DTA"); 365 | CHECK_EQ(route.route_type, RouteType::Bus); 366 | } 367 | 368 | TEST_CASE("Trips") 369 | { 370 | Feed feed(test_feed); 371 | REQUIRE_EQ(feed.read_feed(), ResultCode::OK); 372 | 373 | const auto & trips = feed.get_trips(); 374 | REQUIRE_EQ(trips.size(), 11); 375 | 376 | const size_t i = 4; 377 | CHECK_EQ(trips[i].block_id, "1"); 378 | CHECK_EQ(trips[i].route_id, "AB"); 379 | CHECK_EQ(trips[i].direction_id, TripDirectionId::DefaultDirection); 380 | CHECK_EQ(trips[i].trip_headsign, "to Bullfrog"); 381 | CHECK(trips[i].shape_id.empty()); 382 | CHECK_EQ(trips[i].service_id, "FULLW"); 383 | CHECK_EQ(trips[i].trip_id, "AB1"); 384 | 385 | const auto & trip = feed.get_trip("AB1"); 386 | CHECK(trip.trip_short_name.empty()); 387 | } 388 | 389 | TEST_CASE("Stops") 390 | { 391 | Feed feed(test_feed); 392 | REQUIRE_EQ(feed.read_feed(), ResultCode::OK); 393 | 394 | const auto & stops = feed.get_stops(); 395 | REQUIRE_EQ(stops.size(), 9); 396 | size_t i = 5; 397 | CHECK_EQ(stops[i].stop_lat, 36.425288); 398 | CHECK_EQ(stops[i].stop_lon, -117.133162); 399 | CHECK(stops[i].stop_code.empty()); 400 | CHECK(stops[i].stop_url.empty()); 401 | CHECK_EQ(stops[i].stop_id, "FUR_CREEK_RES"); 402 | CHECK(stops[i].stop_desc.empty()); 403 | CHECK_EQ(stops[i].stop_name, "Furnace Creek Resort (Demo)"); 404 | CHECK_EQ(stops[i].location_type, StopLocationType::StopOrPlatform); 405 | CHECK(stops[i].zone_id.empty()); 406 | 407 | auto const & stop = feed.get_stop("FUR_CREEK_RES"); 408 | CHECK_EQ(stop.stop_name, "Furnace Creek Resort (Demo)"); 409 | } 410 | 411 | TEST_CASE("StopTimes") 412 | { 413 | Feed feed(test_feed); 414 | REQUIRE_EQ(feed.read_feed(), ResultCode::OK); 415 | 416 | const auto & stop_times = feed.get_stop_times(); 417 | REQUIRE_EQ(stop_times.size(), 28); 418 | 419 | const size_t i = 26; 420 | CHECK_EQ(stop_times[i].trip_id, "STBA"); 421 | CHECK_EQ(stop_times[i].arrival_time, Time(06, 00, 00)); 422 | CHECK_EQ(stop_times[i].departure_time, Time(06, 00, 00)); 423 | CHECK_EQ(stop_times[i].stop_id, "STAGECOACH"); 424 | CHECK_EQ(stop_times[i].stop_sequence, 1); 425 | CHECK(stop_times[i].stop_headsign.empty()); 426 | CHECK_EQ(stop_times[i].pickup_type, StopTimeBoarding::RegularlyScheduled); 427 | CHECK_EQ(stop_times[i].drop_off_type, StopTimeBoarding::RegularlyScheduled); 428 | 429 | CHECK_EQ(feed.get_stop_times_for_stop("STAGECOACH").size(), 3); 430 | 431 | CHECK_EQ(std::distance(feed.get_stop_times_for_trip("STBA").first, 432 | feed.get_stop_times_for_trip("STBA").second), 433 | 2); 434 | } 435 | 436 | TEST_CASE("Shapes") 437 | { 438 | Feed feed(test_feed); 439 | REQUIRE_EQ(feed.read_feed(), ResultCode::OK); 440 | 441 | const auto & shapes = feed.get_shapes(); 442 | REQUIRE_EQ(shapes.size(), 8); 443 | CHECK_EQ(shapes[0].shape_id, "10237"); 444 | CHECK_EQ(shapes[0].shape_pt_lat, 43.5176524709); 445 | CHECK_EQ(shapes[0].shape_pt_lon, -79.6906570431); 446 | CHECK_EQ(shapes[0].shape_pt_sequence, 50017); 447 | CHECK_EQ(shapes[0].shape_dist_traveled, 12669); 448 | 449 | const auto & shape = feed.get_shape("10237"); 450 | CHECK_EQ(std::distance(shape.first, shape.second), 4); 451 | } 452 | 453 | TEST_CASE("Calendar") 454 | { 455 | Feed feed(test_feed); 456 | REQUIRE_EQ(feed.read_feed(), ResultCode::OK); 457 | 458 | const auto & calendar = feed.get_calendar(); 459 | REQUIRE_EQ(calendar.size(), 2); 460 | CHECK_EQ(calendar[0].service_id, "FULLW"); 461 | CHECK_EQ(calendar[0].start_date, Date(2007, 01, 01)); 462 | CHECK_EQ(calendar[0].end_date, Date(2010, 12, 31)); 463 | CHECK_EQ(calendar[0].monday, CalendarAvailability::Available); 464 | CHECK_EQ(calendar[0].sunday, CalendarAvailability::Available); 465 | 466 | const auto & calendar_for_service = feed.get_calendar_dates("FULLW"); 467 | CHECK_EQ(std::distance(calendar_for_service.first, calendar_for_service.second), 1); 468 | } 469 | 470 | TEST_CASE("Calendar dates") 471 | { 472 | Feed feed(test_feed); 473 | REQUIRE_EQ(feed.read_feed(), ResultCode::OK); 474 | 475 | const auto & calendar_dates = feed.get_calendar_dates(); 476 | REQUIRE_EQ(calendar_dates.size(), 1); 477 | CHECK_EQ(calendar_dates[0].service_id, "FULLW"); 478 | CHECK_EQ(calendar_dates[0].date, Date(2007, 06, 04)); 479 | CHECK_EQ(calendar_dates[0].exception_type, CalendarDateException::Removed); 480 | 481 | const auto & d = feed.get_calendar_dates("FULLW"); 482 | CHECK_EQ(std::distance(d.first, d.second), 1); 483 | } 484 | 485 | TEST_CASE("Frequencies") 486 | { 487 | Feed feed(test_feed); 488 | REQUIRE_EQ(feed.read_feed(), ResultCode::OK); 489 | 490 | const auto & frequencies = feed.get_frequencies(); 491 | REQUIRE_EQ(frequencies.size(), 11); 492 | const size_t i = 10; 493 | CHECK_EQ(frequencies[i].trip_id, "STBA"); 494 | CHECK_EQ(frequencies[i].start_time, Time(6, 00, 00)); 495 | CHECK_EQ(frequencies[i].end_time, Time(22, 00, 00)); 496 | CHECK_EQ(frequencies[i].headway_secs, 1800); 497 | 498 | const auto & f = feed.get_frequencies("CITY1"); 499 | CHECK_EQ(std::distance(f.first, f.second), 5); 500 | } 501 | 502 | TEST_CASE("Fare attributes") 503 | { 504 | Feed feed(test_feed); 505 | REQUIRE_EQ(feed.read_feed(), ResultCode::OK); 506 | 507 | const auto & attributes = feed.get_fare_attributes(); 508 | REQUIRE_EQ(attributes.size(), 3); 509 | 510 | CHECK_EQ(attributes[0].fare_id, "a"); 511 | CHECK_EQ(attributes[0].price, 5.25); 512 | CHECK_EQ(attributes[0].currency_type, "USD"); 513 | CHECK_EQ(attributes[0].payment_method, FarePayment::BeforeBoarding); 514 | CHECK_EQ(attributes[0].transfers, FareTransfers::Once); 515 | CHECK_EQ(attributes[0].transfer_duration, 0); 516 | 517 | CHECK_EQ(attributes[1].fare_id, "p"); 518 | CHECK_EQ(attributes[1].price, 1.25); 519 | CHECK_EQ(attributes[1].currency_type, "USD"); 520 | CHECK_EQ(attributes[1].payment_method, FarePayment::OnBoard); 521 | CHECK_EQ(attributes[1].transfers, FareTransfers::No); 522 | CHECK_EQ(attributes[1].transfer_duration, 0); 523 | 524 | CHECK_EQ(attributes[2].fare_id, "x"); 525 | CHECK_EQ(attributes[2].price, 20); 526 | CHECK_EQ(attributes[2].currency_type, "USD"); 527 | CHECK_EQ(attributes[2].payment_method, FarePayment::OnBoard); 528 | CHECK_EQ(attributes[2].transfers, FareTransfers::Unlimited); 529 | CHECK_EQ(attributes[2].transfer_duration, 60); 530 | 531 | const auto & attributes_for_id = feed.get_fare_attributes("a"); 532 | REQUIRE_EQ(std::distance(attributes_for_id.first, attributes_for_id.second), 1); 533 | CHECK_EQ(attributes_for_id.first->price, 5.25); 534 | 535 | REQUIRE_EQ(feed.write_fare_attributes(test_output_feed), ResultCode::OK); 536 | Feed feed_copy(test_output_feed); 537 | REQUIRE_EQ(feed_copy.read_feed(false), ResultCode::OK); 538 | CHECK_EQ(attributes, feed_copy.get_fare_attributes()); 539 | } 540 | 541 | TEST_CASE("Fare rules") 542 | { 543 | Feed feed(test_feed); 544 | REQUIRE_EQ(feed.read_feed(), ResultCode::OK); 545 | 546 | const auto & fare_rules = feed.get_fare_rules(); 547 | REQUIRE_EQ(fare_rules.size(), 4); 548 | CHECK_EQ(fare_rules[1].fare_id, "p"); 549 | CHECK_EQ(fare_rules[1].route_id, "AB"); 550 | 551 | const auto & rules_for_id = feed.get_fare_rules("p"); 552 | CHECK_EQ(std::distance(rules_for_id.first, rules_for_id.second), 3); 553 | } 554 | 555 | TEST_CASE("Levels") 556 | { 557 | Feed feed(test_feed); 558 | REQUIRE_EQ(feed.read_feed(), ResultCode::OK); 559 | 560 | const auto & levels = feed.get_levels(); 561 | REQUIRE_EQ(levels.size(), 3); 562 | CHECK_EQ(levels[1].level_id, "U321L1"); 563 | CHECK_EQ(levels[1].level_index, -1.5); 564 | 565 | const auto & level = feed.get_level("U321L2"); 566 | 567 | CHECK_EQ(level.level_index, -2); 568 | CHECK_EQ(level.level_name, "Vestibul2"); 569 | } 570 | 571 | TEST_CASE("Pathways") 572 | { 573 | Feed feed(test_feed); 574 | REQUIRE_EQ(feed.read_feed(), ResultCode::OK); 575 | 576 | const auto & pathways = feed.get_pathways(); 577 | REQUIRE_EQ(pathways.size(), 3); 578 | CHECK_EQ(pathways[0].pathway_id, "T-A01C01"); 579 | CHECK_EQ(pathways[0].from_stop_id, "1073S"); 580 | CHECK_EQ(pathways[0].to_stop_id, "1098E"); 581 | CHECK_EQ(pathways[0].pathway_mode, PathwayMode::Stairs); 582 | CHECK_EQ(pathways[0].signposted_as, "Sign1"); 583 | CHECK_EQ(pathways[0].reversed_signposted_as, "Sign2"); 584 | CHECK_EQ(pathways[0].is_bidirectional, PathwayDirection::Bidirectional); 585 | 586 | const auto & pathways_by_id = feed.get_pathways("T-A01D01"); 587 | 588 | CHECK_EQ(std::distance(pathways_by_id.first, pathways_by_id.second), 2); 589 | CHECK_EQ(pathways_by_id.first->is_bidirectional, PathwayDirection::Unidirectional); 590 | CHECK(pathways_by_id.first->reversed_signposted_as.empty()); 591 | } 592 | 593 | TEST_CASE("Translations") 594 | { 595 | Feed feed(test_feed); 596 | REQUIRE_EQ(feed.read_feed(), ResultCode::OK); 597 | 598 | const auto & translations = feed.get_translations(); 599 | REQUIRE_EQ(translations.size(), 1); 600 | CHECK_EQ(translations[0].table_name, "stop_times"); 601 | CHECK_EQ(translations[0].field_name, "stop_headsign"); 602 | CHECK_EQ(translations[0].language, "en"); 603 | CHECK_EQ(translations[0].translation, "Downtown"); 604 | CHECK(translations[0].record_id.empty()); 605 | CHECK(translations[0].record_sub_id.empty()); 606 | CHECK(translations[0].field_value.empty()); 607 | 608 | auto const & t = feed.get_translations("stop_times"); 609 | CHECK_EQ(std::distance(t.first, t.second), 1); 610 | } 611 | 612 | TEST_CASE("Attributions") 613 | { 614 | Feed feed(test_feed); 615 | REQUIRE_EQ(feed.read_feed(), ResultCode::OK); 616 | 617 | const auto & attributions = feed.get_attributions(); 618 | REQUIRE_EQ(attributions.size(), 1); 619 | CHECK_EQ(attributions[0].attribution_id, "0"); 620 | CHECK_EQ(attributions[0].organization_name, "Test inc"); 621 | CHECK_EQ(attributions[0].is_producer, AttributionRole::Yes); 622 | CHECK_EQ(attributions[0].is_operator, AttributionRole::No); 623 | CHECK_EQ(attributions[0].is_authority, AttributionRole::No); 624 | CHECK_EQ(attributions[0].attribution_url, "https://test.pl/gtfs/"); 625 | CHECK(attributions[0].attribution_email.empty()); 626 | CHECK(attributions[0].attribution_phone.empty()); 627 | } 628 | 629 | TEST_CASE("Feed info") 630 | { 631 | Feed feed(test_feed); 632 | REQUIRE_EQ(feed.read_feed(), ResultCode::OK); 633 | 634 | const auto & info = feed.get_feed_info(); 635 | 636 | CHECK_EQ(info.feed_publisher_name, "Test Solutions, Inc."); 637 | CHECK_EQ(info.feed_publisher_url, "http://test"); 638 | CHECK_EQ(info.feed_lang, "en"); 639 | } 640 | 641 | TEST_SUITE_END(); 642 | 643 | TEST_SUITE_BEGIN("Simple pipelines"); 644 | 645 | TEST_CASE("Agencies create & save") 646 | { 647 | Feed feed_for_writing; 648 | 649 | Agency agency1; 650 | agency1.agency_id = "0Id_b^3 Company"; 651 | agency1.agency_name = R"(Big Big "Bus Company")"; 652 | agency1.agency_email = "b3c@gtfs.com"; 653 | agency1.agency_fare_url = "b3c.no"; 654 | 655 | Agency agency2; 656 | agency2.agency_id = "kwf"; 657 | agency2.agency_name = R"("killer whale ferries")"; 658 | agency2.agency_lang = "en"; 659 | agency2.agency_phone = "842"; 660 | agency2.agency_timezone = "Asia/Tokyo"; 661 | agency2.agency_fare_url = "f@mail.com"; 662 | 663 | feed_for_writing.add_agency(agency1); 664 | feed_for_writing.add_agency(agency2); 665 | 666 | REQUIRE_EQ(feed_for_writing.write_agencies(test_output_feed), ResultCode::OK); 667 | Feed feed_for_testing(test_output_feed); 668 | 669 | REQUIRE_EQ(feed_for_testing.read_feed(false), ResultCode::OK); 670 | CHECK_EQ(feed_for_writing.get_agencies(), feed_for_testing.get_agencies()); 671 | } 672 | 673 | TEST_CASE("Shapes create & save") 674 | { 675 | Feed feed_for_writing; 676 | 677 | feed_for_writing.add_shape(ShapePoint{"id1", 61.197843, -149.867731, 0, 0}); 678 | feed_for_writing.add_shape(ShapePoint{"id1", 61.199419, -149.867680, 1, 178}); 679 | feed_for_writing.add_shape(ShapePoint{"id2", 61.199972, -149.867731, 2, 416}); 680 | 681 | REQUIRE_EQ(feed_for_writing.write_shapes(test_output_feed), ResultCode::OK); 682 | Feed feed_for_testing(test_output_feed); 683 | 684 | REQUIRE_EQ(feed_for_testing.read_feed(false), ResultCode::OK); 685 | CHECK_EQ(feed_for_testing.get_shapes().size(), 3); 686 | } 687 | TEST_SUITE_END(); 688 | -------------------------------------------------------------------------------- /include/just_gtfs/just_gtfs.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | namespace gtfs 25 | { 26 | // File names and other entities defined in GTFS---------------------------------------------------- 27 | inline const std::string file_agency = "agency.txt"; 28 | inline const std::string file_stops = "stops.txt"; 29 | inline const std::string file_routes = "routes.txt"; 30 | inline const std::string file_trips = "trips.txt"; 31 | inline const std::string file_stop_times = "stop_times.txt"; 32 | inline const std::string file_calendar = "calendar.txt"; 33 | inline const std::string file_calendar_dates = "calendar_dates.txt"; 34 | inline const std::string file_fare_attributes = "fare_attributes.txt"; 35 | inline const std::string file_fare_rules = "fare_rules.txt"; 36 | inline const std::string file_shapes = "shapes.txt"; 37 | inline const std::string file_frequencies = "frequencies.txt"; 38 | inline const std::string file_transfers = "transfers.txt"; 39 | inline const std::string file_pathways = "pathways.txt"; 40 | inline const std::string file_levels = "levels.txt"; 41 | inline const std::string file_feed_info = "feed_info.txt"; 42 | inline const std::string file_translations = "translations.txt"; 43 | inline const std::string file_attributions = "attributions.txt"; 44 | 45 | inline constexpr char csv_separator = ','; 46 | inline constexpr char quote = '"'; 47 | 48 | // Helper classes and functions--------------------------------------------------------------------- 49 | struct InvalidFieldFormat : public std::exception 50 | { 51 | public: 52 | explicit InvalidFieldFormat(const std::string & msg) : message(prefix + msg) {} 53 | 54 | const char * what() const noexcept { return message.c_str(); } 55 | 56 | private: 57 | const std::string prefix = "Invalid GTFS field format. "; 58 | std::string message; 59 | }; 60 | 61 | enum ResultCode 62 | { 63 | OK, 64 | END_OF_FILE, 65 | ERROR_INVALID_GTFS_PATH, 66 | ERROR_FILE_ABSENT, 67 | ERROR_REQUIRED_FIELD_ABSENT, 68 | ERROR_INVALID_FIELD_FORMAT 69 | }; 70 | 71 | using Message = std::string; 72 | 73 | struct Result 74 | { 75 | Result() = default; 76 | Result(ResultCode && in_code) : code(in_code) {} 77 | Result(const ResultCode & in_code, const Message & msg) : code(in_code), message(msg) {} 78 | bool operator==(ResultCode result_code) const { return code == result_code; } 79 | bool operator!=(ResultCode result_code) const { return !(*this == result_code); } 80 | 81 | ResultCode code = OK; 82 | Message message; 83 | }; 84 | 85 | inline std::string add_trailing_slash(const std::string & path) 86 | { 87 | auto extended_path = path; 88 | if (!extended_path.empty() && extended_path.back() != '/') 89 | extended_path += "/"; 90 | return extended_path; 91 | } 92 | 93 | inline void write_joined(std::ofstream & out, std::vector && elements) 94 | { 95 | for (size_t i = 0; i < elements.size(); ++i) 96 | { 97 | out << elements[i]; 98 | if (i != elements.size() - 1) 99 | out << csv_separator; 100 | } 101 | out << std::endl; 102 | } 103 | 104 | inline std::string quote_text(const std::string & text) 105 | { 106 | std::stringstream stream; 107 | stream << std::quoted(text, quote, quote); 108 | return stream.str(); 109 | } 110 | 111 | inline std::string unquote_text(const std::string & text) 112 | { 113 | std::string res; 114 | bool prev_is_quote = false; 115 | bool prev_is_skipped = false; 116 | 117 | size_t start_index = 0; 118 | size_t end_index = text.size(); 119 | 120 | // Field values that contain quotation marks or commas must be enclosed within quotation marks. 121 | if (text.size() > 1 && text.front() == quote && text.back() == quote) 122 | { 123 | ++start_index; 124 | --end_index; 125 | } 126 | 127 | // In addition, each quotation mark in the field value must be preceded with a quotation mark. 128 | for (size_t i = start_index; i < end_index; ++i) 129 | { 130 | if (text[i] != quote) 131 | { 132 | res += text[i]; 133 | prev_is_quote = false; 134 | prev_is_skipped = false; 135 | continue; 136 | } 137 | 138 | if (prev_is_quote) 139 | { 140 | if (prev_is_skipped) 141 | res += text[i]; 142 | 143 | prev_is_skipped = !prev_is_skipped; 144 | } 145 | else 146 | { 147 | prev_is_quote = true; 148 | res += text[i]; 149 | } 150 | } 151 | 152 | return res; 153 | } 154 | 155 | // Csv field values that contain quotation marks or commas must be enclosed within quotation marks. 156 | inline std::string wrap(const std::string & text) 157 | { 158 | static const std::string symbols = std::string(1, quote) + std::string(1, csv_separator); 159 | 160 | if (text.find_first_of(symbols) == std::string::npos) 161 | return text; 162 | 163 | return quote_text(text); 164 | } 165 | 166 | // Save to csv enum value as unsigned integer. 167 | template 168 | std::enable_if_t::value || std::is_enum::value, std::string> wrap( 169 | const T & val) 170 | { 171 | return std::to_string(static_cast(val)); 172 | } 173 | 174 | // Save to csv coordinates with custom precision. 175 | inline std::string wrap(double val) 176 | { 177 | std::ostringstream stream; 178 | stream << std::fixed << std::setprecision(6); 179 | stream << val; 180 | return stream.str(); 181 | } 182 | 183 | inline void write_agency_header(std::ofstream & out) 184 | { 185 | std::vector fields = {"agency_id", "agency_name", "agency_url", 186 | "agency_timezone", "agency_lang", "agency_phone", 187 | "agency_fare_url", "agency_email"}; 188 | write_joined(out, std::move(fields)); 189 | } 190 | 191 | inline void write_routes_header(std::ofstream & out) 192 | { 193 | std::vector fields = { 194 | "route_id", "agency_id", "route_short_name", "route_long_name", 195 | "route_desc", "route_type", "route_url", "route_color", 196 | "route_text_color", "route_sort_order", "continuous_pickup", "continuous_drop_off"}; 197 | write_joined(out, std::move(fields)); 198 | } 199 | 200 | inline void write_shapes_header(std::ofstream & out) 201 | { 202 | std::vector fields = {"shape_id", "shape_pt_lat", "shape_pt_lon", 203 | "shape_pt_sequence", "shape_dist_traveled"}; 204 | write_joined(out, std::move(fields)); 205 | } 206 | 207 | inline void write_trips_header(std::ofstream & out) 208 | { 209 | std::vector fields = { 210 | "route_id", "service_id", "trip_id", "trip_headsign", "trip_short_name", 211 | "direction_id", "block_id", "shape_id", "wheelchair_accessible", "bikes_allowed"}; 212 | write_joined(out, std::move(fields)); 213 | } 214 | 215 | inline void write_stops_header(std::ofstream & out) 216 | { 217 | std::vector fields = {"stop_id", "stop_code", "stop_name", 218 | "stop_desc", "stop_lat", "stop_lon", 219 | "zone_id", "stop_url", "location_type", 220 | "parent_station", "stop_timezone", "wheelchair_boarding", 221 | "level_id", "platform_code"}; 222 | write_joined(out, std::move(fields)); 223 | } 224 | 225 | inline void write_stop_times_header(std::ofstream & out) 226 | { 227 | std::vector fields = { 228 | "trip_id", "arrival_time", "departure_time", "stop_id", 229 | "stop_sequence", "stop_headsign", "pickup_type", "drop_off_type", 230 | "continuous_pickup", "continuous_drop_off", "shape_dist_traveled", "timepoint"}; 231 | write_joined(out, std::move(fields)); 232 | } 233 | 234 | inline void write_calendar_header(std::ofstream & out) 235 | { 236 | std::vector fields = {"service_id", "monday", "tuesday", "wednesday", "thursday", 237 | "friday", "saturday", "sunday", "start_date", "end_date"}; 238 | write_joined(out, std::move(fields)); 239 | } 240 | 241 | inline void write_calendar_dates_header(std::ofstream & out) 242 | { 243 | std::vector fields = {"service_id", "date", "exception_type"}; 244 | write_joined(out, std::move(fields)); 245 | } 246 | 247 | inline void write_transfers_header(std::ofstream & out) 248 | { 249 | std::vector fields = {"from_stop_id", "to_stop_id", "transfer_type", 250 | "min_transfer_time"}; 251 | write_joined(out, std::move(fields)); 252 | } 253 | 254 | inline void write_frequencies_header(std::ofstream & out) 255 | { 256 | std::vector fields = {"trip_id", "start_time", "end_time", "headway_secs", 257 | "exact_times"}; 258 | write_joined(out, std::move(fields)); 259 | } 260 | 261 | inline void write_fare_attributes_header(std::ofstream & out) 262 | { 263 | std::vector fields = {"fare_id", "price", "currency_type", "payment_method", 264 | "transfers", "agency_id", "transfer_duration"}; 265 | write_joined(out, std::move(fields)); 266 | } 267 | 268 | inline void write_fare_rules_header(std::ofstream & out) 269 | { 270 | std::vector fields = {"fare_id", "route_id", "origin_id", "destination_id", 271 | "contains_id"}; 272 | write_joined(out, std::move(fields)); 273 | } 274 | 275 | inline void write_pathways_header(std::ofstream & out) 276 | { 277 | std::vector fields = { 278 | "pathway_id", "from_stop_id", "to_stop_id", "pathway_mode", 279 | "is_bidirectional", "length", "traversal_time", "stair_count", 280 | "max_slope", "min_width", "signposted_as", "reversed_signposted_as"}; 281 | write_joined(out, std::move(fields)); 282 | } 283 | 284 | inline void write_levels_header(std::ofstream & out) 285 | { 286 | std::vector fields = {"level_id", "level_index", "level_name"}; 287 | write_joined(out, std::move(fields)); 288 | } 289 | 290 | inline void write_feed_info_header(std::ofstream & out) 291 | { 292 | std::vector fields = { 293 | "feed_publisher_name", "feed_publisher_url", "feed_lang", 294 | "default_lang", "feed_start_date", "feed_end_date", 295 | "feed_version", "feed_contact_email", "feed_contact_url"}; 296 | write_joined(out, std::move(fields)); 297 | } 298 | 299 | inline void write_translations_header(std::ofstream & out) 300 | { 301 | std::vector fields = {"table_name", "field_name", "language", "translation", 302 | "record_id", "record_sub_id", "field_value"}; 303 | write_joined(out, std::move(fields)); 304 | } 305 | 306 | inline void write_attributions_header(std::ofstream & out) 307 | { 308 | std::vector fields = {"attribution_id", "agency_id", "route_id", 309 | "trip_id", "organization_name", "is_producer", 310 | "is_operator", "is_authority", "attribution_url", 311 | "attribution_email", "attribution_phone"}; 312 | write_joined(out, std::move(fields)); 313 | } 314 | 315 | // Csv parser ------------------------------------------------------------------------------------- 316 | class CsvParser 317 | { 318 | public: 319 | CsvParser() = default; 320 | inline explicit CsvParser(const std::string & gtfs_directory); 321 | 322 | inline Result read_header(const std::string & csv_filename); 323 | inline Result read_row(std::map & obj); 324 | 325 | inline static std::vector split_record(const std::string & record, 326 | bool is_header = false); 327 | 328 | private: 329 | std::vector field_sequence; 330 | std::string gtfs_path; 331 | std::ifstream csv_stream; 332 | }; 333 | 334 | inline CsvParser::CsvParser(const std::string & gtfs_directory) : gtfs_path(gtfs_directory) {} 335 | 336 | inline std::string trim_spaces(const std::string & token) 337 | { 338 | static const std::string delimiters = " \t"; 339 | std::string res = token; 340 | res.erase(0, res.find_first_not_of(delimiters)); 341 | res.erase(res.find_last_not_of(delimiters) + 1); 342 | return res; 343 | } 344 | 345 | inline std::string normalize(std::string & token, bool has_quotes) 346 | { 347 | std::string res = trim_spaces(token); 348 | if (has_quotes) 349 | return unquote_text(res); 350 | return res; 351 | } 352 | 353 | inline std::vector CsvParser::split_record(const std::string & record, bool is_header) 354 | { 355 | size_t start_index = 0; 356 | if (is_header) 357 | { 358 | // ignore UTF-8 BOM prefix: 359 | if (record.size() > 2 && record[0] == '\xef' && record[1] == '\xbb' && record[2] == '\xbf') 360 | start_index = 3; 361 | } 362 | 363 | std::vector fields; 364 | fields.reserve(20); 365 | 366 | std::string token; 367 | token.reserve(record.size()); 368 | 369 | bool is_inside_quotes = false; 370 | bool quotes_in_token = false; 371 | 372 | for (size_t i = start_index; i < record.size(); ++i) 373 | { 374 | if (record[i] == quote) 375 | { 376 | is_inside_quotes = !is_inside_quotes; 377 | quotes_in_token = true; 378 | token += record[i]; 379 | continue; 380 | } 381 | 382 | if (record[i] == csv_separator) 383 | { 384 | if (is_inside_quotes) 385 | { 386 | token += record[i]; 387 | continue; 388 | } 389 | 390 | fields.emplace_back(normalize(token, quotes_in_token)); 391 | token.clear(); 392 | quotes_in_token = false; 393 | continue; 394 | } 395 | 396 | // Skip delimiters: 397 | if (record[i] != '\t' && record[i] != '\r') 398 | token += record[i]; 399 | } 400 | 401 | fields.emplace_back(normalize(token, quotes_in_token)); 402 | return fields; 403 | } 404 | 405 | inline Result CsvParser::read_header(const std::string & csv_filename) 406 | { 407 | if (csv_stream.is_open()) 408 | csv_stream.close(); 409 | 410 | csv_stream.open(gtfs_path + csv_filename); 411 | if (!csv_stream.is_open()) 412 | return {ResultCode::ERROR_FILE_ABSENT, "File " + csv_filename + " could not be opened"}; 413 | 414 | std::string header; 415 | if (!getline(csv_stream, header) || header.empty()) 416 | return {ResultCode::ERROR_INVALID_FIELD_FORMAT, "Empty header in file " + csv_filename}; 417 | 418 | field_sequence = split_record(header, true); 419 | return ResultCode::OK; 420 | } 421 | 422 | inline Result CsvParser::read_row(std::map & obj) 423 | { 424 | obj = {}; 425 | std::string row; 426 | if (!getline(csv_stream, row)) 427 | return {ResultCode::END_OF_FILE, {}}; 428 | 429 | if (row == "\r") 430 | return ResultCode::OK; 431 | 432 | const std::vector fields_values = split_record(row); 433 | 434 | // Different count of fields in the row and in the header of csv. 435 | // Typical approach is to skip not required fields. 436 | const size_t fields_count = std::min(field_sequence.size(), fields_values.size()); 437 | 438 | for (size_t i = 0; i < fields_count; ++i) 439 | obj[field_sequence[i]] = fields_values[i]; 440 | 441 | return ResultCode::OK; 442 | } 443 | 444 | // Custom types for GTFS fields -------------------------------------------------------------------- 445 | // Id of GTFS entity, a sequence of any UTF-8 characters. Used as type for ID GTFS fields. 446 | using Id = std::string; 447 | // A string of UTF-8 characters. Used as type for Text GTFS fields. 448 | using Text = std::string; 449 | 450 | // Time in GTFS is in the HH:MM:SS format (H:MM:SS is also accepted) 451 | // Time within a service day can be above 24:00:00, e.g. 28:41:30 452 | class Time 453 | { 454 | public: 455 | inline Time() = default; 456 | inline explicit Time(const std::string & raw_time_str); 457 | inline Time(uint16_t hours, uint16_t minutes, uint16_t seconds); 458 | inline Time(size_t seconds); 459 | inline bool is_provided() const; 460 | inline size_t get_total_seconds() const; 461 | inline std::tuple get_hh_mm_ss() const; 462 | inline std::string get_raw_time() const; 463 | inline bool limit_hours_to_24max(); 464 | 465 | private: 466 | inline void set_total_seconds(); 467 | inline void set_raw_time(); 468 | bool time_is_provided = false; 469 | std::string raw_time; 470 | size_t total_seconds = 0; 471 | uint16_t hh = 0; 472 | uint16_t mm = 0; 473 | uint16_t ss = 0; 474 | }; 475 | 476 | inline bool operator==(const Time & lhs, const Time & rhs) 477 | { 478 | return lhs.get_hh_mm_ss() == rhs.get_hh_mm_ss() && lhs.is_provided() == rhs.is_provided(); 479 | } 480 | 481 | inline bool Time::limit_hours_to_24max() 482 | { 483 | if (hh < 24) 484 | return false; 485 | 486 | hh = hh % 24; 487 | set_total_seconds(); 488 | set_raw_time(); 489 | return true; 490 | } 491 | 492 | inline void Time::set_total_seconds() { total_seconds = hh * 60 * 60 + mm * 60 + ss; } 493 | 494 | inline std::string append_leading_zero(const std::string & s, bool check = true) 495 | { 496 | if (check && s.size() > 2) 497 | throw InvalidFieldFormat("The string for appending zero is too long: " + s); 498 | 499 | if (s.size() == 2) 500 | return s; 501 | return "0" + s; 502 | } 503 | 504 | inline void Time::set_raw_time() 505 | { 506 | const std::string hh_str = append_leading_zero(std::to_string(hh), false); 507 | const std::string mm_str = append_leading_zero(std::to_string(mm)); 508 | const std::string ss_str = append_leading_zero(std::to_string(ss)); 509 | 510 | raw_time = hh_str + ":" + mm_str + ":" + ss_str; 511 | } 512 | 513 | // Time in the HH:MM:SS format (H:MM:SS is also accepted). Used as type for Time GTFS fields. 514 | inline Time::Time(const std::string & raw_time_str) : raw_time(raw_time_str) 515 | { 516 | if (raw_time_str.empty()) 517 | return; 518 | 519 | const size_t len = raw_time.size(); 520 | if (!(len >= 7 && len <= 9) || raw_time[len - 3] != ':' || raw_time[len - 6] != ':') 521 | throw InvalidFieldFormat("Time is not in [[H]H]H:MM:SS format: " + raw_time_str); 522 | 523 | hh = static_cast(std::stoi(raw_time.substr(0, len - 6))); 524 | mm = static_cast(std::stoi(raw_time.substr(len - 5, 2))); 525 | ss = static_cast(std::stoi(raw_time.substr(len - 2))); 526 | 527 | if (mm > 60 || ss > 60) 528 | throw InvalidFieldFormat("Time minutes/seconds wrong value: " + std::to_string(mm) + 529 | " minutes, " + std::to_string(ss) + " seconds"); 530 | 531 | set_total_seconds(); 532 | time_is_provided = true; 533 | } 534 | 535 | inline Time::Time(uint16_t hours, uint16_t minutes, uint16_t seconds) 536 | : hh(hours), mm(minutes), ss(seconds) 537 | { 538 | if (mm > 60 || ss > 60) 539 | throw InvalidFieldFormat("Time is out of range: " + std::to_string(mm) + "minutes " + 540 | std::to_string(ss) + "seconds"); 541 | 542 | set_total_seconds(); 543 | set_raw_time(); 544 | time_is_provided = true; 545 | } 546 | 547 | inline Time::Time(size_t seconds) 548 | : time_is_provided(true) 549 | , total_seconds(seconds) 550 | , hh(seconds / 3600) 551 | , mm((seconds % 3600) / 60) 552 | , ss(seconds % 3600) 553 | { 554 | set_raw_time(); 555 | } 556 | 557 | inline bool Time::is_provided() const { return time_is_provided; } 558 | 559 | inline size_t Time::get_total_seconds() const { return total_seconds; } 560 | 561 | inline std::tuple Time::get_hh_mm_ss() const { return {hh, mm, ss}; } 562 | 563 | inline std::string Time::get_raw_time() const { return raw_time; } 564 | 565 | // Service day in the YYYYMMDD format. 566 | class Date 567 | { 568 | public: 569 | inline Date() = default; 570 | inline Date(uint16_t year, uint16_t month, uint16_t day); 571 | inline explicit Date(const std::string & raw_date_str); 572 | inline bool is_provided() const; 573 | inline std::tuple get_yyyy_mm_dd() const; 574 | inline std::string get_raw_date() const; 575 | 576 | private: 577 | inline void check_valid() const; 578 | 579 | std::string raw_date; 580 | uint16_t yyyy = 0; 581 | uint16_t mm = 0; 582 | uint16_t dd = 0; 583 | bool date_is_provided = false; 584 | }; 585 | 586 | inline bool operator==(const Date & lhs, const Date & rhs) 587 | { 588 | return lhs.get_yyyy_mm_dd() == rhs.get_yyyy_mm_dd() && lhs.is_provided() == rhs.is_provided(); 589 | } 590 | 591 | inline void Date::check_valid() const 592 | { 593 | if (yyyy < 1000 || yyyy > 9999 || mm < 1 || mm > 12 || dd < 1 || dd > 31) 594 | throw InvalidFieldFormat("Date check failed: out of range. " + std::to_string(yyyy) + 595 | " year, " + std::to_string(mm) + " month, " + std::to_string(dd) + 596 | " day"); 597 | 598 | if (mm == 2 && dd > 28) 599 | { 600 | // The year is not leap. Days count should be 28. 601 | if (yyyy % 4 != 0 || (yyyy % 100 == 0 && yyyy % 400 != 0)) 602 | throw InvalidFieldFormat("Invalid days count in February of non-leap year: " + 603 | std::to_string(dd) + " year" + std::to_string(yyyy)); 604 | 605 | // The year is leap. Days count should be 29. 606 | if (dd > 29) 607 | throw InvalidFieldFormat("Invalid days count in February of leap year: " + 608 | std::to_string(dd) + " year" + std::to_string(yyyy)); 609 | } 610 | 611 | if (dd > 30 && (mm == 4 || mm == 6 || mm == 9 || mm == 11)) 612 | throw InvalidFieldFormat("Invalid days count in month: " + std::to_string(dd) + " days in " + 613 | std::to_string(mm)); 614 | } 615 | 616 | inline Date::Date(uint16_t year, uint16_t month, uint16_t day) : yyyy(year), mm(month), dd(day) 617 | { 618 | check_valid(); 619 | const std::string mm_str = append_leading_zero(std::to_string(mm)); 620 | const std::string dd_str = append_leading_zero(std::to_string(dd)); 621 | 622 | raw_date = std::to_string(yyyy) + mm_str + dd_str; 623 | date_is_provided = true; 624 | } 625 | 626 | inline Date::Date(const std::string & raw_date_str) : raw_date(raw_date_str) 627 | { 628 | if (raw_date.empty()) 629 | return; 630 | 631 | if (raw_date.size() != 8) 632 | throw InvalidFieldFormat("Date is not in YYYY:MM::DD format: " + raw_date_str); 633 | 634 | yyyy = static_cast(std::stoi(raw_date.substr(0, 4))); 635 | mm = static_cast(std::stoi(raw_date.substr(4, 2))); 636 | dd = static_cast(std::stoi(raw_date.substr(6, 2))); 637 | 638 | check_valid(); 639 | 640 | date_is_provided = true; 641 | } 642 | 643 | inline bool Date::is_provided() const { return date_is_provided; } 644 | 645 | inline std::tuple Date::get_yyyy_mm_dd() const 646 | { 647 | return {yyyy, mm, dd}; 648 | } 649 | 650 | inline std::string Date::get_raw_date() const { return raw_date; } 651 | 652 | // An ISO 4217 alphabetical currency code. Used as type for Currency Code GTFS fields. 653 | using CurrencyCode = std::string; 654 | // An IETF BCP 47 language code. Used as type for Language Code GTFS fields. 655 | using LanguageCode = std::string; 656 | 657 | // Helper enums for some GTFS fields --------------------------------------------------------------- 658 | enum class StopLocationType : int8_t 659 | { 660 | StopOrPlatform = 0, 661 | Station = 1, 662 | EntranceExit = 2, 663 | GenericNode = 3, 664 | BoardingArea = 4 665 | }; 666 | 667 | // The type of transportation used on a route. 668 | enum class RouteType : int16_t 669 | { 670 | // GTFS route types 671 | Tram = 0, // Tram, Streetcar, Light rail 672 | Subway = 1, // Any underground rail system within a metropolitan area 673 | Rail = 2, // Intercity or long-distance travel 674 | Bus = 3, // Short- and long-distance bus routes 675 | Ferry = 4, // Boat service 676 | CableTram = 5, // Street-level rail cars where the cable runs beneath the vehicle 677 | AerialLift = 6, // Aerial lift, suspended cable car (gondola lift, aerial tramway) 678 | Funicular = 7, // Any rail system designed for steep inclines 679 | Trolleybus = 11, // Electric buses that draw power from overhead wires using poles 680 | Monorail = 12, // Railway in which the track consists of a single rail or a beam 681 | 682 | // Extended route types 683 | // https://developers.google.com/transit/gtfs/reference/extended-route-types 684 | RailwayService = 100, 685 | HighSpeedRailService = 101, 686 | LongDistanceTrains = 102, 687 | InterRegionalRailService = 103, 688 | CarTransportRailService = 104, 689 | SleeperRailService = 105, 690 | RegionalRailService = 106, 691 | TouristRailwayService = 107, 692 | RailShuttleWithinComplex = 108, 693 | SuburbanRailway = 109, 694 | ReplacementRailService = 110, 695 | SpecialRailService = 111, 696 | LorryTransportRailService = 112, 697 | AllRailServices = 113, 698 | CrossCountryRailService = 114, 699 | VehicleTransportRailService = 115, 700 | RackAndPinionRailway = 116, 701 | AdditionalRailService = 117, 702 | 703 | CoachService = 200, 704 | InternationalCoachService = 201, 705 | NationalCoachService = 202, 706 | ShuttleCoachService = 203, 707 | RegionalCoachService = 204, 708 | SpecialCoachService = 205, 709 | SightseeingCoachService = 206, 710 | TouristCoachService = 207, 711 | CommuterCoachService = 208, 712 | AllCoachServices = 209, 713 | 714 | UrbanRailwayService400 = 400, 715 | MetroService = 401, 716 | UndergroundService = 402, 717 | UrbanRailwayService403 = 403, 718 | AllUrbanRailwayServices = 404, 719 | Monorail405 = 405, 720 | 721 | BusService = 700, 722 | RegionalBusService = 701, 723 | ExpressBusService = 702, 724 | StoppingBusService = 703, 725 | LocalBusService = 704, 726 | NightBusService = 705, 727 | PostBusService = 706, 728 | SpecialNeedsBus = 707, 729 | MobilityBusService = 708, 730 | MobilityBusForRegisteredDisabled = 709, 731 | SightseeingBus = 710, 732 | ShuttleBus = 711, 733 | SchoolBus = 712, 734 | SchoolAndPublicServiceBus = 713, 735 | RailReplacementBusService = 714, 736 | DemandAndResponseBusService = 715, 737 | AllBusServices = 716, 738 | 739 | TrolleybusService = 800, 740 | 741 | TramService = 900, 742 | CityTramService = 901, 743 | LocalTramService = 902, 744 | RegionalTramService = 903, 745 | SightseeingTramService = 904, 746 | ShuttleTramService = 905, 747 | AllTramServices = 906, 748 | 749 | WaterTransportService = 1000, 750 | AirService = 1100, 751 | FerryService = 1200, 752 | AerialLiftService = 1300, 753 | FunicularService = 1400, 754 | TaxiService = 1500, 755 | CommunalTaxiService = 1501, 756 | WaterTaxiService = 1502, 757 | RailTaxiService = 1503, 758 | BikeTaxiService = 1504, 759 | LicensedTaxiService = 1505, 760 | PrivateHireServiceVehicle = 1506, 761 | AllTaxiServices = 1507, 762 | MiscellaneousService = 1700, 763 | HorseDrawnCarriage = 1702 764 | }; 765 | 766 | enum class TripDirectionId : bool 767 | { 768 | DefaultDirection = 0, // e.g. outbound 769 | OppositeDirection = 1 // e.g. inbound 770 | }; 771 | 772 | enum class TripAccess : int8_t 773 | { 774 | NoInfo = 0, 775 | Yes = 1, 776 | No = 2 777 | }; 778 | 779 | enum class StopTimeBoarding : int8_t 780 | { 781 | RegularlyScheduled = 0, 782 | No = 1, // Not available 783 | Phone = 2, // Must phone agency to arrange 784 | CoordinateWithDriver = 3 // Must coordinate with driver to arrange 785 | }; 786 | 787 | enum class StopTimePoint : bool 788 | { 789 | Approximate = 0, 790 | Exact = 1 791 | }; 792 | 793 | enum class CalendarAvailability : bool 794 | { 795 | NotAvailable = 0, 796 | Available = 1 797 | }; 798 | 799 | enum class CalendarDateException : int8_t 800 | { 801 | Added = 1, // Service has been added for the specified date 802 | Removed = 2 803 | }; 804 | 805 | enum class FarePayment : bool 806 | { 807 | OnBoard = 0, 808 | BeforeBoarding = 1 // Fare must be paid before boarding 809 | }; 810 | 811 | enum class FareTransfers : int8_t 812 | { 813 | No = 0, // No transfers permitted on this fare 814 | Once = 1, 815 | Twice = 2, 816 | Unlimited = 3 817 | }; 818 | 819 | enum class FrequencyTripService : bool 820 | { 821 | FrequencyBased = 0, // Frequency-based trips 822 | ScheduleBased = 1 // Schedule-based trips with the exact same headway throughout the day 823 | }; 824 | 825 | enum class TransferType : int8_t 826 | { 827 | Recommended = 0, 828 | Timed = 1, 829 | MinimumTime = 2, 830 | NotPossible = 3 831 | }; 832 | 833 | enum class PathwayMode : int8_t 834 | { 835 | Walkway = 1, 836 | Stairs = 2, 837 | MovingSidewalk = 3, // Moving sidewalk/travelator 838 | Escalator = 4, 839 | Elevator = 5, 840 | FareGate = 6, // Payment gate 841 | ExitGate = 7 842 | }; 843 | 844 | enum class PathwayDirection : bool 845 | { 846 | Unidirectional = 0, 847 | Bidirectional = 1 848 | }; 849 | 850 | enum class AttributionRole : bool 851 | { 852 | No = 0, // Organization doesn’t have this role 853 | Yes = 1 // Organization does have this role 854 | }; 855 | 856 | // Structures representing GTFS entities ----------------------------------------------------------- 857 | // Required dataset file 858 | struct Agency 859 | { 860 | // Conditionally optional: 861 | Id agency_id; 862 | 863 | // Required: 864 | Text agency_name; 865 | Text agency_url; 866 | Text agency_timezone; 867 | 868 | // Optional: 869 | Text agency_lang; 870 | Text agency_phone; 871 | Text agency_fare_url; 872 | Text agency_email; 873 | }; 874 | 875 | inline bool operator==(const Agency & lhs, const Agency & rhs) 876 | { 877 | return std::tie(lhs.agency_id, lhs.agency_name, lhs.agency_url, lhs.agency_timezone, 878 | lhs.agency_lang, lhs.agency_phone, lhs.agency_fare_url, lhs.agency_email) == 879 | std::tie(rhs.agency_id, rhs.agency_name, rhs.agency_url, rhs.agency_timezone, 880 | rhs.agency_lang, rhs.agency_phone, rhs.agency_fare_url, rhs.agency_email); 881 | } 882 | 883 | // Required dataset file 884 | struct Stop 885 | { 886 | // Required: 887 | Id stop_id; 888 | 889 | // Conditionally required: 890 | Text stop_name; 891 | 892 | bool coordinates_present = true; 893 | double stop_lat = 0.0; 894 | double stop_lon = 0.0; 895 | Id zone_id; 896 | Id parent_station; 897 | 898 | // Optional: 899 | Text stop_code; 900 | Text stop_desc; 901 | Text stop_url; 902 | StopLocationType location_type = StopLocationType::StopOrPlatform; 903 | Text stop_timezone; 904 | Text wheelchair_boarding; 905 | Id level_id; 906 | Text platform_code; 907 | }; 908 | 909 | inline bool operator==(const Stop & lhs, const Stop & rhs) 910 | { 911 | return std::tie(lhs.stop_id, lhs.stop_name, lhs.coordinates_present, lhs.stop_lat, 912 | lhs.stop_lon, lhs.zone_id, lhs.parent_station, lhs.stop_code, 913 | lhs.stop_desc, lhs.stop_url, lhs.location_type, lhs.stop_timezone, 914 | lhs.wheelchair_boarding, lhs.level_id, lhs.platform_code) == 915 | std::tie(rhs.stop_id, rhs.stop_name, rhs.coordinates_present, rhs.stop_lat, 916 | rhs.stop_lon, rhs.zone_id, rhs.parent_station, rhs.stop_code, 917 | rhs.stop_desc, rhs.stop_url, rhs.location_type, rhs.stop_timezone, 918 | rhs.wheelchair_boarding, rhs.level_id, rhs.platform_code); 919 | } 920 | 921 | // Required dataset file 922 | struct Route 923 | { 924 | // Required: 925 | Id route_id; 926 | RouteType route_type = RouteType::Tram; 927 | 928 | // Conditionally required: 929 | Id agency_id; 930 | Text route_short_name; 931 | Text route_long_name; 932 | 933 | // Optional 934 | Text route_desc; 935 | Text route_url; 936 | Text route_color; 937 | Text route_text_color; 938 | size_t route_sort_order = 0; // Routes with smaller value values should be displayed first 939 | }; 940 | 941 | inline bool operator==(const Route & lhs, const Route & rhs) 942 | { 943 | return std::tie(lhs.route_id, lhs.route_type, lhs.agency_id, lhs.route_short_name, lhs.route_long_name, 944 | lhs.route_desc, lhs.route_url, lhs.route_color, lhs.route_text_color, lhs.route_sort_order) == 945 | std::tie(rhs.route_id, rhs.route_type, rhs.agency_id, rhs.route_short_name, rhs.route_long_name, 946 | rhs.route_desc, rhs.route_url, rhs.route_color, rhs.route_text_color, rhs.route_sort_order); 947 | } 948 | 949 | // Required dataset file 950 | struct Trip 951 | { 952 | // Required: 953 | Id route_id; 954 | Id service_id; 955 | Id trip_id; 956 | 957 | // Optional: 958 | Text trip_headsign; 959 | Text trip_short_name; 960 | TripDirectionId direction_id = TripDirectionId::DefaultDirection; 961 | Id block_id; 962 | Id shape_id; 963 | TripAccess wheelchair_accessible = TripAccess::NoInfo; 964 | TripAccess bikes_allowed = TripAccess::NoInfo; 965 | }; 966 | 967 | inline bool operator==(const Trip & lhs, const Trip & rhs) 968 | { 969 | return std::tie(lhs.route_id, lhs.service_id, lhs.trip_id, lhs.trip_headsign, lhs.trip_short_name, 970 | lhs.direction_id, lhs.block_id, lhs.shape_id, lhs.wheelchair_accessible, lhs.bikes_allowed) == 971 | std::tie(rhs.route_id, rhs.service_id, rhs.trip_id, rhs.trip_headsign, rhs.trip_short_name, 972 | rhs.direction_id, rhs.block_id, rhs.shape_id, rhs.wheelchair_accessible, rhs.bikes_allowed); 973 | } 974 | 975 | // Required dataset file 976 | struct StopTime 977 | { 978 | // Required: 979 | Id trip_id; 980 | Id stop_id; 981 | size_t stop_sequence = 0; 982 | 983 | // Conditionally required: 984 | Time arrival_time; 985 | 986 | Time departure_time; 987 | 988 | // Optional: 989 | Text stop_headsign; 990 | StopTimeBoarding pickup_type = StopTimeBoarding::RegularlyScheduled; 991 | StopTimeBoarding drop_off_type = StopTimeBoarding::RegularlyScheduled; 992 | 993 | double shape_dist_traveled = 0.0; 994 | StopTimePoint timepoint = StopTimePoint::Exact; 995 | }; 996 | 997 | // Conditionally required dataset file: 998 | struct CalendarItem 999 | { 1000 | // Required: 1001 | Id service_id; 1002 | 1003 | // TODO: store this as a mask? 1004 | CalendarAvailability monday = CalendarAvailability::NotAvailable; 1005 | CalendarAvailability tuesday = CalendarAvailability::NotAvailable; 1006 | CalendarAvailability wednesday = CalendarAvailability::NotAvailable; 1007 | CalendarAvailability thursday = CalendarAvailability::NotAvailable; 1008 | CalendarAvailability friday = CalendarAvailability::NotAvailable; 1009 | CalendarAvailability saturday = CalendarAvailability::NotAvailable; 1010 | CalendarAvailability sunday = CalendarAvailability::NotAvailable; 1011 | 1012 | Date start_date; 1013 | Date end_date; 1014 | }; 1015 | 1016 | inline bool operator==(const CalendarItem & lhs, const CalendarItem & rhs) 1017 | { 1018 | return std::tie(lhs.service_id, lhs.monday, lhs.tuesday, lhs.wednesday, lhs.thursday, 1019 | lhs.friday, lhs.saturday, lhs.sunday, lhs.start_date, lhs.end_date) == 1020 | std::tie(rhs.service_id, rhs.monday, rhs.tuesday, rhs.wednesday, rhs.thursday, 1021 | rhs.friday, rhs.saturday, rhs.sunday, rhs.start_date, rhs.end_date); 1022 | } 1023 | 1024 | uint8_t inline availability(const CalendarItem& c) { 1025 | return uint8_t(c.monday == gtfs::CalendarAvailability::Available) | 1026 | uint8_t(c.tuesday == gtfs::CalendarAvailability::Available) << 1 | 1027 | uint8_t(c.wednesday == gtfs::CalendarAvailability::Available) << 2 | 1028 | uint8_t(c.thursday == gtfs::CalendarAvailability::Available) << 3 | 1029 | uint8_t(c.friday == gtfs::CalendarAvailability::Available) << 4 | 1030 | uint8_t(c.saturday == gtfs::CalendarAvailability::Available) << 5 | 1031 | uint8_t(c.sunday == gtfs::CalendarAvailability::Available) << 6; 1032 | } 1033 | 1034 | constexpr uint8_t Monday = 0b00000001; 1035 | constexpr uint8_t Tuesday = 0b00000010; 1036 | constexpr uint8_t Wednesday = 0b00000100; 1037 | constexpr uint8_t Thursday = 0b00001000; 1038 | constexpr uint8_t Friday = 0b00010000; 1039 | constexpr uint8_t Saturday = 0b00100000; 1040 | constexpr uint8_t Sunday = 0b01000000; 1041 | 1042 | // Conditionally required dataset file 1043 | struct CalendarDate 1044 | { 1045 | // Required: 1046 | Id service_id; 1047 | Date date; 1048 | CalendarDateException exception_type = CalendarDateException::Added; 1049 | }; 1050 | 1051 | // Optional dataset file 1052 | struct FareAttributesItem 1053 | { 1054 | // Required: 1055 | Id fare_id; 1056 | double price = 0.0; 1057 | CurrencyCode currency_type; 1058 | FarePayment payment_method = FarePayment::BeforeBoarding; 1059 | FareTransfers transfers = FareTransfers::Unlimited; 1060 | 1061 | // Conditionally required: 1062 | Id agency_id; 1063 | 1064 | // Optional: 1065 | size_t transfer_duration = 0; // Length of time in seconds before a transfer expires 1066 | }; 1067 | 1068 | inline bool operator==(const FareAttributesItem & lhs, const FareAttributesItem & rhs) 1069 | { 1070 | return std::tie(lhs.fare_id, lhs.price, lhs.currency_type, lhs.payment_method, lhs.transfers, 1071 | lhs.agency_id, lhs.transfer_duration) == 1072 | std::tie(rhs.fare_id, rhs.price, rhs.currency_type, rhs.payment_method, rhs.transfers, 1073 | rhs.agency_id, rhs.transfer_duration); 1074 | } 1075 | 1076 | // Optional dataset file 1077 | struct FareRule 1078 | { 1079 | // Required: 1080 | Id fare_id; 1081 | 1082 | // Optional: 1083 | Id route_id; 1084 | Id origin_id; 1085 | Id destination_id; 1086 | Id contains_id; 1087 | }; 1088 | 1089 | // Optional dataset file 1090 | struct ShapePoint 1091 | { 1092 | // Required: 1093 | Id shape_id; 1094 | double shape_pt_lat = 0.0; 1095 | double shape_pt_lon = 0.0; 1096 | size_t shape_pt_sequence = 0; 1097 | 1098 | // Optional: 1099 | double shape_dist_traveled = 0; 1100 | }; 1101 | 1102 | // Optional dataset file 1103 | struct Frequency 1104 | { 1105 | // Required: 1106 | Id trip_id; 1107 | Time start_time; 1108 | Time end_time; 1109 | size_t headway_secs = 0; 1110 | 1111 | // Optional: 1112 | FrequencyTripService exact_times = FrequencyTripService::FrequencyBased; 1113 | }; 1114 | 1115 | // Optional dataset file 1116 | struct Transfer 1117 | { 1118 | // Required: 1119 | Id from_stop_id; 1120 | Id to_stop_id; 1121 | TransferType transfer_type = TransferType::Recommended; 1122 | 1123 | // Optional: 1124 | size_t min_transfer_time = 0; 1125 | }; 1126 | 1127 | // Optional dataset file for the GTFS-Pathways extension 1128 | struct Pathway 1129 | { 1130 | // Required: 1131 | Id pathway_id; 1132 | Id from_stop_id; 1133 | Id to_stop_id; 1134 | PathwayMode pathway_mode = PathwayMode::Walkway; 1135 | PathwayDirection is_bidirectional = PathwayDirection::Unidirectional; 1136 | 1137 | // Optional fields: 1138 | // Horizontal length in meters of the pathway from the origin location 1139 | double length = 0.0; 1140 | // Average time in seconds needed to walk through the pathway from the origin location 1141 | size_t traversal_time = 0; 1142 | // Number of stairs of the pathway 1143 | size_t stair_count = 0; 1144 | // Maximum slope ratio of the pathway 1145 | double max_slope = 0.0; 1146 | // Minimum width of the pathway in meters 1147 | double min_width = 0.0; 1148 | // Text from physical signage visible to transit riders 1149 | Text signposted_as; 1150 | // Same as signposted_as, but when the pathways is used backward 1151 | Text reversed_signposted_as; 1152 | }; 1153 | 1154 | // Optional dataset file 1155 | struct Level 1156 | { 1157 | // Required: 1158 | Id level_id; 1159 | 1160 | // Numeric index of the level that indicates relative position of this level in relation to other 1161 | // levels (levels with higher indices are assumed to be located above levels with lower indices). 1162 | // Ground level should have index 0, with levels above ground indicated by positive indices and 1163 | // levels below ground by negative indices 1164 | double level_index = 0.0; 1165 | 1166 | // Optional: 1167 | Text level_name; 1168 | }; 1169 | 1170 | // Optional dataset file 1171 | struct FeedInfo 1172 | { 1173 | // Required: 1174 | Text feed_publisher_name; 1175 | Text feed_publisher_url; 1176 | LanguageCode feed_lang; 1177 | 1178 | // Optional: 1179 | Date feed_start_date; 1180 | Date feed_end_date; 1181 | Text feed_version; 1182 | Text feed_contact_email; 1183 | Text feed_contact_url; 1184 | }; 1185 | 1186 | // Optional dataset file 1187 | struct Translation 1188 | { 1189 | // Required: 1190 | Text table_name; 1191 | Text field_name; 1192 | LanguageCode language; 1193 | Text translation; 1194 | 1195 | // Conditionally required: 1196 | Id record_id; 1197 | Id record_sub_id; 1198 | Text field_value; 1199 | }; 1200 | 1201 | // Optional dataset file 1202 | struct Attribution 1203 | { 1204 | // Required: 1205 | Text organization_name; 1206 | 1207 | // Optional: 1208 | Id attribution_id; // Useful for translations 1209 | Id agency_id; 1210 | Id route_id; 1211 | Id trip_id; 1212 | 1213 | AttributionRole is_producer = AttributionRole::No; 1214 | AttributionRole is_operator = AttributionRole::No; 1215 | AttributionRole is_authority = AttributionRole::No; 1216 | 1217 | Text attribution_url; 1218 | Text attribution_email; 1219 | Text attribution_phone; 1220 | }; 1221 | 1222 | template 1223 | bool valid(const T & v) { 1224 | static T invalid{}; 1225 | return !(v == invalid); 1226 | } 1227 | 1228 | // Main classes for working with GTFS feeds 1229 | using Agencies = std::vector; 1230 | using Stops = std::vector; 1231 | using Routes = std::vector; 1232 | using Trips = std::vector; 1233 | using StopTimes = std::vector; 1234 | using StopTimesRange = std::pair; 1235 | using Calendar = std::vector; 1236 | using CalendarDates = std::vector; 1237 | using CalendarDatesRange = std::pair; 1238 | 1239 | using FareRules = std::vector; 1240 | using FareRulesRange = std::pair; 1241 | using FareAttributes = std::vector; 1242 | using FareAttributesRange = std::pair; 1243 | using Shapes = std::vector; 1244 | using ShapeRange = std::pair; 1245 | using Frequencies = std::vector; 1246 | using FrequenciesRange = std::pair; 1247 | using Transfers = std::vector; 1248 | using Pathways = std::vector; 1249 | using PathwaysRange = std::pair; 1250 | using Levels = std::vector; 1251 | // FeedInfo is a unique object and doesn't need a container. 1252 | using Translations = std::vector; 1253 | using TranslationsRange = std::pair; 1254 | using Attributions = std::vector; 1255 | 1256 | using ParsedCsvRow = std::map; 1257 | 1258 | class Feed 1259 | { 1260 | public: 1261 | inline Feed() = default; 1262 | inline explicit Feed(const std::string & gtfs_path); 1263 | 1264 | // 'strict' flag is used to interrupt feed parsing in case of absence or errors in 1265 | // required files. If you want to read incomplete feed, set it to false. 1266 | inline Result read_feed(bool strict=true); 1267 | inline Result write_feed(const std::string & gtfs_path) const; 1268 | 1269 | inline Result write_agencies(const std::string & gtfs_path) const; 1270 | 1271 | inline const Agencies & get_agencies() const; 1272 | inline const Agency & get_agency(const Id & agency_id) const; 1273 | inline void add_agency(const Agency & agency); 1274 | 1275 | inline Result write_stops(const std::string & gtfs_path) const; 1276 | 1277 | inline const Stops & get_stops() const; 1278 | inline const Stop & get_stop(const Id & stop_id) const; 1279 | inline void add_stop(const Stop & stop); 1280 | 1281 | inline Result write_routes(const std::string & gtfs_path) const; 1282 | 1283 | inline const Routes & get_routes() const; 1284 | inline const Route & get_route(const Id & route_id) const; 1285 | inline void add_route(const Route & route); 1286 | 1287 | inline Result write_trips(const std::string & gtfs_path) const; 1288 | 1289 | inline const Trips & get_trips() const; 1290 | inline const Trip & get_trip(const Id & trip_id) const; 1291 | inline void add_trip(const Trip & trip); 1292 | 1293 | inline Result write_stop_times(const std::string & gtfs_path) const; 1294 | 1295 | inline const StopTimes & get_stop_times() const; 1296 | inline StopTimes get_stop_times_for_stop(const Id & stop_id) const; 1297 | inline StopTimesRange get_stop_times_for_trip(const Id & trip_id) const; 1298 | inline void add_stop_time(const StopTime & stop_time); 1299 | 1300 | inline Result write_calendar(const std::string & gtfs_path) const; 1301 | 1302 | inline const Calendar & get_calendar() const; 1303 | inline const CalendarItem & get_calendar_item(const Id & service_id) const; 1304 | inline void add_calendar_item(const CalendarItem & calendar_item); 1305 | 1306 | inline Result write_calendar_dates(const std::string & gtfs_path) const; 1307 | 1308 | inline const CalendarDates & get_calendar_dates() const; 1309 | inline CalendarDatesRange get_calendar_dates(const Id & service_id) const; 1310 | inline void add_calendar_date(const CalendarDate & calendar_date); 1311 | 1312 | inline Result write_fare_rules(const std::string & gtfs_path) const; 1313 | 1314 | inline const FareRules & get_fare_rules() const; 1315 | inline FareRulesRange get_fare_rules(const Id & fare_id) const; 1316 | inline void add_fare_rule(const FareRule & fare_rule); 1317 | 1318 | inline Result write_fare_attributes(const std::string & gtfs_path) const; 1319 | 1320 | inline const FareAttributes & get_fare_attributes() const; 1321 | inline FareAttributesRange get_fare_attributes(const Id & fare_id) const; 1322 | inline void add_fare_attributes(const FareAttributesItem & fare_attributes_item); 1323 | 1324 | inline Result write_shapes(const std::string & gtfs_path) const; 1325 | 1326 | inline const Shapes & get_shapes() const; 1327 | inline ShapeRange get_shape(const Id & shape_id) const; 1328 | inline void add_shape(const ShapePoint & shape); 1329 | 1330 | inline Result write_frequencies(const std::string & gtfs_path) const; 1331 | 1332 | inline const Frequencies & get_frequencies() const; 1333 | inline FrequenciesRange get_frequencies(const Id & trip_id) const; 1334 | inline void add_frequency(const Frequency & frequency); 1335 | 1336 | inline Result write_transfers(const std::string & gtfs_path) const; 1337 | 1338 | inline const Transfers & get_transfers() const; 1339 | inline const Transfer & get_transfer(const Id & from_stop_id, const Id & to_stop_id) const; 1340 | inline void add_transfer(const Transfer & transfer); 1341 | 1342 | inline Result write_pathways(const std::string & gtfs_path) const; 1343 | 1344 | inline const Pathways & get_pathways() const; 1345 | inline PathwaysRange get_pathways(const Id & pathway_id) const; 1346 | inline Pathways get_pathways(const Id & from_stop_id, const Id & to_stop_id) const; 1347 | inline void add_pathway(const Pathway & pathway); 1348 | 1349 | inline Result write_levels(const std::string & gtfs_path) const; 1350 | 1351 | inline const Levels & get_levels() const; 1352 | inline const Level & get_level(const Id & level_id) const; 1353 | inline void add_level(const Level & level); 1354 | 1355 | inline Result write_feed_info(const std::string & gtfs_path) const; 1356 | 1357 | inline FeedInfo get_feed_info() const; 1358 | inline void set_feed_info(const FeedInfo & feed_info); 1359 | 1360 | inline Result write_translations(const std::string & gtfs_path) const; 1361 | 1362 | inline const Translations & get_translations() const; 1363 | inline TranslationsRange get_translations(const Text & table_name) const; 1364 | inline void add_translation(const Translation & translation); 1365 | 1366 | inline Result write_attributions(const std::string & gtfs_path) const; 1367 | 1368 | inline const Attributions & get_attributions() const; 1369 | inline void add_attribution(const Attribution & attribution); 1370 | 1371 | private: 1372 | inline Result parse_csv(const std::string & filename, 1373 | const std::function & add_entity); 1374 | 1375 | inline Result write_csv(const std::string & path, const std::string & file, 1376 | const std::function & write_header, 1377 | const std::function & write_entities) const; 1378 | 1379 | inline Result add_agency(const ParsedCsvRow & row); 1380 | inline Result add_route(const ParsedCsvRow & row); 1381 | inline Result add_shape(const ParsedCsvRow & row); 1382 | inline Result add_trip(const ParsedCsvRow & row); 1383 | inline Result add_stop(const ParsedCsvRow & row); 1384 | inline Result add_stop_time(const ParsedCsvRow & row); 1385 | inline Result add_calendar_item(const ParsedCsvRow & row); 1386 | inline Result add_calendar_date(const ParsedCsvRow & row); 1387 | inline Result add_transfer(const ParsedCsvRow & row); 1388 | inline Result add_frequency(const ParsedCsvRow & row); 1389 | inline Result add_fare_attributes(const ParsedCsvRow & row); 1390 | inline Result add_fare_rule(const ParsedCsvRow & row); 1391 | inline Result add_pathway(const ParsedCsvRow & row); 1392 | inline Result add_level(const ParsedCsvRow & row); 1393 | inline Result add_feed_info(const ParsedCsvRow & row); 1394 | inline Result add_translation(const ParsedCsvRow & row); 1395 | inline Result add_attribution(const ParsedCsvRow & row); 1396 | 1397 | inline Result read_agencies(); 1398 | inline Result read_stops(); 1399 | inline Result read_routes(); 1400 | inline Result read_trips(); 1401 | inline Result read_stop_times(); 1402 | inline Result read_calendar(); 1403 | inline Result read_calendar_dates(); 1404 | inline Result read_fare_rules(); 1405 | inline Result read_fare_attributes(); 1406 | inline Result read_shapes(); 1407 | inline Result read_frequencies(); 1408 | inline Result read_transfers(); 1409 | inline Result read_pathways(); 1410 | inline Result read_levels(); 1411 | inline Result read_feed_info(); 1412 | inline Result read_translations(); 1413 | inline Result read_attributions(); 1414 | 1415 | inline void write_agencies(std::ofstream & out) const; 1416 | inline void write_routes(std::ofstream & out) const; 1417 | inline void write_shapes(std::ofstream & out) const; 1418 | inline void write_trips(std::ofstream & out) const; 1419 | inline void write_stops(std::ofstream & out) const; 1420 | inline void write_stop_times(std::ofstream & out) const; 1421 | inline void write_calendar(std::ofstream & out) const; 1422 | inline void write_calendar_dates(std::ofstream & out) const; 1423 | inline void write_transfers(std::ofstream & out) const; 1424 | inline void write_frequencies(std::ofstream & out) const; 1425 | inline void write_fare_attributes(std::ofstream & out) const; 1426 | inline void write_fare_rules(std::ofstream & out) const; 1427 | inline void write_pathways(std::ofstream & out) const; 1428 | inline void write_levels(std::ofstream & out) const; 1429 | inline void write_feed_info(std::ofstream & out) const; 1430 | inline void write_translations(std::ofstream & out) const; 1431 | inline void write_attributions(std::ofstream & out) const; 1432 | 1433 | protected: 1434 | std::string gtfs_directory; 1435 | 1436 | Agencies agencies; 1437 | Stops stops; 1438 | Routes routes; 1439 | Trips trips; 1440 | StopTimes stop_times; 1441 | 1442 | Calendar calendar; 1443 | CalendarDates calendar_dates; 1444 | FareRules fare_rules; 1445 | FareAttributes fare_attributes; 1446 | Shapes shapes; 1447 | Frequencies frequencies; 1448 | Transfers transfers; 1449 | Pathways pathways; 1450 | Levels levels; 1451 | Translations translations; 1452 | Attributions attributions; 1453 | FeedInfo feed_info; 1454 | }; 1455 | 1456 | inline Feed::Feed(const std::string & gtfs_path) : gtfs_directory(add_trailing_slash(gtfs_path)) {} 1457 | 1458 | inline bool ErrorParsingOptionalFile(const Result & res) 1459 | { 1460 | return res != ResultCode::OK && res != ResultCode::ERROR_FILE_ABSENT; 1461 | } 1462 | 1463 | inline Result Feed::read_feed(bool strict) 1464 | { 1465 | // Read required files: 1466 | if (auto res = read_agencies(); strict && res != ResultCode::OK) 1467 | return res; 1468 | 1469 | if (auto res = read_stops(); strict && res != ResultCode::OK) 1470 | return res; 1471 | 1472 | if (auto res = read_routes(); strict && res != ResultCode::OK) 1473 | return res; 1474 | 1475 | if (auto res = read_trips(); strict && res != ResultCode::OK) 1476 | return res; 1477 | 1478 | if (auto res = read_stop_times(); strict && res != ResultCode::OK) 1479 | return res; 1480 | 1481 | // Read conditionally required files: 1482 | if (auto res = read_calendar(); ErrorParsingOptionalFile(res)) 1483 | return res; 1484 | 1485 | if (auto res = read_calendar_dates(); ErrorParsingOptionalFile(res)) 1486 | return res; 1487 | 1488 | // Read optional files: 1489 | if (auto res = read_shapes(); ErrorParsingOptionalFile(res)) 1490 | return res; 1491 | 1492 | if (auto res = read_transfers(); ErrorParsingOptionalFile(res)) 1493 | return res; 1494 | 1495 | if (auto res = read_frequencies(); ErrorParsingOptionalFile(res)) 1496 | return res; 1497 | 1498 | if (auto res = read_fare_attributes(); ErrorParsingOptionalFile(res)) 1499 | return res; 1500 | 1501 | if (auto res = read_fare_rules(); ErrorParsingOptionalFile(res)) 1502 | return res; 1503 | 1504 | if (auto res = read_pathways(); ErrorParsingOptionalFile(res)) 1505 | return res; 1506 | 1507 | if (auto res = read_levels(); ErrorParsingOptionalFile(res)) 1508 | return res; 1509 | 1510 | if (auto res = read_attributions(); ErrorParsingOptionalFile(res)) 1511 | return res; 1512 | 1513 | if (auto res = read_feed_info(); ErrorParsingOptionalFile(res)) 1514 | return res; 1515 | 1516 | if (auto res = read_translations(); ErrorParsingOptionalFile(res)) 1517 | return res; 1518 | 1519 | // we sort all the vectors by id for faster look up later on 1520 | std::sort(agencies.begin(), agencies.end(), 1521 | [](const auto & a, const auto & b) { return a.agency_id < b.agency_id; }); 1522 | std::sort(stops.begin(), stops.end(), 1523 | [](const auto & a, const auto & b) { return a.stop_id < b.stop_id; }); 1524 | std::sort(routes.begin(), routes.end(), 1525 | [](const auto & a, const auto & b) { return a.route_id < b.route_id; }); 1526 | std::sort(trips.begin(), trips.end(), 1527 | [](const auto & a, const auto & b) { return a.trip_id < b.trip_id; }); 1528 | std::sort(stop_times.begin(), stop_times.end(), 1529 | [](const auto & a, const auto & b) 1530 | { return a.trip_id < b.trip_id || (a.trip_id == b.trip_id && a.stop_sequence < b.stop_sequence); }); // could also sort on stop_id 1531 | std::sort(calendar.begin(), calendar.end(), 1532 | [](const auto & a, const auto & b) { return a.service_id < b.service_id; }); 1533 | std::sort(calendar_dates.begin(), calendar_dates.end(), 1534 | [](const auto & a, const auto & b) 1535 | { return a.service_id < b.service_id || (a.service_id == b.service_id && a.date.get_raw_date() < b.date.get_raw_date()); }); 1536 | std::sort(shapes.begin(), shapes.end(), 1537 | [](const auto & a, const auto & b) 1538 | { return a.shape_id < b.shape_id || (a.shape_id == b.shape_id && a.shape_pt_sequence < b.shape_pt_sequence); }); 1539 | std::sort(transfers.begin(), transfers.end(), 1540 | [](const auto & a, const auto & b) 1541 | { return a.from_stop_id < b.from_stop_id || (a.from_stop_id == b.from_stop_id && a.to_stop_id < b.to_stop_id); }); 1542 | std::sort(frequencies.begin(), frequencies.end(), 1543 | [](const auto & a, const auto & b) { return a.trip_id < b.trip_id; }); 1544 | std::sort(fare_attributes.begin(), fare_attributes.end(), 1545 | [](const auto & a, const auto & b) { return a.fare_id < b.fare_id; }); 1546 | std::sort(fare_rules.begin(), fare_rules.end(), 1547 | [](const auto & a, const auto & b) { return a.fare_id < b.fare_id; }); 1548 | std::sort(pathways.begin(), pathways.end(), 1549 | [](const auto & a, const auto & b) { return a.pathway_id < b.pathway_id; }); // could sort on to/from_stop_ids 1550 | std::sort(levels.begin(), levels.end(), 1551 | [](const auto & a, const auto & b) { return a.level_id < b.level_id; }); 1552 | std::sort(translations.begin(), translations.end(), 1553 | [](const auto & a, const auto & b) { return a.table_name < b.table_name; }); 1554 | 1555 | return ResultCode::OK; 1556 | } 1557 | 1558 | inline Result Feed::write_feed(const std::string & gtfs_path) const 1559 | { 1560 | if (gtfs_path.empty()) 1561 | return {ResultCode::ERROR_INVALID_GTFS_PATH, "Empty output path for writing feed"}; 1562 | // TODO Write feed to csv files 1563 | return {}; 1564 | } 1565 | 1566 | inline std::string get_value_or_default(const ParsedCsvRow & container, const std::string & key, 1567 | const std::string & default_value = "") 1568 | { 1569 | const auto it = container.find(key); 1570 | if (it == container.end()) 1571 | return default_value; 1572 | 1573 | return it->second; 1574 | } 1575 | 1576 | template 1577 | inline void set_field(T & field, const ParsedCsvRow & container, const std::string & key, 1578 | bool is_optional = true) 1579 | { 1580 | const std::string key_str = get_value_or_default(container, key); 1581 | if (!key_str.empty() || !is_optional) 1582 | field = static_cast(std::stoi(key_str)); 1583 | } 1584 | 1585 | inline bool set_fractional(double & field, const ParsedCsvRow & container, const std::string & key, 1586 | bool is_optional = true) 1587 | { 1588 | const std::string key_str = get_value_or_default(container, key); 1589 | if (!key_str.empty() || !is_optional) 1590 | { 1591 | field = std::stod(key_str); 1592 | return true; 1593 | } 1594 | return false; 1595 | } 1596 | 1597 | // Throw if not valid WGS84 decimal degrees. 1598 | inline void check_coordinates(double latitude, double longitude) 1599 | { 1600 | if (latitude < -90.0 || latitude > 90.0) 1601 | throw std::out_of_range("Latitude"); 1602 | 1603 | if (longitude < -180.0 || longitude > 180.0) 1604 | throw std::out_of_range("Longitude"); 1605 | } 1606 | 1607 | inline Result Feed::add_agency(const ParsedCsvRow & row) 1608 | { 1609 | Agency agency; 1610 | 1611 | // Conditionally required id: 1612 | agency.agency_id = get_value_or_default(row, "agency_id"); 1613 | 1614 | // Required fields: 1615 | try 1616 | { 1617 | agency.agency_name = row.at("agency_name"); 1618 | agency.agency_url = row.at("agency_url"); 1619 | agency.agency_timezone = row.at("agency_timezone"); 1620 | } 1621 | catch (const std::out_of_range & ex) 1622 | { 1623 | return {ResultCode::ERROR_REQUIRED_FIELD_ABSENT, ex.what()}; 1624 | } 1625 | 1626 | // Optional fields: 1627 | agency.agency_lang = get_value_or_default(row, "agency_lang"); 1628 | agency.agency_phone = get_value_or_default(row, "agency_phone"); 1629 | agency.agency_fare_url = get_value_or_default(row, "agency_fare_url"); 1630 | agency.agency_email = get_value_or_default(row, "agency_email"); 1631 | 1632 | agencies.emplace_back(agency); 1633 | return ResultCode::OK; 1634 | } 1635 | 1636 | inline Result Feed::add_route(const ParsedCsvRow & row) 1637 | { 1638 | Route route; 1639 | 1640 | try 1641 | { 1642 | // Required fields: 1643 | route.route_id = row.at("route_id"); 1644 | set_field(route.route_type, row, "route_type", false); 1645 | 1646 | // Optional: 1647 | set_field(route.route_sort_order, row, "route_sort_order"); 1648 | } 1649 | catch (const std::out_of_range & ex) 1650 | { 1651 | return {ResultCode::ERROR_REQUIRED_FIELD_ABSENT, ex.what()}; 1652 | } 1653 | catch (const std::invalid_argument & ex) 1654 | { 1655 | return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; 1656 | } 1657 | 1658 | // Conditionally required: 1659 | route.agency_id = get_value_or_default(row, "agency_id"); 1660 | 1661 | route.route_short_name = get_value_or_default(row, "route_short_name"); 1662 | route.route_long_name = get_value_or_default(row, "route_long_name"); 1663 | 1664 | if (route.route_short_name.empty() && route.route_long_name.empty()) 1665 | { 1666 | return {ResultCode::ERROR_REQUIRED_FIELD_ABSENT, 1667 | "'route_short_name' or 'route_long_name' must be specified"}; 1668 | } 1669 | 1670 | route.route_color = get_value_or_default(row, "route_color"); 1671 | route.route_text_color = get_value_or_default(row, "route_text_color"); 1672 | route.route_desc = get_value_or_default(row, "route_desc"); 1673 | route.route_url = get_value_or_default(row, "route_url"); 1674 | 1675 | routes.emplace_back(route); 1676 | 1677 | return ResultCode::OK; 1678 | } 1679 | 1680 | inline Result Feed::add_shape(const ParsedCsvRow & row) 1681 | { 1682 | ShapePoint point; 1683 | try 1684 | { 1685 | // Required: 1686 | point.shape_id = row.at("shape_id"); 1687 | point.shape_pt_sequence = std::stoi(row.at("shape_pt_sequence")); 1688 | 1689 | point.shape_pt_lon = std::stod(row.at("shape_pt_lon")); 1690 | point.shape_pt_lat = std::stod(row.at("shape_pt_lat")); 1691 | check_coordinates(point.shape_pt_lat, point.shape_pt_lon); 1692 | 1693 | // Optional: 1694 | set_fractional(point.shape_dist_traveled, row, "shape_dist_traveled"); 1695 | if (point.shape_dist_traveled < 0.0) 1696 | throw std::invalid_argument("Invalid shape_dist_traveled"); 1697 | } 1698 | catch (const std::out_of_range & ex) 1699 | { 1700 | return {ResultCode::ERROR_REQUIRED_FIELD_ABSENT, ex.what()}; 1701 | } 1702 | catch (const std::invalid_argument & ex) 1703 | { 1704 | return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; 1705 | } 1706 | 1707 | shapes.emplace_back(point); 1708 | return ResultCode::OK; 1709 | } 1710 | 1711 | inline Result Feed::add_trip(const ParsedCsvRow & row) 1712 | { 1713 | Trip trip; 1714 | try 1715 | { 1716 | // Required: 1717 | trip.route_id = row.at("route_id"); 1718 | trip.service_id = row.at("service_id"); 1719 | trip.trip_id = row.at("trip_id"); 1720 | 1721 | // Optional: 1722 | set_field(trip.direction_id, row, "direction_id"); 1723 | set_field(trip.wheelchair_accessible, row, "wheelchair_accessible"); 1724 | set_field(trip.bikes_allowed, row, "bikes_allowed"); 1725 | } 1726 | catch (const std::out_of_range & ex) 1727 | { 1728 | return {ResultCode::ERROR_REQUIRED_FIELD_ABSENT, ex.what()}; 1729 | } 1730 | catch (const std::invalid_argument & ex) 1731 | { 1732 | return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; 1733 | } 1734 | 1735 | // Optional: 1736 | trip.shape_id = get_value_or_default(row, "shape_id"); 1737 | trip.trip_headsign = get_value_or_default(row, "trip_headsign"); 1738 | trip.trip_short_name = get_value_or_default(row, "trip_short_name"); 1739 | trip.block_id = get_value_or_default(row, "block_id"); 1740 | 1741 | trips.emplace_back(trip); 1742 | return ResultCode::OK; 1743 | } 1744 | 1745 | inline Result Feed::add_stop(const ParsedCsvRow & row) 1746 | { 1747 | Stop stop; 1748 | 1749 | try 1750 | { 1751 | stop.stop_id = row.at("stop_id"); 1752 | 1753 | // Optional: 1754 | bool const set_lon = set_fractional(stop.stop_lon, row, "stop_lon"); 1755 | bool const set_lat = set_fractional(stop.stop_lat, row, "stop_lat"); 1756 | 1757 | if (!set_lon || !set_lat) 1758 | stop.coordinates_present = false; 1759 | } 1760 | catch (const std::out_of_range & ex) 1761 | { 1762 | return {ResultCode::ERROR_REQUIRED_FIELD_ABSENT, ex.what()}; 1763 | } 1764 | catch (const std::invalid_argument & ex) 1765 | { 1766 | return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; 1767 | } 1768 | 1769 | // Conditionally required: 1770 | stop.stop_name = get_value_or_default(row, "stop_name"); 1771 | stop.parent_station = get_value_or_default(row, "parent_station"); 1772 | stop.zone_id = get_value_or_default(row, "zone_id"); 1773 | 1774 | // Optional: 1775 | stop.stop_code = get_value_or_default(row, "stop_code"); 1776 | stop.stop_desc = get_value_or_default(row, "stop_desc"); 1777 | stop.stop_url = get_value_or_default(row, "stop_url"); 1778 | set_field(stop.location_type, row, "location_type"); 1779 | stop.stop_timezone = get_value_or_default(row, "stop_timezone"); 1780 | stop.wheelchair_boarding = get_value_or_default(row, "wheelchair_boarding"); 1781 | stop.level_id = get_value_or_default(row, "level_id"); 1782 | stop.platform_code = get_value_or_default(row, "platform_code"); 1783 | 1784 | stops.emplace_back(stop); 1785 | 1786 | return ResultCode::OK; 1787 | } 1788 | 1789 | inline Result Feed::add_stop_time(const ParsedCsvRow & row) 1790 | { 1791 | StopTime stop_time; 1792 | 1793 | try 1794 | { 1795 | // Required: 1796 | stop_time.trip_id = row.at("trip_id"); 1797 | stop_time.stop_id = row.at("stop_id"); 1798 | stop_time.stop_sequence = std::stoi(row.at("stop_sequence")); 1799 | 1800 | // Conditionally required: 1801 | stop_time.departure_time = Time(row.at("departure_time")); 1802 | stop_time.arrival_time = Time(row.at("arrival_time")); 1803 | 1804 | // Optional: 1805 | set_field(stop_time.pickup_type, row, "pickup_type"); 1806 | set_field(stop_time.drop_off_type, row, "drop_off_type"); 1807 | 1808 | set_fractional(stop_time.shape_dist_traveled, row, "shape_dist_traveled"); 1809 | if (stop_time.shape_dist_traveled < 0.0) 1810 | throw std::invalid_argument("Invalid shape_dist_traveled"); 1811 | 1812 | set_field(stop_time.timepoint, row, "timepoint"); 1813 | } 1814 | catch (const std::out_of_range & ex) 1815 | { 1816 | return {ResultCode::ERROR_REQUIRED_FIELD_ABSENT, ex.what()}; 1817 | } 1818 | catch (const std::invalid_argument & ex) 1819 | { 1820 | return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; 1821 | } 1822 | catch (const InvalidFieldFormat & ex) 1823 | { 1824 | return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; 1825 | } 1826 | 1827 | // Optional fields: 1828 | stop_time.stop_headsign = get_value_or_default(row, "stop_headsign"); 1829 | 1830 | stop_times.emplace_back(stop_time); 1831 | return ResultCode::OK; 1832 | } 1833 | 1834 | inline Result Feed::add_calendar_item(const ParsedCsvRow & row) 1835 | { 1836 | CalendarItem calendar_item; 1837 | try 1838 | { 1839 | // Required fields: 1840 | calendar_item.service_id = row.at("service_id"); 1841 | 1842 | set_field(calendar_item.monday, row, "monday", false); 1843 | set_field(calendar_item.tuesday, row, "tuesday", false); 1844 | set_field(calendar_item.wednesday, row, "wednesday", false); 1845 | set_field(calendar_item.thursday, row, "thursday", false); 1846 | set_field(calendar_item.friday, row, "friday", false); 1847 | set_field(calendar_item.saturday, row, "saturday", false); 1848 | set_field(calendar_item.sunday, row, "sunday", false); 1849 | 1850 | calendar_item.start_date = Date(row.at("start_date")); 1851 | calendar_item.end_date = Date(row.at("end_date")); 1852 | } 1853 | catch (const std::out_of_range & ex) 1854 | { 1855 | return {ResultCode::ERROR_REQUIRED_FIELD_ABSENT, ex.what()}; 1856 | } 1857 | catch (const std::invalid_argument & ex) 1858 | { 1859 | return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; 1860 | } 1861 | catch (const InvalidFieldFormat & ex) 1862 | { 1863 | return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; 1864 | } 1865 | 1866 | calendar.emplace_back(calendar_item); 1867 | return ResultCode::OK; 1868 | } 1869 | 1870 | inline Result Feed::add_calendar_date(const ParsedCsvRow & row) 1871 | { 1872 | CalendarDate calendar_date; 1873 | try 1874 | { 1875 | // Required fields: 1876 | calendar_date.service_id = row.at("service_id"); 1877 | 1878 | set_field(calendar_date.exception_type, row, "exception_type", false); 1879 | calendar_date.date = Date(row.at("date")); 1880 | } 1881 | catch (const std::out_of_range & ex) 1882 | { 1883 | return {ResultCode::ERROR_REQUIRED_FIELD_ABSENT, ex.what()}; 1884 | } 1885 | catch (const std::invalid_argument & ex) 1886 | { 1887 | return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; 1888 | } 1889 | catch (const InvalidFieldFormat & ex) 1890 | { 1891 | return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; 1892 | } 1893 | 1894 | calendar_dates.emplace_back(calendar_date); 1895 | return ResultCode::OK; 1896 | } 1897 | 1898 | inline Result Feed::add_transfer(const ParsedCsvRow & row) 1899 | { 1900 | Transfer transfer; 1901 | try 1902 | { 1903 | // Required fields: 1904 | transfer.from_stop_id = row.at("from_stop_id"); 1905 | transfer.to_stop_id = row.at("to_stop_id"); 1906 | set_field(transfer.transfer_type, row, "transfer_type", false); 1907 | 1908 | // Optional: 1909 | set_field(transfer.min_transfer_time, row, "min_transfer_time"); 1910 | } 1911 | catch (const std::out_of_range & ex) 1912 | { 1913 | return {ResultCode::ERROR_REQUIRED_FIELD_ABSENT, ex.what()}; 1914 | } 1915 | catch (const std::invalid_argument & ex) 1916 | { 1917 | return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; 1918 | } 1919 | catch (const InvalidFieldFormat & ex) 1920 | { 1921 | return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; 1922 | } 1923 | 1924 | transfers.emplace_back(transfer); 1925 | return ResultCode::OK; 1926 | } 1927 | 1928 | inline Result Feed::add_frequency(const ParsedCsvRow & row) 1929 | { 1930 | Frequency frequency; 1931 | try 1932 | { 1933 | // Required fields: 1934 | frequency.trip_id = row.at("trip_id"); 1935 | frequency.start_time = Time(row.at("start_time")); 1936 | frequency.end_time = Time(row.at("end_time")); 1937 | set_field(frequency.headway_secs, row, "headway_secs", false); 1938 | 1939 | // Optional: 1940 | set_field(frequency.exact_times, row, "exact_times"); 1941 | } 1942 | catch (const std::out_of_range & ex) 1943 | { 1944 | return {ResultCode::ERROR_REQUIRED_FIELD_ABSENT, ex.what()}; 1945 | } 1946 | catch (const std::invalid_argument & ex) 1947 | { 1948 | return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; 1949 | } 1950 | catch (const InvalidFieldFormat & ex) 1951 | { 1952 | return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; 1953 | } 1954 | 1955 | frequencies.emplace_back(frequency); 1956 | return ResultCode::OK; 1957 | } 1958 | 1959 | inline Result Feed::add_fare_attributes(const ParsedCsvRow & row) 1960 | { 1961 | FareAttributesItem item; 1962 | try 1963 | { 1964 | // Required fields: 1965 | item.fare_id = row.at("fare_id"); 1966 | set_fractional(item.price, row, "price", false); 1967 | 1968 | item.currency_type = row.at("currency_type"); 1969 | set_field(item.payment_method, row, "payment_method", false); 1970 | set_field(item.transfers, row, "transfers"); 1971 | 1972 | // Conditionally optional: 1973 | item.agency_id = get_value_or_default(row, "agency_id"); 1974 | set_field(item.transfer_duration, row, "transfer_duration"); 1975 | } 1976 | catch (const std::out_of_range & ex) 1977 | { 1978 | return {ResultCode::ERROR_REQUIRED_FIELD_ABSENT, ex.what()}; 1979 | } 1980 | catch (const std::invalid_argument & ex) 1981 | { 1982 | return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; 1983 | } 1984 | catch (const InvalidFieldFormat & ex) 1985 | { 1986 | return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; 1987 | } 1988 | 1989 | fare_attributes.emplace_back(item); 1990 | return ResultCode::OK; 1991 | } 1992 | 1993 | inline Result Feed::add_fare_rule(const ParsedCsvRow & row) 1994 | { 1995 | FareRule fare_rule; 1996 | try 1997 | { 1998 | // Required fields: 1999 | fare_rule.fare_id = row.at("fare_id"); 2000 | } 2001 | catch (const std::out_of_range & ex) 2002 | { 2003 | return {ResultCode::ERROR_REQUIRED_FIELD_ABSENT, ex.what()}; 2004 | } 2005 | catch (const std::invalid_argument & ex) 2006 | { 2007 | return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; 2008 | } 2009 | catch (const InvalidFieldFormat & ex) 2010 | { 2011 | return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; 2012 | } 2013 | 2014 | // Optional fields: 2015 | fare_rule.route_id = get_value_or_default(row, "route_id"); 2016 | fare_rule.origin_id = get_value_or_default(row, "origin_id"); 2017 | fare_rule.destination_id = get_value_or_default(row, "destination_id"); 2018 | fare_rule.contains_id = get_value_or_default(row, "contains_id"); 2019 | 2020 | fare_rules.emplace_back(fare_rule); 2021 | 2022 | return ResultCode::OK; 2023 | } 2024 | 2025 | inline Result Feed::add_pathway(const ParsedCsvRow & row) 2026 | { 2027 | Pathway path; 2028 | try 2029 | { 2030 | // Required fields: 2031 | path.pathway_id = row.at("pathway_id"); 2032 | path.from_stop_id = row.at("from_stop_id"); 2033 | path.to_stop_id = row.at("to_stop_id"); 2034 | set_field(path.pathway_mode, row, "pathway_mode", false); 2035 | set_field(path.is_bidirectional, row, "is_bidirectional", false); 2036 | 2037 | // Optional fields: 2038 | set_fractional(path.length, row, "length"); 2039 | set_field(path.traversal_time, row, "traversal_time"); 2040 | set_field(path.stair_count, row, "stair_count"); 2041 | set_fractional(path.max_slope, row, "max_slope"); 2042 | set_fractional(path.min_width, row, "min_width"); 2043 | } 2044 | catch (const std::out_of_range & ex) 2045 | { 2046 | return {ResultCode::ERROR_REQUIRED_FIELD_ABSENT, ex.what()}; 2047 | } 2048 | catch (const std::invalid_argument & ex) 2049 | { 2050 | return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; 2051 | } 2052 | catch (const InvalidFieldFormat & ex) 2053 | { 2054 | return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; 2055 | } 2056 | 2057 | path.signposted_as = get_value_or_default(row, "signposted_as"); 2058 | path.reversed_signposted_as = get_value_or_default(row, "reversed_signposted_as"); 2059 | 2060 | pathways.emplace_back(path); 2061 | return ResultCode::OK; 2062 | } 2063 | 2064 | inline Result Feed::add_level(const ParsedCsvRow & row) 2065 | { 2066 | Level level; 2067 | try 2068 | { 2069 | // Required fields: 2070 | level.level_id = row.at("level_id"); 2071 | 2072 | set_fractional(level.level_index, row, "level_index", false); 2073 | } 2074 | catch (const std::out_of_range & ex) 2075 | { 2076 | return {ResultCode::ERROR_REQUIRED_FIELD_ABSENT, ex.what()}; 2077 | } 2078 | catch (const std::invalid_argument & ex) 2079 | { 2080 | return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; 2081 | } 2082 | catch (const InvalidFieldFormat & ex) 2083 | { 2084 | return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; 2085 | } 2086 | 2087 | // Optional field: 2088 | level.level_name = get_value_or_default(row, "level_name"); 2089 | 2090 | levels.emplace_back(level); 2091 | 2092 | return ResultCode::OK; 2093 | } 2094 | 2095 | inline Result Feed::add_feed_info(const ParsedCsvRow & row) 2096 | { 2097 | try 2098 | { 2099 | // Required fields: 2100 | feed_info.feed_publisher_name = row.at("feed_publisher_name"); 2101 | feed_info.feed_publisher_url = row.at("feed_publisher_url"); 2102 | feed_info.feed_lang = row.at("feed_lang"); 2103 | 2104 | // Optional fields: 2105 | feed_info.feed_start_date = Date(get_value_or_default(row, "feed_start_date")); 2106 | feed_info.feed_end_date = Date(get_value_or_default(row, "feed_end_date")); 2107 | } 2108 | catch (const std::out_of_range & ex) 2109 | { 2110 | return {ResultCode::ERROR_REQUIRED_FIELD_ABSENT, ex.what()}; 2111 | } 2112 | catch (const std::invalid_argument & ex) 2113 | { 2114 | return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; 2115 | } 2116 | catch (const InvalidFieldFormat & ex) 2117 | { 2118 | return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; 2119 | } 2120 | 2121 | // Optional fields: 2122 | feed_info.feed_version = get_value_or_default(row, "feed_version"); 2123 | feed_info.feed_contact_email = get_value_or_default(row, "feed_contact_email"); 2124 | feed_info.feed_contact_url = get_value_or_default(row, "feed_contact_url"); 2125 | 2126 | return ResultCode::OK; 2127 | } 2128 | 2129 | inline Result Feed::add_translation(const ParsedCsvRow & row) 2130 | { 2131 | static const std::vector available_tables{"agency", "stops", "routes", "trips", 2132 | "stop_times", "pathways", "levels"}; 2133 | Translation translation; 2134 | 2135 | try 2136 | { 2137 | // Required fields: 2138 | translation.table_name = row.at("table_name"); 2139 | if (std::find(available_tables.begin(), available_tables.end(), translation.table_name) == 2140 | available_tables.end()) 2141 | { 2142 | throw InvalidFieldFormat("Field table_name of translations doesn't have required value"); 2143 | } 2144 | 2145 | translation.field_name = row.at("field_name"); 2146 | translation.language = row.at("language"); 2147 | translation.translation = row.at("translation"); 2148 | 2149 | // Conditionally required: 2150 | translation.record_id = get_value_or_default(row, "record_id"); 2151 | translation.record_sub_id = get_value_or_default(row, "record_sub_id"); 2152 | } 2153 | catch (const std::out_of_range & ex) 2154 | { 2155 | return {ResultCode::ERROR_REQUIRED_FIELD_ABSENT, ex.what()}; 2156 | } 2157 | catch (const std::invalid_argument & ex) 2158 | { 2159 | return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; 2160 | } 2161 | catch (const InvalidFieldFormat & ex) 2162 | { 2163 | return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; 2164 | } 2165 | 2166 | // Conditionally required: 2167 | translation.field_value = get_value_or_default(row, "field_value"); 2168 | 2169 | translations.emplace_back(translation); 2170 | 2171 | return ResultCode::OK; 2172 | } 2173 | 2174 | inline Result Feed::add_attribution(const ParsedCsvRow & row) 2175 | { 2176 | Attribution attribution; 2177 | 2178 | try 2179 | { 2180 | // Required fields: 2181 | attribution.organization_name = row.at("organization_name"); 2182 | 2183 | // Optional fields: 2184 | attribution.attribution_id = get_value_or_default(row, "attribution_id"); 2185 | attribution.agency_id = get_value_or_default(row, "agency_id"); 2186 | attribution.route_id = get_value_or_default(row, "route_id"); 2187 | attribution.trip_id = get_value_or_default(row, "trip_id"); 2188 | 2189 | set_field(attribution.is_producer, row, "is_producer"); 2190 | set_field(attribution.is_operator, row, "is_operator"); 2191 | set_field(attribution.is_authority, row, "is_authority"); 2192 | 2193 | attribution.attribution_url = get_value_or_default(row, "attribution_url"); 2194 | attribution.attribution_email = get_value_or_default(row, "attribution_email"); 2195 | attribution.trip_id = get_value_or_default(row, "attribution_phone"); 2196 | } 2197 | catch (const std::out_of_range & ex) 2198 | { 2199 | return {ResultCode::ERROR_REQUIRED_FIELD_ABSENT, ex.what()}; 2200 | } 2201 | catch (const std::invalid_argument & ex) 2202 | { 2203 | return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; 2204 | } 2205 | catch (const InvalidFieldFormat & ex) 2206 | { 2207 | return {ResultCode::ERROR_INVALID_FIELD_FORMAT, ex.what()}; 2208 | } 2209 | 2210 | attributions.emplace_back(attribution); 2211 | 2212 | return ResultCode::OK; 2213 | } 2214 | 2215 | inline Result Feed::write_csv(const std::string & path, const std::string & file, 2216 | const std::function & write_header, 2217 | const std::function & write_entities) const 2218 | { 2219 | const std::string filepath = add_trailing_slash(path) + file; 2220 | std::ofstream out(filepath); 2221 | if (!out.is_open()) 2222 | return {ResultCode::ERROR_INVALID_GTFS_PATH, "Could not open path for writing " + filepath}; 2223 | 2224 | write_header(out); 2225 | write_entities(out); 2226 | return ResultCode::OK; 2227 | } 2228 | 2229 | inline Result Feed::parse_csv(const std::string & filename, 2230 | const std::function & add_entity) 2231 | { 2232 | CsvParser parser(gtfs_directory); 2233 | auto res_header = parser.read_header(filename); 2234 | if (res_header.code != ResultCode::OK) 2235 | return res_header; 2236 | 2237 | ParsedCsvRow record; 2238 | Result res_row; 2239 | while ((res_row = parser.read_row(record)) != ResultCode::END_OF_FILE) 2240 | { 2241 | if (res_row != ResultCode::OK) 2242 | return res_row; 2243 | 2244 | if (record.empty()) 2245 | continue; 2246 | 2247 | Result res = add_entity(record); 2248 | if (res != ResultCode::OK) 2249 | { 2250 | res.message += " while adding item from " + filename; 2251 | return res; 2252 | } 2253 | } 2254 | 2255 | return {ResultCode::OK, {"Parsed " + filename}}; 2256 | } 2257 | 2258 | inline Result Feed::read_agencies() 2259 | { 2260 | auto handler = [this](const ParsedCsvRow & record) { return this->add_agency(record); }; 2261 | return parse_csv(file_agency, handler); 2262 | } 2263 | 2264 | inline Result Feed::write_agencies(const std::string & gtfs_path) const 2265 | { 2266 | auto container_writer = [this](std::ofstream & out) { return this->write_agencies(out); }; 2267 | return write_csv(gtfs_path, file_agency, write_agency_header, container_writer); 2268 | } 2269 | 2270 | inline const Agencies & Feed::get_agencies() const { return agencies; } 2271 | 2272 | inline const Agency & Feed::get_agency(const Id & agency_id) const 2273 | { 2274 | // agency id is required when the dataset contains data for multiple agencies, 2275 | // otherwise it is optional: 2276 | if (agency_id.empty() && agencies.size() == 1) 2277 | return agencies[0]; 2278 | 2279 | const auto it = 2280 | std::lower_bound(agencies.begin(), agencies.end(), agency_id, 2281 | [](const auto & a, const Id & i){ return a.agency_id < i;}); 2282 | if (it == agencies.end() || it->agency_id != agency_id) 2283 | { 2284 | static Agency invalid; 2285 | return invalid; 2286 | } 2287 | 2288 | return *it; 2289 | } 2290 | 2291 | inline void Feed::add_agency(const Agency & agency) { agencies.emplace_back(agency); } 2292 | 2293 | inline Result Feed::read_stops() 2294 | { 2295 | auto handler = [this](const ParsedCsvRow & record) { return this->add_stop(record); }; 2296 | return parse_csv(file_stops, handler); 2297 | } 2298 | 2299 | inline Result Feed::write_stops(const std::string & gtfs_path) const 2300 | { 2301 | auto container_writer = [this](std::ofstream & out) { return this->write_stops(out); }; 2302 | return write_csv(gtfs_path, file_stops, write_stops_header, container_writer); 2303 | } 2304 | 2305 | inline const Stops & Feed::get_stops() const { return stops; } 2306 | 2307 | inline const Stop & Feed::get_stop(const Id & stop_id) const 2308 | { 2309 | const auto it = 2310 | std::lower_bound(stops.begin(), stops.end(), 2311 | stop_id, [](const auto & a, const Id & i){ return a.stop_id < i;}); 2312 | 2313 | if (it == stops.end() || it->stop_id != stop_id) 2314 | { 2315 | static Stop invalid; 2316 | return invalid; 2317 | } 2318 | 2319 | return *it; 2320 | } 2321 | 2322 | inline void Feed::add_stop(const Stop & stop) { stops.emplace_back(stop); } 2323 | 2324 | inline Result Feed::read_routes() 2325 | { 2326 | auto handler = [this](const ParsedCsvRow & record) { return this->add_route(record); }; 2327 | return parse_csv(file_routes, handler); 2328 | } 2329 | 2330 | inline Result Feed::write_routes(const std::string & gtfs_path) const 2331 | { 2332 | auto container_writer = [this](std::ofstream & out) { return this->write_routes(out); }; 2333 | return write_csv(gtfs_path, file_routes, write_routes_header, container_writer); 2334 | } 2335 | 2336 | inline const Routes & Feed::get_routes() const { return routes; } 2337 | 2338 | inline const Route & Feed::get_route(const Id & route_id) const 2339 | { 2340 | const auto it = 2341 | std::lower_bound(routes.begin(), routes.end(), route_id, 2342 | [](const auto & a, const Id & i){ return a.route_id < i;}); 2343 | 2344 | if (it == routes.end() || it->route_id != route_id) 2345 | { 2346 | static Route invalid; 2347 | return invalid; 2348 | } 2349 | 2350 | return *it; 2351 | } 2352 | 2353 | inline void Feed::add_route(const Route & route) { routes.emplace_back(route); } 2354 | 2355 | inline Result Feed::read_trips() 2356 | { 2357 | auto handler = [this](const ParsedCsvRow & record) { return this->add_trip(record); }; 2358 | return parse_csv(file_trips, handler); 2359 | } 2360 | 2361 | inline Result Feed::write_trips(const std::string & gtfs_path) const 2362 | { 2363 | auto container_writer = [this](std::ofstream & out) { return this->write_trips(out); }; 2364 | return write_csv(gtfs_path, file_trips, write_trips_header, container_writer); 2365 | } 2366 | 2367 | inline const Trips & Feed::get_trips() const { return trips; } 2368 | 2369 | inline const Trip & Feed::get_trip(const Id & trip_id) const 2370 | { 2371 | const auto it = 2372 | std::lower_bound(trips.begin(), trips.end(), trip_id, 2373 | [](const auto & a, const Id & i){ return a.trip_id < i;}); 2374 | 2375 | if (it == trips.end() || it->trip_id != trip_id) 2376 | { 2377 | static Trip invalid; 2378 | return invalid; 2379 | } 2380 | 2381 | return *it; 2382 | } 2383 | 2384 | inline void Feed::add_trip(const Trip & trip) { trips.emplace_back(trip); } 2385 | 2386 | inline Result Feed::read_stop_times() 2387 | { 2388 | auto handler = [this](const ParsedCsvRow & record) { return this->add_stop_time(record); }; 2389 | return parse_csv(file_stop_times, handler); 2390 | } 2391 | 2392 | inline Result Feed::write_stop_times(const std::string & gtfs_path) const 2393 | { 2394 | auto container_writer = [this](std::ofstream & out) { return this->write_stop_times(out); }; 2395 | return write_csv(gtfs_path, file_stop_times, write_stop_times_header, container_writer); 2396 | } 2397 | 2398 | inline const StopTimes & Feed::get_stop_times() const { return stop_times; } 2399 | 2400 | inline StopTimes Feed::get_stop_times_for_stop(const Id & stop_id) const 2401 | { 2402 | StopTimes res; 2403 | for (const auto & stop_time : stop_times) 2404 | { 2405 | if (stop_time.stop_id == stop_id) 2406 | res.emplace_back(stop_time); 2407 | } 2408 | return res; 2409 | } 2410 | 2411 | inline StopTimesRange Feed::get_stop_times_for_trip(const Id & trip_id) const 2412 | { 2413 | const auto start = std::lower_bound(stop_times.begin(), stop_times.end(), trip_id, 2414 | [](const auto & a, const Id & i){ return a.trip_id < i; }); 2415 | const auto end = std::upper_bound(start, stop_times.end(), trip_id, 2416 | [](const Id & i, const auto & a){ return i < a.trip_id; }); 2417 | return {start, end}; 2418 | } 2419 | 2420 | inline void Feed::add_stop_time(const StopTime & stop_time) { stop_times.emplace_back(stop_time); } 2421 | 2422 | inline Result Feed::read_calendar() 2423 | { 2424 | auto handler = [this](const ParsedCsvRow & record) { return this->add_calendar_item(record); }; 2425 | return parse_csv(file_calendar, handler); 2426 | } 2427 | 2428 | inline Result Feed::write_calendar(const std::string & gtfs_path) const 2429 | { 2430 | auto container_writer = [this](std::ofstream & out) { return this->write_calendar(out); }; 2431 | return write_csv(gtfs_path, file_calendar, write_calendar_header, container_writer); 2432 | } 2433 | 2434 | inline const Calendar & Feed::get_calendar() const { return calendar; } 2435 | 2436 | inline const CalendarItem & Feed::get_calendar_item(const Id & service_id) const 2437 | { 2438 | const auto it = 2439 | std::lower_bound(calendar.begin(), calendar.end(), service_id, 2440 | [](const auto & a, const Id & i){ return a.service_id < i; }); 2441 | 2442 | if (it == calendar.end() || it->service_id != service_id) 2443 | { 2444 | static CalendarItem invalid; 2445 | return invalid; 2446 | } 2447 | 2448 | return *it; 2449 | } 2450 | 2451 | inline void Feed::add_calendar_item(const CalendarItem & calendar_item) 2452 | { 2453 | calendar.emplace_back(calendar_item); 2454 | } 2455 | 2456 | inline Result Feed::read_calendar_dates() 2457 | { 2458 | auto handler = [this](const ParsedCsvRow & record) { return this->add_calendar_date(record); }; 2459 | return parse_csv(file_calendar_dates, handler); 2460 | } 2461 | 2462 | inline Result Feed::write_calendar_dates(const std::string & gtfs_path) const 2463 | { 2464 | auto container_writer = [this](std::ofstream & out) { return this->write_calendar_dates(out); }; 2465 | return write_csv(gtfs_path, file_calendar_dates, write_calendar_dates_header, container_writer); 2466 | } 2467 | 2468 | inline const CalendarDates & Feed::get_calendar_dates() const { return calendar_dates; } 2469 | 2470 | inline CalendarDatesRange Feed::get_calendar_dates(const Id & service_id) const 2471 | { 2472 | auto start = std::lower_bound(calendar_dates.begin(), calendar_dates.end(), service_id, 2473 | [](const auto & a, const Id & i){ return a.service_id < i; }); 2474 | auto end = std::upper_bound(start, calendar_dates.end(), service_id, 2475 | [](const Id & i, const auto & a){ return i < a.service_id; }); 2476 | return {start, end}; 2477 | } 2478 | 2479 | inline void Feed::add_calendar_date(const CalendarDate & calendar_date) 2480 | { 2481 | calendar_dates.emplace_back(calendar_date); 2482 | } 2483 | 2484 | inline Result Feed::read_fare_rules() 2485 | { 2486 | auto handler = [this](const ParsedCsvRow & record) { return this->add_fare_rule(record); }; 2487 | return parse_csv(file_fare_rules, handler); 2488 | } 2489 | 2490 | inline Result Feed::write_fare_rules(const std::string & gtfs_path) const 2491 | { 2492 | auto container_writer = [this](std::ofstream & out) { return this->write_fare_rules(out); }; 2493 | return write_csv(gtfs_path, file_fare_rules, write_fare_rules_header, container_writer); 2494 | } 2495 | 2496 | inline const FareRules & Feed::get_fare_rules() const { return fare_rules; } 2497 | 2498 | inline FareRulesRange Feed::get_fare_rules(const Id & fare_id) const 2499 | { 2500 | 2501 | auto start = std::lower_bound(fare_rules.begin(), fare_rules.end(), fare_id, 2502 | [](const auto & a, const Id & i){ return a.fare_id < i; }); 2503 | auto end = std::upper_bound(start, fare_rules.end(), fare_id, 2504 | [](const Id & i, const auto & a){ return i < a.fare_id; }); 2505 | return {start, end}; 2506 | } 2507 | 2508 | inline void Feed::add_fare_rule(const FareRule & fare_rule) { fare_rules.emplace_back(fare_rule); } 2509 | 2510 | inline Result Feed::read_fare_attributes() 2511 | { 2512 | auto handler = [this](const ParsedCsvRow & record) { return this->add_fare_attributes(record); }; 2513 | return parse_csv(file_fare_attributes, handler); 2514 | } 2515 | 2516 | inline Result Feed::write_fare_attributes(const std::string & gtfs_path) const 2517 | { 2518 | auto container_writer = [this](std::ofstream & out) { return this->write_fare_attributes(out); }; 2519 | return write_csv(gtfs_path, file_fare_attributes, write_fare_attributes_header, container_writer); 2520 | } 2521 | 2522 | inline const FareAttributes & Feed::get_fare_attributes() const { return fare_attributes; } 2523 | 2524 | FareAttributesRange Feed::get_fare_attributes(const Id & fare_id) const 2525 | { 2526 | auto start = std::lower_bound(fare_attributes.begin(), fare_attributes.end(), fare_id, 2527 | [](const auto & a, const Id & i){ return a.fare_id < i; }); 2528 | auto end = std::upper_bound(start, fare_attributes.end(), fare_id, 2529 | [](const Id & i, const auto & a){ return i < a.fare_id; }); 2530 | return {start, end}; 2531 | } 2532 | 2533 | inline void Feed::add_fare_attributes(const FareAttributesItem & fare_attributes_item) 2534 | { 2535 | fare_attributes.emplace_back(fare_attributes_item); 2536 | } 2537 | 2538 | inline Result Feed::read_shapes() 2539 | { 2540 | auto handler = [this](const ParsedCsvRow & record) { return this->add_shape(record); }; 2541 | return parse_csv(file_shapes, handler); 2542 | } 2543 | 2544 | inline Result Feed::write_shapes(const std::string & gtfs_path) const 2545 | { 2546 | auto container_writer = [this](std::ofstream & out) { return this->write_shapes(out); }; 2547 | return write_csv(gtfs_path, file_shapes, write_shapes_header, container_writer); 2548 | } 2549 | 2550 | inline const Shapes & Feed::get_shapes() const { return shapes; } 2551 | 2552 | inline ShapeRange Feed::get_shape(const Id & shape_id) const 2553 | { 2554 | auto start = std::lower_bound(shapes.begin(), shapes.end(), shape_id, 2555 | [](const auto & a, const Id & i){ return a.shape_id < i; }); 2556 | auto end = std::upper_bound(start, shapes.end(), shape_id, 2557 | [](const Id & i, const auto & a){ return i < a.shape_id; }); 2558 | return {start, end}; 2559 | } 2560 | 2561 | inline void Feed::add_shape(const ShapePoint & shape) { shapes.emplace_back(shape); } 2562 | 2563 | inline Result Feed::read_frequencies() 2564 | { 2565 | auto handler = [this](const ParsedCsvRow & record) { return this->add_frequency(record); }; 2566 | return parse_csv(file_frequencies, handler); 2567 | } 2568 | 2569 | inline Result Feed::write_frequencies(const std::string & gtfs_path) const 2570 | { 2571 | auto container_writer = [this](std::ofstream & out) { return this->write_frequencies(out); }; 2572 | return write_csv(gtfs_path, file_frequencies, write_frequencies_header, container_writer); 2573 | } 2574 | 2575 | inline const Frequencies & Feed::get_frequencies() const { return frequencies; } 2576 | 2577 | inline FrequenciesRange Feed::get_frequencies(const Id & trip_id) const 2578 | { 2579 | auto start = std::lower_bound(frequencies.begin(), frequencies.end(), trip_id, 2580 | [](const auto & a, const Id & i){ return a.trip_id < i; }); 2581 | auto end = std::upper_bound(start, frequencies.end(), trip_id, 2582 | [](const Id & i, const auto & a){ return i < a.trip_id; }); 2583 | return {start, end}; 2584 | } 2585 | 2586 | inline void Feed::add_frequency(const Frequency & frequency) { frequencies.emplace_back(frequency); } 2587 | 2588 | inline Result Feed::read_transfers() 2589 | { 2590 | auto handler = [this](const ParsedCsvRow & record) { return this->add_transfer(record); }; 2591 | return parse_csv(file_transfers, handler); 2592 | } 2593 | 2594 | inline Result Feed::write_transfers(const std::string & gtfs_path) const 2595 | { 2596 | auto container_writer = [this](std::ofstream & out) { return this->write_transfers(out); }; 2597 | return write_csv(gtfs_path, file_transfers, write_transfers_header, container_writer); 2598 | } 2599 | 2600 | inline const Transfers & Feed::get_transfers() const { return transfers; } 2601 | 2602 | inline const Transfer & Feed::get_transfer(const Id & from_stop_id, 2603 | const Id & to_stop_id) const 2604 | { 2605 | const auto it = 2606 | std::lower_bound(transfers.begin(), transfers.end(), "", 2607 | [&](const auto & a, const Id & ) 2608 | { return a.from_stop_id < from_stop_id || (a.from_stop_id == from_stop_id && a.to_stop_id < to_stop_id); }); 2609 | 2610 | if (it == transfers.end() || it->from_stop_id != it->from_stop_id || it->to_stop_id != to_stop_id) 2611 | { 2612 | static Transfer invalid; 2613 | return invalid; 2614 | } 2615 | 2616 | return *it; 2617 | } 2618 | 2619 | inline void Feed::add_transfer(const Transfer & transfer) { transfers.emplace_back(transfer); } 2620 | 2621 | inline Result Feed::read_pathways() 2622 | { 2623 | auto handler = [this](const ParsedCsvRow & record) { return this->add_pathway(record); }; 2624 | return parse_csv(file_pathways, handler); 2625 | } 2626 | 2627 | inline Result Feed::write_pathways(const std::string & gtfs_path) const 2628 | { 2629 | auto container_writer = [this](std::ofstream & out) { return this->write_pathways(out); }; 2630 | return write_csv(gtfs_path, file_pathways, write_pathways_header, container_writer); 2631 | } 2632 | 2633 | inline const Pathways & Feed::get_pathways() const { return pathways; } 2634 | 2635 | inline PathwaysRange Feed::get_pathways(const Id & pathway_id) const 2636 | { 2637 | auto start = std::lower_bound(pathways.begin(), pathways.end(), pathway_id, 2638 | [](const auto & a, const Id & i){ return a.pathway_id < i; }); 2639 | auto end = std::upper_bound(start, pathways.end(), pathway_id, 2640 | [](const Id & i, const auto & a){ return i < a.pathway_id; }); 2641 | return {start, end}; 2642 | } 2643 | 2644 | inline Pathways Feed::get_pathways(const Id & from_stop_id, const Id & to_stop_id) const 2645 | { 2646 | Pathways res; 2647 | for (const auto & path : pathways) 2648 | { 2649 | if (path.from_stop_id == from_stop_id && path.to_stop_id == to_stop_id) 2650 | res.emplace_back(path); 2651 | } 2652 | return res; 2653 | } 2654 | 2655 | inline void Feed::add_pathway(const Pathway & pathway) { pathways.emplace_back(pathway); } 2656 | 2657 | inline Result Feed::read_levels() 2658 | { 2659 | auto handler = [this](const ParsedCsvRow & record) { return this->add_level(record); }; 2660 | return parse_csv(file_levels, handler); 2661 | } 2662 | 2663 | inline Result Feed::write_levels(const std::string & gtfs_path) const 2664 | { 2665 | auto container_writer = [this](std::ofstream & out) { return this->write_levels(out); }; 2666 | return write_csv(gtfs_path, file_levels, write_levels_header, container_writer); 2667 | } 2668 | 2669 | inline const Levels & Feed::get_levels() const { return levels; } 2670 | 2671 | inline const Level & Feed::get_level(const Id & level_id) const 2672 | { 2673 | const auto it = 2674 | std::lower_bound(levels.begin(), levels.end(), level_id, 2675 | [&](const auto & a, const Id & i) { return a.level_id < i; }); 2676 | 2677 | if (it == levels.end() || it->level_id != level_id) 2678 | { 2679 | static Level invalid; 2680 | return invalid; 2681 | } 2682 | 2683 | return *it; 2684 | } 2685 | 2686 | inline void Feed::add_level(const Level & level) { levels.emplace_back(level); } 2687 | 2688 | inline Result Feed::read_feed_info() 2689 | { 2690 | auto handler = [this](const ParsedCsvRow & record) { return this->add_feed_info(record); }; 2691 | return parse_csv(file_feed_info, handler); 2692 | } 2693 | 2694 | inline Result Feed::write_feed_info(const std::string & gtfs_path) const 2695 | { 2696 | auto container_writer = [this](std::ofstream & out) { return this->write_feed_info(out); }; 2697 | return write_csv(gtfs_path, file_feed_info, write_feed_info_header, container_writer); 2698 | } 2699 | 2700 | inline FeedInfo Feed::get_feed_info() const { return feed_info; } 2701 | 2702 | inline void Feed::set_feed_info(const FeedInfo & info) { feed_info = info; } 2703 | 2704 | inline Result Feed::read_translations() 2705 | { 2706 | auto handler = [this](const ParsedCsvRow & record) { return this->add_translation(record); }; 2707 | return parse_csv(file_translations, handler); 2708 | } 2709 | 2710 | inline Result Feed::write_translations(const std::string & gtfs_path) const 2711 | { 2712 | auto container_writer = [this](std::ofstream & out) { return this->write_translations(out); }; 2713 | return write_csv(gtfs_path, file_translations, write_translations_header, container_writer); 2714 | } 2715 | 2716 | inline const Translations & Feed::get_translations() const { return translations; } 2717 | 2718 | inline TranslationsRange Feed::get_translations(const Text & table_name) const 2719 | { 2720 | auto start = std::lower_bound(translations.begin(), translations.end(), table_name, 2721 | [](const auto & a, const Id & i){ return a.table_name < i; }); 2722 | auto end = std::upper_bound(start, translations.end(), table_name, 2723 | [](const Id & i, const auto & a){ return i < a.table_name; }); 2724 | return {start, end}; 2725 | } 2726 | 2727 | inline void Feed::add_translation(const Translation & translation) 2728 | { 2729 | translations.emplace_back(translation); 2730 | } 2731 | 2732 | inline Result Feed::read_attributions() 2733 | { 2734 | auto handler = [this](const ParsedCsvRow & record) { return this->add_attribution(record); }; 2735 | return parse_csv(file_attributions, handler); 2736 | } 2737 | 2738 | inline Result Feed::write_attributions(const std::string & gtfs_path) const 2739 | { 2740 | auto container_writer = [this](std::ofstream & out) { return this->write_attributions(out); }; 2741 | return write_csv(gtfs_path, file_attributions, write_attributions_header, container_writer); 2742 | } 2743 | 2744 | inline const Attributions & Feed::get_attributions() const { return attributions; } 2745 | 2746 | inline void Feed::add_attribution(const Attribution & attribution) 2747 | { 2748 | attributions.emplace_back(attribution); 2749 | } 2750 | 2751 | inline void Feed::write_agencies(std::ofstream & out) const 2752 | { 2753 | for (const auto & agency : agencies) 2754 | { 2755 | std::vector fields{wrap(agency.agency_id), wrap(agency.agency_name), 2756 | wrap(agency.agency_url), agency.agency_timezone, 2757 | agency.agency_lang, wrap(agency.agency_phone), 2758 | agency.agency_fare_url, agency.agency_email}; 2759 | write_joined(out, std::move(fields)); 2760 | } 2761 | } 2762 | 2763 | inline void Feed::write_routes(std::ofstream & out) const 2764 | { 2765 | for (const auto & route : routes) 2766 | { 2767 | std::vector fields{wrap(route.route_id), 2768 | wrap(route.agency_id), 2769 | wrap(route.route_short_name), 2770 | wrap(route.route_long_name), 2771 | wrap(route.route_desc), 2772 | wrap(route.route_type), 2773 | route.route_url, 2774 | route.route_color, 2775 | route.route_text_color, 2776 | wrap(route.route_sort_order), 2777 | "" /* continuous_pickup */, 2778 | "" /* continuous_drop_off */}; 2779 | // TODO: handle new route fields. 2780 | write_joined(out, std::move(fields)); 2781 | } 2782 | } 2783 | 2784 | inline void Feed::write_shapes(std::ofstream & out) const 2785 | { 2786 | for (const auto & shape : shapes) 2787 | { 2788 | std::vector fields{wrap(shape.shape_id), wrap(shape.shape_pt_lat), 2789 | wrap(shape.shape_pt_lon), wrap(shape.shape_pt_sequence), 2790 | wrap(shape.shape_dist_traveled)}; 2791 | write_joined(out, std::move(fields)); 2792 | } 2793 | } 2794 | 2795 | inline void Feed::write_trips(std::ofstream & out) const 2796 | { 2797 | for (const auto & trip : trips) 2798 | { 2799 | std::vector fields{ 2800 | wrap(trip.route_id), wrap(trip.service_id), wrap(trip.trip_id), 2801 | wrap(trip.trip_headsign), wrap(trip.trip_short_name), wrap(trip.direction_id), 2802 | wrap(trip.block_id), wrap(trip.shape_id), wrap(trip.wheelchair_accessible), 2803 | wrap(trip.bikes_allowed)}; 2804 | write_joined(out, std::move(fields)); 2805 | } 2806 | } 2807 | 2808 | inline void Feed::write_stops(std::ofstream & out) const 2809 | { 2810 | for (const auto & stop : stops) 2811 | { 2812 | std::vector fields{ 2813 | wrap(stop.stop_id), wrap(stop.stop_code), wrap(stop.stop_name), 2814 | wrap(stop.stop_desc), wrap(stop.stop_lat), wrap(stop.stop_lon), 2815 | wrap(stop.zone_id), stop.stop_url, wrap(stop.location_type), 2816 | wrap(stop.parent_station), stop.stop_timezone, wrap(stop.wheelchair_boarding), 2817 | wrap(stop.level_id), wrap(stop.platform_code)}; 2818 | write_joined(out, std::move(fields)); 2819 | } 2820 | } 2821 | 2822 | inline void Feed::write_stop_times(std::ofstream & out) const 2823 | { 2824 | for (const auto & stop_time : stop_times) 2825 | { 2826 | std::vector fields{wrap(stop_time.trip_id), 2827 | stop_time.arrival_time.get_raw_time(), 2828 | stop_time.departure_time.get_raw_time(), 2829 | wrap(stop_time.stop_id), 2830 | wrap(stop_time.stop_sequence), 2831 | wrap(stop_time.stop_headsign), 2832 | wrap(stop_time.pickup_type), 2833 | wrap(stop_time.drop_off_type), 2834 | "" /* continuous_pickup */, 2835 | "" /* continuous_drop_off */, 2836 | wrap(stop_time.shape_dist_traveled), 2837 | wrap(stop_time.timepoint)}; 2838 | // TODO: handle new stop_times fields. 2839 | write_joined(out, std::move(fields)); 2840 | } 2841 | } 2842 | 2843 | inline void Feed::write_calendar(std::ofstream & out) const 2844 | { 2845 | for (const auto & item : calendar) 2846 | { 2847 | std::vector fields{ 2848 | wrap(item.service_id), wrap(item.monday), wrap(item.tuesday), 2849 | wrap(item.wednesday), wrap(item.thursday), wrap(item.friday), 2850 | wrap(item.saturday), wrap(item.sunday), item.start_date.get_raw_date(), 2851 | item.end_date.get_raw_date()}; 2852 | write_joined(out, std::move(fields)); 2853 | } 2854 | } 2855 | 2856 | inline void Feed::write_calendar_dates(std::ofstream & out) const 2857 | { 2858 | for (const auto & date : calendar_dates) 2859 | { 2860 | std::vector fields{wrap(date.service_id), date.date.get_raw_date(), 2861 | wrap(date.exception_type)}; 2862 | write_joined(out, std::move(fields)); 2863 | } 2864 | } 2865 | 2866 | inline void Feed::write_transfers(std::ofstream & out) const 2867 | { 2868 | for (const auto & transfer : transfers) 2869 | { 2870 | std::vector fields{wrap(transfer.from_stop_id), wrap(transfer.to_stop_id), 2871 | wrap(transfer.transfer_type), wrap(transfer.min_transfer_time)}; 2872 | write_joined(out, std::move(fields)); 2873 | } 2874 | } 2875 | 2876 | inline void Feed::write_frequencies(std::ofstream & out) const 2877 | { 2878 | for (const auto & frequency : frequencies) 2879 | { 2880 | std::vector fields{wrap(frequency.trip_id), frequency.start_time.get_raw_time(), 2881 | frequency.end_time.get_raw_time(), wrap(frequency.headway_secs), 2882 | wrap(frequency.exact_times)}; 2883 | write_joined(out, std::move(fields)); 2884 | } 2885 | } 2886 | 2887 | inline void Feed::write_fare_attributes(std::ofstream & out) const 2888 | { 2889 | for (const auto & attribute : fare_attributes) 2890 | { 2891 | std::vector fields{ 2892 | wrap(attribute.fare_id), wrap(attribute.price), attribute.currency_type, 2893 | wrap(attribute.payment_method), 2894 | // Here we handle GTFS specification corner case: "The fact that this field can be left 2895 | // empty is an exception to the requirement that a Required field must not be empty.": 2896 | attribute.transfers == FareTransfers::Unlimited ? "" : wrap(attribute.transfers), 2897 | wrap(attribute.agency_id), wrap(attribute.transfer_duration)}; 2898 | write_joined(out, std::move(fields)); 2899 | } 2900 | } 2901 | 2902 | inline void Feed::write_fare_rules(std::ofstream & out) const 2903 | { 2904 | for (const auto & rule : fare_rules) 2905 | { 2906 | std::vector fields{wrap(rule.fare_id), wrap(rule.route_id), wrap(rule.origin_id), 2907 | wrap(rule.destination_id), wrap(rule.contains_id)}; 2908 | write_joined(out, std::move(fields)); 2909 | } 2910 | } 2911 | 2912 | inline void Feed::write_pathways(std::ofstream & out) const 2913 | { 2914 | for (const auto & path : pathways) 2915 | { 2916 | std::vector fields{ 2917 | wrap(path.pathway_id), wrap(path.from_stop_id), wrap(path.to_stop_id), 2918 | wrap(path.pathway_mode), wrap(path.is_bidirectional), wrap(path.length), 2919 | wrap(path.traversal_time), wrap(path.stair_count), wrap(path.max_slope), 2920 | wrap(path.min_width), wrap(path.signposted_as), wrap(path.reversed_signposted_as)}; 2921 | write_joined(out, std::move(fields)); 2922 | } 2923 | } 2924 | 2925 | inline void Feed::write_levels(std::ofstream & out) const 2926 | { 2927 | for (const auto & level : levels) 2928 | { 2929 | std::vector fields{wrap(level.level_id), wrap(level.level_index), 2930 | wrap(level.level_name)}; 2931 | write_joined(out, std::move(fields)); 2932 | } 2933 | } 2934 | 2935 | inline void Feed::write_feed_info(std::ofstream & out) const 2936 | { 2937 | std::vector fields{wrap(feed_info.feed_publisher_name), 2938 | feed_info.feed_publisher_url, 2939 | feed_info.feed_lang, 2940 | "" /* default_lang */, 2941 | feed_info.feed_start_date.get_raw_date(), 2942 | feed_info.feed_end_date.get_raw_date(), 2943 | wrap(feed_info.feed_version), 2944 | feed_info.feed_contact_email, 2945 | feed_info.feed_contact_url}; 2946 | // TODO: handle new field_info field. 2947 | write_joined(out, std::move(fields)); 2948 | } 2949 | 2950 | inline void Feed::write_translations(std::ofstream & out) const 2951 | { 2952 | for (const auto & translation : translations) 2953 | { 2954 | std::vector fields{translation.table_name, translation.field_name, 2955 | translation.language, wrap(translation.translation), 2956 | wrap(translation.record_id), wrap(translation.record_sub_id), 2957 | wrap(translation.field_value)}; 2958 | write_joined(out, std::move(fields)); 2959 | } 2960 | } 2961 | 2962 | inline void Feed::write_attributions(std::ofstream & out) const 2963 | { 2964 | for (const auto & attr : attributions) 2965 | { 2966 | std::vector fields{ 2967 | wrap(attr.attribution_id), wrap(attr.agency_id), wrap(attr.route_id), 2968 | wrap(attr.trip_id), wrap(attr.organization_name), wrap(attr.is_producer), 2969 | wrap(attr.is_operator), wrap(attr.is_authority), attr.attribution_url, 2970 | attr.attribution_email, attr.attribution_phone}; 2971 | write_joined(out, std::move(fields)); 2972 | } 2973 | } 2974 | } // namespace gtfs 2975 | --------------------------------------------------------------------------------