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..f9fbf8df --- /dev/null +++ b/rust/bambam-modal-metrics/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "bambam-modal-metrics" +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 } +log = { workspace = true } +rayon = { workspace = true } +routee-compass-core = { workspace = true } +rstar = { workspace = true } +serde = { workspace = true } +thiserror = { workspace = true } +uom = { workspace = true } diff --git a/rust/bambam-modal-metrics/README.md b/rust/bambam-modal-metrics/README.md new file mode 100644 index 00000000..68646863 --- /dev/null +++ b/rust/bambam-modal-metrics/README.md @@ -0,0 +1,30 @@ +# `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) + +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: + +### 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? + +### 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 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 52% 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..59fcf5b9 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,17 @@ 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, +{ + // determine the modal metric to compute based on the provided metric name. let metric: ModalMetric = metric_name.parse()?; log::info!( @@ -32,29 +41,31 @@ 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)?; + // 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"); - let rtree = RTree::bulk_load(way_rtree_entries.clone()); + // build an RTree with the edge entries. + let rtree = RTree::bulk_load(edge_rtree_entries.clone()); 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(way_rtree_entries.len()) + .total(edge_rtree_entries.len()) .build()?, )); - let values: Vec = way_rtree_entries + // compute the modal metric for each edge in parallel via par_iter + 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); @@ -66,6 +77,7 @@ pub fn bulk_compute_modal_metric( eprintln!(); + // write to file let file = File::create(output_file)?; let mut writer = BufWriter::new(file); @@ -76,7 +88,7 @@ pub fn bulk_compute_modal_metric( 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-osm/src/app/network/common/cycleway_tag.rs b/rust/bambam-modal-metrics/src/common/cycleway_tag.rs similarity index 96% 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..c7d3c818 100644 --- a/rust/bambam-osm/src/app/network/common/cycleway_tag.rs +++ b/rust/bambam-modal-metrics/src/common/cycleway_tag.rs @@ -2,8 +2,8 @@ /// level of safety of a cycleway. /// /// You can generate a new cycleway tag by passing in -/// the OSM way's cycleway attribute. -#[derive(Debug)] +/// the edge's cycleway attribute. +#[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 52% 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..4abee605 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,54 +1,61 @@ -use crate::model::osm::graph::OsmWayDataSerializable; use geo::{BoundingRect, Centroid, Distance, Euclidean}; use rstar::{PointDistance, RTreeObject, AABB}; -/// `WayRTreeEntry` wraps `OsmWayDataSerializable` and caches the bounding box -/// and centroid of the way's `linestring`. It is used solely for efficient spatial queries +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; + +/// `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. /// /// 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 { - type Envelope = AABB<[f32; 2]>; // Envelope should be the same type as the bbox of WayRTreeEntry +impl RTreeObject for EdgeRTreeEntry { + 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 } } -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 74% 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..977ea13b 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::{compute_wci, WciComponents}; +use crate::wci::wci::WciError; use rstar::RTree; use std::{error::Error, io::Write, str::FromStr}; @@ -49,25 +52,29 @@ 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)) } } } - + /// 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/common/ops.rs b/rust/bambam-modal-metrics/src/common/ops.rs new file mode 100644 index 00000000..cb5d1180 --- /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_edges: &[&EdgeRTreeEntry], +) -> Option +where + E: EdgeForModalMetric + SpatialEdge, +{ + let speeds_and_distances: Vec<(f32, f32)> = neighboring_edges + .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 59% 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..755aaa75 100644 --- a/rust/bambam-osm/src/app/network/lts/compute_lts.rs +++ b/rust/bambam-modal-metrics/src/lts/compute_lts.rs @@ -1,49 +1,48 @@ 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::{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}; -/// Computes the level of traffic stress for a given way entry. -pub fn compute_lts(rtree: &RTree, entry: &WayRTreeEntry) -> Result { - // Some ways are inherently unsuitable for bikes. - if is_unbikeable_way(&entry.way.highway) { +/// Computes the level of traffic stress for a given edge. +pub fn compute_lts( + rtree: &RTree>, + entry: &EdgeRTreeEntry, +) -> Result +where + E: SpatialEdge + EdgeForModalMetric, +{ + // 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 - if is_non_motorized_way(&entry.way.highway) { + // An edge that is non-motorized is inherently low-stress + 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(); + // compute the LTS value for this edge via the table lookup Lts::from_table_lookup(speed.round() as u8, cycleway_tag, oneway) } #[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 93% rename from rust/bambam-osm/src/app/network/lts/lts.rs rename to rust/bambam-modal-metrics/src/lts/lts.rs index 738f53ca..e95a4d1d 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. @@ -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-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..b30651a3 --- /dev/null +++ b/rust/bambam-modal-metrics/src/wci/compute_wci.rs @@ -0,0 +1,301 @@ +use rstar::RTree; + +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::{Wci, WciError, MAX_WCI, MIN_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 { + pub total: Wci, + pub walkability: Option, + pub traffic_speed_comfort: Option, + pub cycleway_comfort: Option, + pub traffic_signal_comfort: Option, +} + +impl WciComponents { + /// Returns the minimum WCI (no components) + pub fn min_wci() -> Result { + Ok(Self { + total: Wci::new(MIN_WCI)?, + ..Default::default() + }) + } + /// Returns the maximum WCI (no components) + pub fn max_wci() -> 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, +{ + // general walk-eligibility based on edge attributes and neighbors. + let is_walk_eligible = is_walk_eligible(rtree, entry); + + // grab the neighboring edges + let neighboring_edges = find_neighboring_edges(entry, rtree); + + if !is_walk_eligible { + // 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 edges) + WciComponents::max_wci() + } else { + // Compute all component scores. + + let walkability = Wci::walkability(&entry.edge); + + let cycleway_comfort = Wci::cycleway_comfort(entry, &neighboring_edges); + + let traffic_speed_comfort = Wci::traffic_speed_comfort(entry, &neighboring_edges); + + 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-modal-metrics/src/wci/mod.rs b/rust/bambam-modal-metrics/src/wci/mod.rs new file mode 100644 index 00000000..1b6b00c5 --- /dev/null +++ b/rust/bambam-modal-metrics/src/wci/mod.rs @@ -0,0 +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 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 new file mode 100644 index 00000000..9416aa3a --- /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 edges +pub fn cycleway_comfort_from_neighbors( + entry: &EdgeRTreeEntry, + neighboring_edges: &[&EdgeRTreeEntry], +) -> i32 { + let mut total_distance: f32 = 0.0; + let mut scored: Vec<(i32, f32)> = Vec::new(); + + 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() { + 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 edges if the +/// edge of interest does not have a speed limit. +pub fn traffic_speed_comfort_from_neighbors( + entry: &EdgeRTreeEntry, + neighboring_edges: &[&EdgeRTreeEntry], +) -> i32 { + let speed_mph = estimated_speed_from_neighbors(entry, neighboring_edges).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 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 63% rename from rust/bambam-osm/src/app/network/wci/wci.rs rename to rust/bambam-modal-metrics/src/wci/wci.rs index ea1cee67..4db67c9b 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; @@ -78,37 +78,43 @@ impl Wci { } } - /// Computes the walkability `Wci` for a way. - pub fn walkability(way: &OsmWayDataSerializable) -> Wci { - if way_is_sidewalk(way) || way_is_footway(way) { + /// Computes the walkability `Wci` for an edge. + pub fn walkability(edge: &dyn EdgeForModalMetric) -> Wci { + if edge.is_sidewalk() || edge.is_footway() { Wci(2) } else { Wci(-2) } } - /// Computes the traffic signal `Wci` for a way. - pub fn traffic_signal_comfort(src_node: &OsmNodeDataSerializable) -> Wci { - if has_traffic_signals(src_node) { + /// 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) - } else if has_stop_sign(src_node) { + } else if src_vertex.has_stop_sign() { Wci(1) } else { Wci(0) } } - /// Computes the cycleway `Wci` for a way. - pub fn cycleway_comfort(entry: &WayRTreeEntry, neighboring_ways: &Vec<&WayRTreeEntry>) -> 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))), + /// Computes the cycleway `Wci` for an edge. + pub fn cycleway_comfort( + entry: &EdgeRTreeEntry, + neighboring_ways: &Vec<&EdgeRTreeEntry>, + ) -> Wci { + // 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 - pub fn traffic_speed_comfort(entry: &WayRTreeEntry, neighbors: &Vec<&WayRTreeEntry>) -> Wci { + /// Computes the traffic speed `Wci` for an edge. + 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..582dfc64 100644 --- a/rust/bambam-osm/Cargo.toml +++ b/rust/bambam-osm/Cargo.toml @@ -12,6 +12,7 @@ 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 } 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/mod.rs b/rust/bambam-osm/src/app/network/wci/mod.rs deleted file mode 100644 index 2cc0f8e7..00000000 --- a/rust/bambam-osm/src/app/network/wci/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod compute_wci; -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-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..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 @@ -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,103 @@ 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::(); + Some(mph.round() 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..d3d7c4ee 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)] @@ -36,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, }, @@ -214,10 +215,24 @@ impl App { Self::ModalMetricSet { metric_name, 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:?}")), + 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). + // + // 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_file, + vertices_file, + output_file, + ) + .map_err(|e| format!("failed to run bulk compute modal metric: {e:?}")) + } Self::PreProcessGrid { acs_type, acs_year, diff --git a/script/publish_crates.sh b/script/publish_crates.sh index 6afadde3..3d293b7a 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' @@ -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