diff options
author | isabelle-dr | 2022-03-22 19:10:07 -0400 |
---|---|---|
committer | isabelle-dr | 2022-03-22 19:10:07 -0400 |
commit | 0763b1a00defec962245c0e90068206917054460 (patch) | |
tree | 598876eb4501ea305dd9a80454a46bbfbeaa20b8 | |
parent | e59a98b722d64478028b082e21dcf237901890c8 (diff) |
Add gtfs-book files
27 files changed, 2706 insertions, 19 deletions
@@ -1,18 +1 @@ -# Build and Release Folders -bin-debug/ -bin-release/ -[Oo]bj/ -[Bb]in/ - -# Other files and folders -.settings/ - -# Executables -*.swf -*.air -*.ipa -*.apk - -# Project files, i.e. `.project`, `.actionScriptProperties` and `.flexProperties` -# should NOT be excluded as they contain compiler settings and other important -# information for Eclipse / Flash Builder. +.DS_Store diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..73f69e0 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/GTFS-books.iml b/.idea/GTFS-books.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/GTFS-books.iml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="JAVA_MODULE" version="4"> + <component name="NewModuleRootManager" inherit-compiler-output="true"> + <exclude-output /> + <content url="file://$MODULE_DIR$" /> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + </component> +</module>
\ No newline at end of file diff --git a/.idea/gtfs-book/ch-00-definitive-guide-to-gtfs.md b/.idea/gtfs-book/ch-00-definitive-guide-to-gtfs.md new file mode 100644 index 0000000..2c32c67 --- /dev/null +++ b/.idea/gtfs-book/ch-00-definitive-guide-to-gtfs.md @@ -0,0 +1,55 @@ +# The Definitive Guide to GTFS + +*Consuming open public transportation data with the General Transit Feed +Specification* + +Originally written by Quentin Zervaas. + +## About This Book + +This book is a comprehensive guide to GTFS -- the General Transit Feed +Specification. It is comprised of two main sections. + +The first section describes what GTFS is and provides details about the +specification itself. In addition to this it also provides various +discussion points and things to consider for each of the files in the +specification. + +The second section covers a number of topics that relate to actually +using GTFS feeds, such as how to calculate fares, how to search for +trips, how to optimize feed data and more. + +This book is written for developers that are using transit data for web +sites, mobile applications and more. It aims to be as language-agnostic +as possible, but uses SQL to demonstrate concepts of extracting data +from a GTFS feed. + +### About The Author + +Quentin Zervaas is a software developer from Adelaide, Australia. + +Quentin was the founder of TransitFeeds (now <OpenMobilityData.org>), a web +site that provides a comprehensive listing of public transportation data +available around the world. This site is referenced various times +throughout this book. + +### Credits + +First Edition. Published in February 2014. + +**Technical Reviewer** + +Rupert Hanson + +**Copy Editor** + +Miranda Little + +**Disclaimer** + +The information in this book is distributed on an "as is" basis, without +warranty. Although every precaution has been taken in the preparation of +this work, the author shall not be liable to any person or entity with +respect to any loss or damage caused or alleged to be caused directly or +indirectly by the information contained in this book. + diff --git a/.idea/gtfs-book/ch-01-introduction.md b/.idea/gtfs-book/ch-01-introduction.md new file mode 100644 index 0000000..3b3765c --- /dev/null +++ b/.idea/gtfs-book/ch-01-introduction.md @@ -0,0 +1,45 @@ +## 1. Introduction to GTFS + +GTFS (General Transit Feed Specification) is a data standard developed +by Google used to describe a public transportation system. Its primary +purpose was to enable public transit agencies to upload their schedules +to Google Transit so that users of Google Maps could easily figure out +which bus, train, ferry or otherwise to catch. + +> **A GTFS feed is a ZIP file that contains a series of CSV files that +list routes, stops and trips in a public transportation system.** + +This book examines GTFS in detail, including which data from a public +transportation system can be represented, how to extract data, and +explores some more advanced techniques for optimizing and querying data. + +The official GTFS specification has been referenced a number of times in +this book. It is strongly recommended you are familiar with it. You can +view at the following URL: + +<https://developers.google.com/transit/gtfs/reference> + +### Structure of a GTFS Feed + +A GTFS feed is a series of CSV files, which means that it is trivial to +include additional files in a feed. Additionally, files required as part +of the specification can also include additional columns. For this +reason, feeds from different agencies generally include different levels +of detail. + +***Note:** The files in a GTFS feed are CSV files, but use a file +extension of `.txt`.* + +A GTFS feed can be described as follows: + +> **A GTFS feed has one or more routes. Each route (`routes.txt`) has one or +more trips (`trips.txt`). Each trip visits a series of stops (`stops.txt`) +at specified times (`stop_times.txt`). Trips and stop times only contain +time of day information; the calendar is used to determine on which days +a given trip runs (`calendar.txt` and `calendar_dates.txt`).** + +The following chapters cover the main files that are included in all +GTFS feeds. For each file, the main columns are covered, as well as +optional columns that can be included. This book also covers some of the +unofficial columns that some agencies choose to include. + diff --git a/.idea/gtfs-book/ch-02-agencies.md b/.idea/gtfs-book/ch-02-agencies.md new file mode 100644 index 0000000..cb837ae --- /dev/null +++ b/.idea/gtfs-book/ch-02-agencies.md @@ -0,0 +1,47 @@ +## 2. Agencies (agency.txt) + +*This file is ***required*** to be included in GTFS feeds.* + +The `agency.txt` file is used to represent the agencies that provide +data for this feed. While its presence is optional, if there are routes +from multiple agencies included, then records in `routes.txt` make +reference to agencies in this file. + +| Field | Required? | Description | +| :----------------------------------------------------- | :--------: | :-------- | +| `agency_id` | Optional | An ID that uniquely identifies a single transit agency in the feed. If a feed only contains routes for a single agency then this value is optional. | +| `agency_name` | Required | The full name of the transit agency. | +| `agency_url` | Required | The URL of the transit agency. Must be a complete URL only, beginning with `http://` or `https://`. | +| `agency_timezone` | Required | Time zone of agency. All times in `stop_times.txt` use this time zone, unless overridden by its corresponding stop. All agencies in a single feed must use the same time zone. Example: **America/New_York** (See <http://en.wikipedia.org/wiki/List_of_tz_database_time_zones> for more examples) | +| `agency_lang` | Required | Contains a two-letter ISO-639-1 code (such as `en` or `EN` for English) for the language used in this feed. | +| `agency_phone` | Optional | A single voice telephone number for the agency that users can dial if required. | +| `agency_fare_url` | Optional | A URL that describes fare information for the agency. Must be a complete URL only, beginning with `http://` or `https://`. | + +### Sample Data + +The following extract is taken from the GTFS feed of TriMet (Portland, +USA), located at <https://openmobilitydata.org/p/trimet>. + +| `agency_name` | `agency_url` | `agency_timezone` | `agency_lang` | `agency_phone` | +| :------------ | :------------------------------------------- | :-------------------- | :------------ | :--------------- | +| `TriMet` | `[https://trimets.org](https://trimet.org/)` | `America/Los_Angeles` | `en` | `(503) 238-7433` | + +In this example, the `agency_id` column is included, but as there is +only a single entry the value can be empty. This means the `agency_id` +column in `routes.txt` also is not required. + +### Discussion + +The data in this file is typically used to provide additional +information to users of your app or web site in case schedules derived +from the rest of this feed are not sufficient (or in the case of +`agency_fare_url`, an easy way to provide a reference point to users +if the fare information in the feed is not being used). + +If you refer to the following screenshot, taken from Google Maps, you +can see the information from `agency.txt` represented in the +lower-left corner as an example of how it can be used. + +![GTFS agency](images/agency-google-maps.png) + + diff --git a/.idea/gtfs-book/ch-03-stops.md b/.idea/gtfs-book/ch-03-stops.md new file mode 100644 index 0000000..cd8dedb --- /dev/null +++ b/.idea/gtfs-book/ch-03-stops.md @@ -0,0 +1,117 @@ +## 3. Stops & Stations (stops.txt) + +*This file is ***required*** to be included in GTFS feeds.* + +The individual locations where vehicles pick up or drop off passengers +are represented by `stops.txt`. Records in this file are referenced in +`stop_times.txt`. A record in this file can be either a stop or a +station. A station has one or more child stops, as indicated using the +`parent_station` value. Entries that are marked as stations may not +appear in `stop_times.txt`. + +| Field | Required? | Description | +| :----- | :-------- | :---------- | +| `stop_id` | Required | An ID to uniquely identify a stop or station. | +| `stop_code` | Optional | A number or short string used to identify a stop to passengers. This is typically displayed at the physical stop or on printed schedules. | +| `stop_name` | Required | The name of the stop as passengers know it by. | +| `stop_desc` | Optional | A description of the stop. If provided, this should provide additional information to the `stop_name` value. | +| `stop_lat` | Required | The latitude of the stop (a number in the range of `-90` to `90`). | +| `stop_lon` | Required | The longitude of the stop (a number in the range of `-180` to `180`). | +| `zone_id` | Optional | This is an identifier used to calculate fares. A single zone ID may appear in multiple stops, but is ignored if the stop is marked as a station. | +| `stop_url` | Optional | A URL that provides information about this stop. It should be specific to this stop and not simply link to the agency's web site. | +| `location_type` | Optional | Indicates if a record is a stop or station. `0` or blank means a stop, `1` means a station. | +| `parent_station` | Optional | If a record is marked as a stop and has a parent station, this contains the ID of the parent (the parent must have a `location_type` of `1`). | +| `stop_timezone` | Optional | If a stop is located in a different time zone to the one specified in `agency.txt`, then it can be overridden here. | +| `wheelchair_boarding` | Optional | A value of `1` indicates it is possible for passengers in wheelchairs to board or alight. A value of `2` means the stop is not wheelchair accessible, while `0` or an empty value means no information is available. If the stop has a parent station, then 0 or an empty value means to inherit from its parent. | + +### Sample Data + +The following extract is taken from the TriMet GTFS feed +(<https://openmobilitydata.org/p/trimet>). + +| `stop_id` | `stop_code` | `stop_name` | `stop_lat` | `stop_lon` | `stop_url` | +| :-------- | :----------- | :------------------ | :---------- | :------------ | :---------------------------------------------------| +| `2` | `2` | `A Ave & Chandler` | `45.420595` | `-122.675676` |` <http://trimet.org/arrivals/tracker?locationID=2>` | +| `3` | `3` | `A Ave & Second St` | `45.419386` | `-122.665341` |` <http://trimet.org/arrivals/tracker?locationID=3>` | +| `4` | `4` | `A Ave & 10th St` | `45.420703` | `-122.675152` |` <http://trimet.org/arrivals/tracker?locationID=4>` | +| `6` | `6` | `A Ave & 8th St` | `45.420217` | `-122.67307` |` <http://trimet.org/arrivals/tracker?locationID=6>` | + +The following diagram shows how these points look if you plot them onto +a map. + +![Stops](images/stops-sample.png) + +In this extract, TriMet use the same value for stop IDs and stop codes. +This is useful, because it means the stop IDs are stable (that is, they +do not change between feed versions). This means that if you want to +save a particular stop (for instance, if a user wants to save a +"favorite stop") you can trust that saving the ID will get the job done. + +**Note:** This is not always the case though, which means you may have +to save additional information if you want to save a stop. For instance, +you may need to save the coordinates or the list of routes a stop serves +so you can find it again if the stop ID has changed in a future version +of the feed. + +### Stops & Stations + +Specifying an entry in this file as a *station* is typically used when +there are many stops located within a single physical entity, such as a +train station or bus depot. While many feeds do not offer this +information, some large train stations may have up to 20 or 30 +platforms. + +Knowing the platform for a specific trip is extremely useful, but if a +passenger wants to select a starting point for their trip, showing them +a list of platforms may be confusing. + +Passenger: + +**"I want to travel from *Central Station* to *Airport Station*."** + +Web site / App: + +**"Board at *Central Station platform 5*, disembark at *Airport Station platform 1*."** + +In this example, the passenger selects the parent station, but they are +presented with the specific stop so they know exactly where within the +station they need to embark or disembark. + +### Wheelchair Accessibility + +If you are showing wheelchair accessibility information, it is important +to differentiate between "no access" and "no information", as knowing a +stop is not accessible is as important as knowing it is. + +If a stop is marked as being wheelchair accessible, you must check that +trips that visit the stop are also accessible (using the +`wheelchair_accessible` field in `trips.txt`). If the value in +`trips.txt` is blank, `0` or `1` then it is safe to assume the +trip can be accessed. If the stop is accessible and the trip is not, +then passengers in wheelchairs cannot use the trip. + +### Stop Features + +One of the proposed changes to GTFS is the addition of a file called +`stop_features.txt`. This is used to define characteristics about +stops. The great thing about this file is that it allows you to indicate +to users when a stop has a ticket machine, bike storage, lighting, or an +electronic display with real-time information. + +TriMet is one of the few agencies including this file. The following is +a sample of this file. + +| `stop_id` | `feature_type` | +| :-------- | :------------- | +| `61` | `4110` | +| `61` | `2310` | +| `61` | `5200` | + +This data indicate that stop `61` (NE Alberta & 24th) has a *Printed +Schedule Display* (`4110`), a *Bike Rack* (`2310`) and a *Street +Light* (`5200`). + +For more information about this proposal and a list of values and their +meanings, refer to +<https://sites.google.com/site/gtfschanges/proposals/stop-amenity>. + diff --git a/.idea/gtfs-book/ch-04-routes.md b/.idea/gtfs-book/ch-04-routes.md new file mode 100644 index 0000000..2e9716e --- /dev/null +++ b/.idea/gtfs-book/ch-04-routes.md @@ -0,0 +1,82 @@ +## 4. Routes (routes.txt) + +*This file is ***required*** to be included in GTFS feeds.* + +A route is a group of trips that are displayed to riders as a single +service. + +| Field | Required? | Description | +| :----- | :-------- | :---------- | +| `route_id` | Required | An ID that uniquely identifies the route. | +| `agency_id` | Optional | The ID of the agency a route belongs to, as it appears in `agency.txt`. Only required if there are multiple agencies in the feed. | +| `route_short_name` | Required | A nickname or code to represent this service. If this is left empty then the `route_long_name` must be included. | +| `route_long_name` | Required | The route full name. If this is left empty then the `route_short_name` must be included. | +| `route_desc` | Optional | A description of the route, such as where and when the route operates. | +| `route_type` | Required | The type of transportation used on a route (such as bus, train or ferry). See below for more information. | +| `route_url` | Optional | A URL of a web page that describes this particular route. | +| `route_color` | Optional | If applicable, a route can have a color assigned to it. This is useful for systems that use colors to identify routes. This value is a six-character hexadecimal number (for example, `FF0000` is red). | +| `route_text_color` | Optional | For routes that specify the `route_color`, a corresponding text color should also be specified. | + +### Sample Data + +The following extract is taken from the TriMet GTFS feed +(<https://openmobilitydata.org/p/trimet>). + +| `route_id` | `route_short_name` | `route_long_name` | `route_type` | +| :--------- | :----------------- | :--------------------------- | :----------- | +| `1` | `1` | `Vermont` | `3` | +| `4` | `4` | `Division / Fessenden` | `3` | +| `6` | `6` | `Martin Luther King Jr Blvd` | `3` | + +This sample shows three different bus routes for the greater Portland +area. The `route_type` value of `3` indicates they are buses. See +the next section for more information about route types in GTFS. + +There is no agency ID value in this feed, as TriMet is the only agency +represented in the feed. + +The other thing to note about this data is that TriMet use the same +value for both `route_id` and `route_short_name`. This is very +useful, because it means if you have a user that wants to save +information about a particular route you can trust the `route_id` +value. Unfortunately, this is not the case in all GTFS feeds. Sometimes, +the `route_id` value may change with every version of a feed (or at +least, semi-frequently). Additionally, some feeds may also have multiple +routes with the same `route_short_name`. This can present challenges +when trying to save user data. + +### Route Types + +To indicate a route's mode of transport, the `route_type` column is +used. + +| Value | Description | +| :---- | :---------------- | +| `0` | Tram / Light Rail | +| `1` | Subway / Metro | +| `2` | Rail | +| `3` | Bus | +| `4` | Ferry | +| `5` | Cable Car | +| `6` | Gondola | +| `7` | Funicular | + +Agencies may interpret the meaning of these route types differently. For +instance, some agencies specify their subway service as rail (value of +`2` instead of `1`), while some specify their trains as light rail +(`0` instead of `2`). + +These differences between agencies occur mainly because of the vague +descriptions for each of these route types. If you use Google Transit to +find directions, you may notice route types referenced that are +different to those listed above. This is because Google Transit also +supports additional route types. You can read more about these +additional route types at +<https://support.google.com/transitpartners/answer/3520902?hl=en>. + +Very few GTFS feeds made available to third-party developers actually +make use of these values, but it is useful to know in case you come +across one that does. For instance, Sydney Buses include their school +buses with a route type of `712`, while other buses in the feed have +route type `700`. + diff --git a/.idea/gtfs-book/ch-05-trips.md b/.idea/gtfs-book/ch-05-trips.md new file mode 100644 index 0000000..ee2b814 --- /dev/null +++ b/.idea/gtfs-book/ch-05-trips.md @@ -0,0 +1,222 @@ +## 5. Trips (trips.txt) + +*This file is ***required*** to be included in GTFS feeds.* + +The `trips.txt` file contains trips for each route. The specific stop +times are specified in `stop_times.txt`, and the days each trip runs +on are specified in `calendar.txt` and `calendar_dates.txt`. + +| Field | Required? | Description | +| :----- | :-------- | :---------- | +| `route_id` | Required | The ID of the route a trip belongs to as it appears in `routes.txt`. | +| `service_id` | Required | The ID of the service as it appears in `calendar.txt` or `calendar_dates.txt`, which identifies the dates on which a trip runs. | +| `trip_id` | Required | A unique identifier for a trip in this file. This value is referenced in `stop_times.txt` when specifying individual stop times. | +| `trip_headsign` | Optional | The text that appears to passengers as the destination or description of the trip. Mid-trip changes to the headsign can be specified in `stop_times.txt`. | +| `trip_short_name` | Optional | A short name or code to identify the particular trip, different to the route's short name. This may identify a particular train number or a route variation. | +| `direction_id` | Optional | Indicates the direction of travel for a trip, such as to differentiate between an inbound and an outbound trip. | +| `block_id` | Optional | A block is a series of trips conducted by the same vehicle. This ID is used to group 2 or more trips together. | +| `shape_id` | Optional | This value references a value from `shapes.txt` to define a shape for a trip. | +| `wheelchair_accessible` | Optional | `0` or blank indicates unknown, while `1` indicates the vehicle can accommodate at least one wheelchair passenger. A value of `2` indicates no wheelchairs can be accommodated. | + +### Sample Data + +Consider the following extract, taken from the `trips.txt` file of the +TriMet GTFS feed (<https://openmobilitydata.org/p/trimet>). + +| `route_id` | `service_id` | `trip_id` | `direction_id` | `block_id` | `shape_id` | +| ---------- | ------------ | ----------- | -------------- | ---------- | ---------- | +| 1 | W.378 | 4282257 | 0 | 103 | 185327 | +| 1 | W.378 | 4282256 | 0 | 101 | 185327 | +| 1 | W.378 | 4282255 | 0 | 102 | 185327 | +| 1 | W.378 | 4282254 | 0 | 103 | 185327 | + +This data describes four individual trips for the "Vermont" bus route +(this was determined by looking up the `route_id` value in +`routes.txt`). While the values for `trip_id`, `block_id` and +`shape_id` are all integers in this particular instance, this is not a +requirement. Just like `service_id`, there may be non-numeric +characters. + +As each of these trips is for the same route and runs in the same +direction (based on `direction_id`) they can all be represented by the +same shape. Note however that this is not always be the case as some +agencies may start or finish trips for a single route at different +locations depending on the time of the day. If this were the case, then +the trip's shape would differ slightly (and therefore have a different +shape to represent it in `shapes.txt`). + +Although this example does not include `trip_headsign`, many feeds do +include this value. This is useful for indicating to a passenger where +the trip is headed. When the trip headsign is not provided in the feed, +you can determine the destination by using the final stop in a trip. + +**Tip:** If you are determining the destination based on the final stop, +you can either present the stop name to the user, or you can +reverse-geocode the stop's coordinates to determine the its locality. + +### Blocks + +In the preceding example, each trip has a value for `block_id`. The +first and the last trips here both have a `block_id` value of `103`. +This indicates that the same physical vehicle completes both of these +trips. As each of these trips go in the same direction, it is likely +that they start at the same location. + +This means there is probably another trip in the feed for the same block +that exists between the trips listed here. It would likely travel from +the finishing point of the first trip (`4282257`) to the starting +point of the other trip (`4282254`). If you dig a little deeper in the +feed you will find the trip shown in the following table. + +| `route_id` | `service_id` | `trip_id` | `direction_id` | `block_id` | `shape_id` | +| :--------- | :----------- | :-------- | :------------- | :--------- | :--------- | +| 1 | W.378 | 4282270 | 1 | 103 | 185330 | + +This is a trip traveling in the opposite direction for the same block. +It has a different shape ID because it is traveling in the opposite +direction; a shape's points must advance in the same direction a trip's +stop times do. + +***Note:** You should perform some validation when grouping trips +together using block IDs. For instance, if trips share a block_id value +then they should also have the same service_id value. You should also +check that the times do not overlap; otherwise the same vehicle would be +unable to service both trips.* + +If you dig even further in this feed, there are actually seven different +trips all using block `103` for the **W.378** service period. This +roughly represents a full day's work for a single vehicle. + +For more discussion on blocks and how to utilize them effectively, refer +to *Working With Trip Blocks*. + +### Wheelchair Accessibility + +Similar to `stops.txt`, you can specify the wheelchair accessibility +of a specific trip using the `wheelchair_accessible` field. While many +feeds do not provide this information (often because vehicles in a fleet +can be changed at the last minute, so agencies do not want to guarantee +this information), your wheelchair-bound users will love you if you can +provide this information. + +As mentioned in the section on `stops.txt`, it is equally important to +tell a user that a specific vehicle cannot accommodate wheelchairs as to +when it can. Additionally, if the stops in a feed also have wheelchair +information, then both the stop and trip must be wheelchair accessible +for a passenger to be able to access a trip at the given stop. + +### Trip Direction + +One of the optional fields in `trips.txt` is `direction_id`, which +is used to indicate the general direction a vehicle is traveling. At +present the only possible values are `0` to represent "inbound" and +`1` to represent "outbound". There are no specific guidelines as to +what each value means, but the intention is that an inbound trip on a +particular route should be traveling in the opposite direction to an +outbound trip. + +Many GTFS feeds do not provide this information. In fact, there are a +handful of feeds that include two entries in `routes.txt` for each +route (one for each direction). + +One of the drawbacks of `direction_id` is that there are many routes +for which "inbound" or "outbound" do not actually mean anything. Many +cities have loop services that start and finish each trip at the same +location. Some cities have one or more loops that travel in both +directions (generally indicated by "clockwise loop" and +"counter-clockwise loop", or words to that effect). In these instances, +the `direction_id` can be used to determine which direction the route +is traveling. + +### Trip Short Name + +The `trip_short_name` field that appears in `trips.txt` is used to +provide a vehicle-specific code to a particular trip on a route. Based +on GTFS feeds that are currently in publication, it appears there are +two primary use-cases for this field: + +* Specifying a particular train number for all trains on a route +* Specifying a route "sub-code" for a route variant. + +### Specifying a Train Number + +For certain commuter rail systems, such as SEPTA in Philadelphia or MBTA +in Boston, each train has a specific number associated with it. This +number is particularly meaningful to passengers as trains on the same +route may have different stopping patterns or even different features +(for instance, only a certain train may have air-conditioning or +wheelchair access). + +Consider the following extract from SEPTA's rail feed +(<https://openmobilitydata.org/p/septa>). + +| `route_id` | `service_id` | `trip_id` | `trip_headsign` | `block_id` | `trip_short_name` | +| ---------- | ------------ | ------------ | ------------------------ | ---------- | ----------------- | +| AIR | S5 | AIR_1404_V25 | Center City Philadelphia | 1404 | 1404 | +| AIR | S1 | AIR_402_V5 | Center City Philadelphia | 402 | 402 | +| AIR | S1 | AIR_404_V5 | Center City Philadelphia | 404 | 404 | +| AIR | S5 | AIR_406_V25 | Center City Philadelphia | 406 | 406 | + +In this data, there are four different trains all heading to the same +destination. The `trip_short_name` is a value that can safely be +presented to users as it has meaning to them. In this case, you could +present the first trip to passengers as: + +**"Train 1404 on the Airport line heading to Center City +Philadelphia."** + +In this particular feed, SEPTA use the same value for +`trip_short_name` and for `block_id`, because the train number +belongs to a specific train. This means after it completes the trip to +Center City Philadelphia it continues on. In this particular feed, the +following trip also exists: + +**"Train 1404 on the Warminster line heading to Glenside."** + +You can therefore think of the `trip_short_name` value as a +"user-facing" version of `block_id`. + +### Specifying a Route Sub-Code + +The other use-case for `trip_short_name` is for specifying a route +sub-code. For instance, consider an agency that has a route with short +name `100` that travels from stop `S1` to stop `S2`. At night the +agency only has a limited number of routes running, so they extend this +route to also visit stop `S3` (so it travels from `S1` to `S2` +then to `S3`). As it is a minor variation of the main path, the agency +calls this trip `100A`. + +The agency could either create a completely separate entry in +`routes.txt` (so they would have `100` and `100A`), or they can +override the handful of trips in the evening by setting the +`trip_short_name` to `100A`. The following table shows how this +example might be represented. + +| `route_id` | `trip_id` | `service_id` | `trip_short_name` | +| ---------- | --------- | ------------ | ----------------- | +| 100 | T1 | C1 | | +| 100 | T2 | C1 | | +| 100 | T3 | C1 | | +| 100 | T4 | C1 | 100A | + +In this example the `trip_short_name` does not need to be set for the +first three trips as they use the `route_short_name` value from +`routes.txt`. + +### Specifying Bicycle Permissions + +A common field that appears in many GTFS fields is +`trip_bikes_allowed`, which is used to indicate whether or not +passengers are allowed to take bicycles on board. This is useful for +automated trip planning when bicycle options can be included in the +results. + +The way this field works is similar to the wheelchair information; `0` +or empty means no information provided; `1` means no bikes allowed; +while `2` means at least one bike can be accommodated. + +**Note:** Unfortunately, this value is backwards when you compare it to +wheelchair accessibility fields. For more discussion on this matter, +refer to the topic on the Google Group for GTFS Changes +(<https://groups.google.com/d/topic/gtfs-changes/rEiSeKNc4cs/discussion>). + diff --git a/.idea/gtfs-book/ch-06-stop-times.md b/.idea/gtfs-book/ch-06-stop-times.md new file mode 100644 index 0000000..9eb0f93 --- /dev/null +++ b/.idea/gtfs-book/ch-06-stop-times.md @@ -0,0 +1,160 @@ +## 6. Stop Times (stop_times.txt) + +*This file is ***required*** to be included in GTFS feeds.* + +The `stop_times.txt` file specifies individual stop arrivals and +departures for each trip. This file is typically the largest in a GTFS +feed as it contains many records that correspond to each entry in +`trips.txt`. + +| Field | Required? | Description | +| :----- | :-------- | :---------- | +| `trip_id` | Required | References a trip from `trips.txt`. This ID is referenced for every stop in a trip. | +| `arrival_time` | Required | The arrival time in `HH:MM:SS` format. Can be left blank, except for at least the first and last stop time in a trip. This value is typically be the same as `departure_time`. | +| `departure_time` | Required | The departure time in `HH:MM:SS` format. Can be left blank, except for at least the first and last stop time in a trip. This value is typically be the same as `arrival_time`. | +| `stop_id` | Required | References a single stop from `stops.txt`. | +| `stop_sequence` | Required | A unique number for a given trip to indicate the stopping order. Typically these values appear in order and increment by 1 for each stop time, but this is not always the case. | +| `stop_headsign` | Optional | This is text that appears to passengers at this stop to identify the trip's destination. It should only be used to override the `trip_headsign` value from `trips.txt`. | +| `pickup_type` | Optional | Indicates if passengers can be picked up at this stop. Sometimes a stop is drop-off only. | +| `drop_off_type` | Optional | Indicates if passengers can be dropped off at this stop. Sometimes a stop is pick-up only. | +| `shape_dist_traveled` | Optional | If a trip has an associated shape, this value indicates how far along that shape the vehicle has traveled when at this stop. Values in this file and `shapes.txt` must use the same unit. | + +### Sample Data + +Consider the following extract, taken from stop_times.txt in the TriMet +GTFS feed (<https://openmobilitydata.org/p/trimet>). This represents the +first ten stops of a trip for bus route `1` ("Vermont") in Portland, as +covered in Sample Data for `trips.txt`. + +| `trip_id` | `arrival_time` | `departure_time` | `stop_id` | `stop_sequence` | `shape_dist_traveled` | +| :-------- | :------------- | :--------------- | :-------- | :-------------- | :-------------------- | +| 4282247 | 06:47:00 | 06:47:00 | 13170 | 1 | 0.0 | +| 4282247 | 06:48:18 | 06:48:18 | 7631 | 2 | 867.5 | +| 4282247 | 06:50:13 | 06:50:13 | 7625 | 3 | 2154.9 | +| 4282247 | 06:52:07 | 06:52:07 | 7612 | 4 | 3425.5 | +| 4282247 | 06:53:42 | 06:53:42 | 7616 | 5 | 4491.1 | +| 4282247 | 06:55:16 | 06:55:16 | 10491 | 6 | 5536.2 | +| 4282247 | 06:57:06 | 06:57:06 | 7588 | 7 | 6767.1 | +| 4282247 | 06:58:00 | 06:58:00 | 7591 | 8 | 7364.4 | +| 4282247 | 06:58:32 | 06:58:32 | 175 | 9 | 8618.7 | +| 4282247 | 06:58:50 | 06:58:50 | 198 | 10 | 9283.8 | + +If you were to plot the full trip on a map (including using its shape +file, as specified in `shapes.txt` and referenced in `trips.txt`), +it would look like the following diagram. The first stop is selected. + +![Stop Times](images/stop-times.png) + +### Arrival Time vs. Departure Time + +The first thing to notice about this data is that the values in +`arrival_time` and `departure_time` are the same. In reality, most +of the time these values are the same. The situation where these values +differ is typically when a vehicle is required to wait for a period of +time before departing. For instance: + +**"The train departs the domestic airport at 6:30am, arrives at the +international terminal at 6:35am. It waits for 10 minutes for passengers +who have just landed to board, then departs for the city at 6:45am."** + +While this is not typically something you need to worry about, be aware +that some feeds differ and the holdover time could be large. A person +rushing to a train wants to know the time it departs, while a husband +waiting at a stop to meet his wife wants to know what time it arrives. + +**Note:** In a situation where the difference between the arrival time +and departure is small, you may be better off always displaying the +earlier time to the user. The driver may view a one or two minute +holdover as an opportunity to keep on time, whereas a ten or fifteen +minute holdover is unlikely to be ignored as doing so would +significantly alter the schedule. + +### Scheduling Past Midnight + +One of the most important concepts to understand about +`stop_times.txt` is that times later than midnight can be specified. +For example, if a trip starts at 11:45 PM and takes an hour to complete, +its finishing time is 12:45 AM the next day. + +The `departure_time` value for the first stop is be `23:45:00`, +while the `arrival_time` value for the final stop is `24:45:00`. +If you were to specify the final arrival time as `00:45:00`, it +would be referencing 12:45 AM prior to the trip's starting time. + +While you could use the `stop_sequence` to determine which day the +trip fell on, it would be impossible to do a quick search purely based +on the given final stop. + +A trip may start and finish after midnight without being considered as +part of the next day's service. Many transit systems shut down +overnight, but may have a few services that run after midnight until +about 1 AM or 2 AM. It is logical to group them all together with the +same service as earlier trips, which this functionality allows you to +do. + +However, this has implications when searching for trips. For instance, +if you want to find all trips between 12:00 AM and 1:00 AM on 30 January +2014, then you need to search: + +* Between `00:00:00` and `01:00:00` for trips with service on `20140130` +* Between `24:00:00` and `25:00:00` for trips with service on` 20140129` + +In *Searching for Trips* you can see how to apply this to your +trip searches. + +### Time Points + +Most GTFS feeds provide arrival/departure times for every single stop. +In reality, most agencies do not have known times (or at least, they do +not publish times) for many of their stops. + +Typically, for routes that describe trains, subways or ferries that make +relatively few stops in a trip, all stops have a specified time. +However, often for bus routes that may make many stops in a single trip, +generally only the main stops have times shown on printed schedules. + +Typically, the bus drivers are required to meet these time points; if +they are ahead of schedule they might wait at a time point until the +scheduled time; if they are running late they might try to catch up in +order to adhere to the specified time point. + +This means that the intermediate points are likely estimates that have +been interpolated based on the amount of time between time points. If +there are multiple stops in-between the time points then the distance +between stops may also be used to calculate the estimate. + +In actual fact, GTFS feeds do not have to specify times for all stops. +The data in the following table is perfectly valid for a trip. + +| `trip_id` | `arrival_time` | `stop_id` | `stop_sequence` | `shape_dist_traveled` | +| :-------- | :------------- | :-------- | :-------------- | :-------------------- | +| T1 | 10:00:00 | S1 | 1 | 0 | +| T1 | | S2 | 2 | 1500 | +| T1 | | S3 | 3 | 3000 | +| T1 | 10:12:00 | S4 | 4 | 6000 | + +Based on this data, without taking into account the distance traveled, +you may estimate that the second stop arrives at 10:04 AM while the +third stop arrives at 10:08. + +If you consider the distance traveled, you might conclude the second +stop arrives at 10:03 AM while the third stop arrives at 10:06 AM. + +Some agencies include an additional column in `stop_times.txt` called +`timepoint`. This is used when they specify the times for all stops +but also want to indicate if only certain stops are guaranteed times. + +The following table shows how this would look using the previous data as +its basis. + +| `trip_id` | `arrival_time` | `stop_id` | `stop_sequence` | `shape_dist_traveled` | `timepoint` | +| :--------- | :------------- | :-------- | :-------------- | :-------------------- | :---------- | +| T1 | 10:00:00 | S1 | 1 | 0 | 1 | +| T1 | 10:03:00 | S2 | 2 | 1500 | 0 | +| T1 | 10:06:00 | S3 | 3 | 3000 | 0 | +| T1 | 10:12:00 | S4 | 4 | 6000 | 1 | + +This can be especially useful if you want to highlight these time points +so as to represent the printed schedules accurately, or even if you are +a transit agency just using the data for internal reporting. + diff --git a/.idea/gtfs-book/ch-07-calendar.md b/.idea/gtfs-book/ch-07-calendar.md new file mode 100644 index 0000000..aebf131 --- /dev/null +++ b/.idea/gtfs-book/ch-07-calendar.md @@ -0,0 +1,138 @@ +## 7. Trip Schedules (calendar.txt & calendar_dates.txt) + +*Each of these files are ***optional*** in a GTFS feed, but at least one +of them is ***required***.* + +The `calendar.txt` file is used to indicate the range of dates on +which trips are running. It works by including a start date and a finish +date (typically a range of 3-6 months), then a marker for each day of +the week on which it operates. If there are single-day scheduling +changes that occur during this period, then the `calendar_dates.txt` +file can be used to override the schedule for each of these days. + +The following table shows the specification for `calendar.txt`. + +| Field | Required? | Description | +| :----- | :-------- | :---------- | +| `service_id` | Required | A unique ID for a single service. This value is referenced by trips in `trips.txt`. | +| `start_date` | Required | This indicates the start date for a given service, in `YYYYMMDD` format. | +| `end_date` | Required | This indicates the end date for a given service, in `YYYYMMDD` format. | +| `monday` | Required | Contains `1` if trips run on Mondays between the start and end dates, `0` or empty if not. | +| `tuesday` | Required | Contains `1` if trips run on Tuesdays between the start and end dates, `0` or empty if not. | +| `wednesday` | Required | Contains `1` if trips run on Wednesdays between the start and end dates, `0` or empty if not. | +| `thursday` | Required | Contains `1` if trips run on Thursdays between the start and end dates, `0` or empty if not. | +| `friday` | Required | Contains `1` if trips run on Fridays between the start and end dates, `0` or empty if not. | +| `saturday` | Required | Contains `1` if trips run on Saturdays between the start and end dates, `0` or empty if not. | +| `sunday` | Required | Contains `1` if trips run on Sundays between the start and end dates, `0` or empty if not. | + +As mentioned above, the `calendar_dates.txt` file is used to define +exceptions to entries in `calendar.txt`. For instance, if a 3-month +service is specified in `calendar.txt` and a holiday lies on a Monday +during this period, then you can use calendar_dates.txt to override this +single date. + +If the weekend schedule were used for a holiday, then you would add a +record to remove the regular schedule for the holiday date, and another +record to add the weekend schedule for the holiday date. + +Some feeds choose only to include `calendar_dates.txt` and not +`calendar.txt`, in which case there is an "add service" record for +every service and every date in this file. + +The following table shows the specification for `calendar_dates.txt`. + +| Field | Required? | Description | +| :----- | :-------- | :---------- | +| `service_id` | Required | The service ID that an exception is being defined for. This is referenced in both `calendar.txt` and in `trips.txt`. Unlike `calendar.txt`, it is possible for a `service_id` value to appear multiple times in this file. | +| `date` | Required | The date for which the exception is occurring, in `YYYYMMDD` format. | +| `exception_type` | Required | This indicates whether the exception is denoting an added service (`1`) or a removed service (`2`). | + +### Sample Data + +The following is an extract from the `calendar.txt` file in Adelaide +Metro's GTFS feed (<https://openmobilitydata.org/p/adelaide-metro>). This +extract includes schedules from the start of 2014 until the end of March +2014. + +| `service_id` | `monday` | `tuesday` | `wednesday` | `thursday` | `friday` | `saturday` | `sunday` | `start_date` | `end_date` | +| :----------- | :------- | :-------- | :---------- | :--------- | :------- | :--------- | :------- | :----------- | :--------- | +| 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 20140102 | 20140331 | +| 11 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 20140102 | 20140331 | +| 12 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 20140102 | 20140331 | + +Any trip in the corresponding `trips.txt` file with a `service_id` +value of `1` runs from Monday to Friday. Trips with a `service_id` +of `11` run only on Saturday, while those with `12` run only on +Sunday. + +Now consider an extract from `calendar_dates.txt` from the same feed, +as shown in the following table. + +| `service_id` | `date` | `exception_type` | +| ------------ | -------- | ---------------- | +| 1 | 20140127 | 2 | +| 1 | 20140310 | 2 | +| 12 | 20140127 | 1 | +| 12 | 20140310 | 1 | + +The first two rows mean that on January 27 and March 10 trips with +`service_id` of `1` are not running. The final two rows mean that on +those same dates trips with `service_id` of `12` are running. This +has the following meaning: + +**"On 27 January and 10 March, use the Sunday timetable instead of the +Monday-Friday timetable."** + +In Adelaide, these two dates are holidays (Australia Day and Labour +Day). It is Adelaide Metro's policy to run their Sunday timetable on +public holidays, which is reflected by the above records in their +`calendar_dates.txt` file. + +### Structuring Services + +The case described above is the ideal case for specifying services in a +GTFS feed (dates primarily specified in `calendar.txt` with a handful +of exceptions in `calendar_dates.txt`). + +Be aware that there are two other major ways that services are specified +in feeds. + +1. Using only `calendar_dates.txt` and expressly including every + single date within the service range. Each of these is included as + "service added" (an `exception_type` value of `1`). The + following table shows how this might look. + +| `service_id` | `date` | `exception_type` | +| :----------- | :------- | :--------------- | +| 1 | 20140102 | 1 | +| 1 | 20140103 | 1 | +| 11 | 20140104 | 1 | +| 12 | 20140105 | 1 | + +2. Not using `calendar_dates.txt`, but creating many records in + `calendar.txt` instead to span various dates. The following table + shows how you can represent Monday-Friday from the sample data in + this fashion. + +| `service_id` | `monday` | `tuesday` | `wednesday` | `thursday` | `friday` | `saturday` | `sunday` | `start_date` | `end_date` | +| :----------- | :------- | :-------- | :---------- | :--------- | :------- | :--------- | :------- | :----------- | :--------- | +| 1a | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 20140102 | 20140126 | +| holiday1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 20140127 | 20140127 | +| 1b | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 20140128 | 20140309 | +| holiday2 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 20140310 | 20140310 | +| 1c | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 20140311 | 20140331 | + +In this example, each holiday has its own row in `calendar.txt` that +runs for a single day only. + +Refer to *Finding Service IDs* to see how to determine +services that are running for a given day. + +### Service Name + +There are a number of feeds that specify a column in `calendar.txt` +called `service_name`. This is used to give a descriptive name to each +service. For example, the Sedona Roadrunner in Arizona +(<https://openmobilitydata.org/p/sedona-roadrunner>) has services called +"Weekday Service", "Weekend Service" and "New Year's Eve Service". + diff --git a/.idea/gtfs-book/ch-08-fares.md b/.idea/gtfs-book/ch-08-fares.md new file mode 100644 index 0000000..88bcbea --- /dev/null +++ b/.idea/gtfs-book/ch-08-fares.md @@ -0,0 +1,115 @@ +## 8. Fare Definitions (fare_attributes.txt & fare_rules.txt) + +*These files are ***optional*** in a GTFS feed, but any rules specified +must reference a fare attributes record.* + +These two files define the types of fares that exist in a system, +including their price and transfer information. The attributes of a +particular fare exist in `fare_attributes.txt`, which has the +following columns. + +| Field | Required? | Description | +| :----- | :-------- | :---------- | +| `fare_id` | Required | A value that uniquely identifies a fare listed in this file. It must only appear once in this file. | +| `price` | Required | This field specifies the cost of the fare. For instance, if a trip costs $2 USD, then this value should be either `2.00` or `2`, and the `currency_type` should be `USD`. | +| `currency_type` | Required | This is the currency code that `price` is specified in, such as USD for United States Dollar. | +| `payment_method` | Required | This indicates when the fare is to be paid. `0` means it can be paid on board, while `1` means it must be paid before boarding. | +| `transfers` | Required | The number of transfers that may occur. This must either be empty (unlimited) transfers, or the number of transfers. | +| `transfer_duration` | Optional | This is the number of seconds a transfer is valid for. | + +The following table shows the specification for `fare_rules.txt`, +which defines the rules used to apply a fare to a particular trip. + +| Field | Required? | Description | +| :----- | :-------- | :---------- | +| `fare_id` | Required | This is the ID of the fare that a rule applies to as it appears in `fare_attributes.txt`. | +| `route_id` | Optional | The ID of a route as it appears in `routes.txt` for which this rule applies. If there are several routes with the same fare attributes, there may be a row in `fare_rules.txt` for each route. | +| `origin_id` | Optional | This value corresponds to a `zone_id` value from `stops.txt`. If specified, this means a trip must begin in the given zone in order to qualify for this fare. | +| `destination_id` | Optional | This value corresponds to a `zone_id` value from `stops.txt`. If specified, this means a trip must begin in the given zone in order to qualify for this fare. | +| `contains_id` | Optional | This value corresponds to a `zone_id` value from `stops.txt`. If specified, this means a trip must pass through every `contains_id` zone for the given fare (in other words, several rules may need to be checked). | + +Note that aside from `fare_id`, all fields are optional in this file. +This means some very complex rules can be made (especially when +transfers come into the calculation). The following URL has discussion +about different rules and some complex fare examples: + +<https://code.google.com/p/googletransitdatafeed/wiki/FareExamples> + +Refer to *Calculating Fares* for discussion about the +algorithm for calculating fares for trips both with and without +transfers. + +### Sample Data + +The following data is taken from the TriMet GTFS feed +(<https://openmobilitydata.org/p/trimet>). Firstly, the data from the +`fare_attributes.txt` file. + +| `fare_id` | `price` | `currency` | `payment_method` | `transfers` | `transfer_duration` | +| :-------- | :------ | :--------- | :--------------- | :---------- | :-------------------| +| B | 2.5 | USD | 0 | | 7200 | +| R | 2.5 | USD | 1 | | 7200 | +| BR | 2.5 | USD | 0 | | 7200 | +| RB | 2.5 | USD | 1 | | 7200 | +| SC | 1 | USD | 1 | 0 | | +| AT | 4 | USD | 1 | 0 | | +| VT | 0 | USD | 1 | 0 | | + +The data from the `fare_rules.txt` file is shown in the following +table. + +| `fare_id` | `route_id` | `origin_id` | `destination_id` | `contains_id` | +| :-------- | :--------- | :---------- | :--------------- | :------------ | +| B | | B | | B | +| R | | R | | R | +| BR | | B | | B | +| BR | | B | | R | +| RB | | R | | B | +| RB | | R | | R | +| SC | 193 | | | | +| SC | 194 | | | | +| AT | 208 | | | | +| VT | 250 | | | | + +In this sample data, TriMet have named some of their fares the same as +the zones specified in `stops.txt`. In this particular feed, bus stops +have a `zone_id` of `B`, while rail stops have `R`. + +The fares in this file are as follows: + +* Fare `B`. If you start at a bus stop (`zone_id` value of `B`), + you can buy your ticket on board (`payment_method` of `0`). You + may transfer an unlimited number (empty transfers value) for 2 hours + (`transfer_duration` of `7200`). The cost is $2.50 USD. +* Fare `R`. If you start at a rail stop, you must pre-purchase your + ticket. You may transfer an unlimited number of times to other rail + services for up to 2 hours. The cost is $2.50 USD. + +The `BR` fare describes a trip that begins on a bus then transfers to +a rail service (while `RB` is the opposite). This fare is not be +matched if the passenger does not travel on both, as all `contains_id` +values must be matched in order to apply a fare. + +The other fares (`SC`, `AT` and `VT`) all apply to their +respective `route_id` values, regardless of start and finish stops. +Tickets must be pre-purchased, and transfers are not allowed. The `VT` +fare (which corresponds to TriMet's Vintage Trolley) is free to ride +since it has a price of `0`. + +### Assigning Fares to Agencies + +One of the extensions available to `fare_attributes.txt` is to include +an `agency_id` column. This is to limit a specific fare to only routes +from the specified agency, in the case where a feed has multiple +agencies. + +This is useful because there may be two agencies in a feed that define +fares with no specific rules (in other words, the fare applies to all +trips). If the price differs, then GTFS dictates that the cheapest fare +is always applied. Using `agency_id` means these fares can be +differentiated accordingly. + +For more information about this extension, refer to the Google Transit +GTFS Extensions page at +<https://support.google.com/transitpartners/answer/2450962>. + diff --git a/.idea/gtfs-book/ch-09-shapes.md b/.idea/gtfs-book/ch-09-shapes.md new file mode 100644 index 0000000..bc05636 --- /dev/null +++ b/.idea/gtfs-book/ch-09-shapes.md @@ -0,0 +1,76 @@ +## 9. Trip Shapes (shapes.txt) + +*This file is ***optional*** in a GTFS feed.* + +Each trip in `trips.txt` can have a shape associated with it. The +shapes.txt file defines the points that make up an individual shape in +order to plot a trip on a map. Two or more records in `shapes.txt` +with the same `shape_id` value define a shape. + +The amount of data stored in this file can be quite large. In +*Optimizing Shapes* there are some strategies to efficiently +reduce the amount of shape data. + +| Field | Required? | Description | +| :----- | :-------- | :---------- | +| `shape_id` | Required | An ID to uniquely identify a shape. Every point for a shape contains the same value. | +| `shape_pt_lat` | Required | The latitude for a given point in the range of `-90` to `90`. | +| `shape_pt_lon` | Required | The longitude for a given point in the range of `-180` to `180`. | +| `shape_pt_sequence` | Required | A non-negative number that defines the ordering for points in a shape. A value must not be repeated within a single shape. | +| `shape_dist_traveled` | Optional | This value represents how far along a shape a particular point exists. This is a distance in a unit such as feet or kilometers. This unit must be the same as that used in `stop_times.txt`. | + +### Sample Data + +The following table shows a portion of a shape from the TriMet GTFS +feed. It is a portion of the shape that corresponds to the sample data +in the `stop_times.txt` section. + +| `shape_id` | `shape_pt_lat` | `shape_pt_lon` | `shape_pt_sequence` | `shape_dist_traveled` | +| :--------- | :------------- | :------------- | :------------------ | :-------------------- | +| 185328 | 45.52291 | -122.677372 | 1 | 0.0 | +| 185328 | 45.522921 | -122.67737 | 2 | 3.7 | +| 185328 | 45.522991 | -122.677432 | 3 | 34.0 | +| 185328 | 45.522992 | -122.677246 | 4 | 81.5 | +| 185328 | 45.523002 | -122.676567 | 5 | 255.7 | +| 185328 | 45.523004 | -122.676486 | 6 | 276.4 | +| 185328 | 45.523007 | -122.676386 | 7 | 302.0 | +| 185328 | 45.523024 | -122.675386 | 8 | 558.4 | +| 185328 | 45.522962 | -122.67538 | 9 | 581.0 | + +In this sample data, the `shape_dist_traveled` is listed in feet. +There is no way to specify in a GTFS feed which units are used for this +column -- it could be feet, miles, meters, kilometers. In actual fact, +it does not really matter, just as long as the units are the same as in +`stop_times.txt`. + +If you need to present a distance to your users (such as how far you +need to travel on a bus), you can calculate it instead by adding up the +distance between each point and formatting it based on the user's +locale settings. + +### Point Sequences + +In most GTFS feeds the `shape_pt_sequence` value starts at 1 and +increments by 1 for every subsequent point. Additionally, points are +typically listed in order of their sequence. + +You should not rely on these two statements though, as this is not a +requirement of GTFS. Many transit agencies have automated systems that +export their GTFS from a separate system, which can sometimes result in +an unpredictable output format. + +For instance, a trip that has stop times listed with the sequences `1`, `2`, +`9`, `18`, `7`, `3` is perfectly valid. + +### Distance Travelled + +The `shape_dist_traveled` column is used so you can programmatically +determine how much of a shape to draw when showing a map to users of +your web site or app. If you use techniques in *Optimizing Shapes* +to reduce the file size of shape data, then it becomes difficult to +use this value. + +Alternatively, you can calculate portions of shapes by determining which +point in a shape travels closest to the start and finish points of a +trip. + diff --git a/.idea/gtfs-book/ch-10-frequencies.md b/.idea/gtfs-book/ch-10-frequencies.md new file mode 100644 index 0000000..45021ab --- /dev/null +++ b/.idea/gtfs-book/ch-10-frequencies.md @@ -0,0 +1,98 @@ +## 10. Repeating Trips (frequencies.txt) + +*This file is ***optional*** in a GTFS feed.* + +In some cases a route may repeat a particular stopping pattern every few +minutes (picture a subway line that runs every 5 minutes). Rather than +including entries in `trips.txt` and `stop_times.txt` for every +single occurrence, you can include the trip once then define rules for +it to repeat for a period of time. + +Having a trip repeat only works in the case where the timing between +stops remains consistent for all stops. Using `frequencies.txt`, you +use the relative times between stops alongside a calculated starting +time for the trip in order to determine the specific stop times. + +| Field | Required? | Description | +| :----- | :-------- | :---------- | +| `trip_id` | Required | The ID of the trip as it appears in `trips.txt` that is being repeated. A single trip can appear multiple times for different time ranges. | +| `start_time` | Required | The time at which a given trip starts repeating, in `HH:MM:SS` format. | +| `end_time` | Required | The time at which a given trip stops repeating, in `HH:MM:SS` format. | +| `headway_secs` | Required | The time in seconds between departures from a given stop during the time range. | +| `exact_times` | Optional | Whether or not repeating trips should be exactly scheduled. See below for discussion. | + +### Sample Data + +The following sample data is taken from Société de transport de Montréal +(STM) in Montreal +(<https://openmobilitydata.org/p/societe-de-transport-de-montreal>). + +| `trip_id` | `start_time` | `end_time` | `headway_secs` | +| :----------------------- | :----------- | :--------- | :------------- | +| 13S_13S_F1_1_2_0.26528 | 05:30:00 | 07:25:30 | 630 | +| 13S_13S_F1_1_6_0.34167 | 07:25:30 | 08:40:10 | 560 | +| 13S_13S_F1_1_10_0.42500 | 08:40:10 | 12:19:00 | 505 | +| 13S_13S_F1_1_7_0.58750 | 12:19:00 | 15:00:00 | 460 | +| 13S_13S_F1_1_11_0.66875 | 15:00:00 | 18:23:00 | 420 | +| 13S_13S_F1_1_5_0.78889 | 18:23:00 | 21:36:35 | 505 | + +Each of the trips listed here have corresponding entries in +`trips.txt` and `stop_times.txt` (more on that shortly). This data +can be interpreted as follows. + +* The first trip runs every 10m 30s from 5:30am until 7:25am. +* The second trip runs every 9m 20s from 7:25am until 8:40am, and so on. + +The following table shows some of the stop times for the first trip +(`departure_time` is omitted here for brevity, since it is identical +to `arrival_time`). + +| `trip_id` | `stop_id` | `arrival_time` | `stop_sequence` | +| :---------------------- | :-------- | :------------- | :-------------- | +| 13S_13S_F1_1_2_0.26528 | 18 | 06:22:00 | 1 | +| 13S_13S_F1_1_2_0.26528 | 19 | 06:22:59 | 2 | +| 13S_13S_F1_1_2_0.26528 | 20 | 06:24:00 | 3 | +| 13S_13S_F1_1_2_0.26528 | 21 | 06:26:00 | 4 | + +As this trip runs to the specified frequency, the specific times do not +matter. Instead, the differences are used. For the above stop times, +there is a 59 second gap between the first and second time, a 61 second +gap between the second and third, and a 120 second gap between the third +and fourth. + +The stop times for the first frequency record (10.5 minutes apart) can +be calculated as follows. + +* `05:30:00`, `05:30:59`, `05:32:00`, `05:34:00` +* `05:40:30`, `05:41:29`, `05:42:30`, `05:44:30` +* `05:51:00`, `05:51:59`, `05:53:00`, `05:55:00` +* ... +* `07:25:30`, `07:26:29`, `07:27:30`, `07:29:30` + +### Specifying Exact Times + +In the file definition at the beginning of this chapter there is an +optional field called `exact_times`. It may not be immediately clear +what this field means, so to explain it better, consider the frequency +definitions in the following table. + +| `trip_id` | `start_time` | `end_time` | `headway_secs` | `exact_times` | +| :-------- | :----------- | :--------- | :------------- | :------------ | +| T1 | 09:00:00 | 10:00:00 | 300 | 0 | +| T2 | 09:00:00 | 10:00:00 | 300 | 1 | + +These two frequencies are the same, with only the `exact_times` value +different. The first (`T1`) should be presented in a manner such as: + +**"Between 9 AM and 10 AM this trip departs every 5 minutes."** + +The second trip (`T2`) should be presented as follows: + +**"This trip departs at 9 AM, 9:05 AM, 9:10 AM, ..."** + +While ultimately the meaning is the same, this difference is used in +order to allow agencies to represent their schedules more accurately. +Often, schedules that convey to passengers that they will not have to +wait more than five minutes do so without having to explicitly list +every departure time. + diff --git a/.idea/gtfs-book/ch-11-stop-transfers.md b/.idea/gtfs-book/ch-11-stop-transfers.md new file mode 100644 index 0000000..6a43674 --- /dev/null +++ b/.idea/gtfs-book/ch-11-stop-transfers.md @@ -0,0 +1,65 @@ +## 11. Stop Transfers (transfers.txt) + +*This file is ***optional*** in a GTFS feed.* + +To define how passengers can transfer between routes at specific stops +feed providers can include `transfers.txt`. This does not mean +passengers cannot transfer elsewhere, but it does indicate if a transfer +is not possible between certain stops, or a minimum time required if +transfer is possible. + +| Field | Required? | Description | +| :----- | :-------- | :---------- | +| `from_stop_id` | Required | The ID of stop as it appears in `stops.txt` where the connection begins. If this references a station, then this rule applies to all stops within the station. | +| `to_stop_id` | Required | The ID of the stop as it appears in `stops.txt` where the connection between trips ends. If this references a station, then this rule applies to all stops within the station. | +| `transfer_type` | Required | `0` or blank means the recommended transfer point, `1` means the secondary vehicle will wait for the first, `2` means a minimum amount of time is required, `3` means transfer is not possible. | +| `min_transfer_time` | Optional | If the `transfer_type` value is `2` then this value must be specified. It indicates the number of seconds required to transfer between the given stops. | + +It is also possible that records in this file are specified for +ticketing reasons. For instance, some train stations are set up so that +passengers can transfer between routes without needing to validate their +ticket again or buy a transfer. Other stations that are shared between +those same routes might not have this open transfer area, thereby +requiring you to exit one route fully before buying another ticket to +access the second. + +### Sample Data + +The following table shows some sample transfer rules from TriMet in +Portland's GTFS feed (<https://openmobilitydata.org/p/trimet>). + +| `from_stop_id` | `to_stop_id` | `transfer_type` | `min_transfer_time` | +| :------------- | :----------- | :-------------- | :------------------ | +| 7807 | 5020 | 0 | | +| 7807 | 7634 | 0 | | +| 7807 | 7640 | 0 | | + +These rules indicate that if you are transferring from a route that +visits stop `7807` to any route that visits the other stops (`5020`, +`7634` or `7640`), then this is the ideal place to do it. + +In other words, if there are other locations along the first route where +you could transfer to the second route, then those stops should not be +used. These rules say this is the best place to transfer. + +Consider the transfer rule in the following table, taken from the New +York City Subway GTFS feed (<https://openmobilitydata.org/p/mta/79>). + +| `from_stop_id` | `to_stop_id` | `transfer_type` | `min_transfer_time` | +| :------------- | :----------- | :-------------- | :------------------ | +| 121 | 121 | 2 | 180 | + +In this data, the MTA specifies how long it takes to transfer to +different platforms within the same station. The stop with ID 121 refers +to the 86th St station (as specified in `stops.txt`). It has a +`location_type` of `1` and two stops within it (`121N` and +`121S`). The above transfer rule says that if you need to transfer +from `121N` to `121S` (or vice-versa) then a minimum time of 3 +minutes (180 seconds) must be allocated. + +If you were to calculate the time taken to transfer using the +coordinates of each of these platforms, it would only take a few seconds +as they are physically close to each other. In reality though, you must +exit one platform then walk around and enter the other platform (often +having to use stairs). + diff --git a/.idea/gtfs-book/ch-12-feed-information.md b/.idea/gtfs-book/ch-12-feed-information.md new file mode 100644 index 0000000..ffad2ab --- /dev/null +++ b/.idea/gtfs-book/ch-12-feed-information.md @@ -0,0 +1,36 @@ +## 12. Feed Information (feed_info.txt) + +*This file is ***optional*** in a GTFS feed.* + +Feed providers can include additional information about a feed using +`feed_info.txt`. It should only ever have a single row (other than the +CSV header row). + +| Field | Required? | Description | +| :----- | :-------- | :---------- | +| `feed_publisher_name` | Required | The name of the organization that publishes the feed. This may or may not be the same as any agency in `agency.txt`. | +| `feed_publisher_url` | Required | The URL of the feed publisher's web site. | +| `feed_lang` | Required | This specifies the language used in the feed. If an agency also has a language specified, then the agency's value should override this value. | +| `feed_start_date` | Optional | This value is a date in `YYYYMMDD` format that asserts the data in this feed is valid from this date. If specified, it typically matches up with the earliest date in `calendar.txt` or `calendar_dates.txt`, but if it is earlier, this is explicitly saying there are no services running between this date and the earliest service date. | +| `feed_end_date` | Optional | This value is a date in `YYYYMMDD` format that asserts the data in this feed is valid until this date. If specified, it typically matches up with the latest date in `calendar.txt` or `calendar_dates.txt`, but if it is earlier, this is explicitly saying there are no services running between the latest service date and this date. | +| `feed_version` | Optional | A string that indicates the version of this feed. This can be useful to let feed publishers know whether the latest version of their feed has been incorporated. | + +### Sample Data + +The following sample data is taken from the GTFS feed of TriMet in +Portland (<https://openmobilitydata.org/p/trimet>). + +| `feed_publisher_name` | `feed_publisher_url` | `feed_lang` | `feed_start_date` | `feed_end_date` | `feed_version` | +| :-------------------- | :-------------------------------------- | :---------- | :---------------- | :-------------- | :---------------- | +| TriMet | [http://trimet.org](http://trimet.org/) | en | | | 20140121-20140421 | + +In this example, TriMet do not include the start or end dates, meaning +you should derive the dates this feed is active for by the dates in +`calendar_dates.txt` (this particular feed does not have a +`calendar.txt` file). + +TriMet use date stamps to indicate the feed version. This feed was +published on 21 January 2014 and includes data up until 21 April 2014, +so it appears they use the first/last dates as a way to specify their +version. Each agency has its own method. + diff --git a/.idea/gtfs-book/ch-13-importing-to-sql.md b/.idea/gtfs-book/ch-13-importing-to-sql.md new file mode 100644 index 0000000..e96517c --- /dev/null +++ b/.idea/gtfs-book/ch-13-importing-to-sql.md @@ -0,0 +1,125 @@ +## 13. Importing a GTFS Feed to SQL + +One of the great things about GTFS is that it is already in a format +conducive to being used in an SQL database. The presence of various IDs +in each of the different files makes it easy to join the tables in order +to extract the data you require. + +To try this yourself, download `GtfsToSql` +(<https://github.com/OpenMobilityData/GtfsToSql>). This is a Java +command-line application that imports a GTFS feed to an SQLite database. +This application also supports PostgreSQL, but the examples used here +are for SQLite. + +The pre-compiled `GtfsToSql` Java archive can be downloaded from its +GitHub repository at +<https://github.com/OpenMobilityData/GtfsToSql/tree/master/dist>. + +To use `GtfsToSql`, all you need is an extracted GTFS feed. The +following instructions demonstrate how you to import the TriMet feed +that has been referenced throughout this book. + +Firstly, download and extract the feed. The following commands use curl +to download the file, then unzip to extract the file to a sub-directory +called `trimet`. + +``` +$ curl http://developer.trimet.org/schedule/gtfs.zip > gtfs.zip + +$ unzip gtfs.zip -d trimet/ +``` + +To create an SQLite database from this feed, the following command can +be used. + +``` +$ java -jar GtfsToSql.jar -s jdbc:sqlite:./db.sqlite -g ./trimet +``` + +This may take a minute or two to complete (you will see progress as it +imports the feed and then creates indexes), and at the end you will have +a GTFS database in a file called `db.sqlite`. You can then query this +database with the command-line `sqlite3` tool, as shown in the +following example. + +``` +$ sqlite3 db.sqlite +sqlite> SELECT * FROM agency; +|TriMet|http://trimet.org|America/Los_Angeles|en|503-238-7433 + +sqlite> SELECT * FROM routes WHERE route_type = 0; +90|||MAX Red Line||0|http://trimet.org/schedules/r090.htm|| +100|||MAX Blue Line||0|http://trimet.org/schedules/r100.htm|| +190|||MAX Yellow Line||0|http://trimet.org/schedules/r190.htm|| +193||Portland Streetcar|NS Line||0|http://trimet.org/schedules/r193.htm|| +194||Portland Streetcar|CL Line||0|http://trimet.org/schedules/r194.htm|| +200|||MAX Green Line||0|http://trimet.org/schedules/r200.htm|| +250|||Vintage Trolley||0|http://trimet.org/schedules/r250.htm|| +``` + +The first query above finds all agencies stored in the database, while +the second finds all routes marked as Light Rail (`route_type` of +`0`). + +**Note: In the following chapters there are more SQL examples. All of +these examples are geared towards running on an SQLite database that has +been created in this manner.** + +All tables in this database match up with the corresponding GTFS +filename (so for `agency.txt`, the table name is `agency`, while for +`stop_times.txt` the table name is `stop_times`). The columns in SQL +have the same name as the value in the corresponding GTFS file. + +***Note:** All data imported using this tool is stored as text in the +database. This means you may need to be careful when querying integer +data. For example, ordering stop times by stop_sequence may not produce +expected results (for instance, 29 as a string comes before 3). Although +it is a performance hit, you can change this behavior by casting the +value to integer, such as: ORDER BY stop_sequence + 0. The reason +`GtfsToSql` works in this way is because it is intended as a lightweight +tool to be able to quickly query GTFS data. I recommend rolling your own +importer to treat data exactly as you need it, especially in conjunction +with some of the optimization techniques recommended later in this book.* + +### File Encodings + +The GTFS specification does not indicate whether files should be encoded +using UTF-8, ISO-8859-1 or otherwise. Since a GTFS feed is not +necessarily in English, you must be willing to handle an extended +character set. + +The GtfsToSql tool introduced above automatically detects the encoding +of each file using the juniversalchardet Java library +(<https://code.google.com/p/juniversalchardet/>). + +I recommend you take some time looking at the source code of GtfsToSql +to further understand this so you are aware of handling encodings +correctly if you write your own parser. + +### Optimizing GTFS Feeds + +If you are creating a database that is to be distributed onto a mobile +device such as an iPhone or Android phone, then disk space and +computational power is at a premium. Even if you are setting up a +database to be queried on a server only, then making the database +perform as quickly as possible is still important. + +In the following chapters are techniques for optimizing GTFS feeds. +There are many techniques that can be applied to improve the performance +of GTFS, such as: + +* Using integer identifiers rather than string identifiers (for route + IDs, trip IDs, stop IDs, etc.) and creating appropriate indexes +* Removing redundant shape points and encoding shapes +* Deleting unused data +* Reusing repeating trip patterns. + +Changing the data to use integer IDs makes the greatest improvement to +performance, but the other techniques also help significantly. + +Depending on your needs, there are other optimizations that can be made +to reduce file size and speed up querying of the data, but the ease of +implementing them may depend on your database storage system and the +programming language used to query the data. The above list is a good +starting point. + diff --git a/.idea/gtfs-book/ch-14-switching-to-integer-ids.md b/.idea/gtfs-book/ch-14-switching-to-integer-ids.md new file mode 100644 index 0000000..4d3914a --- /dev/null +++ b/.idea/gtfs-book/ch-14-switching-to-integer-ids.md @@ -0,0 +1,111 @@ +## 14. Switching to Integer IDs + +There are a number of instances in a GTFS feed where IDs are used, such +as to identify routes, trips, stops and shapes. There are no specific +guidelines in GTFS as to the type of data or length an ID can be. As +such, IDs in some GTFS feeds maybe anywhere up to 30 or 40 characters +long. + +Using long strings as IDs is extremely inefficient as they make the size +of a database much larger than it needs to be, as well as making +querying the data much slower. + +To demonstrate, consider a GTFS feed where trip IDs are 30 characters +long. If there are 10,000 trips, each with an average of 30 stops in +`stop_times.txt`, then the IDs alone take up 9.3 MB of storage. +Realistically speaking, you need to index the `trip_id` field in order +to look up a trip's stop times quickly, which uses even more space. + +The following SQL statements show how you might represent GTFS without +optimizing the identifiers. For brevity, not all fields from the GTFS +feed are included here. + +```sql +CREATE TABLE trips ( + trip_id TEXT, + route_id TEXT, + service_id TEXT +); + +CREATE INDEX trips_trip_id ON trips (trip_id); + +CREATE INDEX trips_route_id ON trips (route_id); + +CREATE INDEX trips_service_id ON trips (service_id); + +CREATE TABLE stop_times ( + trip_id TEXT, + stop_id TEXT, + stop_sequence INTEGER +); + +CREATE INDEX stop_times_trip_id ON stop_times (trip_id); + +CREATE INDEX stop_times_stop_id on stop_times (stop_id); +``` + +If you were to add an integer column to `trips` called, say, +`trip_index`, then you can reference that value from `stop_times` +instead of `trip_id`. The following SQL statements show this. + +```sql +CREATE TABLE trips ( + trip_id TEXT, + trip_index INTEGER, + route_id TEXT, + service_id TEXT +); + +CREATE INDEX trips_trip_id ON trips (trip_id); +CREATE INDEX trips_trip_index ON trips (trip_index); +CREATE INDEX trips_route_id ON trips (route_id); +CREATE INDEX trips_service_id ON trips (service_id); + +CREATE TABLE stop_times ( + trip_index INTEGER, + stop_id TEXT, + stop_sequence INTEGER +); + +CREATE INDEX stop_times_trip_index ON stop_times (trip_index); + +CREATE INDEX stop_times_stop_id on stop_times (stop_id); +``` + +This results in a significant space saving (when you consider how large +`stop_times` can be), as well as being far quicker to look up stop +times based on a trip ID. Note that the original `trip_id` value is +retained so it can be referenced if required. + +Without adding `trip_index`, you would use the following query to find +stop times given a trip ID. + +```sql +SELECT * FROM stop_times + WHERE trip_id = 'SOME_LONG_TRIP_ID' + ORDER BY stop_sequence; +``` + +With the addition of `trip_index`, you need to first find the record +in `trips`. This can be achieved using the following query. This is a +small sacrifice compared to performing string comparison on all stop +times. + +```sql +SELECT * FROM stop_times + WHERE trip_index = ( + SELECT trip_index FROM trips WHERE trip_id = 'SOME_LONG_TRIP_ID' + ) + ORDER BY stop_sequence; +``` + +You can make the same change for the other IDs in the feed, such as +`route_id` and `stop_id`. For these columns you still keep (and +index) the original values in `routes` and `stops` respectively, +since you may still need to look up records based on these values. + +***Note:** Even though this book recommends optimizing feeds in this +manner, the remainder of examples in this book only use their original +IDs, in order to simplify the examples and to ensure compatibility with +the `GtfsToSql` tool introduced previously.* + diff --git a/.idea/gtfs-book/ch-15-optimizing-shapes.md b/.idea/gtfs-book/ch-15-optimizing-shapes.md new file mode 100644 index 0000000..fbb1125 --- /dev/null +++ b/.idea/gtfs-book/ch-15-optimizing-shapes.md @@ -0,0 +1,158 @@ +## 15. Optimizing Shapes + +Shape data in a GTFS feed (that is, the records from `shapes.txt`) +represents a large amount of data. There are a number of ways to reduce +this data, which can help to: + +* Speed up data retrieval +* Reduce the amount of data to transmit to app / web site users +* Speed up rendering of the shape onto a map (such as a native mobile + map or a JavaScript map). + +Two ways to reduce shape data are as follows: + +* **Reducing the number of points in a shape.** The shapes included in + GTFS are often very precise and include a number of redundant + points. Many of these can be removed without a noticeable loss of + shape quality using the *Douglas-Peucker Algorithm*. +* **Encoding all points in a shape into a single value.** The *Encoded + Polyline Algorithm* used in the Google Maps JavaScript API can also + be used with GTFS shapes. This reduces the amount of storage + required and also makes looking up all points in a shape far + quicker. + +### Reducing Points in a Shape + +Many of the shapes you find in GTFS feeds are extremely detailed. They +often follow the exact curvature of the road and may consist of hundreds +or thousands of points for a trip that might have only 30 or 40 stops. + +While this level of detail is useful, the sheer amount of data required +to be rendered on a map can be a massive performance hit from the +perspective of retrieving the data as well as rendering on a map. +Realistically, shapes do not need this much detail in order to convey +their message to your users. + +Consider the following shape from Portland that has been rendered using +Google Maps. The total shape consists of 1913 points. + +![Original Shape](images/shape-original.jpg) + +Compare this now to the same shape that has had redundant points +removed. The total number of points in this shape is 175, which +represents about a 90% reduction. + +![Reduced Shape](images/shape-reduced.jpg) + +If you look closely, you can see some minor loss of detail, but for the +most part, the shapes are almost identical. + +This reduction in points can be achieved using the Douglas-Peucker +Algorithm. It does so by discarding points that do not deviate +significantly between its surrounding points. + +The Douglas-Peucker Algorithm works as follows: + +* Begin with the first and last points in the path (A and B). These + are always kept. +* Find the point between the first and last that is furthest away from + the line joining the first and last line (the orthogonal distance -- + see the figure below). +* If this point is greater than the allowed distance (the tolerance + level), the point is kept (call it X). +* Repeat this algorithm twice: once using A as the first point and X + as the last point, then again using X as the first point and B as + the last point. + +This algorithm is recursive, and continues until all points have been +checked. + +***Note:** The tolerance level determines how aggressively points are +removed. A higher tolerance value is less aggressive and discards less +data, while a lower tolerance discards more data.* + +The following diagram shows what orthogonal distance means. + +![Orthogonal Distance](images/orthogonal-distance.jpg) + +The following resources provide more information about the +Douglas-Peucker Algorithm and describe how to implement it in your own +systems: + +* <http://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm> +* <http://www.loughrigg.org/rdp/> +* <http://stackoverflow.com/questions/2573997/reduce-number-of-points-in-line>. + +You can often discard about 80-90% of all shape data before seeing a +significant loss of line detail. + +### Encoding Shape Points + +A single entry in `shapes.txt` corresponds to a single point in a +single shape. Each entry includes a shape ID, a latitude and longitude. + +***Note:** The `shape_dist_traveled` field is also included, but you do not +strictly need to use this field (nor the corresponding field in +stop_times.txt). The technique described in this section will not work +if you intend to use `shape_dist_traveled`.* + +This means if you want to look up a shape by its ID, you may need to +retrieve several hundreds of rows from a database. Using the Encoded +Polyline Algorithm you can change your GTFS database so each shape is +represented by a single row in a database. This means the shape can be +found much more quickly and much less data needs to be processed to +determine the shape. + +Consider the following data, taken from TriMet's `shapes.txt` file. +This data represents the first five points of a shape. + +| `shape_id` | `shape_pt_lat` | `shape_pt_lon` | `shape_pt_sequence` | `shape_dist_traveled` | +| :--------- | :------------- | :------------- | :------------------- | :--------------------- | +| `185328` | `45.52291 ` | `-122.677372` | `1` | `0.0` | +| `185328` | `45.522921` | `-122.67737` | `2` | `3.7` | +| `185328` | `45.522991` | `-122.677432` | `3` | `34.0` | +| `185328` | `45.522992` | `-122.677246` | `4` | `81.5` | +| `185328` | `45.523002` | `-122.676567` | `5` | `255.7` | + +If you apply the Encoded Polyline Algorithm to this data, the +coordinates can be represented using the following string. + +``` +eeztGrlwkVAAML?e@AgC +``` + +To learn how to arrive at this value, you can read up on the Encoded +Polyline Algorithm at +<https://developers.google.com/maps/documentation/utilities/polylinealgorithm>. + +Instead of having every single shape point in a single table, you can +create a table that has one record per shape. The following SQL +statement is a way you could achieve this. + +```sql +CREATE TABLE shapes ( + shape_id TEXT, + encoded_shape TEXT +); +``` + +The following table shows how this data could be represented in a +database. + +| `shape_id` | `encoded_shape` | +| :--------- | :--------------------- | +| `185328` | `eeztGrlwkVAAML?e@AgC` | + +Storing the shape in this manner means you can retrieve an entire shape +by looking up only one database row and running it through your decoder. + +To further demonstrate how both the encoding and decoding works, try out +the polyline utility at +<https://developers.google.com/maps/documentation/utilities/polylineutility>. + +You can find implementations for encoding and decoding points for +various languages at the following locations: + +* <http://facstaff.unca.edu/mcmcclur/GoogleMaps/EncodePolyline/> +* <https://github.com/emcconville/google-map-polyline-encoding-tool> + diff --git a/.idea/gtfs-book/ch-16-deleting-unused-data.md b/.idea/gtfs-book/ch-16-deleting-unused-data.md new file mode 100644 index 0000000..f2a2fc0 --- /dev/null +++ b/.idea/gtfs-book/ch-16-deleting-unused-data.md @@ -0,0 +1,106 @@ +## 16. Deleting Unused Data** + +Once you have imported a GTFS feed into a database, it is possible for +there to be a lot of redundant data. This can ultimately slow down any +querying of that data as well as bloating the size of the database. If +you are making an app where the GTFS database is queried on the device +then disk space and computational time are at a premium, so you must do +what you can to reduce resource usage. + +The first thing to check for is expired services. You can do this by +searching `calendar.txt` for entries that expire before today's date. +Be aware though, you also need to ensure there are no +`calendar_dates.txt` entries overriding these services (a service +could have an `end_date` of, say, `20140110`, but also have a +`calendar_dates.txt` entry for `20140131`). + +Firstly, find service IDs in `calendar_dates.txt` that are still +active using the following query. + +```sql +SELECT service_id FROM calendar_dates WHERE date >= '20140110'; +``` + +***Note:** In order to improve performance of searching for dates, you +should import the date field in `calendar_dates.txt` as an integer, as +well as `start_date` and `end_date` in `calendar.txt`.* + +Any services matched in this query should not be removed. You can then +find service IDs in `calendar.txt` with the following SQL query. + +```sql +SELECT * FROM calendar WHERE end_date < '20140110' + AND service_id NOT IN ( + SELECT service_id FROM calendar_dates WHERE date >= '20140110' + ); +``` + +Before deleting these services, corresponding trips and stop times must +be removed since you need to know the service ID in order to delete a +trip. Likewise, stop times must be deleted before trips since you need +to know the trip IDs to be removed. + +```sql +DELETE FROM stop_times WHERE trip_id IN ( + SELECT trip_id FROM trips WHERE service_id IN ( + SELECT service_id FROM calendar WHERE end_date < '20140110' + AND service_id NOT IN ( + SELECT service_id FROM calendar_dates WHERE date >= '20140110' + ) + ) + ); +``` + +Now there may be a series of trips with no stop times. Rather than +repeating the above sub-queries, a more thorough way of removing trips +is to remove trips with no stop times. + +```sql +DELETE FROM trips WHERE trip_id NOT IN ( + SELECT DISTINCT trip_id FROM stop_times +); +``` + +With all `service_id` references removed, you can remove the expired +rows from `calendar.txt` using the following SQL query. + +```sql +DELETE FROM calendar WHERE end_date < '20140110' + AND service_id NOT IN ( + SELECT DISTINCT service_id FROM calendar_dates WHERE date >= '20140110' + ); +``` + +The expired rows in `calendar_dates.txt` can also be removed, which +can be achieved using the following query. + +```sql +DELETE FROM calendar_dates WHERE date < '20140110'; +``` + +There may now be some stops that are not used by any trips. These can be +removed using the following query. + +```sql +DELETE FROM stops WHERE stop_id NOT IN ( + SELECT DISTINCT stop_id FROM stop_times +); +``` + +Additionally, you can remove unused shapes and routes using the +following queries. + +```sql +DELETE FROM shapes WHERE shape_id NOT IN ( + SELECT DISTINCT shape_id FROM trips +); + +DELETE FROM routes WHERE route_id NOT IN ( + SELECT DISTINCT route_id FROM trips +); +``` + +There are other potential rows that can be removed (such as records in +`transfers.txt` that reference non-existent stops), but hopefully you +get the idea from the previous queries. + diff --git a/.idea/gtfs-book/ch-17-searching-for-trips.md b/.idea/gtfs-book/ch-17-searching-for-trips.md new file mode 100644 index 0000000..dd0a901 --- /dev/null +++ b/.idea/gtfs-book/ch-17-searching-for-trips.md @@ -0,0 +1,346 @@ +## 17. Searching for Trips** + +This section shows you how to search for trips in a GTFS feed based on +specified times and stops. + +The three scenarios covered are: + +* Finding all stops departing from a given stop after a certain time +* Finding all stops arriving at a given stop before a certain time +* Finding all trips between two stops after a given time + +The first and second scenarios are the simplest, because they only rely +on a single end of each trip. The third scenario is more complex because +you have to ensure that each trip returned visits both the start and +finish stops. + +When searching for trips, the first thing you need to know is which +services are running for the given search time. + +### Finding Service IDs + +The first step in searching for trips is to determine which services are +running. To begin with, you need to find service IDs for a given date. +You then need to handle exceptions accordingly. That is, you need to add +service IDs and remove service IDs based on the rules in +`calendar_dates.txt`. + +***Note:** In *Scheduling Past Midnight*, you were shown how +GTFS works with times past midnight. The key takeaway from this is that +you have to search for trips for two sets of service IDs. This is +covered as this chapter progresses.* + +In Australia, 27 January 2014 (Monday) was the holiday for Australia +Day. This is used as an example to demonstrate how to retrieve service +IDs. + +Firstly, you need the main set of service IDs for the day. The following +SQL query achieves this. + +```sql +SELECT service_id FROM calendar + WHERE start_date <= '20140127' AND end_date >= '20140127' + AND monday = 1; + +# Result: 1, 2, 6, 9, 18, 871, 7501 +``` + +Next you need to find service IDs that are to be excluded, as achieved +by the following SQL query. + +```sql +SELECT service_id FROM calendar_dates + WHERE date = '20140127' AND exception_type = 2; + +# Result: 1, 2, 6, 9, 18, 871, 7501 +``` + +Finally, you need to find service IDs that are to be added. This query +is identical to the previous query, except for the different +`exception_type` value. + +```sql +SELECT service_id FROM calendar_dates + WHERE date = '20140127' AND exception_type = 1; + +# Result: 12, 874, 4303, 7003 +``` + +You can combine these three queries all into a single query in SQLite +using `EXCEPT` and `UNION`, as shown in the following SQL query. + +```sql +SELECT service_id FROM calendar + WHERE start_date <= '20140127' AND end_date >= '20140127' + AND monday = 1 + +UNION + +SELECT service_id FROM calendar_dates + WHERE date = '20140127' AND exception_type = 1 + +EXCEPT + +SELECT service_id FROM calendar_dates + WHERE date = '20140127' AND exception_type = 2; + +# Result: 12, 874, 4304, 7003 +``` + +Now when you search for trips, only trips that have a matching +`service_id` value are included. + +### Finding Trips Departing a Given Stop + +In order to determine the list of services above, a base timestamp on +which to search is needed. For the purposes of this example, assume that +timestamp is 27 January 2014 at 1 PM (`13:00:00` when using GTFS). + +This example searches for all services departing from Adelaide Railway +Station, which has stop ID `6665` in the Adelaide Metro GTFS feed. To +find all matching stop times, the following query can be performed. + +```sql +SELECT * FROM stop_times + WHERE stop_id = '6665' + AND departure_time >= '13:00:00' + AND pickup_type = 0 + ORDER BY departure_time; +``` + +This returns a series of stop times that match the given criteria. The +only problem is it does not yet take into account valid service IDs. + +***Note:** This query may also return stop times that are the final stop +on a trip, which is not useful for somebody trying to find departures. +You may want to modify your database importer to override the final stop +time of each trip so its `pickup_type` has a value of `1` (no pick-up) and +its first stop time so it has a `drop_off_type` of `1` (no drop-off).* + +To make sure only the correct trips are returned, join `stop_times` +with `trips` using `trip_id`, and then include the list of service +IDs. For the purposes of this example the service IDs, stop ID and +departure time are being hard-coded. You can either embed a sub-query, +or include the service IDs via code. + +```sql +SELECT t.*, st.* FROM stop_times st, trips t + WHERE st.stop_id = '6665' + AND st.trip_id = t.trip_id + AND t.service_id IN ('12', '874', '4304', '7003') + AND st.departure_time >= '13:00:00' + AND st.pickup_type = 0 + ORDER BY st.departure_time; +``` + +This gives you the final list of stop times matching the desired +criteria. You can then decide specifically which data you need to +retrieve; you now have the `trip_id`, meaning you can find all stop +times for a given trip if required. + +If you need to restrict the results to only those that occur after the +starting stop, you can retrieve stop times with only a `stop_sequence` +larger than that of the stop time returned in the above query. + +### Finding Trips Arriving at a Given Stop + +In order to find the trips arriving at a given stop before a specified +time, it is just a matter of making slight modifications to the above +query. Firstly, check the `arrival_time` instead of +`departure_time`. Also, check the `drop_off_type` value instead of +`pickup_type`. + +```sql +SELECT t.*, st.* FROM stop_times st, trips t + WHERE st.stop_id = '6665' + AND st.trip_id = t.trip_id + AND t.service_id IN ('12', '874', '4304', '7003') + AND st.arrival_time <= '13:00:00' + AND st.drop_off_type = 0 + ORDER BY st.arrival_time DESC; +``` + +For this particular data set, there are trips from four different routes +returned. If you want to restrict this to a particular route, you can +filter on the `t.route_id` value. + +```sql +SELECT t.*, st.* FROM stop_times st, trips t + WHERE st.stop_id = '6665' + AND st.trip_id = t.trip_id + AND t.service_id IN ('12', '874', '4304', '7003') + AND t.route_id = 'BEL' + AND st.arrival_time <= '13:00:00' + AND st.drop_off_type = 0 + ORDER BY st.arrival_time DESC; +``` + +### Performance of Searching Text-Based Times + +These examples search using text-based arrival/departure times (such as +`13:00:00`). This works because the GTFS specification mandates that +all times are `HH:MM:SS` format (although `H:MM:SS` is allowed for times +earlier than 10 AM). + +Doing this kind of comparison (especially if you are scanning millions +of rows) is quite slow and expensive. It is more efficient to convert +all times stored in the database to integers that represent the number +of seconds since midnight. + +***Note:** The GTFS specification states that arrival and departure times +are "noon minus 12 hours" in order to account for daylight savings time. +This is effectively midnight, except for the days that daylight savings +starts or finishes.* + +In order to achieve this, you can convert the text-based time to an +integer with `H * 3600 + M * 60 + S`. For example, `13:35:21` can +be converted using the following steps. + +``` + (13 * 3600) + (35 * 60) + (21) += 46800 + 2100 + 21 += 48921 +``` + +You can then convert back to hours, minutes and seconds in order to +generate timestamps in your application as shown in the following +algorithm. + +``` +H = floor( 48921 / 3600 ) + = floor( 13.59 ) + = 13 + +M = floor( 48921 / 60 ) % 60 + = floor( 815.35 ) % 60 + = 815 % 60 + = 35 + +S = 48921 % 60 + = 21 +``` + +### Finding Trips Between Two Stops + +Now that you know how to look up a trip from or to a given stop, the +previous query can be expanded so both the start and finish stop are +specified. The following example finds trips that depart after 1 PM. The +search only returns trips departing from *Adelaide Railway Station* +(stop ID `6665`) are shown. Additionally, only trips that then visit +*Blackwood Railway Station* (stop IDs `6670` and `101484`) are +included. + +In order to achieve this, the following changes must be made to the +previous examples. + +* **Join against stop_times twice.** Once for the departure stop time + and once for the arrival stop time. +* **Allow for multiple stop IDs at one end.** The destination in this + example has two platforms, so you need to check both of them. +* **Ensure the departure time is earlier than the arrival time. + **Otherwise trips heading in the opposite direction may also be + returned. + +The following query demonstrates how this is achieved. + +```sql +SELECT t.*, st1.*, st2.* + FROM trips t, stop_times st1, stop_times st2 + WHERE st1.trip_id = t.trip_id + AND st2.trip_id = t.trip_id + AND st1.stop_id = '6665' + AND st2.stop_id IN ('6670', '101484') + AND t.service_id IN ('12', '874', '4304', '7003') + AND st1.departure_time >= '13:00:00' + AND st1.pickup_type = 0 + AND st2.drop_off_type = 0 + AND st1.departure_time < st2.arrival_time + ORDER BY st1.departure_time; +``` + +In this example, the table alias `st1` is used for the departure stop +time. Once again, the stop ID must match, as well as the departure time +and pick-up type. + +For the arrival stop time the alias `st2` is used. This table also +joins the `trips` table using `trip_id`. Since the destination has +multiple stop IDs, the SQL `IN` construct is used. The arrival time is +not important in this example, so only the departure is checked. + +The final thing to check is that the departure occurs before the +arrival. If you do not perform this step, then trips traveling in the +opposite direction may also be returned. + +***Note:** Technically, there may be multiple results returned for the +same trip. For some transit agencies, a single trip may visit the a stop +more than once. If this is the case, you should also check the trip +duration (arrival time minus departure time) and use the shortest trip +when the same trip is returned multiple times.* + +### Accounting for Midnight + +As discussed previously, GTFS has the ability for the trips to depart or +arrive after midnight for a given service day without having to specify +it as part of the next service day. Consequently, while the queries +above are correct, they do not necessarily paint the full picture. + +In reality, when performing a trip search, you need to take into account +trips that have wrapped times (for instance, where 12:30 AM is specified +as `24:30:00`). If you want to find trips that depart after 12:30 AM +on a given day, you need to check for trips departing after +`00:30:00` on that day, as well as for trips departing at +`24:30:00` on the previous day. + +This means that for each trip search you are left with two sets of +trips, which you must then merge and present as appropriate. + +***Note:** In reality, agencies generally do not have trips that overlap +from multiple service days, so technically you often only need one query +(for example, a train service might end on 12:30 AM then restart on the +next service day at 4:30 AM). If your app / web site only uses a single +feed where you can tune your queries manually based on how the agency +operates, then you can get away with only querying a single service day. +On the other hand, if you are building a scalable system that works with +data from many agencies, then you need to check both days.* + +To demonstrate how this works in practice, the following example +searches for all trips that depart after 12:30:00 AM on 14 March 2014. +The examples earlier in this chapter showed how to find the service IDs +for a given date. To account for midnight, service IDs for both March 14 +(the "main" service date) and March 13 (the overlapping date) need to be +determined. + +Assume that March 13 has a service ID of `C1` and March 14 has a +service ID of `C2`. First you need to find the departures for March +14, as shown in the following query. + +```sql +SELECT t.*, st.* FROM stop_times st, trips t + WHERE st.stop_id = 'S1' + AND st.trip_id = t.trip_id + AND t.service_id IN ('C1') + AND st.departure_time >= '00:30:00' + AND st.pickup_type = 0 + ORDER BY st.departure_time; +``` + +The resultant list needs to be combined with the trips that depart after +midnight from the March 13 service. To check this, it is just a matter +of swapping in the right service IDs, then adding 24 hours to the search +time. + +```sql +SELECT t.*, st.* FROM stop_times st, trips t + WHERE st.stop_id = 'S1' + AND st.trip_id = t.trip_id + AND t.service_id IN ('C2') + AND st.departure_time >= '24:30:00' + AND st.pickup_type = 0 + ORDER BY st.departure_time; +``` + +In order to get your final list of trips you must combine the results +from both of these queries. If you are generating complete timestamps in +order to present the options to your users, just remember to account for +the results from the second query being 24 hours later. + diff --git a/.idea/gtfs-book/ch-18-working-with-trip-blocks.md b/.idea/gtfs-book/ch-18-working-with-trip-blocks.md new file mode 100644 index 0000000..d59b2c4 --- /dev/null +++ b/.idea/gtfs-book/ch-18-working-with-trip-blocks.md @@ -0,0 +1,109 @@ +## 18. Working With Trip Blocks** + +One of the more complex aspects of GTFS is how to properly use the +`block_id` field in `trips.txt`. The concept is simple, but +incorporating this information in a manner that is simple to understand +for a passenger can be more difficult. + +A single vehicle (such as a bus or train) completes multiple trips in a +single service day. For instance, once a bus completes its trip (Trip X) +from Location A to Location B, it then begins another trip (Trip Y) from +Location B to Location C. It then completes a final trip (Trip Z) from +Location C back to Location A. + +If a passenger boards in the middle of Trip X and is allowed to stay on +the bus until the end of the Trip Y, then this should be represented in +GTFS by giving Trips X, Y, Z the same `block_id` value. + +The following diagram shows a vehicle that performs two trips, +completing opposite directions of the same route. Often a single vehicle +will do many more than just two trips in a day. + +![Block](images/block-01.png) + +***Note:** When trips in a block are represented in stop_times.txt, the +final stop of a trip and the first stop of the subsequent trip must both +be included, even though they are typically at the same stop (and often +the same arrival & departure time). The final stop of the first trip is +drop-off only, while the first stop of the next trip is pick-up only.* + +In the following diagram, the vehicle completes three trips, each for +separate routes. As in the previous diagram, the vehicle will likely +complete more trips over a single day. + +![Block](images/block-02.png) + +These two diagrams show the difficulties that can arise when trying to +calculate trips across multiple blocks. The first diagram is repeated +below, this time with stops marked so trips between two locations can be +found. + +![Block](images/block-03.png) + +Consider the scenario where you want to retrieve all trip times from +Stop S2 to Stop S1. Since the vehicle gets to Stop S3 then turns around +and heads back to S1, the trip search returns two options: + +1. Board at S2 on the upper trip, travel via S3, then get to S1. +2. Board at S2 on the lower trip, travel directly to S1. + +The second option is a subset of the first option, which means it is a +far shorter trip (plus it avoids the passenger getting annoyed from +passing their starting point again). To determine which option to use, +you can use the trip duration. + +Now consider the other type of block formation, where a vehicle +completes subsequent trips from different routes. + +![Block](images/block-04.png) + +If a passenger wants to travel from Stop S2 to S4, you will not get the +situation where the vehicle travels past the same location twice. +However, it is also possible for a passenger to travel from Stop S2 to +Stop S6 without ever having to change vehicles. + +Referring back to the previous chapter about performing trip searches, +it is relatively straightforward to account for blocks in these +searches. Using the query to find trips between two stops as a +reference, the following changes need to be made: + +* Instead of joining both occurrences of `stop_times` to the + `trips` table, you now need two occurrences of `trips`. +* Each `stop_times` table occurrence is joined to a separate + `trips` table occurrence. +* The two occurrences of `trips` are joined on the `block_id` + value (if it is available). + +The following query shows how to find trips that start at stop `S2` +and finish at stop `S4`, departing after 1 PM on the day with service +ID `C1`. + +```sql +SELECT t1.*, t2.*, st1.*, st2.* + FROM trips t1, trips t2, stop_times st1, stop_times st2 + WHERE st1.trip_id = t1.trip_id + AND st2.trip_id = t2.trip_id + AND st1.stop_id = 'S2' + AND st2.stop_id = 'S4' + AND t1.service_id = 'C1' + AND ( + t1.trip_id = t2.trip_id + OR ( + LENGTH(t1.block_id) > 0 AND t1.block_id = t2.block_id + ) + ) + AND st1.departure_time >= '13:00:00' + AND st1.pickup_type = 0 + AND st2.drop_off_type = 0 + AND st1.departure_time < st2.arrival_time + ORDER BY st1.departure_time; +``` + +Since `block_id` may be unpopulated, the query joins on both the +`trip_id` and the `block_id`. + +***Note:** An alternative is to guarantee every single trip has a +`block_id` value when importing -- even if some blocks only consist of +a single trip. If you can guarantee this condition, then you can +simplify this query by using `t1.block_id = t2.block_id`.* + diff --git a/.idea/gtfs-book/ch-19-calculating-fares.md b/.idea/gtfs-book/ch-19-calculating-fares.md new file mode 100644 index 0000000..df24dd4 --- /dev/null +++ b/.idea/gtfs-book/ch-19-calculating-fares.md @@ -0,0 +1,296 @@ +## 19. Calculating Fares** + +In order to calculate a fare for a trip in GTFS, you must use data from +`fare_attributes.txt` and `fare_rules.txt`. It is relatively +straightforward to calculate the cost of a single trip (that is, +boarding a vehicle, traveling for a number of stops, then disembarking), +but it becomes much more complicated when you need to take into account +multiple trip segments (that is, one or more transfers). + +***Note:** As it stands, many feed providers do not include fare +information. This is because many systems have a unique set of rules +that cannot be modelled with the current structure of fares in GTFS. +Additionally, it is not possible to determine different classes of +pricing (such as having a separate price for adults and children). For +the purposes of this chapter, these limitations are ignored.* + +For more discussion on how fares work in GTFS, refer to *Fare +Definitions (`fare_attributes.txt` & `fare_rules.txt`)*. + +This chapter first shows you how to calculate fares for a single trip, +then how to account for transfers and for multiple trips. + +### Calculating a Single Trip Fare + +Much of the logic used when calculating fares requires knowledge of the +zones used in a trip. + +***Note:** A zone is a physical area within a transit system that +contains a series of stops. They are used to group trip pricing into key +areas in a system. Some transit systems do not work like this (for +instance, they may measure the physical distance travelled rather than +specific stops), which is one reason why the GTFS fares model does not +work in all cases.* + +A zone is defined in GTFS by the `zone_id` column in `stops.txt`. A +single stop can only belong to one zone. + +Fares are matched to a trip using a combination of any of the following: + +* The route of the trip +* The zone of the stop the passenger boards from +* The zone of the stop the passenger disembarks +* The zone(s) of any stops on the trip that are passed while the + passenger is on board. + +Consider the following simplified data set that may appear in +`stops.txt` and `stop_times.txt`. Assume for this example that the +trip `T1` belongs to a route with an ID of `R1`. + +``` +stop_id,zone_id +S1,Z1 +S2,Z1 +S3,Z2 +S4,Z3 + +trip_id,stop_id,stop_sequence +T1,S1,1 +T1,S2,2 +T1,S3,3 +T1,S4,4 +``` + +If a passenger travels from stop S1 to stop S4, then their starting zone +is Z1, their finishing zone is Z3, and the zones they pass through are +Z1, Z2 and Z3. + +***Note:** When calculating fares, the start and finish zones are also +included in the zones passed through, so in this example you Z3 is also +considered as a zone that the trip passes through.* + +Using this data, you can now calculate matching fares. To do so, you +need to find all fares that match either of the following: + +* Fares that have no associated rules. +* Fares that have rules that match the specified trip. If a fare + defines multiple zones that must be passed through (using + `contains_id`), then all zones must be matched. + +If multiple fares qualify for a trip, then the cheapest fare is the one +to use. + +### Finding Fares With No Rules + +This is the simplest use-case for fares. You can find all matching fares +with the following SQL. + +```sql +SELECT * FROM fare_attributes WHERE fare_id NOT IN ( + SELECT DISTINCT fare_id FROM fare_rules +); +``` + +If a feed only has `fare_attributes.txt` records with no rules, then +the difference between the fares is in the transfer rules. This section +only covers calculating fares for a single trip with no transfers, so +for now you can just select the cheapest fare using the following SQL. + +```sql +SELECT * FROM fare_attributes WHERE fare_id NOT IN ( + SELECT DISTINCT fare_id FROM fare_rules + ) + ORDER BY price + 0 LIMIT 1; +``` + +***Note:** You still need to check for fares with one or more rules in +order to find the cheapest price. Also, `0` is added in this query in +order to cast a string to a number. When you roll your own importer you +should instead import this as a numerical value.* + +### Finding Fares With Matched Rules + +Next you must check against specific rules for a fare. In order to do +this, you need the starting zone, finishing zone, and all zones passed +through (including the start and finish zones). + +Referring back to the previous example, if a trip starts at `Z1`, +passes through `Z2` and finishes at `Z3`, you can find fare +candidates (that is, trips that *may* match), using the following SQL +query. + +```sql +SELECT * FROM fare_attributes WHERE fare_id IN ( + SELECT fare_id FROM fare_rules + WHERE (LENGTH(route_id) = 0 OR route_id = 'R1') + AND (LENGTH(origin_id) = 0 OR origin_id = 'Z1') + AND (LENGTH(destination_id) = 0 OR destination_id = 'Z3') + AND (LENGTH(contains_id) = 0 OR contains_id IN ('Z1', 'Z2', 'Z3') + ) + ); +``` + +This returns a list of fares that may qualify for the given trip. As +some fares have multiple rules, all must be checked. The algorithm to +represent this is as follows. + +``` +fares = [ result from above query ] + +qualifyingFares = [ ] + +for (fare in fares) { + if (qualifies(fare)) + qualifyingFares.add(fare) +} + +allFares = qualifyFares + faresWithNoRules + +passengerFare = cheapest(allFares) +``` + +As shown on the final two lines, once you have the list of qualifying +fares, you can combine these with fares that have no rules (from the +previous section) and then determine the cheapest fare. + +First though, you must determine if a fare with rules qualifies for the +given trip. If a fare specifies zones that must be passed through, then +all rules must be matched. + +***Note:** If a particular rule specifies a different route, start, or +finish than the one you are checking, you do not need to ensure the +`contains_id` matches, since this rule no longer applies. You still need +to check the other rules for this fare.* + +The algorithm needs to build up a list of zone IDs from the fare rules +in order to check against the trip. Once this has been done, you need to +check that every zone ID collected from the rules is contained in the +trip's list of zones. + +``` +qualifies(fare, routeId, originId, destinationId, containsIds) { + + fareContains = [ ] + + for (rule in fare.rules) { + if (rule.contains.length == 0) + continue + + if (rule.route.length > 0 AND rule.route != routeId) + continue + + if (rule.origin.length > 0 AND rule.origin != originId) + continue + + if (rule.desination.length > 0 AND rule.destination != destinationId) + continue + + fareContains.add(rule.containsId); + } + + if (fareContains.size == 0) + return YES + + if (containIds HAS EVERY ELEMENT IN fareContains) + return YES + else + return NO +} +``` + +This algorithm achieves the following: + +* Only rules that have a value for `contains_id` are relevant. Rules + that do not have this value fall through and should be considered as + qualified. +* If the route is specified but not equal to the one being checked, it + is safe to ignore the rule's `contains_id`. If the route is empty + or equal, the loop iteration can continue. +* Check for the `origin_id` and `destination_id` in the same + manner as `route_id`. +* If the route, origin and destination all qualify then store the + `contains_id` so it can be checked after the loop. + +The algorithm returns *yes* if the fare qualifies, meaning you can save +it as a qualifying fare. You can then return the cheapest qualifying +fare to the user. + +### Calculating Trips With Transfers + +Once you introduce transfers, fare calculation becomes more complicated. +A "trip with a transfer" is considered to be a trip where the passenger +boards a vehicle, disembarks, and then gets on another vehicle. For +example: + +* Travel on trip T1 from Stop S1 to Stop S2 +* Walk from Stop S2 to Stop S3 +* Travel on trip T2 from Stop S3 to Stop S4. + +In order to calculate the total fare for a trip with transfers, the +following algorithm is used: + +1. Retrieve list of qualifying fares for each trip individually +2. Create a list of every fare combination possible +3. Loop over all combinations and find the total cost +4. Return the lowest cost from Step 3. + +Step 1 was covered in *Calculating a Single Trip Fare*, but +you must skip the final step of finding the cheapest fare. This is +because the cheapest fare may change depending on subsequent transfers. +Instead, this step is performed once the cheapest *combination* is +determined. + +To demonstrate Step 2, consider the following example: + +* The trip on T1 from S1 to S2 yields the following qualifying fares: + F1, F2. +* The subsequent trip on T2 from S3 to S4 yields the following + qualifying fares: F3, F4. + +Generating every combination of these fares yields the following +possibilities: + +* F1 + F3 +* F1 + F4 +* F2 + F3 +* F2 + F4. + +Step 3 can now be performed, which involves finding the total cost for +each combinations. As you need to take into account the possibility of +timed transfers (according to the data stored in +`fare_attributes.txt`), you also need to know about the times of these +trips. + +The following algorithm can be used to calculate the total cost using +transfer rules. In this example, you would call this function once for +each fare combination. + +``` +function totalCost(fares) { + total = 0 + + for (fare in fares) { + freeTransfer = NO + + if (previousFare ALLOWS TRANSFERS) { + if (HAS ENOUGH TRANSFERS REMAINING) { + if (TRANSFER NOT EXPIRED) { + freeTransfer = YES + } + } + } + + if (!freeTransfer) + total = total + fare.price; + + previousFare = fare; + } + + return total; +} +``` + +Once all combinations have called the `totalCost` algorithm, you will +have a price for each trip. You can then return the lowest price as the +final price for the trip. + diff --git a/.idea/gtfs-book/ch-20-trip-patterns.md b/.idea/gtfs-book/ch-20-trip-patterns.md new file mode 100644 index 0000000..4b0cf73 --- /dev/null +++ b/.idea/gtfs-book/ch-20-trip-patterns.md @@ -0,0 +1,141 @@ +## 20. Trip Patterns** + +In a GTFS feed, a route typically has multiple trips that start and +finish at the same stops. If you are looking to reduce the size of the +data stored, then converting data from `stop_times.txt` into a series +of reusable patterns is an excellent way to do so. + +For two trips to share a common pattern, the following must hold true: + +* The stops visited and the order in which they are visited must be the same +* The time differences between each stop must be the same. + +The following table shows some fictional trips to demonstrate this. + +| **Stop** | **Trip 1** | **Trip 2** | **Trip 3** | +| :------- | :--------- | :--------- | :--------- | +| S1 | 10:00:00 | 10:10:00 | 10:20:00 | +| S2 | 10:02:00 | 10:13:00 | 10:22:00 | +| S3 | 10:05:00 | 10:15:00 | 10:25:00 | +| S4 | 10:06:00 | 10:18:00 | 10:26:00 | +| S5 | 10:10:00 | 10:21:00 | 10:30:00 | + +In a GTFS feed, this would correspond to 15 records in +`stop_times.txt`. If you look more closely though, you can see the +trips are very similar. The following table shows the differences +between each stop time, instead of the actual time. + +| **Stop** | **Trip 1** | **Trip 2** | **Trip 3** | +| :------- | :-------------- | :-------------- | :-------------- | +| S1 | 00:00:00 | 00:00:00 | 00:00:00 | +| S2 | 00:02:00 (+2m) | 00:03:00 (+3m) | 00:02:00 (+2m) | +| S3 | 00:05:00 (+5m) | 00:05:00 (+5m) | 00:05:00 (+5m) | +| S4 | 00:06:00 (+6m) | 00:08:00 (+8m) | 00:06:00 (+6m) | +| S5 | 00:10:00 (+10m) | 00:11:00 (+11m) | 00:10:00 (+10m) | + +You can see from this table that the first and third trip, although they +start at different times, have the same offsets between stops (as well +as stopping at identical stops). + +Instead of using a table to store stop times, you can store patterns. By +storing the ID of the pattern with each trip, you can reduce the list of +stop times in this example from 15 to 10. As only time offsets are +stored for each patterns, the trip starting time also needs to be saved +with each trip. + +You could use SQL such as the following to model this. + +```sql +CREATE TABLE trips ( + trip_id TEXT, + pattern_id INTEGER, + start_time TEXT, + start_time_secs INTEGER +); + +CREATE TABLE patterns ( + pattern_id INTEGER, + stop_id TEXT, + time_offset INTEGER, + stop_sequence INTEGER +); +``` + +The data you would store for trips in this example is shown in the +following table. + +| `trip_id` | `pattern_id` | `start_time` | `start_time_secs` | +| :-------- | :----------- | :----------- | :---------------- | +| T1 | 1 | 10:00:00 | 36000 | +| T2 | 2 | 10:10:00 | 36600 | +| T3 | 1 | 10:20:00 | 37200 | + +***Note:** The above table includes start_time_secs, which is an integer +value representing the number of seconds since the day started. Using +the hour, minutes and seconds in start_time, this value is `H * 3600 + M * 60 + S`.* + +In the `patterns` table, you would store data as in the following +table. + +| `pattern_id` | `stop_id` | `time_offset` | `stop_sequence` | +| :----------- | :-------- | :------------ | :-------------- | +| 1 | S1 | 0 | 1 | +| 1 | S2 | 120 | 2 | +| 1 | S3 | 300 | 3 | +| 1 | S4 | 360 | 4 | +| 1 | S5 | 600 | 5 | +| 2 | S1 | 0 | 1 | +| 2 | S2 | 180 | 2 | +| 2 | S4 | 300 | 3 | +| 2 | S5 | 480 | 4 | +| 2 | S6 | 660 | 5 | + +As you can see, this represents an easy way to significantly reduce the +amount of data stored. You could have tens or hundreds of trips each +sharing the same pattern. When you scale this to the entire feed, this +could reduce, say, 3 million records to about 200,000. + +***Note:** This is a somewhat simplified example, as there is other data +available in `stop_times.txt` (such as separate arrival/departure times, +drop-off type and pick-up type). You should take all of this data into +account when determining how to allocate patterns.* + +### Updating Trip Searches + +Changing your model to reuse patterns instead of storing every stop time +means your data lookup routines must also be changed. + +For example, to find all stop times for a given trip, you must now find +the pattern using the following SQL query. + +```sql +SELECT * FROM patterns + WHERE pattern_id = (SELECT pattern_id FROM trips WHERE trip_id = 'YOUR_TRIP_ID') + ORDER BY stop_sequence; +``` + +If you want to determine the arrival/departure time, you must add the +offset stored for the pattern record to the starting time stored with +the trip. This involves joining the tables and adding `time_offset` to +`start_time_secs`, as shown in the following query. + +```sql +SELECT t.start_time_secs + p.time_offset, p.stop_id + FROM patterns p, trips t + WHERE p.pattern_id = t.pattern_id + AND t.trip_id = 'YOUR_TRIP_ID' + ORDER BY p.stop_sequence; +``` + +### Other Data Reduction Methods + +There are other ways you can reduce the amount of data, such as only +using patterns to store the stops (and not timing offsets), and then +storing the timings with each trip record. A technique such as this +further reduces the size of the database, but the trade-off is that +querying the data becomes slightly more complex. + +Hopefully you can see that by using the method described in this chapter +there are a number of ways to be creative with GTFS data, and that you +must make decisions when it comes to speed, size, and ease of querying +data. diff --git a/.idea/gtfs-book/ch-21-conclusion.md b/.idea/gtfs-book/ch-21-conclusion.md new file mode 100644 index 0000000..0bdc2e0 --- /dev/null +++ b/.idea/gtfs-book/ch-21-conclusion.md @@ -0,0 +1,23 @@ +## Conclusion + +Thanks for reading *The Definitive Guide to GTFS*. While there are many +techniques that may take some time to comprehend in this book, the +content should bring you up to speed with GTFS quickly. + +At first glance, GTFS appears to be very simple, but there are a number +of "gotchas" which are not immediately apparent until you have spent +significant time working with a range of feeds. + +The key takeaways from this book are: + +* How GTFS feeds are structured +* How to import a GTFS feed to SQL and perform queries +* How to search for trips, handle blocks and calculate fares +* How to optimize an SQL database to minimize resource usage on mobile + devices. + +If you have enjoyed this book, please share it or feel free to contribution improvements or changes as necessary. + +*Quentin Zervaas* +February, 2014 + diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..639900d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectRootManager"> + <output url="file://$PROJECT_DIR$/out" /> + </component> +</project>
\ No newline at end of file @@ -1 +1,11 @@ -# GTFS-books
\ No newline at end of file +# GTFS-books + +This repository contains the source for the following two books: + +* The Definitive Guide to GTFS (2014) +* The Definitive Guide to GTFS-realtime (2015) + +These were originally published commercially, but have now been donated to OpenMobilityData. + +Some of the content may be outdated. You are encouraged to contribute any fixes or changes in order to keep it as up-to-date as possible. + |