Skip to content

Commit ce236ef

Browse files
Merge pull request #122 from NatLabRockies/sa/integrate-gtfs-flex
Sa/integrate gtfs flex
2 parents 819b717 + 7d3f09a commit ce236ef

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1582
-68
lines changed

rust/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ members = [
55
"bambam-core",
66
"bambam-gbfs",
77
"bambam-gtfs",
8+
"bambam-gtfs-flex",
89
"bambam-omf",
910
"bambam-osm",
1011
"bambam-py",

rust/bambam-core/src/util/geo_utils.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use geo::Centroid;
1+
use geo::{Centroid, MapCoords};
22
use geo::{Geometry, Point};
33
use rstar::RTreeObject;
44
use rstar::AABB;
@@ -30,3 +30,25 @@ pub fn get_centroid_as_envelope(geometry: &Geometry<f32>) -> Option<AABB<Point<f
3030
Geometry::Triangle(g) => Some(AABB::from_point(g.centroid())),
3131
}
3232
}
33+
34+
/// attempt to convert a geometry from 64 bit to 32 bit floating point representation.
35+
/// this operation is destructive but warranted when working with lat/lon values and
36+
/// scaling RAM consumption for national runs.
37+
pub fn try_convert_f32(g: &Geometry<f64>) -> Result<Geometry<f32>, String> {
38+
let (min, max) = (f32::MIN as f64, f32::MAX as f64);
39+
g.try_map_coords(|geo::Coord { x, y }| {
40+
if x < min || max < x {
41+
Err(format!("could not express x value '{x}' as f32, exceeds range of possible values [{min}, {max}]"))
42+
} else if y < min || max < y {
43+
Err(format!("could not express y value '{y}' as f32, exceeds range of possible values [{min}, {max}]"))
44+
} else {
45+
let x32 = std::panic::catch_unwind(|| x as f32).map_err(|_| {
46+
format!("could not express x value '{x}' as f32")
47+
})?;
48+
let y32 = std::panic::catch_unwind(|| y as f32).map_err(|_| {
49+
format!("could not express y value '{x}' as f32")
50+
})?;
51+
Ok(geo::Coord { x: x32, y: y32 })
52+
}
53+
})
54+
}

rust/bambam-gtfs-flex/Cargo.toml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
[package]
2+
name = "bambam-gtfs-flex"
3+
version = "0.2.4"
4+
edition = "2021"
5+
license = "BSD-3-Clause"
6+
description = "Simple GTFS-Flex scanner"
7+
repository = "https://github.com/NREL/bambam"
8+
readme = "README.md"
9+
10+
[features]
11+
default = ["type4"]
12+
type4 = ["dep:gtfs-structures"]
13+
14+
[dependencies]
15+
bambam-core = { version = "0.2.3", path = "../bambam-core" }
16+
chrono = { workspace = true }
17+
clap = { workspace = true }
18+
csv = { workspace = true }
19+
geo = { workspace = true }
20+
geo-types = { workspace = true }
21+
geozero = { workspace = true }
22+
gtfs-structures = { workspace = true, optional = true }
23+
kdam = { workspace = true }
24+
ordered-float = { workspace = true }
25+
routee-compass-core = { workspace = true }
26+
serde = { workspace = true }
27+
serde_json = { workspace = true }
28+
skiplist = { workspace = true }
29+
thiserror = { workspace = true }
30+
uom = { workspace = true }
31+
zip = { workspace = true }
32+
# No external dependencies needed for the simple version

