Skip to content

Commit 7642b7e

Browse files
Merge pull request #72 from NREL/rjf/mode-edgelists
Rjf/mode edgelists
2 parents dd87218 + 71a5553 commit 7642b7e

30 files changed

+1277
-256
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@ configuration/test_gtfs_config_boulder_gtfs.toml
1414
denver_co/
1515
boulder_co/
1616
denver_rtd/
17-
17+
out/

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ First, install using cargo (via [rustup](rustup.rs)): `cargo build --release --m
2424
- **bambam_util** - runs specific support utilities
2525
- **bambam_gtfs** - [GTFS](https://gtfs.org/documentation/schedule/reference/) analysis and import script
2626
- **bambam_osm** - [OpenStreetMap](https://www.openstreetmap.org/) import script
27+
- **bambam_omf** - [OvertureMaps](https://docs.overturemaps.org/) import script
2728

2829
We can list the command arguments for bambam (will document app as "RouteE Compass"):
2930

@@ -95,8 +96,8 @@ $ RUST_LOG=info ./rust/target/release/bambam --config-file configuration/boulder
9596
- [ ] R API
9697
- [ ] OvertureMaps network import
9798
- [ ] methodological improvements for walk/bike/drive realism
98-
- [ ] transit-mode travel using GTFS Schedule data networks
99-
- [ ] multimodal route planning
99+
- [x] transit-mode travel using GTFS Schedule data networks
100+
- [x] multimodal route planning
100101

101102
# License
102103

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"edge_lists": [
3+
{
4+
"mode": "walk",
5+
"filter": [
6+
{ "type": "subtype", "subtype": "road" },
7+
{
8+
"type": "class",
9+
"classes": ["secondary", "tertiary", "residential", "living_street", "unclassified", "service", "pedestrian", "footway", "steps", "path", "track", "bridleway", "unknown"],
10+
"ignore_unset": true,
11+
"behavior": "include"
12+
},
13+
{ "type": "access_mode", "modes": ["foot"] }
14+
]
15+
},
16+
{
17+
"mode": "bike",
18+
"filter": [
19+
{ "type": "subtype", "subtype": "road" },
20+
{
21+
"type": "class",
22+
"classes": ["primary", "secondary", "tertiary", "residential", "living_street", "unclassified", "service", "pedestrian", "path", "track", "cycleway", "bridleway", "unknown"],
23+
"ignore_unset": true,
24+
"behavior": "include"
25+
},
26+
{ "type": "access_mode", "modes": ["bicycle"] }
27+
]
28+
},
29+
{
30+
"mode": "drive",
31+
"filter": [
32+
{ "type": "subtype", "subtype": "road" },
33+
{
34+
"type": "class",
35+
"classes": ["motorway", "trunk", "primary", "secondary", "tertiary", "residential", "living_street", "unclassified", "service", "unknown"],
36+
"ignore_unset": true,
37+
"behavior": "include"
38+
},
39+
{ "type": "access_mode", "modes": ["motor_vehicle", "car", "truck", "motorcycle"] }
40+
]
41+
}
42+
]
43+
}

rust/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ kdam = "0.6.2"
4242
log = "0.4.19"
4343
num-traits = "0.2.19"
4444
object_store = { "version" = "0.12.0", features = ["aws"] }
45+
opening-hours-syntax = "1.1.5"
4546
ordered-float = { version = "5.1.0", features = ["serde"] }
4647
osmio = "0.14.0"
4748
osmpbf = "0.3.4"

rust/bambam-omf/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ routee-compass-powertrain = { workspace = true }
2929
arrow = { workspace = true }
3030
chrono = { workspace = true }
3131
clap = { workspace = true }
32+
config = { workspace = true }
3233
csv = { workspace = true }
3334
env_logger = { workspace = true }
3435
flate2 = { workspace = true }
@@ -41,6 +42,7 @@ kdam = { workspace = true }
4142
log = { workspace = true }
4243
object_store = { workspace = true }
4344
parquet = { workspace = true }
45+
opening-hours-syntax = { workspace = true }
4446
ordered-float = { workspace = true }
4547
rayon = { workspace = true }
4648
reqwest = { workspace = true }
@@ -50,3 +52,4 @@ serde_arrow = { workspace = true }
5052
thiserror = { workspace = true }
5153
tokio = { workspace = true }
5254
uom = { workspace = true }
55+
wkb = { workspace = true }
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
use serde::{Deserialize, Serialize};
2+
3+
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
4+
pub struct CliBoundingBox {
5+
pub xmin: f32,
6+
pub xmax: f32,
7+
pub ymin: f32,
8+
pub ymax: f32,
9+
}
10+
11+
pub fn parse_bbox(s: &str) -> Result<CliBoundingBox, String> {
12+
let parts: Vec<&str> = s.split(',').collect();
13+
if parts.len() != 4 {
14+
return Err(format!("expected format: xmin,xmax,ymin,ymax, got: {}", s));
15+
}
16+
17+
let xmin = parse_lon(parts[0])?;
18+
let xmax = parse_lon(parts[1])?;
19+
20+
let ymin = parse_lat(parts[2])?;
21+
let ymax = parse_lat(parts[3])?;
22+
23+
if !(xmin < xmax) {
24+
Err(format!(
25+
"bbox: xmin must be less than xmax, but found [{xmin},{xmax}]"
26+
))
27+
} else if !(ymin < ymax) {
28+
Err(format!(
29+
"bbox: ymin must be less than ymax, but found [{ymin},{ymax}]"
30+
))
31+
} else {
32+
Ok(CliBoundingBox {
33+
xmin,
34+
xmax,
35+
ymin,
36+
ymax,
37+
})
38+
}
39+
}
40+
41+
impl std::fmt::Display for CliBoundingBox {
42+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43+
write!(f, "{},{},{},{}", self.xmin, self.xmax, self.ymin, self.ymax)
44+
}
45+
}
46+
47+
fn parse_lat(lat: &str) -> Result<f32, String> {
48+
parse_num(lat, -90.0, 90.0).map_err(|e| format!("invalid latitude: {e}"))
49+
}
50+
51+
fn parse_lon(lat: &str) -> Result<f32, String> {
52+
parse_num(lat, -180.0, 180.0).map_err(|e| format!("invalid longitude: {e}"))
53+
}
54+
55+
fn parse_num(s: &str, min: f32, max: f32) -> Result<f32, String> {
56+
let v = s
57+
.trim()
58+
.parse::<f32>()
59+
.map_err(|_| format!("not a number: {s}"))?;
60+
if v < min || max < v {
61+
Err(format!(
62+
"number '{v}' is not valid, must be in range [{min},{max}]"
63+
))
64+
} else {
65+
Ok(v)
66+
}
67+
}

rust/bambam-omf/src/app/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
mod cli_bbox;
12
mod omf_app;
23

34
pub use omf_app::OmfApp;
5+
pub mod network;
6+
pub use cli_bbox::CliBoundingBox;

rust/bambam-omf/src/app/network.rs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
use std::path::Path;
2+
3+
use serde::{Deserialize, Serialize};
4+
5+
use crate::{
6+
app::CliBoundingBox,
7+
collection::{
8+
filter::TravelModeFilter, ObjectStoreSource, OvertureMapsCollectionError,
9+
OvertureMapsCollectorConfig, ReleaseVersion, TransportationCollection,
10+
},
11+
graph::OmfGraphVectorized,
12+
util,
13+
};
14+
15+
#[derive(Serialize, Deserialize, Clone, Debug)]
16+
pub struct NetworkEdgeListConfiguration {
17+
pub mode: String,
18+
pub filter: Vec<TravelModeFilter>,
19+
}
20+
21+
/// runs an OMF network import using the provided configuration.
22+
pub fn run(
23+
bbox: Option<&CliBoundingBox>,
24+
modes: &[NetworkEdgeListConfiguration],
25+
output_directory: &Path,
26+
local_source: Option<&Path>,
27+
write_json: bool,
28+
) -> Result<(), OvertureMapsCollectionError> {
29+
let collection: TransportationCollection = match local_source {
30+
Some(src_path) => read_local(src_path),
31+
None => run_collector(bbox),
32+
}?;
33+
34+
if write_json {
35+
util::fs::create_dirs(output_directory)?;
36+
collection.to_json(output_directory)?;
37+
}
38+
39+
let vectorized_graph = OmfGraphVectorized::new(&collection, modes)?;
40+
vectorized_graph.write_compass(output_directory, true)?;
41+
42+
Ok(())
43+
}
44+
45+
fn read_local(path: &Path) -> Result<TransportationCollection, OvertureMapsCollectionError> {
46+
let contents = std::fs::read(path).map_err(|e| OvertureMapsCollectionError::ReadError {
47+
path: path.to_owned(),
48+
message: e.to_string(),
49+
})?;
50+
let collection =
51+
serde_json::from_slice::<TransportationCollection>(&contents).map_err(|e| {
52+
OvertureMapsCollectionError::ReadError {
53+
path: path.to_owned(),
54+
message: format!("failed to deserialize from JSON: {e}"),
55+
}
56+
})?;
57+
Ok(collection)
58+
}
59+
60+
/// retrieve a TransportationCollection from a URL.
61+
fn run_collector(
62+
bbox_arg: Option<&CliBoundingBox>,
63+
) -> Result<TransportationCollection, OvertureMapsCollectionError> {
64+
let object_store = ObjectStoreSource::AmazonS3;
65+
let batch_size = 128;
66+
let collector = OvertureMapsCollectorConfig::new(object_store, batch_size).build()?;
67+
let release = ReleaseVersion::Latest;
68+
let bbox = bbox_arg.ok_or_else(|| {
69+
let msg = String::from("must provide bbox argument for download");
70+
OvertureMapsCollectionError::InvalidUserInput(msg)
71+
})?;
72+
log::info!(
73+
"running OMF import with
74+
object store {object_store:?}
75+
batch size {batch_size}
76+
release {release}
77+
(xmin,xmax,ymin,ymax): {bbox}"
78+
);
79+
80+
TransportationCollection::try_from_collector(collector, release, Some(bbox.into()))
81+
}

rust/bambam-omf/src/app/omf_app.rs

Lines changed: 49 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
use std::path::Path;
22

33
use clap::{Parser, Subcommand};
4-
use routee_compass_core::model::network::EdgeListId;
4+
use config::{Config, File};
55
use serde::{Deserialize, Serialize};
66

77
use crate::{
8-
collection::{
9-
ObjectStoreSource, OvertureMapsCollectionError, OvertureMapsCollectorConfig,
10-
ReleaseVersion, RowFilterConfig, TransportationCollection,
11-
},
12-
graph::OmfGraphVectorized,
8+
app::{cli_bbox::parse_bbox, network::NetworkEdgeListConfiguration, CliBoundingBox},
9+
collection::OvertureMapsCollectionError,
1310
};
1411

1512
/// Command line tool for batch downloading and summarizing of OMF (Overture Maps Foundation) data
@@ -24,41 +21,64 @@ pub struct OmfApp {
2421
#[derive(Debug, Clone, Serialize, Deserialize, Subcommand)]
2522
pub enum OmfOperation {
2623
/// download all of the OMF transportation data
27-
Download {
24+
Network {
25+
/// configuration file defining how the network is imported and separated
26+
/// into mode-specific edge lists.
27+
#[arg(short, long)]
28+
configuration_file: String,
29+
2830
/// location on disk to write output files. if not provided,
2931
/// use the current working directory.
3032
#[arg(short, long)]
3133
output_directory: Option<String>,
34+
35+
/// use a stored raw data export from a previous run of OmfOperation::Network
36+
/// which is a JSON file containing a TransportationCollection.
37+
#[arg(short, long)]
38+
local_source: Option<String>,
39+
40+
/// write the raw OMF dataset as a JSON blob to the output directory.
41+
#[arg(short, long)]
42+
store_raw: bool,
43+
44+
/// bounding box to filter data (format: xmin,xmax,ymin,ymax)
45+
#[arg(short, long, value_parser = parse_bbox, allow_hyphen_values(true))]
46+
bbox: Option<CliBoundingBox>,
3247
},
3348
}
3449

3550
impl OmfOperation {
3651
pub fn run(&self) -> Result<(), OvertureMapsCollectionError> {
3752
match self {
38-
OmfOperation::Download { output_directory } => {
39-
let collector =
40-
OvertureMapsCollectorConfig::new(ObjectStoreSource::AmazonS3, 128).build()?;
41-
let release = ReleaseVersion::Latest;
42-
let row_filter_config = RowFilterConfig::Bbox {
43-
xmin: -105.254,
44-
xmax: -105.197,
45-
ymin: 39.733,
46-
ymax: 39.784,
47-
};
48-
49-
let collection = TransportationCollection::try_from_collector(
50-
collector,
51-
release,
52-
Some(row_filter_config),
53-
)?;
54-
let vectorized_graph = OmfGraphVectorized::new(collection, EdgeListId(0))?;
55-
let output_path = match output_directory {
56-
Some(o) => Path::new(o),
53+
OmfOperation::Network {
54+
configuration_file,
55+
output_directory,
56+
local_source,
57+
store_raw,
58+
bbox,
59+
} => {
60+
let filepath = Path::new(configuration_file);
61+
let config = Config::builder()
62+
.add_source(File::from(filepath))
63+
.build()
64+
.map_err(|e| {
65+
let msg = format!("file '{configuration_file}' produced error: {e}");
66+
OvertureMapsCollectionError::InvalidUserInput(msg)
67+
})?;
68+
let network_config = config
69+
.get::<Vec<NetworkEdgeListConfiguration>>("edge_lists")
70+
.map_err(|e| {
71+
let msg = format!(
72+
"error reading 'edge_lists' key in '{configuration_file}': {e}"
73+
);
74+
OvertureMapsCollectionError::InvalidUserInput(msg)
75+
})?;
76+
let outdir = match output_directory {
77+
Some(out) => Path::new(out),
5778
None => Path::new(""),
5879
};
59-
vectorized_graph.write_compass(output_path, true)?;
60-
61-
Ok(())
80+
let local = local_source.as_ref().map(Path::new);
81+
crate::app::network::run(bbox.as_ref(), &network_config, outdir, local, *store_raw)
6282
}
6383
}
6484
}

rust/bambam-omf/src/app/serialize_options.rs

Lines changed: 0 additions & 13 deletions
This file was deleted.

0 commit comments

Comments
 (0)