From 7836a09a6aae6ebc32ec141ebf4341c58a2a7d47 Mon Sep 17 00:00:00 2001 From: admrtin Date: Thu, 13 Aug 2026 13:20:39 -0600 Subject: [PATCH 01/10] Migrate modal metric computations to a new crate --- rust/Cargo.toml | 4 +- rust/bambam-modal-metrics/Cargo.toml | 17 + .../src}/common/bulk_compute_modal_metric.rs | 35 ++- .../src}/common/cycleway_tag.rs | 2 +- .../src/common/edge_rtree_entry.rs} | 47 ++- rust/bambam-modal-metrics/src/common/mod.rs | 5 + .../src/common/modal_metrics.rs} | 37 ++- rust/bambam-modal-metrics/src/common/ops.rs | 102 ++++++ .../src/lib.rs} | 1 + .../src}/lts/compute_lts.rs | 42 ++- .../src}/lts/lts.rs | 2 +- .../src}/lts/mod.rs | 1 - .../network_traits/edge_for_modal_metric.rs | 34 ++ .../src/network_traits/mod.rs | 3 + .../src/network_traits/spatial_edge.rs | 17 + .../network_traits/vertex_for_modal_metric.rs | 12 + .../src/wci/compute_wci.rs | 295 +++++++++++++++++ .../src}/wci/mod.rs | 4 +- rust/bambam-modal-metrics/src/wci/ops.rs | 85 +++++ .../src}/wci/wci.rs | 34 +- rust/bambam-osm/Cargo.toml | 1 + rust/bambam-osm/src/app/mod.rs | 1 - rust/bambam-osm/src/app/network/common/mod.rs | 6 - rust/bambam-osm/src/app/network/common/ops.rs | 107 ------- rust/bambam-osm/src/app/network/lts/ops.rs | 20 -- .../src/app/network/wci/compute_wci.rs | 296 ------------------ rust/bambam-osm/src/app/network/wci/ops.rs | 154 --------- rust/bambam-osm/src/lib.rs | 1 - .../osm/graph/osm_node_data_serializable.rs | 14 + .../osm/graph/osm_way_data_serializable.rs | 97 ++++++ rust/bambam/Cargo.toml | 3 +- rust/bambam/src/bin/bambam_util.rs | 21 +- 32 files changed, 827 insertions(+), 673 deletions(-) create mode 100644 rust/bambam-modal-metrics/Cargo.toml rename rust/{bambam-osm/src/app/network => bambam-modal-metrics/src}/common/bulk_compute_modal_metric.rs (62%) rename rust/{bambam-osm/src/app/network => bambam-modal-metrics/src}/common/cycleway_tag.rs (98%) rename rust/{bambam-osm/src/app/network/common/way_rtree_entry.rs => bambam-modal-metrics/src/common/edge_rtree_entry.rs} (57%) create mode 100644 rust/bambam-modal-metrics/src/common/mod.rs rename rust/{bambam-osm/src/app/network/common/modal_metric.rs => bambam-modal-metrics/src/common/modal_metrics.rs} (75%) create mode 100644 rust/bambam-modal-metrics/src/common/ops.rs rename rust/{bambam-osm/src/app/network/mod.rs => bambam-modal-metrics/src/lib.rs} (63%) rename rust/{bambam-osm/src/app/network => bambam-modal-metrics/src}/lts/compute_lts.rs (65%) rename rust/{bambam-osm/src/app/network => bambam-modal-metrics/src}/lts/lts.rs (97%) rename rust/{bambam-osm/src/app/network => bambam-modal-metrics/src}/lts/mod.rs (72%) create mode 100644 rust/bambam-modal-metrics/src/network_traits/edge_for_modal_metric.rs create mode 100644 rust/bambam-modal-metrics/src/network_traits/mod.rs create mode 100644 rust/bambam-modal-metrics/src/network_traits/spatial_edge.rs create mode 100644 rust/bambam-modal-metrics/src/network_traits/vertex_for_modal_metric.rs create mode 100644 rust/bambam-modal-metrics/src/wci/compute_wci.rs rename rust/{bambam-osm/src/app/network => bambam-modal-metrics/src}/wci/mod.rs (70%) create mode 100644 rust/bambam-modal-metrics/src/wci/ops.rs rename rust/{bambam-osm/src/app/network => bambam-modal-metrics/src}/wci/wci.rs (71%) delete mode 100644 rust/bambam-osm/src/app/mod.rs delete mode 100644 rust/bambam-osm/src/app/network/common/mod.rs delete mode 100644 rust/bambam-osm/src/app/network/common/ops.rs delete mode 100644 rust/bambam-osm/src/app/network/lts/ops.rs delete mode 100644 rust/bambam-osm/src/app/network/wci/compute_wci.rs delete mode 100644 rust/bambam-osm/src/app/network/wci/ops.rs diff --git a/rust/Cargo.toml b/rust/Cargo.toml index fcadbe77..254125cf 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -6,6 +6,7 @@ members = [ "bambam-gbfs", "bambam-gtfs", "bambam-gtfs-flex", + "bambam-modal-metrics", "bambam-omf", "bambam-osm", "bambam-py", @@ -21,7 +22,8 @@ bambam = { version = "0.3.3", path = "bambam" } bambam-core = { version = "0.3.3", path = "bambam-core" } bambam-gbfs = { version = "0.3.3", path = "bambam-gbfs" } bambam-gtfs = { version = "0.3.3", path = "bambam-gtfs" } -bambam-gtfs-flex = { version = "0.3.3", path = "bambam-gtfs-flex" } +bambam-gtfs-flex = { version = "0.3.3", path = "bambam-gtfs-flex" } +bambam-modal-metrics = { version = "0.3.3", path = "bambam-modal-metrics" } bambam-omf = { version = "0.3.3", path = "bambam-omf" } bambam-osm = { version = "0.3.3", path = "bambam-osm" } bamcensus = { version = "0.1.0" } diff --git a/rust/bambam-modal-metrics/Cargo.toml b/rust/bambam-modal-metrics/Cargo.toml new file mode 100644 index 00000000..a138bea0 --- /dev/null +++ b/rust/bambam-modal-metrics/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "bambam-modal-metrics" +edition = "2024" +version.workspace = true + +[dependencies] +routee-compass-core = { workspace = true } + +csv = { workspace = true } +geo = { workspace = true } +kdam = { workspace = true } +rayon = { workspace = true } +rstar = { workspace = true } +serde = { workspace = true } +thiserror = { workspace = true } +uom = { workspace = true } +log = { workspace = true } \ No newline at end of file diff --git a/rust/bambam-osm/src/app/network/common/bulk_compute_modal_metric.rs b/rust/bambam-modal-metrics/src/common/bulk_compute_modal_metric.rs similarity index 62% rename from rust/bambam-osm/src/app/network/common/bulk_compute_modal_metric.rs rename to rust/bambam-modal-metrics/src/common/bulk_compute_modal_metric.rs index 12fd29d6..5f2ad21d 100644 --- a/rust/bambam-osm/src/app/network/common/bulk_compute_modal_metric.rs +++ b/rust/bambam-modal-metrics/src/common/bulk_compute_modal_metric.rs @@ -1,10 +1,14 @@ -use crate::app::network::common::modal_metric::{ModalMetric, ModalMetricError, ModalMetricValue}; -use crate::app::network::common::ops::load_way_rtree_entries; -use crate::model::osm::graph::OsmNodeDataSerializable; +use crate::common::modal_metrics::{ModalMetric, ModalMetricError, ModalMetricValue}; +use crate::common::ops::load_edge_rtree_entries; +use crate::network_traits::{ + edge_for_modal_metric::EdgeForModalMetric, spatial_edge::SpatialEdge, + vertex_for_modal_metric::VertexForModalMetric, +}; use kdam::{Bar, BarBuilder, BarExt}; use rayon::prelude::*; use routee_compass_core::util::fs::read_utils; use rstar::RTree; +use serde::de::DeserializeOwned; use std::sync::{Arc, Mutex}; use std::{ error::Error, @@ -18,12 +22,16 @@ use std::{ /// `metric_name` can either be: /// - "WCI" for the Walking Comfort Index metric /// - "LTS" for the Level of Traffic Stress (cycling comfort) metric -pub fn bulk_compute_modal_metric( +pub fn bulk_compute_modal_metric( metric_name: &str, edges_file: &str, vertices_file: &str, output_file: &str, -) -> Result<(), Box> { +) -> Result<(), Box> +where + E: SpatialEdge + EdgeForModalMetric + DeserializeOwned + Clone + Send + Sync, + V: VertexForModalMetric + DeserializeOwned + Send + Sync, +{ let metric: ModalMetric = metric_name.parse()?; log::info!( @@ -32,12 +40,11 @@ pub fn bulk_compute_modal_metric( ); log::info!("Reading:\n\t- vertex set @ {vertices_file}\n\t- edge set @ {edges_file}\n"); - let vertices: Box<[OsmNodeDataSerializable]> = - read_utils::from_csv(&vertices_file, true, None, None)?; - let way_rtree_entries = load_way_rtree_entries(edges_file, &vertices)?; + let vertices: Box<[V]> = read_utils::from_csv(&vertices_file, true, None, None)?; + let edge_rtree_entries = load_edge_rtree_entries::(edges_file, &vertices)?; log::info!("Edges and vertices read successfully.\n"); - let rtree = RTree::bulk_load(way_rtree_entries.clone()); + let rtree = RTree::bulk_load(edge_rtree_entries.clone()); let bar: Arc> = Arc::new(Mutex::new( BarBuilder::default() @@ -45,16 +52,16 @@ pub fn bulk_compute_modal_metric( "Computing {:?} for ways in the road network", metric )) - .total(way_rtree_entries.len()) + .total(edge_rtree_entries.len()) .build()?, )); - let values: Vec = way_rtree_entries + let values: Vec = edge_rtree_entries .par_iter() - .map(|way_entry| { - let src_node = vertices.get(way_entry.way.src_vertex_id.0); + .map(|edge_entry| { + let src_vertex = vertices.get(edge_entry.edge.src_vertex_id()); - let result = metric.compute_metric(&rtree, way_entry, src_node)?; + let result = metric.compute_metric(&rtree, edge_entry, src_vertex)?; if let Ok(mut bar) = bar.lock() { let _ = bar.update(1); diff --git a/rust/bambam-osm/src/app/network/common/cycleway_tag.rs b/rust/bambam-modal-metrics/src/common/cycleway_tag.rs similarity index 98% rename from rust/bambam-osm/src/app/network/common/cycleway_tag.rs rename to rust/bambam-modal-metrics/src/common/cycleway_tag.rs index 701bf0fa..a335d11e 100644 --- a/rust/bambam-osm/src/app/network/common/cycleway_tag.rs +++ b/rust/bambam-modal-metrics/src/common/cycleway_tag.rs @@ -3,7 +3,7 @@ /// /// You can generate a new cycleway tag by passing in /// the OSM way's cycleway attribute. -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum CyclewayTag { DedicatedWithBuffer, DedicatedNoBuffer, diff --git a/rust/bambam-osm/src/app/network/common/way_rtree_entry.rs b/rust/bambam-modal-metrics/src/common/edge_rtree_entry.rs similarity index 57% rename from rust/bambam-osm/src/app/network/common/way_rtree_entry.rs rename to rust/bambam-modal-metrics/src/common/edge_rtree_entry.rs index f5da3ca2..c06f4362 100644 --- a/rust/bambam-osm/src/app/network/common/way_rtree_entry.rs +++ b/rust/bambam-modal-metrics/src/common/edge_rtree_entry.rs @@ -1,6 +1,12 @@ -use crate::model::osm::graph::OsmWayDataSerializable; use geo::{BoundingRect, Centroid, Distance, Euclidean}; -use rstar::{PointDistance, RTreeObject, AABB}; +use rstar::{AABB, PointDistance, RTreeObject}; + +use crate::network_traits::spatial_edge::SpatialEdge; + +/// The maximum distance (in degrees) within which another edge is considered a +/// spatial "neighbor" for the purpose of neighbor-aware modal penalty scores. +/// Roughly 15 meters at mid latitudes. +pub const MIN_DISTANCE_RTREE_NEIGHBOR: f32 = 0.0001378; /// `WayRTreeEntry` wraps `OsmWayDataSerializable` and caches the bounding box /// and centroid of the way's `linestring`. It is used solely for efficient spatial queries @@ -17,38 +23,39 @@ use rstar::{PointDistance, RTreeObject, AABB}; /// This allows us to compute the bounding box and centroid once, and reuse them in O(1) /// for multiple spatial queries. #[derive(Clone)] -pub struct WayRTreeEntry { +pub struct EdgeRTreeEntry { bbox: AABB<[f32; 2]>, pub centroid: geo::Point, - pub way: OsmWayDataSerializable, + pub edge: E, } -impl WayRTreeEntry { - pub fn new(way: OsmWayDataSerializable) -> Option { +impl EdgeRTreeEntry { + pub fn new(edge: E) -> Option { + let linestring = edge.linestring()?; // Grab the bounding rectangle of the linestring. If it doesn't exist, return None. - let rect = way.linestring.bounding_rect()?; + let rect = linestring.bounding_rect()?; // Compute the centroid of the linestring. If it doesn't exist, return None. - let centroid = way.linestring.centroid()?; + let centroid = linestring.centroid()?; // Create the bounding box from the linestring's bounding rectangle Some(Self { bbox: AABB::from_corners([rect.min().x, rect.min().y], [rect.max().x, rect.max().y]), centroid, - way, + edge, }) } } -impl RTreeObject for WayRTreeEntry { +impl RTreeObject for EdgeRTreeEntry { type Envelope = AABB<[f32; 2]>; // Envelope should be the same type as the bbox of WayRTreeEntry fn envelope(&self) -> Self::Envelope { self.bbox // return the cached bounding box } } -impl PointDistance for WayRTreeEntry { - // NOTE: The PointDistance trait for WayRTreeEntry uses euclidean distance. +impl PointDistance for EdgeRTreeEntry { + // NOTE: The PointDistance trait for EdgeRTreeEntry uses euclidean distance. // We may want to consider using haversine distance since we are working with geographic coordinates. // However, for small distances (in the case of local navigation), the difference may be negligible. fn distance_2(&self, point: &[f32; 2]) -> f32 { @@ -57,3 +64,19 @@ impl PointDistance for WayRTreeEntry { distance * distance } } + +/// Find the neighboring edges within [`MIN_DISTANCE_RTREE_NEIGHBOR`] of the +/// query edge's centroid, excluding the query edge itself (by [`SpatialEdge::id`]). +pub fn find_neighboring_edges<'a, E: SpatialEdge>( + query: &EdgeRTreeEntry, + rtree: &'a rstar::RTree>, +) -> Vec<&'a EdgeRTreeEntry> { + let query_id = query.edge.id(); + rtree + .locate_within_distance( + [query.centroid.x(), query.centroid.y()], + MIN_DISTANCE_RTREE_NEIGHBOR, + ) + .filter(|entry| entry.edge.id() != query_id) + .collect() +} diff --git a/rust/bambam-modal-metrics/src/common/mod.rs b/rust/bambam-modal-metrics/src/common/mod.rs new file mode 100644 index 00000000..19a1c799 --- /dev/null +++ b/rust/bambam-modal-metrics/src/common/mod.rs @@ -0,0 +1,5 @@ +pub mod bulk_compute_modal_metric; +pub mod cycleway_tag; +pub mod edge_rtree_entry; +pub mod modal_metrics; +pub mod ops; diff --git a/rust/bambam-osm/src/app/network/common/modal_metric.rs b/rust/bambam-modal-metrics/src/common/modal_metrics.rs similarity index 75% rename from rust/bambam-osm/src/app/network/common/modal_metric.rs rename to rust/bambam-modal-metrics/src/common/modal_metrics.rs index c9ed0b5a..1ffe941b 100644 --- a/rust/bambam-osm/src/app/network/common/modal_metric.rs +++ b/rust/bambam-modal-metrics/src/common/modal_metrics.rs @@ -1,10 +1,13 @@ -use crate::app::network::common::way_rtree_entry::WayRTreeEntry; -use crate::app::network::lts::compute_lts::compute_lts; -use crate::app::network::lts::lts::Lts; -use crate::app::network::lts::lts::LtsError; -use crate::app::network::wci::compute_wci::{compute_wci, WciComponents}; -use crate::app::network::wci::wci::WciError; -use crate::model::osm::graph::OsmNodeDataSerializable; +use crate::common::edge_rtree_entry::EdgeRTreeEntry; +use crate::lts::compute_lts::compute_lts; +use crate::lts::lts::Lts; +use crate::lts::lts::LtsError; +use crate::network_traits::{ + edge_for_modal_metric::EdgeForModalMetric, spatial_edge::SpatialEdge, + vertex_for_modal_metric::VertexForModalMetric, +}; +use crate::wci::compute_wci::{WciComponents, compute_wci}; +use crate::wci::wci::WciError; use rstar::RTree; use std::{error::Error, io::Write, str::FromStr}; @@ -49,20 +52,24 @@ impl FromStr for ModalMetric { } impl ModalMetric { - /// Computes the specified modal metric for the given way entry and source node. - pub fn compute_metric( + /// Computes the specified modal metric for the given edge entry and source vertex. + pub fn compute_metric( &self, - rtree: &RTree, - way_entry: &WayRTreeEntry, - src_node: Option<&OsmNodeDataSerializable>, - ) -> Result { + rtree: &RTree>, + edge_entry: &EdgeRTreeEntry, + src_vertex: Option<&V>, + ) -> Result + where + E: SpatialEdge + EdgeForModalMetric, + V: VertexForModalMetric, + { match self { ModalMetric::WalkingComfortIndex => { - let wci = compute_wci(rtree, way_entry, src_node)?; + let wci = compute_wci(rtree, edge_entry, src_vertex)?; Ok(ModalMetricValue::Wci(wci)) } ModalMetric::LevelOfTrafficStress => { - let lts = compute_lts(rtree, way_entry)?; + let lts = compute_lts(rtree, edge_entry)?; Ok(ModalMetricValue::Lts(lts)) } } diff --git a/rust/bambam-modal-metrics/src/common/ops.rs b/rust/bambam-modal-metrics/src/common/ops.rs new file mode 100644 index 00000000..799da908 --- /dev/null +++ b/rust/bambam-modal-metrics/src/common/ops.rs @@ -0,0 +1,102 @@ +use std::error::Error; + +use geo::{Distance, Euclidean}; +use serde::de::DeserializeOwned; + +use crate::{ + common::edge_rtree_entry::EdgeRTreeEntry, + network_traits::{ + edge_for_modal_metric::EdgeForModalMetric, spatial_edge::SpatialEdge, + vertex_for_modal_metric::VertexForModalMetric, + }, +}; + +/// Load edges from a CSV file and create R-tree entries for each edge. +pub fn load_edge_rtree_entries( + edges_file: &str, + vertices: &[V], +) -> Result>, Box> +where + E: SpatialEdge + DeserializeOwned, + V: VertexForModalMetric, +{ + let mut edge_reader = csv::Reader::from_path(edges_file)?; + let mut edge_entries = Vec::new(); + + for record in edge_reader.deserialize::() { + let edge = match record { + Ok(edge) => edge, + Err(err) => { + eprintln!("Error reading row: {err}"); + continue; + } + }; + + let src = edge.src_vertex_id(); + if vertices.get(src).is_none() { + eprintln!( + "Warning: source vertex {src} not found for edge {}; skipping", + edge.id() + ); + continue; + } + + let id = edge.id(); + let Some(entry) = EdgeRTreeEntry::new(edge) else { + eprintln!("Warning: could not create R-tree entry for edge {id}"); + continue; + }; + + edge_entries.push(entry); + } + + Ok(edge_entries) +} + +/// Traffic speed limit in MPH, if known. +pub fn traffic_speed_from_maxspeed(entry: &EdgeRTreeEntry) -> Option +where + E: EdgeForModalMetric + SpatialEdge, +{ + entry.edge.get_traffic_speed_limit().map(|mph| mph as f32) +} + +/// Computes a weighted estimated speed (in MPH) from nearby ways +pub fn estimated_speed_from_neighbors( + entry: &EdgeRTreeEntry, + neighboring_ways: &[&EdgeRTreeEntry], +) -> Option +where + E: EdgeForModalMetric + SpatialEdge, +{ + let speeds_and_distances: Vec<(f32, f32)> = neighboring_ways + .iter() + .filter_map(|neighbor| { + neighbor.edge.get_traffic_speed_limit().map(|mph| { + let speed = mph as f32; + let distance = Euclidean.distance(entry.centroid, neighbor.centroid); + (speed, distance) + }) + }) + .collect(); + + if speeds_and_distances.is_empty() { + return None; + } + + let sum_distances: f32 = speeds_and_distances + .iter() + .map(|(_, distance)| *distance) + .sum(); + + if sum_distances == 0.0 { + return None; + } + + let weighted_speed: f32 = speeds_and_distances + .iter() + .map(|(speed, distance)| speed * distance / sum_distances) + .sum(); + + Some(weighted_speed) +} diff --git a/rust/bambam-osm/src/app/network/mod.rs b/rust/bambam-modal-metrics/src/lib.rs similarity index 63% rename from rust/bambam-osm/src/app/network/mod.rs rename to rust/bambam-modal-metrics/src/lib.rs index b7b7772b..7ba528b9 100644 --- a/rust/bambam-osm/src/app/network/mod.rs +++ b/rust/bambam-modal-metrics/src/lib.rs @@ -1,3 +1,4 @@ pub mod common; pub mod lts; +pub mod network_traits; pub mod wci; diff --git a/rust/bambam-osm/src/app/network/lts/compute_lts.rs b/rust/bambam-modal-metrics/src/lts/compute_lts.rs similarity index 65% rename from rust/bambam-osm/src/app/network/lts/compute_lts.rs rename to rust/bambam-modal-metrics/src/lts/compute_lts.rs index 05c9898f..3b13dc0e 100644 --- a/rust/bambam-osm/src/app/network/lts/compute_lts.rs +++ b/rust/bambam-modal-metrics/src/lts/compute_lts.rs @@ -1,41 +1,39 @@ use rstar::RTree; -use crate::app::network::{ - common::{ - cycleway_tag::CyclewayTag, - ops::{estimated_speed_from_neighbors, find_neighboring_ways, traffic_speed_from_maxspeed}, - way_rtree_entry::WayRTreeEntry, - }, - lts::{ - lts::{Lts, LtsError, MAX_LTS, MIN_LTS}, - ops::{is_non_motorized_way, is_unbikeable_way}, - }, -}; +use crate::common::cycleway_tag::CyclewayTag; +use crate::common::edge_rtree_entry::{EdgeRTreeEntry, find_neighboring_edges}; +use crate::common::ops::{estimated_speed_from_neighbors, traffic_speed_from_maxspeed}; +use crate::lts::lts::{Lts, LtsError, MAX_LTS, MIN_LTS}; +use crate::network_traits::{edge_for_modal_metric::EdgeForModalMetric, spatial_edge::SpatialEdge}; -/// Computes the level of traffic stress for a given way entry. -pub fn compute_lts(rtree: &RTree, entry: &WayRTreeEntry) -> Result { +/// Computes the level of traffic stress for a given edge entry. +pub fn compute_lts( + rtree: &RTree>, + entry: &EdgeRTreeEntry, +) -> Result +where + E: SpatialEdge + EdgeForModalMetric, +{ // Some ways are inherently unsuitable for bikes. - if is_unbikeable_way(&entry.way.highway) { + if entry.edge.is_unbikeable() { return Lts::new(MAX_LTS); } // A highway that is non-motorized is inherently low-stress - if is_non_motorized_way(&entry.way.highway) { + if entry.edge.is_non_motorized() { return Lts::new(MIN_LTS); } let speed = traffic_speed_from_maxspeed(entry).unwrap_or_else(|| { - let neighboring_ways = find_neighboring_ways(entry, rtree); + let neighboring_ways = find_neighboring_edges(entry, rtree); estimated_speed_from_neighbors(entry, &neighboring_ways).unwrap_or(25.0) }); let cycleway_tag = entry - .way - .cycleway - .as_ref() - .map(|tag| CyclewayTag::new(tag)) + .edge + .get_cycleway_tag() .unwrap_or(CyclewayTag::NoDedicatedNoFacilities); - let oneway = entry.way.oneway.as_deref() == Some("yes"); + let oneway = entry.edge.is_oneway(); Lts::from_table_lookup(speed.round() as u8, cycleway_tag, oneway) } @@ -43,7 +41,7 @@ pub fn compute_lts(rtree: &RTree, entry: &WayRTreeEntry) -> Resul #[cfg(test)] mod tests { use super::*; - use crate::app::network::common::cycleway_tag::CyclewayTag; + use crate::common::cycleway_tag::CyclewayTag; #[test] fn dedicated_with_buffer_is_always_lts1() { diff --git a/rust/bambam-osm/src/app/network/lts/lts.rs b/rust/bambam-modal-metrics/src/lts/lts.rs similarity index 97% rename from rust/bambam-osm/src/app/network/lts/lts.rs rename to rust/bambam-modal-metrics/src/lts/lts.rs index 738f53ca..2446482f 100644 --- a/rust/bambam-osm/src/app/network/lts/lts.rs +++ b/rust/bambam-modal-metrics/src/lts/lts.rs @@ -1,4 +1,4 @@ -use crate::app::network::common::cycleway_tag::CyclewayTag; +use crate::common::cycleway_tag::CyclewayTag; pub const MIN_LTS: u8 = 1; // the best LTS score. pub const MAX_LTS: u8 = 4; // the worst LTS score. diff --git a/rust/bambam-osm/src/app/network/lts/mod.rs b/rust/bambam-modal-metrics/src/lts/mod.rs similarity index 72% rename from rust/bambam-osm/src/app/network/lts/mod.rs rename to rust/bambam-modal-metrics/src/lts/mod.rs index ad8a527d..0c4f37da 100644 --- a/rust/bambam-osm/src/app/network/lts/mod.rs +++ b/rust/bambam-modal-metrics/src/lts/mod.rs @@ -1,3 +1,2 @@ pub mod compute_lts; pub mod lts; -pub mod ops; diff --git a/rust/bambam-modal-metrics/src/network_traits/edge_for_modal_metric.rs b/rust/bambam-modal-metrics/src/network_traits/edge_for_modal_metric.rs new file mode 100644 index 00000000..92ce45aa --- /dev/null +++ b/rust/bambam-modal-metrics/src/network_traits/edge_for_modal_metric.rs @@ -0,0 +1,34 @@ +use crate::common::cycleway_tag::CyclewayTag; +/// An edge (graph link) that carries the attributes required to compute +/// modal metrics such as the Walking Comfort Index (WCI). +/// +/// Implement this trait for a map provider's edge/way type +/// (e.g. OpenStreetMap way, Overture Maps segment). +/// +/// The predicates on this trait are "self-contained": they only inspect the +/// edge's own attributes. +pub trait EdgeForModalMetric { + // For both LTS/WCI + /// the posted traffic speed limit for this edge, in miles per hour, if known. + fn get_traffic_speed_limit(&self) -> Option; + /// the cycleway classification for this edge, if the edge carries one. + /// Returns `None` when the edge has no cycleway attribute, in which case + /// the compute layer may infer a score from neighboring edges. + fn get_cycleway_tag(&self) -> Option; + // WCI - only + /// returns true if the edge is walk-eligible based solely on its own attributes. + fn is_walkable(&self) -> bool; + /// returns true if the edge is a low-traffic / low-speed walkable roadway. + fn is_walkable_highway(&self) -> bool; + /// returns true if the edge is a sidewalk. + fn is_sidewalk(&self) -> bool; + /// returns true if the edge is a footway. + fn is_footway(&self) -> bool; + // LTS - only + /// returns true if the edge is unbikeable. + fn is_unbikeable(&self) -> bool; + /// returns true if the edge is non-motorized. + fn is_non_motorized(&self) -> bool; + /// returns true if the edge is oneway. + fn is_oneway(&self) -> bool; +} diff --git a/rust/bambam-modal-metrics/src/network_traits/mod.rs b/rust/bambam-modal-metrics/src/network_traits/mod.rs new file mode 100644 index 00000000..4216a924 --- /dev/null +++ b/rust/bambam-modal-metrics/src/network_traits/mod.rs @@ -0,0 +1,3 @@ +pub mod edge_for_modal_metric; +pub mod spatial_edge; +pub mod vertex_for_modal_metric; diff --git a/rust/bambam-modal-metrics/src/network_traits/spatial_edge.rs b/rust/bambam-modal-metrics/src/network_traits/spatial_edge.rs new file mode 100644 index 00000000..032f11cf --- /dev/null +++ b/rust/bambam-modal-metrics/src/network_traits/spatial_edge.rs @@ -0,0 +1,17 @@ +use geo::LineString; +/// An edge that exposes the geometry and identity required to participate in a +/// spatial index (R-tree) for neighbor-aware modal penalty computations. +/// +/// This is kept separate from [`EdgeForModalMetric`] so that non-spatial +/// callers are not required to provide geometry. +pub trait SpatialEdge { + /// a stable identifier for this edge, used to exclude an edge from its own + /// neighbor set during spatial queries. + fn id(&self) -> String; + /// the linestring geometry of this edge, used to compute its bounding box + /// and centroid for spatial indexing. Returns `None` when the edge has no + /// linestring geometry, in which case it is omitted from the spatial index. + fn linestring(&self) -> Option<&LineString>; + /// the source vertex of the edge. + fn src_vertex_id(&self) -> usize; +} diff --git a/rust/bambam-modal-metrics/src/network_traits/vertex_for_modal_metric.rs b/rust/bambam-modal-metrics/src/network_traits/vertex_for_modal_metric.rs new file mode 100644 index 00000000..90fbb336 --- /dev/null +++ b/rust/bambam-modal-metrics/src/network_traits/vertex_for_modal_metric.rs @@ -0,0 +1,12 @@ +/// A vertex (graph node) that carries the attributes required to compute +/// modal metrics such as the Walking Comfort Index (WCI) or Level of Traffic Stress (LTS). +/// +/// Implement this trait for a map provider's vertex/node type +/// (e.g. OpenStreetMap node, Overture Maps connector). +pub trait VertexForModalMetric { + // WCI - only + /// returns true if the vertex has a traffic signal. + fn has_traffic_signals(&self) -> bool; + /// returns true if the vertex has a stop sign. + fn has_stop_sign(&self) -> bool; +} diff --git a/rust/bambam-modal-metrics/src/wci/compute_wci.rs b/rust/bambam-modal-metrics/src/wci/compute_wci.rs new file mode 100644 index 00000000..acf1226c --- /dev/null +++ b/rust/bambam-modal-metrics/src/wci/compute_wci.rs @@ -0,0 +1,295 @@ +use rstar::RTree; + +use crate::common::edge_rtree_entry::{EdgeRTreeEntry, find_neighboring_edges}; +use crate::network_traits::{ + edge_for_modal_metric::EdgeForModalMetric, spatial_edge::SpatialEdge, + vertex_for_modal_metric::VertexForModalMetric, +}; +use crate::wci::ops::way_is_walk_eligible; +use crate::wci::wci::{MAX_WCI, MIN_WCI, Wci, WciError}; + +/// The Walking Comfort Index (WCI) components for a way, including total WCI +/// and all components that went into the total WCI. +#[derive(Default)] +pub struct WciComponents { + pub total: Wci, + pub walkability: Option, + pub traffic_speed_comfort: Option, + pub cycleway_comfort: Option, + pub traffic_signal_comfort: Option, +} + +impl WciComponents { + pub fn min_wci_score() -> Result { + Ok(Self { + total: Wci::new(MIN_WCI)?, + ..Default::default() + }) + } + + pub fn max_wci_score() -> Result { + Ok(Self { + total: Wci::new(MAX_WCI)?, + ..Default::default() + }) + } +} + +/// Computes the walking comfort index (WCI) score for a given edge (as EdgeRTreeEntry), +/// the edge's source vertex, and the R-tree of all edges in the network. +pub fn compute_wci( + rtree: &RTree>, + entry: &EdgeRTreeEntry, + src_node: Option<&V>, +) -> Result +where + E: SpatialEdge + EdgeForModalMetric, + V: VertexForModalMetric, +{ + let way_is_walk_eligible = way_is_walk_eligible(rtree, entry); + + let neighboring_ways = find_neighboring_edges(entry, rtree); + + if !way_is_walk_eligible { + // Total WCI score = Min WCI score (unwalkable roadway) + WciComponents::min_wci_score() + } else if entry.edge.is_footway() || (neighboring_ways.is_empty() && entry.edge.is_sidewalk()) { + // Total WCI score = Max WCI score (footway or sidewalk with no adjacent ways) + WciComponents::max_wci_score() + } else { + let walkability = Wci::walkability(&entry.edge); + + let cycleway_comfort = Wci::cycleway_comfort(entry, &neighboring_ways); + + let traffic_speed_comfort = Wci::traffic_speed_comfort(entry, &neighboring_ways); + + let traffic_signal_comfort = src_node + .map(|v| Wci::traffic_signal_comfort(v)) + .unwrap_or_else(|| Wci::ZERO); + + // Total = Sum of WCI component scores + Ok(WciComponents { + total: &walkability + + &traffic_speed_comfort + + &cycleway_comfort + + &traffic_signal_comfort, + walkability: Some(walkability), + traffic_speed_comfort: Some(traffic_speed_comfort), + cycleway_comfort: Some(cycleway_comfort), + traffic_signal_comfort: Some(traffic_signal_comfort), + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::common::cycleway_tag::CyclewayTag; + use geo::LineString; + + #[derive(Clone)] + struct TestEdge { + id: usize, + walkable: bool, + sidewalk: bool, + footway: bool, + speed_limit: Option, + cycleway: Option, + linestring: LineString, + } + + impl SpatialEdge for TestEdge { + fn id(&self) -> String { + self.id.to_string() + } + fn src_vertex_id(&self) -> usize { + 0 + } + fn linestring(&self) -> std::option::Option<&geo::LineString> { + Some(&self.linestring) + } + } + + impl EdgeForModalMetric for TestEdge { + fn get_traffic_speed_limit(&self) -> Option { + self.speed_limit + } + fn get_cycleway_tag(&self) -> Option { + self.cycleway.clone() + } + fn is_walkable(&self) -> bool { + self.walkable + } + fn is_walkable_highway(&self) -> bool { + false + } + fn is_sidewalk(&self) -> bool { + self.sidewalk + } + fn is_footway(&self) -> bool { + self.footway + } + fn is_unbikeable(&self) -> bool { + false + } + fn is_non_motorized(&self) -> bool { + false + } + fn is_oneway(&self) -> bool { + false + } + } + + struct TestVertex { + has_signals: bool, + has_stop: bool, + } + + impl VertexForModalMetric for TestVertex { + fn has_traffic_signals(&self) -> bool { + self.has_signals + } + fn has_stop_sign(&self) -> bool { + self.has_stop + } + } + + #[test] + fn test_min_wci() { + let edge = TestEdge { + id: 42, + walkable: false, + sidewalk: false, + footway: false, + speed_limit: Some(65), + cycleway: None, + linestring: LineString::from(vec![(-105.170016, 39.773648), (-105.165381, 39.774176)]), + }; + let src_vertex = TestVertex { + has_signals: false, + has_stop: false, + }; + + let entry = EdgeRTreeEntry::new(edge).unwrap(); + let rtree: RTree> = RTree::new(); + + let wci = compute_wci(&rtree, &entry, Some(&src_vertex)).unwrap(); + assert_eq!(wci.total, Wci::new(MIN_WCI).unwrap()); + } + + #[test] + fn test_max_wci() { + let edge = TestEdge { + id: 42, + walkable: true, + sidewalk: false, + footway: true, + speed_limit: None, + cycleway: None, + linestring: LineString::from(vec![(-105.170016, 39.773648), (-105.165381, 39.774176)]), + }; + let src_vertex = TestVertex { + has_signals: false, + has_stop: false, + }; + + let entry = EdgeRTreeEntry::new(edge).unwrap(); + let rtree: RTree> = RTree::new(); + + let wci = compute_wci(&rtree, &entry, Some(&src_vertex)).unwrap(); + assert_eq!(wci.total, Wci::new(MAX_WCI).unwrap()); + } + + #[test] + fn test_positive_wci() { + let edge = TestEdge { + id: 42, + walkable: true, + sidewalk: false, + footway: false, + speed_limit: Some(25), + cycleway: Some(CyclewayTag::NoDedicatedWithFacilities), + linestring: LineString::from(vec![(-105.170016, 39.773648), (-105.165381, 39.774176)]), + }; + let src_vertex = TestVertex { + has_signals: false, + has_stop: true, + }; + + let entry = EdgeRTreeEntry::new(edge).unwrap(); + let rtree: RTree> = RTree::new(); + + let wci = compute_wci(&rtree, &entry, Some(&src_vertex)).unwrap(); + assert_eq!(wci.traffic_speed_comfort, Some(Wci::new(2).unwrap())); + assert_eq!(wci.traffic_signal_comfort, Some(Wci::new(1).unwrap())); + assert_eq!(wci.cycleway_comfort, Some(Wci::new(0).unwrap())); + assert_eq!(wci.walkability, Some(Wci::new(-2).unwrap())); + assert!(wci.total > Wci::new(0).unwrap()); + } + + #[test] + fn test_negative_wci() { + let edge = TestEdge { + id: 42, + walkable: true, + sidewalk: false, + footway: false, + speed_limit: Some(45), + cycleway: None, + linestring: LineString::from(vec![(-105.170016, 39.773648), (-105.165381, 39.774176)]), + }; + let src_vertex = TestVertex { + has_signals: false, + has_stop: true, + }; + + let entry = EdgeRTreeEntry::new(edge).unwrap(); + let rtree: RTree> = RTree::new(); + + let wci = compute_wci(&rtree, &entry, Some(&src_vertex)).unwrap(); + assert_eq!(wci.traffic_speed_comfort, Some(Wci::new(-1).unwrap())); + assert_eq!(wci.traffic_signal_comfort, Some(Wci::new(1).unwrap())); + assert_eq!(wci.cycleway_comfort, Some(Wci::new(-2).unwrap())); + assert_eq!(wci.walkability, Some(Wci::new(-2).unwrap())); + assert_eq!(wci.total, Wci::new(-4).unwrap()); + assert!(wci.total < Wci::new(0).unwrap()); + } + + #[test] + fn test_neighbor_wci_contribution() { + const WAY_SCORE_NO_NEIGHBORS: i32 = -4; + let edge = TestEdge { + id: 42, + walkable: true, + sidewalk: false, + footway: false, + speed_limit: Some(45), + cycleway: None, + linestring: LineString::from(vec![(-105.170016, 39.773648), (-105.165381, 39.774176)]), + }; + + let neighbor = TestEdge { + id: 43, + walkable: true, + sidewalk: false, + footway: false, + speed_limit: Some(25), + cycleway: Some(CyclewayTag::DedicatedNoBuffer), + linestring: LineString::from(vec![(-105.168085, 39.773772), (-105.166755, 39.773937)]), + }; + + let src_vertex = TestVertex { + has_signals: false, + has_stop: true, + }; + + let entry = EdgeRTreeEntry::new(edge).unwrap(); + let neighbor_entry = EdgeRTreeEntry::new(neighbor).unwrap(); + let mut rtree: RTree> = RTree::new(); + + rtree.insert(entry.clone()); + rtree.insert(neighbor_entry); + let wci = compute_wci(&rtree, &entry, Some(&src_vertex)).unwrap(); + assert!(wci.total > Wci::new(WAY_SCORE_NO_NEIGHBORS).unwrap()); + } +} diff --git a/rust/bambam-osm/src/app/network/wci/mod.rs b/rust/bambam-modal-metrics/src/wci/mod.rs similarity index 70% rename from rust/bambam-osm/src/app/network/wci/mod.rs rename to rust/bambam-modal-metrics/src/wci/mod.rs index 2cc0f8e7..dc309feb 100644 --- a/rust/bambam-osm/src/app/network/wci/mod.rs +++ b/rust/bambam-modal-metrics/src/wci/mod.rs @@ -1,6 +1,4 @@ pub mod compute_wci; -mod ops; +pub mod ops; pub mod wci; -pub use compute_wci::compute_wci; -pub use wci::Wci; const NO_CYCLEWAY_FOUND_SCORE: i32 = -2; // If there is no cycleway found for a way, cycle component of WCI. diff --git a/rust/bambam-modal-metrics/src/wci/ops.rs b/rust/bambam-modal-metrics/src/wci/ops.rs new file mode 100644 index 00000000..df617799 --- /dev/null +++ b/rust/bambam-modal-metrics/src/wci/ops.rs @@ -0,0 +1,85 @@ +use geo::{Distance, Euclidean}; + +use crate::common::cycleway_tag::CyclewayTag; +use crate::common::edge_rtree_entry::{EdgeRTreeEntry, MIN_DISTANCE_RTREE_NEIGHBOR}; +use crate::common::ops::estimated_speed_from_neighbors; +use crate::network_traits::{edge_for_modal_metric::EdgeForModalMetric, spatial_edge::SpatialEdge}; +use crate::wci::NO_CYCLEWAY_FOUND_SCORE; +use rstar::RTree; + +/// Converts a cycleway tag classification to a numerical comfort index. +pub fn cycleway_comfort_from_tag(tag: &CyclewayTag) -> i32 { + match tag { + CyclewayTag::DedicatedWithBuffer => 2, + CyclewayTag::DedicatedNoBuffer => 2, + CyclewayTag::NoDedicatedWithFacilities => 0, + CyclewayTag::NoDedicatedNoFacilities => -2, + } +} + +/// Computes the cycleway comfort index from neighboring ways +pub fn cycleway_comfort_from_neighbors( + entry: &EdgeRTreeEntry, + neighboring_ways: &[&EdgeRTreeEntry], +) -> i32 { + let mut total_distance: f32 = 0.0; + let mut scored: Vec<(i32, f32)> = Vec::new(); + + for neighbor in neighboring_ways { + let distance = Euclidean.distance(entry.centroid, neighbor.centroid); + total_distance += distance; + if let Some(tag) = neighbor.edge.get_cycleway_tag() { + scored.push((cycleway_comfort_from_tag(&tag), distance)); + } + } + + if scored.is_empty() || total_distance == 0.0 { + return NO_CYCLEWAY_FOUND_SCORE; + } + + let weighted: f32 = scored + .iter() + .map(|&(score, d)| score as f32 * (d / total_distance)) + .sum(); + + weighted as i32 +} + +/// Converts a speed in MPH to a numerical comfort index. +pub fn traffic_speed_comfort_from_speed(speed_mph: i32) -> i32 { + if speed_mph <= 25 { + 2 + } else if speed_mph > 25 && speed_mph <= 30 { + 1 + } else if speed_mph > 30 && speed_mph <= 40 { + 0 + } else if speed_mph > 40 && speed_mph <= 45 { + -1 + } else { + -2 + } +} + +/// Computes a weighted traffic speed comfort index from nearby ways if the +/// way of interest does not have a speed limit. +pub fn traffic_speed_comfort_from_neighbors( + entry: &EdgeRTreeEntry, + neighboring_ways: &[&EdgeRTreeEntry], +) -> i32 { + let speed_mph = estimated_speed_from_neighbors(entry, neighboring_ways).unwrap_or(0.0); + traffic_speed_comfort_from_speed(speed_mph.round() as i32) +} + +/// Determines if the edge is walk-eligible based on its own attributes or nearby sidewalk edges. +pub fn way_is_walk_eligible( + rtree: &RTree>, + entry: &EdgeRTreeEntry, +) -> bool { + entry.edge.is_walkable() + || rtree + .locate_within_distance( + [entry.centroid.x(), entry.centroid.y()], + MIN_DISTANCE_RTREE_NEIGHBOR, + ) + .any(|neighbor| neighbor.edge.is_sidewalk()) +} diff --git a/rust/bambam-osm/src/app/network/wci/wci.rs b/rust/bambam-modal-metrics/src/wci/wci.rs similarity index 71% rename from rust/bambam-osm/src/app/network/wci/wci.rs rename to rust/bambam-modal-metrics/src/wci/wci.rs index ea1cee67..5815f1e6 100644 --- a/rust/bambam-osm/src/app/network/wci/wci.rs +++ b/rust/bambam-modal-metrics/src/wci/wci.rs @@ -1,12 +1,12 @@ -use num_traits::CheckedAdd; - use super::ops::*; use crate::{ - app::network::common::{ - cycleway_tag::CyclewayTag, ops::traffic_speed_from_maxspeed, way_rtree_entry::WayRTreeEntry, + common::{edge_rtree_entry::EdgeRTreeEntry, ops::traffic_speed_from_maxspeed}, + network_traits::{ + edge_for_modal_metric::EdgeForModalMetric, spatial_edge::SpatialEdge, + vertex_for_modal_metric::VertexForModalMetric, }, - model::osm::graph::{OsmNodeDataSerializable, OsmWayDataSerializable}, }; +use uom::num_traits::CheckedAdd; pub const MIN_WCI: i32 = -6; pub const MAX_WCI: i32 = 9; @@ -79,8 +79,8 @@ impl Wci { } /// Computes the walkability `Wci` for a way. - pub fn walkability(way: &OsmWayDataSerializable) -> Wci { - if way_is_sidewalk(way) || way_is_footway(way) { + pub fn walkability(edge: &dyn EdgeForModalMetric) -> Wci { + if edge.is_sidewalk() || edge.is_footway() { Wci(2) } else { Wci(-2) @@ -88,10 +88,10 @@ impl Wci { } /// Computes the traffic signal `Wci` for a way. - pub fn traffic_signal_comfort(src_node: &OsmNodeDataSerializable) -> Wci { - if has_traffic_signals(src_node) { + pub fn traffic_signal_comfort(src_vertex: &dyn VertexForModalMetric) -> Wci { + if src_vertex.has_traffic_signals() { Wci(2) - } else if has_stop_sign(src_node) { + } else if src_vertex.has_stop_sign() { Wci(1) } else { Wci(0) @@ -99,16 +99,22 @@ impl Wci { } /// Computes the cycleway `Wci` for a way. - pub fn cycleway_comfort(entry: &WayRTreeEntry, neighboring_ways: &Vec<&WayRTreeEntry>) -> Wci { + pub fn cycleway_comfort( + entry: &EdgeRTreeEntry, + neighboring_ways: &Vec<&EdgeRTreeEntry>, + ) -> Wci { // if the way has a cycleway tag (string), use that, otherwise, use neighbors - match &entry.way.cycleway { - Some(tag) => Wci(cycleway_comfort_from_tag(&CyclewayTag::new(tag))), + match &entry.edge.get_cycleway_tag() { + Some(tag) => Wci(cycleway_comfort_from_tag(tag)), None => Wci(cycleway_comfort_from_neighbors(entry, neighboring_ways)), } } /// Computes the traffic speed `Wci` for a way - pub fn traffic_speed_comfort(entry: &WayRTreeEntry, neighbors: &Vec<&WayRTreeEntry>) -> Wci { + pub fn traffic_speed_comfort( + entry: &EdgeRTreeEntry, + neighbors: &Vec<&EdgeRTreeEntry>, + ) -> Wci { Wci(traffic_speed_from_maxspeed(entry) .map(|speed_mph| traffic_speed_comfort_from_speed(speed_mph.round() as i32)) .unwrap_or_else(|| traffic_speed_comfort_from_neighbors(entry, neighbors))) diff --git a/rust/bambam-osm/Cargo.toml b/rust/bambam-osm/Cargo.toml index 92277f1f..3e4c13b7 100644 --- a/rust/bambam-osm/Cargo.toml +++ b/rust/bambam-osm/Cargo.toml @@ -15,6 +15,7 @@ categories = ["command-line-utilities", "science", "science::geo"] bamcensus = { workspace = true } bamcensus-acs = { workspace = true } bamcensus-core = { workspace = true } +bambam-modal-metrics = { workspace = true } clap = { workspace = true } config = { workspace = true } diff --git a/rust/bambam-osm/src/app/mod.rs b/rust/bambam-osm/src/app/mod.rs deleted file mode 100644 index a61610bd..00000000 --- a/rust/bambam-osm/src/app/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod network; diff --git a/rust/bambam-osm/src/app/network/common/mod.rs b/rust/bambam-osm/src/app/network/common/mod.rs deleted file mode 100644 index 96450fdb..00000000 --- a/rust/bambam-osm/src/app/network/common/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod bulk_compute_modal_metric; -pub mod cycleway_tag; -pub mod modal_metric; -pub mod ops; -pub mod way_rtree_entry; -pub const MIN_DISTANCE_RTREE_NEIGHBOR: f32 = 0.0001378; diff --git a/rust/bambam-osm/src/app/network/common/ops.rs b/rust/bambam-osm/src/app/network/common/ops.rs deleted file mode 100644 index a3816500..00000000 --- a/rust/bambam-osm/src/app/network/common/ops.rs +++ /dev/null @@ -1,107 +0,0 @@ -use super::MIN_DISTANCE_RTREE_NEIGHBOR; -use crate::app::network::common::way_rtree_entry::WayRTreeEntry; -use crate::model::osm::graph::OsmNodeDataSerializable; -use crate::model::osm::graph::OsmWayDataSerializable; -use geo::Distance; -use geo::Euclidean; -use rstar::RTree; -use std::error::Error; - -/// Find the neighboring ways in the RTree from a given way centroid -pub fn find_neighboring_ways<'a>( - query_entry: &WayRTreeEntry, - rtree: &'a RTree, -) -> Vec<&'a WayRTreeEntry> { - rtree - .locate_within_distance( - [query_entry.centroid.x(), query_entry.centroid.y()], - MIN_DISTANCE_RTREE_NEIGHBOR, - ) - .filter(|entry_in_rtree| entry_in_rtree.way.osmid != query_entry.way.osmid) - .collect() -} - -/// Load ways from a CSV file and create R-tree entries for each way. -/// TODO: incorporate Overture's way attributes and move the logic for WayRTreeEntry up to bambam-core. -pub fn load_way_rtree_entries( - edges_file: &str, - nodes: &[OsmNodeDataSerializable], -) -> Result, Box> { - let mut edge_reader = csv::Reader::from_path(edges_file)?; - let mut way_entries = Vec::new(); - - for record in edge_reader.deserialize::() { - let way = match record { - Ok(way) => way, - Err(err) => { - eprintln!("Error reading row: {err}"); - continue; - } - }; - - if nodes.get(way.src_vertex_id.0).is_none() { - eprintln!( - "Warning: source vertex {} not found for way {}; skipping", - way.src_vertex_id.0, way.osmid - ); - continue; - } - - let osmid = way.osmid; - let Some(entry) = WayRTreeEntry::new(way) else { - eprintln!("Warning: could not create R-tree entry for way {osmid}"); - continue; - }; - - way_entries.push(entry); - } - - Ok(way_entries) -} - -/// Converts from whichever OSM maxspeed unit to MPH -pub fn traffic_speed_from_maxspeed(entry: &WayRTreeEntry) -> Option { - match entry.way.get_speed("maxspeed_raw", true) { - Ok(Some(velocity)) => { - let speed_mph = velocity.get::(); - Some(speed_mph as f32) - } - _ => None, - } -} - -/// Computes a weighted estimated speed (in MPH) from nearby ways -pub fn estimated_speed_from_neighbors( - entry: &WayRTreeEntry, - neighboring_ways: &[&WayRTreeEntry], -) -> Option { - let speeds_and_distances: Vec<(f32, f32)> = neighboring_ways - .iter() - .filter_map(|neighbor| { - traffic_speed_from_maxspeed(neighbor).map(|speed| { - let distance = Euclidean.distance(entry.centroid, neighbor.centroid); - (speed, distance) - }) - }) - .collect(); - - if speeds_and_distances.is_empty() { - return None; - } - - let sum_distances: f32 = speeds_and_distances - .iter() - .map(|(_, distance)| *distance) - .sum(); - - if sum_distances == 0.0 { - return None; - } - - let weighted_speed: f32 = speeds_and_distances - .iter() - .map(|(speed, distance)| speed * distance / sum_distances) - .sum(); - - Some(weighted_speed) -} diff --git a/rust/bambam-osm/src/app/network/lts/ops.rs b/rust/bambam-osm/src/app/network/lts/ops.rs deleted file mode 100644 index 54d82245..00000000 --- a/rust/bambam-osm/src/app/network/lts/ops.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::model::feature::highway::Highway; - -pub fn is_non_motorized_way(highway: &Highway) -> bool { - matches!( - highway, - Highway::Cycleway - | Highway::Path - | Highway::Footway - | Highway::Pedestrian - | Highway::LivingStreet - ) -} - -pub fn is_unbikeable_way(highway: &Highway) -> bool { - matches!( - highway, - // Major roadways and their links - Highway::Motorway | Highway::Trunk | Highway::MotorwayLink | Highway::TrunkLink - ) -} diff --git a/rust/bambam-osm/src/app/network/wci/compute_wci.rs b/rust/bambam-osm/src/app/network/wci/compute_wci.rs deleted file mode 100644 index 3c936c01..00000000 --- a/rust/bambam-osm/src/app/network/wci/compute_wci.rs +++ /dev/null @@ -1,296 +0,0 @@ -use crate::{ - app::network::{ - common::{ops::find_neighboring_ways, way_rtree_entry::WayRTreeEntry}, - wci::{ - ops::*, - wci::{Wci, WciError, MAX_WCI, MIN_WCI}, - }, - }, - model::osm::graph::OsmNodeDataSerializable, -}; -use rstar::RTree; - -/// The Walking Comfort Index (WCI) components for a way, including total WCI -/// and all components that went into the total WCI. -#[derive(Default)] -pub struct WciComponents { - pub total: Wci, - pub walkability: Option, - pub traffic_speed_comfort: Option, - pub cycleway_comfort: Option, - pub traffic_signal_comfort: Option, -} - -impl WciComponents { - pub fn min_wci_score() -> Result { - Ok(Self { - total: Wci::new(MIN_WCI)?, - ..Default::default() - }) - } - - pub fn max_wci_score() -> Result { - Ok(Self { - total: Wci::new(MAX_WCI)?, - ..Default::default() - }) - } -} - -/// Computes the walking comfort index (WCI) score for a given way (as WayRTreeEntry), -/// the way's source node, and the R-tree of all ways in the network. -pub fn compute_wci( - rtree: &RTree, - entry: &WayRTreeEntry, - src_node: Option<&OsmNodeDataSerializable>, -) -> Result { - let way_is_walk_eligible = way_is_walk_eligible(rtree, entry); - - let neighboring_ways = find_neighboring_ways(entry, rtree); - - if !way_is_walk_eligible { - // Total WCI score = Min WCI score (unwalkable roadway) - WciComponents::min_wci_score() - } else if way_is_footway(&entry.way) - || (neighboring_ways.is_empty() && way_is_sidewalk(&entry.way)) - { - // Total WCI score = Max WCI score (footway or sidewalk with no adjacent ways) - WciComponents::max_wci_score() - } else { - let walkability = Wci::walkability(&entry.way); - - let cycleway_comfort = Wci::cycleway_comfort(entry, &neighboring_ways); - - let traffic_speed_comfort = Wci::traffic_speed_comfort(entry, &neighboring_ways); - - let traffic_signal_comfort = src_node - .map(Wci::traffic_signal_comfort) - .unwrap_or_else(|| Wci::ZERO); - - // Total = Sum of WCI component scores - Ok(WciComponents { - total: &walkability - + &traffic_speed_comfort - + &cycleway_comfort - + &traffic_signal_comfort, - walkability: Some(walkability), - traffic_speed_comfort: Some(traffic_speed_comfort), - cycleway_comfort: Some(cycleway_comfort), - traffic_signal_comfort: Some(traffic_signal_comfort), - }) - } -} - -#[cfg(test)] -mod test { - use super::compute_wci; - use super::WciComponents; - use crate::{ - app::network::{ - common::way_rtree_entry::WayRTreeEntry, - wci::{wci::MAX_WCI, wci::MIN_WCI, Wci}, - }, - model::osm::graph::{OsmNodeDataSerializable, OsmWayDataSerializable}, - }; - use rstar::RTree; - use serde_json; - - /// Unwalkable highway gives the minimum WCI score - #[test] - fn test_min_wci() { - let way: OsmWayDataSerializable = serde_json::from_str( - r#"{ - "osmid": 42, - "src_vertex_id": 0, - "dst_vertex_id": 1, - "highway": "motorway", - "maxspeed_raw": "65 mph", - "linestring": "LINESTRING (-105.170016 39.773648, -105.165381 39.774176)", - "length_meters": 400.0 - }"#, - ) - .unwrap(); - - let src_vertex: OsmNodeDataSerializable = serde_json::from_str( - r#"{ - "osmid": 0, - "x": -105.170016, - "y": 39.773648 - }"#, - ) - .unwrap(); - - let entry = WayRTreeEntry::new(way).unwrap(); - let rtree: RTree = RTree::new(); // just need this to pass into wci, not using it. - - let wci: WciComponents = compute_wci(&rtree, &entry, Some(&src_vertex)).unwrap(); - assert_eq!(wci.total, Wci::new(MIN_WCI).unwrap()); - } - - /// A footway gives the max WCI score. - #[test] - fn test_max_wci() { - let way: OsmWayDataSerializable = serde_json::from_str( - r#"{ - "osmid": 42, - "src_vertex_id": 0, - "dst_vertex_id": 1, - "highway": "footway", - "footway": "alley", - "maxspeed_raw": "", - "linestring": "LINESTRING (-105.170016 39.773648, -105.165381 39.774176)", - "length_meters": 400.0 - }"#, - ) - .unwrap(); - - let src_vertex: OsmNodeDataSerializable = serde_json::from_str( - r#"{ - "osmid": 0, - "x": -105.170016, - "y": 39.773648 - }"#, - ) - .unwrap(); - - let entry = WayRTreeEntry::new(way).unwrap(); - let rtree: RTree = RTree::new(); // just need this to pass into wci, not using it. - - let wci: WciComponents = compute_wci(&rtree, &entry, Some(&src_vertex)).unwrap(); - assert_eq!(wci.total, Wci::new(MAX_WCI).unwrap()); - } - - // a residential roadway with speed limit 25mph, a shared-lane - // cycleway, and a stop sign at the source node should have a positive wci score - #[test] - fn test_positive_wci() { - let way: OsmWayDataSerializable = serde_json::from_str( - r#"{ - "osmid": 42, - "src_vertex_id": 0, - "dst_vertex_id": 1, - "highway": "residential", - "cycleway": "shared_lane", - "maxspeed_raw": "25 mph", - "linestring": "LINESTRING (-105.170016 39.773648, -105.165381 39.774176)", - "length_meters": 400.0 - }"#, - ) - .unwrap(); - - let src_vertex: OsmNodeDataSerializable = serde_json::from_str( - r#"{ - "osmid": 0, - "x": -105.170016, - "y": 39.773648, - "highway": "stop" - }"#, - ) - .unwrap(); - - let entry = WayRTreeEntry::new(way).unwrap(); - let rtree: RTree = RTree::new(); // just need this to pass into wci, not using it. - - // compute wci for the residential highway with nearby sidewalk - let wci: WciComponents = compute_wci(&rtree, &entry, Some(&src_vertex)).unwrap(); - assert_eq!(wci.traffic_speed_comfort, Some(Wci::new(2).unwrap())); - assert_eq!(wci.traffic_signal_comfort, Some(Wci::new(1).unwrap())); - assert_eq!(wci.cycleway_comfort, Some(Wci::new(0).unwrap())); - assert_eq!(wci.walkability, Some(Wci::new(-2).unwrap())); - assert!(wci.total > Wci::new(0).unwrap()); - } - - // A residential highway with speed limit 45 mph and a stop sign at the source node - // should have a negative WCI score - #[test] - fn test_negative_wci() { - let way: OsmWayDataSerializable = serde_json::from_str( - r#"{ - "osmid": 42, - "src_vertex_id": 0, - "dst_vertex_id": 1, - "highway": "residential", - "maxspeed_raw": "45 mph", - "linestring": "LINESTRING (-105.170016 39.773648, -105.165381 39.774176)", - "length_meters": 400.0 - }"#, - ) - .unwrap(); - - let src_vertex: OsmNodeDataSerializable = serde_json::from_str( - r#"{ - "osmid": 0, - "x": -105.170016, - "y": 39.773648, - "highway": "stop" - }"#, - ) - .unwrap(); - - let entry = WayRTreeEntry::new(way).unwrap(); - let rtree: RTree = RTree::new(); // just need this to pass into wci, not using it. - - // compute wci - let wci: WciComponents = compute_wci(&rtree, &entry, Some(&src_vertex)).unwrap(); - assert_eq!(wci.traffic_speed_comfort, Some(Wci::new(-1).unwrap())); - assert_eq!(wci.traffic_signal_comfort, Some(Wci::new(1).unwrap())); - assert_eq!(wci.cycleway_comfort, Some(Wci::new(-2).unwrap())); - assert_eq!(wci.walkability, Some(Wci::new(-2).unwrap())); - assert_eq!(wci.total, Wci::new(-4).unwrap()); - assert!(wci.total < Wci::new(0).unwrap()); - } - - /// A residential highway with a bad score get's its - /// score buffed by a neighboring road with cycleway and low speed limit - #[test] - fn test_neighbor_wci_contribution() { - const WAY_SCORE_NO_NEIGHBORS: i32 = -4; // from the previous test - let way: OsmWayDataSerializable = serde_json::from_str( - r#"{ - "osmid": 42, - "src_vertex_id": 0, - "dst_vertex_id": 1, - "highway": "residential", - "maxspeed_raw": "45 mph", - "linestring": "LINESTRING (-105.170016 39.773648, -105.165381 39.774176)", - "length_meters": 400.0 - }"#, - ) - .unwrap(); - - // This neighbor has a cycleway, and a low speed limit, so it's - // weighted score should contribute positively to the query's score - let neighbor: OsmWayDataSerializable = serde_json::from_str( - r#"{ - "osmid": 43, - "src_vertex_id": 2, - "dst_vertex_id": 3, - "highway": "residential", - "maxspeed_raw": "25 mph", - "cycleway": "lane", - "linestring": "LINESTRING (-105.168085 39.773772, -105.166755 39.773937)", - "length_meters": 100 - }"#, - ) - .unwrap(); - - let src_vertex: OsmNodeDataSerializable = serde_json::from_str( - r#"{ - "osmid": 0, - "x": -105.170016, - "y": 39.773648, - "highway": "stop" - }"#, - ) - .unwrap(); - - let entry = WayRTreeEntry::new(way).unwrap(); - let neighbor_entry = WayRTreeEntry::new(neighbor).unwrap(); - let mut rtree: RTree = RTree::new(); - - rtree.insert(entry.clone()); - rtree.insert(neighbor_entry); - let wci = compute_wci(&rtree, &entry, Some(&src_vertex)).unwrap(); - assert!(wci.total > Wci::new(WAY_SCORE_NO_NEIGHBORS).unwrap()); - } -} diff --git a/rust/bambam-osm/src/app/network/wci/ops.rs b/rust/bambam-osm/src/app/network/wci/ops.rs deleted file mode 100644 index d9382cfa..00000000 --- a/rust/bambam-osm/src/app/network/wci/ops.rs +++ /dev/null @@ -1,154 +0,0 @@ -use super::NO_CYCLEWAY_FOUND_SCORE; -use crate::app::network::common::cycleway_tag::CyclewayTag::{ - self, DedicatedNoBuffer, DedicatedWithBuffer, NoDedicatedNoFacilities, - NoDedicatedWithFacilities, -}; -use crate::app::network::common::ops::estimated_speed_from_neighbors; -use crate::app::network::common::way_rtree_entry::WayRTreeEntry; -use crate::app::network::common::MIN_DISTANCE_RTREE_NEIGHBOR; -use crate::model::feature::highway::Highway; -use crate::model::osm::graph::{OsmNodeDataSerializable, OsmWayDataSerializable}; -use geo::{Distance, Euclidean}; -use rstar::RTree; - -/// Determines if a way is walk-eligible based on sidewalk/footway attributes or highway type. -/// -/// Args: -/// - way: the OsmWayDataSerializable to check -fn is_walkable(way: &OsmWayDataSerializable) -> bool { - let is_sidewalk = way_is_sidewalk(way); - - let is_footway = way_is_footway(way); - - let is_walkable_highway = way_is_walkable_highway(way); - - is_sidewalk || is_footway || is_walkable_highway -} - -/// Determines if the way is walk-eligible based on it's OSM attributes. -/// If the way is not walk-eligible, checks if any neighboring ways within a distance of 15 meters are walk-eligible. -/// -/// Args: -/// - `rtree`: RTree of all ways in the network -/// - `entry`: The way of interest (as WayRTreeEntry) -pub fn way_is_walk_eligible(rtree: &RTree, entry: &WayRTreeEntry) -> bool { - is_walkable(&entry.way) // check the way itself - || rtree // check neighboring ways - .locate_within_distance([entry.centroid.x(), entry.centroid.y()], MIN_DISTANCE_RTREE_NEIGHBOR) - .any(|neighbor| way_is_sidewalk(&neighbor.way)) -} - -// Checks if the way is a sidewalk -pub fn way_is_sidewalk(way: &OsmWayDataSerializable) -> bool { - way.sidewalk - .as_ref() - .is_some_and(|s| s != "no" && s != "none") - || way.footway == Some("sidewalk".to_string()) -} - -/// Checks if the way is a footway -pub fn way_is_footway(way: &OsmWayDataSerializable) -> bool { - way.footway - .as_ref() - .is_some_and(|s| s != "no" && s != "none") -} - -/// A walkable highway is a normal roadway that is typically low traffic or -/// low speed. -pub fn way_is_walkable_highway(way: &OsmWayDataSerializable) -> bool { - matches!( - way.highway, - Highway::Residential - | Highway::Unclassified - | Highway::LivingStreet - | Highway::Service - | Highway::Pedestrian - | Highway::Trailhead - | Highway::Track - | Highway::Footway - | Highway::Bridleway - | Highway::Steps - | Highway::Corridor - | Highway::Path - | Highway::Elevator - ) -} - -/// returns true if the node has a stop sign -pub fn has_stop_sign(node: &OsmNodeDataSerializable) -> bool { - node.clone() - .highway - .as_ref() - .is_some_and(|highway| highway.contains("stop")) -} - -/// returns true if the node has a traffic light -pub fn has_traffic_signals(node: &OsmNodeDataSerializable) -> bool { - node.clone() - .highway - .as_ref() - .is_some_and(|highway| highway.contains("traffic_signals")) -} - -/// Converts a cycleway tag classification to a numerical comfort index. -pub fn cycleway_comfort_from_tag(tag: &CyclewayTag) -> i32 { - match tag { - DedicatedWithBuffer => 2, - DedicatedNoBuffer => 2, - NoDedicatedWithFacilities => 0, - NoDedicatedNoFacilities => -2, - } -} - -/// Computes the cycleway comfort index from neighboring ways -pub fn cycleway_comfort_from_neighbors( - entry: &WayRTreeEntry, - neighboring_ways: &[&WayRTreeEntry], -) -> i32 { - let mut total_distance: f32 = 0.0; - let mut scored: Vec<(i32, f32)> = Vec::new(); - - for neighbor in neighboring_ways { - let distance = Euclidean.distance(entry.centroid, neighbor.centroid); - total_distance += distance; - if let Some(tag) = neighbor.way.cycleway.as_ref() { - scored.push((cycleway_comfort_from_tag(&CyclewayTag::new(tag)), distance)); - } - } - - if scored.is_empty() || total_distance == 0.0 { - return NO_CYCLEWAY_FOUND_SCORE; - } - - let weighted: f32 = scored - .iter() - .map(|&(score, d)| score as f32 * (d / total_distance)) - .sum(); - - weighted as i32 -} - -/// Converts a speed in MPH to a numerical comfort index. -pub fn traffic_speed_comfort_from_speed(speed_mph: i32) -> i32 { - if speed_mph <= 25 { - 2 - } else if speed_mph > 25 && speed_mph <= 30 { - 1 - } else if speed_mph > 30 && speed_mph <= 40 { - 0 - } else if speed_mph > 40 && speed_mph <= 45 { - -1 - } else { - -2 - } -} - -/// Computes a weighted traffic speed comfort index from nearby ways if the -/// way of interest does not have a speed limit -pub fn traffic_speed_comfort_from_neighbors( - entry: &WayRTreeEntry, - neighboring_ways: &[&WayRTreeEntry], -) -> i32 { - let speed_mph = estimated_speed_from_neighbors(entry, neighboring_ways).unwrap_or(0.0); - traffic_speed_comfort_from_speed(speed_mph.round() as i32) -} diff --git a/rust/bambam-osm/src/lib.rs b/rust/bambam-osm/src/lib.rs index 01cbde14..3347d243 100644 --- a/rust/bambam-osm/src/lib.rs +++ b/rust/bambam-osm/src/lib.rs @@ -1,4 +1,3 @@ pub mod algorithm; -pub mod app; pub mod config; pub mod model; diff --git a/rust/bambam-osm/src/model/osm/graph/osm_node_data_serializable.rs b/rust/bambam-osm/src/model/osm/graph/osm_node_data_serializable.rs index 6fff2c05..221efa2b 100644 --- a/rust/bambam-osm/src/model/osm/graph/osm_node_data_serializable.rs +++ b/rust/bambam-osm/src/model/osm/graph/osm_node_data_serializable.rs @@ -1,4 +1,5 @@ use super::{OsmNodeData, OsmNodeId}; +use bambam_modal_metrics::network_traits::vertex_for_modal_metric::VertexForModalMetric; use itertools::Itertools; use serde::{Deserialize, Serialize}; @@ -38,6 +39,19 @@ impl From<&OsmNodeData> for OsmNodeDataSerializable { } } +impl VertexForModalMetric for OsmNodeDataSerializable { + fn has_traffic_signals(&self) -> bool { + self.highway + .as_ref() + .is_some_and(|highway| highway.contains("traffic_signals")) + } + + fn has_stop_sign(&self) -> bool { + self.highway + .as_ref() + .is_some_and(|highway| highway.contains("stop")) + } +} fn replace_delimiter(value: &Option, delimiter: &'static str) -> Option { value .as_ref() diff --git a/rust/bambam-osm/src/model/osm/graph/osm_way_data_serializable.rs b/rust/bambam-osm/src/model/osm/graph/osm_way_data_serializable.rs index 62027c97..6833d14c 100644 --- a/rust/bambam-osm/src/model/osm/graph/osm_way_data_serializable.rs +++ b/rust/bambam-osm/src/model/osm/graph/osm_way_data_serializable.rs @@ -1,6 +1,9 @@ use super::osm_way_ops::{self, deserialize_linestring, serialize_linestring}; use super::{OsmGraph, OsmNodeData, OsmNodeId, OsmWayData, OsmWayId}; use crate::model::{feature::highway::Highway, osm::OsmError}; +use bambam_modal_metrics::common::cycleway_tag::CyclewayTag; +use bambam_modal_metrics::network_traits::edge_for_modal_metric::EdgeForModalMetric; +use bambam_modal_metrics::network_traits::spatial_edge::SpatialEdge; use geo::{Convert, Coord, Haversine, Length, LineString}; use geozero::ToWkt; use itertools::Itertools; @@ -193,6 +196,100 @@ impl OsmWayDataSerializable { } } +/// OSM Way modal metric implementation +impl EdgeForModalMetric for OsmWayDataSerializable { + fn get_traffic_speed_limit(&self) -> Option { + match self.get_speed("maxspeed_raw", true) { + Ok(Some(velocity)) => { + let mph = velocity.get::(); + Some(mph as i32) + } + _ => None, + } + } + + fn get_cycleway_tag(&self) -> Option { + self.cycleway.as_ref().map(|tag| CyclewayTag::new(tag)) + } + + fn is_walkable(&self) -> bool { + let is_sidewalk = self.is_sidewalk(); + + let is_footway = self.is_footway(); + + let is_walkable_highway = self.is_walkable_highway(); + + is_sidewalk || is_footway || is_walkable_highway + } + + fn is_walkable_highway(&self) -> bool { + matches!( + self.highway, + Highway::Residential + | Highway::Unclassified + | Highway::LivingStreet + | Highway::Service + | Highway::Pedestrian + | Highway::Trailhead + | Highway::Track + | Highway::Footway + | Highway::Bridleway + | Highway::Steps + | Highway::Corridor + | Highway::Path + | Highway::Elevator + ) + } + + fn is_sidewalk(&self) -> bool { + self.sidewalk + .as_ref() + .is_some_and(|s| s != "no" && s != "none") + || self.footway == Some("sidewalk".to_string()) + } + + fn is_footway(&self) -> bool { + self.footway + .as_ref() + .is_some_and(|s| s != "no" && s != "none") + } + + fn is_unbikeable(&self) -> bool { + matches!( + self.highway, + Highway::Motorway | Highway::Trunk | Highway::MotorwayLink | Highway::TrunkLink + ) + } + + fn is_non_motorized(&self) -> bool { + matches!( + self.highway, + Highway::Cycleway + | Highway::Path + | Highway::Footway + | Highway::Pedestrian + | Highway::LivingStreet + ) + } + + fn is_oneway(&self) -> bool { + self.oneway.as_deref() == Some("yes") + } +} + +impl SpatialEdge for OsmWayDataSerializable { + fn id(&self) -> String { + self.osmid.to_string() + } + + fn linestring(&self) -> Option<&LineString> { + Some(&self.linestring) + } + + fn src_vertex_id(&self) -> usize { + self.src_vertex_id.0 + } +} /// shorten the value, assumed a delimited string of categoricals, so that /// it contains only the unique set of categories. fn unique(value: Option<&String>) -> Option { diff --git a/rust/bambam/Cargo.toml b/rust/bambam/Cargo.toml index fca7cdab..2a5f77ac 100644 --- a/rust/bambam/Cargo.toml +++ b/rust/bambam/Cargo.toml @@ -15,7 +15,8 @@ categories = ["command-line-utilities", "science", "science::geo"] bambam-core = { workspace = true } bambam-gbfs = { workspace = true } bambam-gtfs = { workspace = true } -bambam-gtfs-flex = { workspace = true } +bambam-gtfs-flex = { workspace = true } +bambam-modal-metrics = { workspace = true } bambam-omf = { workspace = true } bambam-osm = { workspace = true } bamcensus = { workspace = true } diff --git a/rust/bambam/src/bin/bambam_util.rs b/rust/bambam/src/bin/bambam_util.rs index 0ebcfdda..e4fcf90c 100644 --- a/rust/bambam/src/bin/bambam_util.rs +++ b/rust/bambam/src/bin/bambam_util.rs @@ -3,7 +3,8 @@ use bambam::app::oppvec::{self, oppvec_ops}; use bambam::app::overlay::{ self, GeometryColumnType, GeometryFormat, OverlayOperation, OverlaySource, }; -use bambam_osm::app::network::common::bulk_compute_modal_metric::bulk_compute_modal_metric; +use bambam_modal_metrics::common::bulk_compute_modal_metric::bulk_compute_modal_metric; +use bambam_osm::model::osm::graph::{OsmNodeDataSerializable, OsmWayDataSerializable}; use clap::{Parser, Subcommand}; #[derive(Parser)] #[command(author, version, about, long_about = None)] @@ -216,8 +217,22 @@ impl App { output_file, edges_osm, vertices_osm, - } => bulk_compute_modal_metric(metric_name, edges_osm, vertices_osm, output_file) - .map_err(|e| format!("failed to run bulk compute modal metric: {e:?}")), + } => + // TODO: eventually support other types of graph data beyond OSM for modal metric computation. + // Generic traits are available (see bambam-modal-metrics crate). + // + // Currently, only OSM graph data is supported for modal metric computation. + // Once OMF graph data support is implemented, we can add an option to the CLI + // to specify the type of graph data to use. + { + bulk_compute_modal_metric::( + metric_name, + edges_osm, + vertices_osm, + output_file, + ) + .map_err(|e| format!("failed to run bulk compute modal metric: {e:?}")) + } Self::PreProcessGrid { acs_type, acs_year, From e48ecb83e856204f9e018610421c607c73079cba Mon Sep 17 00:00:00 2001 From: admrtin Date: Thu, 13 Aug 2026 13:42:08 -0600 Subject: [PATCH 02/10] Code commenting and readability tweaks --- .../src/common/bulk_compute_modal_metric.rs | 5 ++++ .../src/common/modal_metrics.rs | 2 +- .../src/lts/compute_lts.rs | 1 + rust/bambam-modal-metrics/src/lts/lts.rs | 2 +- .../src/wci/compute_wci.rs | 30 +++++++++++-------- rust/bambam-modal-metrics/src/wci/ops.rs | 2 +- .../osm/graph/osm_way_data_serializable.rs | 3 ++ 7 files changed, 30 insertions(+), 15 deletions(-) diff --git a/rust/bambam-modal-metrics/src/common/bulk_compute_modal_metric.rs b/rust/bambam-modal-metrics/src/common/bulk_compute_modal_metric.rs index 5f2ad21d..aa6c35b3 100644 --- a/rust/bambam-modal-metrics/src/common/bulk_compute_modal_metric.rs +++ b/rust/bambam-modal-metrics/src/common/bulk_compute_modal_metric.rs @@ -32,6 +32,7 @@ where E: SpatialEdge + EdgeForModalMetric + DeserializeOwned + Clone + Send + Sync, V: VertexForModalMetric + DeserializeOwned + Send + Sync, { + // determine the modal metric to compute based on the provided metric name. let metric: ModalMetric = metric_name.parse()?; log::info!( @@ -40,10 +41,12 @@ where ); log::info!("Reading:\n\t- vertex set @ {vertices_file}\n\t- edge set @ {edges_file}\n"); + // load vertices and edges. let vertices: Box<[V]> = read_utils::from_csv(&vertices_file, true, None, None)?; let edge_rtree_entries = load_edge_rtree_entries::(edges_file, &vertices)?; log::info!("Edges and vertices read successfully.\n"); + // build an RTree with the edge entries. let rtree = RTree::bulk_load(edge_rtree_entries.clone()); let bar: Arc> = Arc::new(Mutex::new( @@ -56,6 +59,7 @@ where .build()?, )); + // compute the modal metric for each edge in parallel via par_iter let values: Vec = edge_rtree_entries .par_iter() .map(|edge_entry| { @@ -73,6 +77,7 @@ where eprintln!(); + // write to file let file = File::create(output_file)?; let mut writer = BufWriter::new(file); diff --git a/rust/bambam-modal-metrics/src/common/modal_metrics.rs b/rust/bambam-modal-metrics/src/common/modal_metrics.rs index 1ffe941b..6baeee79 100644 --- a/rust/bambam-modal-metrics/src/common/modal_metrics.rs +++ b/rust/bambam-modal-metrics/src/common/modal_metrics.rs @@ -74,7 +74,7 @@ impl ModalMetric { } } } - + /// Writes the CSV header for the specified modal metric. pub fn write_csv_header(&self, writer: &mut impl Write) -> Result<(), Box> { match self { ModalMetric::WalkingComfortIndex => { diff --git a/rust/bambam-modal-metrics/src/lts/compute_lts.rs b/rust/bambam-modal-metrics/src/lts/compute_lts.rs index 3b13dc0e..9faae7ae 100644 --- a/rust/bambam-modal-metrics/src/lts/compute_lts.rs +++ b/rust/bambam-modal-metrics/src/lts/compute_lts.rs @@ -35,6 +35,7 @@ where let oneway = entry.edge.is_oneway(); + // compute the LTS value for this edge via the table lookup Lts::from_table_lookup(speed.round() as u8, cycleway_tag, oneway) } diff --git a/rust/bambam-modal-metrics/src/lts/lts.rs b/rust/bambam-modal-metrics/src/lts/lts.rs index 2446482f..e95a4d1d 100644 --- a/rust/bambam-modal-metrics/src/lts/lts.rs +++ b/rust/bambam-modal-metrics/src/lts/lts.rs @@ -26,7 +26,7 @@ impl Lts { Ok(Lts(value)) } } - + /// Computes the LTS value for a given edge based on traffic speed, cycleway tag, and oneway status. pub fn from_table_lookup( traffic_speed: u8, // assumed in mph. cycleway_tag: CyclewayTag, diff --git a/rust/bambam-modal-metrics/src/wci/compute_wci.rs b/rust/bambam-modal-metrics/src/wci/compute_wci.rs index acf1226c..480c0e40 100644 --- a/rust/bambam-modal-metrics/src/wci/compute_wci.rs +++ b/rust/bambam-modal-metrics/src/wci/compute_wci.rs @@ -5,7 +5,7 @@ use crate::network_traits::{ edge_for_modal_metric::EdgeForModalMetric, spatial_edge::SpatialEdge, vertex_for_modal_metric::VertexForModalMetric, }; -use crate::wci::ops::way_is_walk_eligible; +use crate::wci::ops::is_walk_eligible; use crate::wci::wci::{MAX_WCI, MIN_WCI, Wci, WciError}; /// The Walking Comfort Index (WCI) components for a way, including total WCI @@ -20,14 +20,15 @@ pub struct WciComponents { } impl WciComponents { - pub fn min_wci_score() -> Result { + /// Returns the minimum WCI (no components) + pub fn min_wci() -> Result { Ok(Self { total: Wci::new(MIN_WCI)?, ..Default::default() }) } - - pub fn max_wci_score() -> Result { + /// Returns the maximum WCI (no components) + pub fn max_wci() -> Result { Ok(Self { total: Wci::new(MAX_WCI)?, ..Default::default() @@ -46,22 +47,27 @@ where E: SpatialEdge + EdgeForModalMetric, V: VertexForModalMetric, { - let way_is_walk_eligible = way_is_walk_eligible(rtree, entry); + // general walk-eligibility based on edge attributes and neighbors. + let is_walk_eligible = is_walk_eligible(rtree, entry); - let neighboring_ways = find_neighboring_edges(entry, rtree); + // grab the neighboring edges + let neighboring_edges = find_neighboring_edges(entry, rtree); - if !way_is_walk_eligible { + if !is_walk_eligible { // Total WCI score = Min WCI score (unwalkable roadway) - WciComponents::min_wci_score() - } else if entry.edge.is_footway() || (neighboring_ways.is_empty() && entry.edge.is_sidewalk()) { + WciComponents::min_wci() + } else if entry.edge.is_footway() || (neighboring_edges.is_empty() && entry.edge.is_sidewalk()) + { // Total WCI score = Max WCI score (footway or sidewalk with no adjacent ways) - WciComponents::max_wci_score() + WciComponents::max_wci() } else { + // Compute all component scores. + let walkability = Wci::walkability(&entry.edge); - let cycleway_comfort = Wci::cycleway_comfort(entry, &neighboring_ways); + let cycleway_comfort = Wci::cycleway_comfort(entry, &neighboring_edges); - let traffic_speed_comfort = Wci::traffic_speed_comfort(entry, &neighboring_ways); + let traffic_speed_comfort = Wci::traffic_speed_comfort(entry, &neighboring_edges); let traffic_signal_comfort = src_node .map(|v| Wci::traffic_signal_comfort(v)) diff --git a/rust/bambam-modal-metrics/src/wci/ops.rs b/rust/bambam-modal-metrics/src/wci/ops.rs index df617799..9b2ceb2e 100644 --- a/rust/bambam-modal-metrics/src/wci/ops.rs +++ b/rust/bambam-modal-metrics/src/wci/ops.rs @@ -71,7 +71,7 @@ pub fn traffic_speed_comfort_from_neighbors } /// Determines if the edge is walk-eligible based on its own attributes or nearby sidewalk edges. -pub fn way_is_walk_eligible( +pub fn is_walk_eligible( rtree: &RTree>, entry: &EdgeRTreeEntry, ) -> bool { diff --git a/rust/bambam-osm/src/model/osm/graph/osm_way_data_serializable.rs b/rust/bambam-osm/src/model/osm/graph/osm_way_data_serializable.rs index 6833d14c..7031059a 100644 --- a/rust/bambam-osm/src/model/osm/graph/osm_way_data_serializable.rs +++ b/rust/bambam-osm/src/model/osm/graph/osm_way_data_serializable.rs @@ -199,6 +199,9 @@ impl OsmWayDataSerializable { /// OSM Way modal metric implementation impl EdgeForModalMetric for OsmWayDataSerializable { fn get_traffic_speed_limit(&self) -> Option { + // TODO: since the US speed limits are in MPH, we grab the maxspeed_raw because it is typically + // of the form "XX mph". We should ideally try to grab either "maxspeed" or "maxspeed_raw" + // so that this will work for Non-US speed limits. match self.get_speed("maxspeed_raw", true) { Ok(Some(velocity)) => { let mph = velocity.get::(); From 7133f86b8233de51fa8d0bc4b546cc8c02b3e5ae Mon Sep 17 00:00:00 2001 From: admrtin Date: Thu, 13 Aug 2026 13:43:14 -0600 Subject: [PATCH 03/10] crate publish script update --- script/publish_crates.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/publish_crates.sh b/script/publish_crates.sh index 6afadde3..ae42d926 100755 --- a/script/publish_crates.sh +++ b/script/publish_crates.sh @@ -5,7 +5,7 @@ set -eu MANIFEST_PATH="rust/Cargo.toml" DRY_RUN=0 CRATES_CSV="" -DEFAULT_CRATES="bambam-core bambam-osm bambam-gtfs bambam-gbfs bambam-omf bambam-gtfs-flex bambam" +DEFAULT_CRATES="bambam-core bambam-modal-metrics bambam-osm bambam-gtfs bambam-gbfs bambam-omf bambam-gtfs-flex bambam" usage() { cat <<'EOF' From 21a59cefd7740975b5d8fde325948cd87ccf0a70 Mon Sep 17 00:00:00 2001 From: admrtin Date: Thu, 13 Aug 2026 14:12:45 -0600 Subject: [PATCH 04/10] cargo sort --- rust/bambam-modal-metrics/Cargo.toml | 4 ++-- rust/bambam-osm/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rust/bambam-modal-metrics/Cargo.toml b/rust/bambam-modal-metrics/Cargo.toml index a138bea0..e499bea3 100644 --- a/rust/bambam-modal-metrics/Cargo.toml +++ b/rust/bambam-modal-metrics/Cargo.toml @@ -4,14 +4,14 @@ edition = "2024" version.workspace = true [dependencies] -routee-compass-core = { workspace = true } csv = { workspace = true } geo = { workspace = true } kdam = { workspace = true } +log = { workspace = true } rayon = { workspace = true } +routee-compass-core = { workspace = true } rstar = { workspace = true } serde = { workspace = true } thiserror = { workspace = true } uom = { workspace = true } -log = { workspace = true } \ No newline at end of file diff --git a/rust/bambam-osm/Cargo.toml b/rust/bambam-osm/Cargo.toml index 3e4c13b7..582dfc64 100644 --- a/rust/bambam-osm/Cargo.toml +++ b/rust/bambam-osm/Cargo.toml @@ -12,10 +12,10 @@ keywords = ["nlr", "access-model", "accessibility", "multimodal", "transit"] categories = ["command-line-utilities", "science", "science::geo"] [dependencies] +bambam-modal-metrics = { workspace = true } bamcensus = { workspace = true } bamcensus-acs = { workspace = true } bamcensus-core = { workspace = true } -bambam-modal-metrics = { workspace = true } clap = { workspace = true } config = { workspace = true } From 91402bdb393aa554ac9c0c88987a211add45590f Mon Sep 17 00:00:00 2001 From: Adam Martin <73398481+admrtin@users.noreply.github.com> Date: Thu, 13 Aug 2026 15:35:54 -0600 Subject: [PATCH 05/10] round mph before casting to i32 Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../bambam-osm/src/model/osm/graph/osm_way_data_serializable.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/bambam-osm/src/model/osm/graph/osm_way_data_serializable.rs b/rust/bambam-osm/src/model/osm/graph/osm_way_data_serializable.rs index 7031059a..0e6ac2c4 100644 --- a/rust/bambam-osm/src/model/osm/graph/osm_way_data_serializable.rs +++ b/rust/bambam-osm/src/model/osm/graph/osm_way_data_serializable.rs @@ -205,7 +205,7 @@ impl EdgeForModalMetric for OsmWayDataSerializable { match self.get_speed("maxspeed_raw", true) { Ok(Some(velocity)) => { let mph = velocity.get::(); - Some(mph as i32) +Some(mph.round() as i32) } _ => None, } From c0bb8dd89feca2040f637bf629b76da1dcbccc0f Mon Sep 17 00:00:00 2001 From: admrtin Date: Thu, 13 Aug 2026 15:39:12 -0600 Subject: [PATCH 06/10] fmt --- .../bambam-osm/src/model/osm/graph/osm_way_data_serializable.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/bambam-osm/src/model/osm/graph/osm_way_data_serializable.rs b/rust/bambam-osm/src/model/osm/graph/osm_way_data_serializable.rs index 0e6ac2c4..d0cf0fab 100644 --- a/rust/bambam-osm/src/model/osm/graph/osm_way_data_serializable.rs +++ b/rust/bambam-osm/src/model/osm/graph/osm_way_data_serializable.rs @@ -205,7 +205,7 @@ impl EdgeForModalMetric for OsmWayDataSerializable { match self.get_speed("maxspeed_raw", true) { Ok(Some(velocity)) => { let mph = velocity.get::(); -Some(mph.round() as i32) + Some(mph.round() as i32) } _ => None, } From 5a6379f5ec7114ff3e32b98c03aaa1961973998a Mon Sep 17 00:00:00 2001 From: admrtin Date: Thu, 13 Aug 2026 15:57:43 -0600 Subject: [PATCH 07/10] Cargo updates and readme --- rust/bambam-modal-metrics/Cargo.toml | 10 ++++++++-- rust/bambam-modal-metrics/README.md | 26 ++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 rust/bambam-modal-metrics/README.md diff --git a/rust/bambam-modal-metrics/Cargo.toml b/rust/bambam-modal-metrics/Cargo.toml index e499bea3..f9fbf8df 100644 --- a/rust/bambam-modal-metrics/Cargo.toml +++ b/rust/bambam-modal-metrics/Cargo.toml @@ -1,10 +1,16 @@ [package] name = "bambam-modal-metrics" -edition = "2024" +edition = "2021" +license = "BSD-3-Clause" version.workspace = true +readme = "README.md" +repository = "https://github.com/NatLabRockies/bambam" +documentation = "https://docs.rs/bambam-modal-metrics" +description = "Modal Metrics for road network data in The Behavior and Advanced Mobility Big Access Model" +keywords = ["nlr", "access-model", "accessibility", "multimodal", "transit"] +categories = ["science", "science::geo"] [dependencies] - csv = { workspace = true } geo = { workspace = true } kdam = { workspace = true } diff --git a/rust/bambam-modal-metrics/README.md b/rust/bambam-modal-metrics/README.md new file mode 100644 index 00000000..5244ffd4 --- /dev/null +++ b/rust/bambam-modal-metrics/README.md @@ -0,0 +1,26 @@ +# `bambam-modal-metrics` + +## A library crate containing: + +- Extensible traits for road network data that can facilitate modal metric computations + +- The logic for computing modal metrics such as the Walking Comfort Index (WCI) or the Level of Traffic Stress (LTS) + +## What is a modal metric? + +A modal metric is a value that qualitatively describes links (or edges) in a transportation network for a specific modality. Two examples of this type of metric are: + +### **Walking Comfort Index**: +How comfortable are links in a network in terms of walkability? +#### Considerations: +- Link and neighboring link traffic speed +- Link type and characteristics (sidewalk? footway? cycleway?) +- Does the link contain either a stop sign or a traffic signal to allow for crossings and speed limiting? + +### **Level of Traffic Stress**: +How stressful are links in a network for cyclists? +#### Considerations: +- Link and neighboring link traffic speed +- Link type +- Cycleway infrastructure +- Single lane vs. multi-lane. \ No newline at end of file From cbd18d081fecd967b69a67c1bf9f40c1a3da2fb0 Mon Sep 17 00:00:00 2001 From: admrtin Date: Thu, 13 Aug 2026 16:03:36 -0600 Subject: [PATCH 08/10] format --- rust/bambam-modal-metrics/README.md | 12 +++++++----- rust/bambam-modal-metrics/src/lts/compute_lts.rs | 2 +- rust/bambam-modal-metrics/src/wci/compute_wci.rs | 4 ++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/rust/bambam-modal-metrics/README.md b/rust/bambam-modal-metrics/README.md index 5244ffd4..2a6c2de0 100644 --- a/rust/bambam-modal-metrics/README.md +++ b/rust/bambam-modal-metrics/README.md @@ -2,25 +2,27 @@ ## A library crate containing: -- Extensible traits for road network data that can facilitate modal metric computations +Extensible traits for road network data that can facilitate modal metric computations -- The logic for computing modal metrics such as the Walking Comfort Index (WCI) or the Level of Traffic Stress (LTS) +The logic for computing modal metrics such as the Walking Comfort Index (WCI) or the Level of Traffic Stress (LTS) ## What is a modal metric? A modal metric is a value that qualitatively describes links (or edges) in a transportation network for a specific modality. Two examples of this type of metric are: -### **Walking Comfort Index**: +### 1. **Walking Comfort Index**: How comfortable are links in a network in terms of walkability? + #### Considerations: - Link and neighboring link traffic speed - Link type and characteristics (sidewalk? footway? cycleway?) - Does the link contain either a stop sign or a traffic signal to allow for crossings and speed limiting? -### **Level of Traffic Stress**: +### 2. **Level of Traffic Stress**: How stressful are links in a network for cyclists? + #### Considerations: - Link and neighboring link traffic speed - Link type - Cycleway infrastructure -- Single lane vs. multi-lane. \ No newline at end of file +- Single lane vs. multi-lane \ No newline at end of file diff --git a/rust/bambam-modal-metrics/src/lts/compute_lts.rs b/rust/bambam-modal-metrics/src/lts/compute_lts.rs index 9faae7ae..f8e6d131 100644 --- a/rust/bambam-modal-metrics/src/lts/compute_lts.rs +++ b/rust/bambam-modal-metrics/src/lts/compute_lts.rs @@ -1,7 +1,7 @@ use rstar::RTree; use crate::common::cycleway_tag::CyclewayTag; -use crate::common::edge_rtree_entry::{EdgeRTreeEntry, find_neighboring_edges}; +use crate::common::edge_rtree_entry::{find_neighboring_edges, EdgeRTreeEntry}; use crate::common::ops::{estimated_speed_from_neighbors, traffic_speed_from_maxspeed}; use crate::lts::lts::{Lts, LtsError, MAX_LTS, MIN_LTS}; use crate::network_traits::{edge_for_modal_metric::EdgeForModalMetric, spatial_edge::SpatialEdge}; diff --git a/rust/bambam-modal-metrics/src/wci/compute_wci.rs b/rust/bambam-modal-metrics/src/wci/compute_wci.rs index 480c0e40..60280bb1 100644 --- a/rust/bambam-modal-metrics/src/wci/compute_wci.rs +++ b/rust/bambam-modal-metrics/src/wci/compute_wci.rs @@ -1,12 +1,12 @@ use rstar::RTree; -use crate::common::edge_rtree_entry::{EdgeRTreeEntry, find_neighboring_edges}; +use crate::common::edge_rtree_entry::{find_neighboring_edges, EdgeRTreeEntry}; use crate::network_traits::{ edge_for_modal_metric::EdgeForModalMetric, spatial_edge::SpatialEdge, vertex_for_modal_metric::VertexForModalMetric, }; use crate::wci::ops::is_walk_eligible; -use crate::wci::wci::{MAX_WCI, MIN_WCI, Wci, WciError}; +use crate::wci::wci::{Wci, WciError, MAX_WCI, MIN_WCI}; /// The Walking Comfort Index (WCI) components for a way, including total WCI /// and all components that went into the total WCI. From e00c436f442bae405bb2c8abe2d3e0d8712bbfdc Mon Sep 17 00:00:00 2001 From: admrtin Date: Thu, 13 Aug 2026 16:05:02 -0600 Subject: [PATCH 09/10] format #2 --- rust/bambam-modal-metrics/src/common/edge_rtree_entry.rs | 2 +- rust/bambam-modal-metrics/src/common/modal_metrics.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rust/bambam-modal-metrics/src/common/edge_rtree_entry.rs b/rust/bambam-modal-metrics/src/common/edge_rtree_entry.rs index c06f4362..e194cf8f 100644 --- a/rust/bambam-modal-metrics/src/common/edge_rtree_entry.rs +++ b/rust/bambam-modal-metrics/src/common/edge_rtree_entry.rs @@ -1,5 +1,5 @@ use geo::{BoundingRect, Centroid, Distance, Euclidean}; -use rstar::{AABB, PointDistance, RTreeObject}; +use rstar::{PointDistance, RTreeObject, AABB}; use crate::network_traits::spatial_edge::SpatialEdge; diff --git a/rust/bambam-modal-metrics/src/common/modal_metrics.rs b/rust/bambam-modal-metrics/src/common/modal_metrics.rs index 6baeee79..977ea13b 100644 --- a/rust/bambam-modal-metrics/src/common/modal_metrics.rs +++ b/rust/bambam-modal-metrics/src/common/modal_metrics.rs @@ -6,7 +6,7 @@ use crate::network_traits::{ edge_for_modal_metric::EdgeForModalMetric, spatial_edge::SpatialEdge, vertex_for_modal_metric::VertexForModalMetric, }; -use crate::wci::compute_wci::{WciComponents, compute_wci}; +use crate::wci::compute_wci::{compute_wci, WciComponents}; use crate::wci::wci::WciError; use rstar::RTree; use std::{error::Error, io::Write, str::FromStr}; From b4f30f6ee356297045c3943f24343ec3697c6fa8 Mon Sep 17 00:00:00 2001 From: admrtin Date: Thu, 20 Aug 2026 09:39:58 -0600 Subject: [PATCH 10/10] Update publish crates script and remove references to "ways" in docstrings --- rust/bambam-modal-metrics/README.md | 2 ++ .../src/common/bulk_compute_modal_metric.rs | 4 ++-- .../src/common/cycleway_tag.rs | 2 +- .../src/common/edge_rtree_entry.rs | 8 ++++---- rust/bambam-modal-metrics/src/common/ops.rs | 4 ++-- .../src/lts/compute_lts.rs | 6 +++--- .../src/wci/compute_wci.rs | 6 +++--- rust/bambam-modal-metrics/src/wci/mod.rs | 2 +- rust/bambam-modal-metrics/src/wci/ops.rs | 14 +++++++------- rust/bambam-modal-metrics/src/wci/wci.rs | 10 +++++----- rust/bambam/src/bin/bambam_util.rs | 18 +++++++++--------- script/publish_crates.sh | 2 +- 12 files changed, 40 insertions(+), 38 deletions(-) diff --git a/rust/bambam-modal-metrics/README.md b/rust/bambam-modal-metrics/README.md index 2a6c2de0..68646863 100644 --- a/rust/bambam-modal-metrics/README.md +++ b/rust/bambam-modal-metrics/README.md @@ -6,6 +6,8 @@ Extensible traits for road network data that can facilitate modal metric computa The logic for computing modal metrics such as the Walking Comfort Index (WCI) or the Level of Traffic Stress (LTS) +This library is wired into `bambam` through the set of commands defined in `bamam_util`. You can run the `bambam_util modal_metric` command from the `bambam` crate to compute modal metrics such as WCI and LTS for a given road network. As of now, `bambam-modal-metrics` only supports OpenStreetMaps way/node data as input, but extension to OvertureMaps is planned. + ## What is a modal metric? A modal metric is a value that qualitatively describes links (or edges) in a transportation network for a specific modality. Two examples of this type of metric are: diff --git a/rust/bambam-modal-metrics/src/common/bulk_compute_modal_metric.rs b/rust/bambam-modal-metrics/src/common/bulk_compute_modal_metric.rs index aa6c35b3..59fcf5b9 100644 --- a/rust/bambam-modal-metrics/src/common/bulk_compute_modal_metric.rs +++ b/rust/bambam-modal-metrics/src/common/bulk_compute_modal_metric.rs @@ -52,7 +52,7 @@ where let bar: Arc> = Arc::new(Mutex::new( BarBuilder::default() .desc(format!( - "Computing {:?} for ways in the road network", + "Computing {:?} for edges in the road network", metric )) .total(edge_rtree_entries.len()) @@ -88,7 +88,7 @@ where writer.flush()?; log::info!( - "\n\n{:?} values computed successfully.\n\nOutputfile saved @ {output_file}.", + "\n\n{:?} values computed successfully.\n\nOutput file saved @ {output_file}.", metric ); Ok(()) diff --git a/rust/bambam-modal-metrics/src/common/cycleway_tag.rs b/rust/bambam-modal-metrics/src/common/cycleway_tag.rs index a335d11e..c7d3c818 100644 --- a/rust/bambam-modal-metrics/src/common/cycleway_tag.rs +++ b/rust/bambam-modal-metrics/src/common/cycleway_tag.rs @@ -2,7 +2,7 @@ /// level of safety of a cycleway. /// /// You can generate a new cycleway tag by passing in -/// the OSM way's cycleway attribute. +/// the edge's cycleway attribute. #[derive(Debug, Clone)] pub enum CyclewayTag { DedicatedWithBuffer, diff --git a/rust/bambam-modal-metrics/src/common/edge_rtree_entry.rs b/rust/bambam-modal-metrics/src/common/edge_rtree_entry.rs index e194cf8f..4abee605 100644 --- a/rust/bambam-modal-metrics/src/common/edge_rtree_entry.rs +++ b/rust/bambam-modal-metrics/src/common/edge_rtree_entry.rs @@ -8,15 +8,15 @@ use crate::network_traits::spatial_edge::SpatialEdge; /// Roughly 15 meters at mid latitudes. pub const MIN_DISTANCE_RTREE_NEIGHBOR: f32 = 0.0001378; -/// `WayRTreeEntry` wraps `OsmWayDataSerializable` and caches the bounding box -/// and centroid of the way's `linestring`. It is used solely for efficient spatial queries +/// `EdgeRTreeEntry` wraps the network edge and caches the bounding box +/// and centroid of the edge's `linestring`. It is used solely for efficient spatial queries /// in an R-tree data structure. /// /// It is used in spatial queries for network analysis, such as computing /// the Walking Comfort Index (WCI) or Level of Traffic Stress (LTS) for a way using /// information from the way's geometry, attributes, and nearby ways. /// -/// If we were to implement the `RTreeObject` trait directly on `OsmWayDataSerializable`, +/// If we were to implement the `RTreeObject` trait directly on the network edge, /// we would have to compute the bounding box every time the `envelope()` method /// is called, which is inefficient. /// @@ -48,7 +48,7 @@ impl EdgeRTreeEntry { } impl RTreeObject for EdgeRTreeEntry { - type Envelope = AABB<[f32; 2]>; // Envelope should be the same type as the bbox of WayRTreeEntry + type Envelope = AABB<[f32; 2]>; // Envelope should be the same type as the bbox of EdgeRTreeEntry fn envelope(&self) -> Self::Envelope { self.bbox // return the cached bounding box } diff --git a/rust/bambam-modal-metrics/src/common/ops.rs b/rust/bambam-modal-metrics/src/common/ops.rs index 799da908..cb5d1180 100644 --- a/rust/bambam-modal-metrics/src/common/ops.rs +++ b/rust/bambam-modal-metrics/src/common/ops.rs @@ -64,12 +64,12 @@ where /// Computes a weighted estimated speed (in MPH) from nearby ways pub fn estimated_speed_from_neighbors( entry: &EdgeRTreeEntry, - neighboring_ways: &[&EdgeRTreeEntry], + neighboring_edges: &[&EdgeRTreeEntry], ) -> Option where E: EdgeForModalMetric + SpatialEdge, { - let speeds_and_distances: Vec<(f32, f32)> = neighboring_ways + let speeds_and_distances: Vec<(f32, f32)> = neighboring_edges .iter() .filter_map(|neighbor| { neighbor.edge.get_traffic_speed_limit().map(|mph| { diff --git a/rust/bambam-modal-metrics/src/lts/compute_lts.rs b/rust/bambam-modal-metrics/src/lts/compute_lts.rs index f8e6d131..755aaa75 100644 --- a/rust/bambam-modal-metrics/src/lts/compute_lts.rs +++ b/rust/bambam-modal-metrics/src/lts/compute_lts.rs @@ -6,7 +6,7 @@ use crate::common::ops::{estimated_speed_from_neighbors, traffic_speed_from_maxs use crate::lts::lts::{Lts, LtsError, MAX_LTS, MIN_LTS}; use crate::network_traits::{edge_for_modal_metric::EdgeForModalMetric, spatial_edge::SpatialEdge}; -/// Computes the level of traffic stress for a given edge entry. +/// Computes the level of traffic stress for a given edge. pub fn compute_lts( rtree: &RTree>, entry: &EdgeRTreeEntry, @@ -14,11 +14,11 @@ pub fn compute_lts( where E: SpatialEdge + EdgeForModalMetric, { - // Some ways are inherently unsuitable for bikes. + // Some edges are inherently unsuitable for bikes. if entry.edge.is_unbikeable() { return Lts::new(MAX_LTS); } - // A highway that is non-motorized is inherently low-stress + // An edge that is non-motorized is inherently low-stress if entry.edge.is_non_motorized() { return Lts::new(MIN_LTS); } diff --git a/rust/bambam-modal-metrics/src/wci/compute_wci.rs b/rust/bambam-modal-metrics/src/wci/compute_wci.rs index 60280bb1..b30651a3 100644 --- a/rust/bambam-modal-metrics/src/wci/compute_wci.rs +++ b/rust/bambam-modal-metrics/src/wci/compute_wci.rs @@ -8,7 +8,7 @@ use crate::network_traits::{ use crate::wci::ops::is_walk_eligible; use crate::wci::wci::{Wci, WciError, MAX_WCI, MIN_WCI}; -/// The Walking Comfort Index (WCI) components for a way, including total WCI +/// The Walking Comfort Index (WCI) components for an edge, including total WCI /// and all components that went into the total WCI. #[derive(Default)] pub struct WciComponents { @@ -54,11 +54,11 @@ where let neighboring_edges = find_neighboring_edges(entry, rtree); if !is_walk_eligible { - // Total WCI score = Min WCI score (unwalkable roadway) + // Total WCI score = Min WCI score (unwalkable edge) WciComponents::min_wci() } else if entry.edge.is_footway() || (neighboring_edges.is_empty() && entry.edge.is_sidewalk()) { - // Total WCI score = Max WCI score (footway or sidewalk with no adjacent ways) + // Total WCI score = Max WCI score (footway or sidewalk with no adjacent edges) WciComponents::max_wci() } else { // Compute all component scores. diff --git a/rust/bambam-modal-metrics/src/wci/mod.rs b/rust/bambam-modal-metrics/src/wci/mod.rs index dc309feb..1b6b00c5 100644 --- a/rust/bambam-modal-metrics/src/wci/mod.rs +++ b/rust/bambam-modal-metrics/src/wci/mod.rs @@ -1,4 +1,4 @@ pub mod compute_wci; pub mod ops; pub mod wci; -const NO_CYCLEWAY_FOUND_SCORE: i32 = -2; // If there is no cycleway found for a way, cycle component of WCI. +const NO_CYCLEWAY_FOUND_SCORE: i32 = -2; // If there is no cycleway found for an edge, cycle component of WCI. diff --git a/rust/bambam-modal-metrics/src/wci/ops.rs b/rust/bambam-modal-metrics/src/wci/ops.rs index 9b2ceb2e..9416aa3a 100644 --- a/rust/bambam-modal-metrics/src/wci/ops.rs +++ b/rust/bambam-modal-metrics/src/wci/ops.rs @@ -17,15 +17,15 @@ pub fn cycleway_comfort_from_tag(tag: &CyclewayTag) -> i32 { } } -/// Computes the cycleway comfort index from neighboring ways +/// Computes the cycleway comfort index from neighboring edges pub fn cycleway_comfort_from_neighbors( entry: &EdgeRTreeEntry, - neighboring_ways: &[&EdgeRTreeEntry], + neighboring_edges: &[&EdgeRTreeEntry], ) -> i32 { let mut total_distance: f32 = 0.0; let mut scored: Vec<(i32, f32)> = Vec::new(); - for neighbor in neighboring_ways { + for neighbor in neighboring_edges { let distance = Euclidean.distance(entry.centroid, neighbor.centroid); total_distance += distance; if let Some(tag) = neighbor.edge.get_cycleway_tag() { @@ -60,13 +60,13 @@ pub fn traffic_speed_comfort_from_speed(speed_mph: i32) -> i32 { } } -/// Computes a weighted traffic speed comfort index from nearby ways if the -/// way of interest does not have a speed limit. +/// Computes a weighted traffic speed comfort index from nearby edges if the +/// edge of interest does not have a speed limit. pub fn traffic_speed_comfort_from_neighbors( entry: &EdgeRTreeEntry, - neighboring_ways: &[&EdgeRTreeEntry], + neighboring_edges: &[&EdgeRTreeEntry], ) -> i32 { - let speed_mph = estimated_speed_from_neighbors(entry, neighboring_ways).unwrap_or(0.0); + let speed_mph = estimated_speed_from_neighbors(entry, neighboring_edges).unwrap_or(0.0); traffic_speed_comfort_from_speed(speed_mph.round() as i32) } diff --git a/rust/bambam-modal-metrics/src/wci/wci.rs b/rust/bambam-modal-metrics/src/wci/wci.rs index 5815f1e6..4db67c9b 100644 --- a/rust/bambam-modal-metrics/src/wci/wci.rs +++ b/rust/bambam-modal-metrics/src/wci/wci.rs @@ -78,7 +78,7 @@ impl Wci { } } - /// Computes the walkability `Wci` for a way. + /// Computes the walkability `Wci` for an edge. pub fn walkability(edge: &dyn EdgeForModalMetric) -> Wci { if edge.is_sidewalk() || edge.is_footway() { Wci(2) @@ -87,7 +87,7 @@ impl Wci { } } - /// Computes the traffic signal `Wci` for a way. + /// Computes the traffic signal `Wci` for an edge (given the source vertex). pub fn traffic_signal_comfort(src_vertex: &dyn VertexForModalMetric) -> Wci { if src_vertex.has_traffic_signals() { Wci(2) @@ -98,19 +98,19 @@ impl Wci { } } - /// Computes the cycleway `Wci` for a way. + /// Computes the cycleway `Wci` for an edge. pub fn cycleway_comfort( entry: &EdgeRTreeEntry, neighboring_ways: &Vec<&EdgeRTreeEntry>, ) -> Wci { - // if the way has a cycleway tag (string), use that, otherwise, use neighbors + // if the edge has a cycleway tag (string), use that, otherwise, use neighbors match &entry.edge.get_cycleway_tag() { Some(tag) => Wci(cycleway_comfort_from_tag(tag)), None => Wci(cycleway_comfort_from_neighbors(entry, neighboring_ways)), } } - /// Computes the traffic speed `Wci` for a way + /// Computes the traffic speed `Wci` for an edge. pub fn traffic_speed_comfort( entry: &EdgeRTreeEntry, neighbors: &Vec<&EdgeRTreeEntry>, diff --git a/rust/bambam/src/bin/bambam_util.rs b/rust/bambam/src/bin/bambam_util.rs index e4fcf90c..d3d7c4ee 100644 --- a/rust/bambam/src/bin/bambam_util.rs +++ b/rust/bambam/src/bin/bambam_util.rs @@ -37,13 +37,13 @@ pub enum App { /// modal metric type to compute, either "WCI" or "LTS" #[arg(long)] metric_name: String, - /// input csv file with edges OSM data + /// input csv file with edges data #[arg(long)] - edges_osm: String, - /// input csv file with vertices OSM data + edges_file: String, + /// input csv file with vertices data #[arg(long)] - vertices_osm: String, - /// file to write modal metric values to, one per line + vertices_file: String, + /// file to write modal metric values to #[arg(long)] output_file: String, }, @@ -215,8 +215,8 @@ impl App { Self::ModalMetricSet { metric_name, output_file, - edges_osm, - vertices_osm, + edges_file, + vertices_file, } => // TODO: eventually support other types of graph data beyond OSM for modal metric computation. // Generic traits are available (see bambam-modal-metrics crate). @@ -227,8 +227,8 @@ impl App { { bulk_compute_modal_metric::( metric_name, - edges_osm, - vertices_osm, + edges_file, + vertices_file, output_file, ) .map_err(|e| format!("failed to run bulk compute modal metric: {e:?}")) diff --git a/script/publish_crates.sh b/script/publish_crates.sh index ae42d926..3d293b7a 100755 --- a/script/publish_crates.sh +++ b/script/publish_crates.sh @@ -78,7 +78,7 @@ run_publish() { } for crate in $CRATES; do - if [ "$DRY_RUN" -eq 0 ] && [ "$crate" = "bambam" ]; then + if [ "$DRY_RUN" -eq 0 ]; then # crates.io indexing can lag briefly; wait before publishing the umbrella crate. sleep 2 fi