rust/bambam-gtfs-flex/README.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# bambam-gtfs-flex
2+
A set of extensions that enable On-Demand Transit modeling in BAMBAM using [GTFS-Flex](https://gtfs.org/community/extensions/flex/) datasets.
3+
4+
## GTFS Flex Service Types
5+
6+
We have observed four types of services:
7+
8+
1. **Within a Single Zone**
9+
Pickups and drop-offs at any two points within the same zone.
10+
11+
2. **Across Multiple Zones**
12+
Trips are allowed between any points in different zones (no within-zone trips).
13+
14+
3. **With Specified Stops**
15+
Similar to services (1) and (2), but pickups and drop-offs are allowed only at designated stops.
16+
17+
4. **With Deviated Route**
18+
Vehicles follow a fixed route but can deviate to pick up or drop off between stops.
19+
20+
## Model Design
21+
22+
To track the state of a GTFS Flex trip, we store a few additional fields on the trip state. These fields and their implications in service types 1-3 are described below. Note: service type 4 is implemented via network dataset modifications and not in the search algorithm state.
23+
24+
### Service Type 1: Within a Single Zone
25+
26+
In this service type, trips are assigned a `src_zone_id` when they board. The trip may travel anywhere but may only treat locations within this zone as destinations. This requires a lookup table from `EdgeId -> ZoneId`.
27+
28+
### Service Type 2: Across Multiple Zones
29+
30+
In this service type, trips are assigned a `src_zone_id` and `departure_time` when they board. The trip may travel anywhere but may only treat particular locations as destinations. This requires the above `EdgeId -> ZoneId` lookup as well as a `(ZoneId, DepartureTime) -> [ZoneId]` lookup.
31+
32+
### Service Type 3: With Specified Stops
33+
34+
In this service type, trips are assigned a `src_zone_id` and `departure_time` when they board. Using the same lookups as (2), we additionally require an `EdgeId -> bool` mask function which further restricts where boarding and alighting may occur.
35+
36+
### Service Type 4: With Deviated Route
37+
38+
In this service type, we are actually running GTFS-style routing. However, we also need to modify some static weights based on the expected delays due to trip deviations. These weights should be modified during trip/model initialization but made fixed to ensure search correctness.
39+
40+
## Processing GTFS Flex Feeds Using CLI
41+
42+
To process GTFS Flex feeds, you can use the provided command-line interface (CLI) tool. Follow the steps below:
43+
44+
1. **Install Dependencies**
45+
Ensure you have Rust installed on your system. If not, install it from [rustup.rs](https://rustup.rs/).
46+
47+
2. **Build the Project**
48+
Navigate to the project directory and build the CLI tool:
49+
```bash
50+
cd rust/bambam-gtfs-flex
51+
cargo build --release
52+
```
53+
54+
3. **Run the CLI Tool**
55+
Use the following command to process a GTFS Flex feed:
56+
```bash
57+
./target/release/bambam-gtfs-flex process-feeds ./src/test/assets 20240903 valid_zone.csv
58+
```
59+
If you want to process GTFS-Flex feeds without completing `cargo build --release` in Step 2, you can simply use the following command:
60+
```bash
61+
cd rust/bambam-gtfs-flex
62+
cargo run -- process-feeds ./src/test/assets 20240903
63+
```
64+
Replace `./src/test/assets` with the path to the folder where your GTFS-Flex feeds (.zip files) are located, `20240903` with the desired date in `YYYYMMDD` format for which you want to process the feeds, and `valid_zones.csv` (optional) with the name of the output CSV file, which will be written to the GTFS-Flex feeds directory.
65+
66+
4. **Verify Output**
67+
After processing, the output directory will contain the processed valid zone CSV for the requested date, ready for use in BAMBAM.
68+
69+
Refer to the project's documentation for more details on further usage and configuration.
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
use csv::ReaderBuilder;
2+
use serde::{self, Deserialize};
3+
use std::{fs::File, io, path::Path};
4+
use zip::ZipArchive;
5+
6+
/// single row from agency.txt in a GTFS-Flex feed
7+
#[derive(Debug, Deserialize)]
8+
pub struct Agency {
9+
/// unique agency identifier
10+
pub agency_id: Option<String>,
11+
12+
/// agency URL
13+
pub _agency_url: Option<String>,
14+
15+
/// primary language used by the agency
16+
pub _agency_lang: Option<String>,
17+
18+
/// full agency name
19+
pub _agency_name: Option<String>,
20+
21+
/// agency phone number
22+
pub _agency_phone: Option<String>,
23+
24+
/// agency timezone
25+
pub _agency_timezone: Option<String>,
26+
27+
/// URL to fare information
28+
pub _agency_fare_url: Option<String>,
29+
30+
/// text-to-speech version of agency name
31+
pub _tts_agency_name: Option<String>,
32+
}
33+
34+
/// read `agency.txt` from a single GTFS-Flex zip file
35+
///
36+
/// streams data directly from the zip
37+
/// returns None if agency.txt is missing or duplicated
38+
/// returns typed Agency rows on success
39+
pub fn read_agency_from_flex(zip_path: &Path) -> io::Result<Option<Vec<Agency>>> {
40+
// open the zip file
41+
let file = File::open(zip_path)?;
42+
let mut archive = ZipArchive::new(file)?;
43+
44+
// locate agency.txt inside the zip
45+
let mut agency_name: Option<String> = None;
46+
47+
for i in 0..archive.len() {
48+
let file_in_zip = archive.by_index(i)?;
49+
50+
if file_in_zip.name().ends_with("agency.txt") {
51+
// do not allow multiple agency.txt files
52+
if agency_name.is_some() {
53+
eprintln!(
54+
"WARNING: Multiple agency.txt found in {:?}. Skipping ZIP.",
55+
zip_path
56+
);
57+
return Ok(None);
58+
}
59+
60+
agency_name = Some(file_in_zip.name().to_string());
61+
}
62+
}
63+
64+
// handle missing agency.txt
65+
let agency_name = match agency_name {
66+
Some(name) => name,
67+
None => {
68+
println!("No agency.txt found in {:?}", zip_path);
69+
return Ok(None);
70+
}
71+
};
72+
73+
// open agency.txt as streaming reader
74+
let file_in_zip = archive.by_name(&agency_name)?;
75+
76+
// create CSV reader
77+
let mut rdr = ReaderBuilder::new()
78+
.has_headers(true)
79+
.from_reader(file_in_zip);
80+
81+
// deserialize rows into Agency struct
82+
let mut agencies = Vec::new();
83+
84+
for result in rdr.deserialize::<Agency>() {
85+
let agency = result.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
86+
87+
agencies.push(agency);
88+
}
89+
90+
Ok(Some(agencies))
91+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
use clap::{Parser, Subcommand};
2+
3+
/// command line tool providing GTFS-Flex processing scripts
4+
#[derive(Parser, Debug)]
5+
#[command(author, version, about, long_about = None)]
6+
pub struct Cli {
7+
#[command(subcommand)]
8+
pub command: Commands,
9+
}
10+
11+
#[derive(Subcommand, Debug)]
12+
pub enum Commands {
13+
/// process GTFS-Flex feeds
14+
#[command(name = "process-feeds")]
15+
ProcessGtfsFlexFeeds(GTFSFLexCliArguments),
16+
}
17+
18+
/// arguments for the process-gtfs-flex-feeds subcommand
19+
#[derive(Parser, Debug)]
20+
pub struct GTFSFLexCliArguments {
21+
/// directory containing GTFS-Flex feeds to process
22+
pub flex_dir: String,
23+
24+
/// date for which to process GTFS-Flex feeds (format: YYYYMMDD)
25+
pub date_requested: String,
26+
27+
/// output CSV file name for valid zones
28+
/// file will be created in the specified GTFS-Flex directory
29+
#[arg(short, long, default_value = "valid-zones.csv")]
30+
pub output_csv: String,
31+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
mod gtfs_flex_cli;
2+
3+
pub use gtfs_flex_cli::{Cli, Commands};
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
use chrono::NaiveDate;
2+
use csv::ReaderBuilder;
3+
use serde::{self, Deserialize, Deserializer};
4+
use std::{fs::File, io, path::Path};
5+
use zip::ZipArchive;
6+
7+
/// a single row from calendar.txt in a GTFS-Flex feed
8+
#[derive(Debug, Deserialize)]
9+
pub struct Calendar {
10+
/// unique service identifier
11+
pub service_id: String,
12+
13+
/// service availability by day (0 or 1)
14+
pub monday: u8,
15+
pub tuesday: u8,
16+
pub wednesday: u8,
17+
pub thursday: u8,
18+
pub friday: u8,
19+
pub saturday: u8,
20+
pub sunday: u8,
21+
22+
/// service start date (YYYYMMDD)
23+
#[serde(deserialize_with = "gtfs_flex_date")]
24+
pub start_date: NaiveDate,
25+
26+
/// service end date (YYYYMMDD)
27+
#[serde(deserialize_with = "gtfs_flex_date")]
28+
pub end_date: NaiveDate,
29+
}
30+
31+
/// deserialize GTFS-Flex dates in YYYYMMDD format
32+
fn gtfs_flex_date<'de, D>(deserializer: D) -> Result<NaiveDate, D::Error>
33+
where
34+
D: Deserializer<'de>,
35+
{
36+
let s = String::deserialize(deserializer)?;
37+
NaiveDate::parse_from_str(&s, "%Y%m%d").map_err(serde::de::Error::custom)
38+
}
39+
40+
/// read calendar.txt from a single GTFS-Flex ZIP file
41+
///
42+
/// streams data directly from the ZIP
43+
/// returns None if calendar.txt is missing or duplicated
44+
/// returns typed Calendar rows on success
45+
pub fn read_calendar_from_flex(zip_path: &Path) -> io::Result<Option<Vec<Calendar>>> {
46+
// open the zip file
47+
let file = File::open(zip_path)?;
48+
let mut archive = ZipArchive::new(file)?;
49+
50+
// locate calendar.txt
51+
let mut calendar_name: Option<String> = None;
52+
53+
for i in 0..archive.len() {
54+
let file_in_zip = archive.by_index(i)?;
55+
56+
if file_in_zip.name().ends_with("calendar.txt") {
57+
// donot allow multiple calendar.txt files in a zip
58+
if calendar_name.is_some() {
59+
eprintln!(
60+
"WARNING: Multiple calendar.txt found in {:?}. Skipping ZIP.",
61+
zip_path
62+
);
63+
return Ok(None);
64+
}
65+
66+
calendar_name = Some(file_in_zip.name().to_string());
67+
}
68+
}
69+
70+
// handle missing calendar.txt
71+
let calendar_name = match calendar_name {
72+
Some(name) => name,
73+
None => {
74+
println!("No calendar.txt found in {:?}", zip_path);
75+
return Ok(None);
76+
}
77+
};
78+
79+
// open calendar.txt as a streaming reader
80+
let file_in_zip = archive.by_name(&calendar_name)?;
81+
82+
// create a CSV reader
83+
let mut rdr = ReaderBuilder::new()
84+
.has_headers(true)
85+
.from_reader(file_in_zip);
86+
87+
// deserialize each row into Calendar
88+
let mut calendars = Vec::new();
89+
90+
for result in rdr.deserialize::<Calendar>() {
91+
let calendar = result.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
92+
93+
calendars.push(calendar);
94+
}
95+
96+
Ok(Some(calendars))
97+
}

0 commit comments

Comments
 (0)