Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion rust/bambam-osm/src/app/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1 @@
pub mod wci;
pub mod network;
38 changes: 38 additions & 0 deletions rust/bambam-osm/src/app/network/common/cycleway_tag.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/// The CyclewayTag is a type that characterizes the
/// level of safety of a cycleway.
///
/// You can generate a new cycleway tag by passing in
/// the OSM way's cycleway attribute.
#[derive(Debug)]
pub enum CyclewayTag {
DedicatedNoBuffer,
NoDedicatedWithFacilities,
NoDedicatedNoFacilities,
}

impl CyclewayTag {
pub fn new(tag: &str) -> Self {
if Self::is_dedicated_no_buffer(tag) {
CyclewayTag::DedicatedNoBuffer
} else if Self::is_no_dedicated_with_facilities(tag) {
CyclewayTag::NoDedicatedWithFacilities
} else {
CyclewayTag::NoDedicatedNoFacilities
}
}
//TODO: ADD DedicatedWithBuffer variant for LTS computations.
/// DedicatedNoBuffer variant is when a cycleway
/// is it's own dedicated space, but is still
/// a part of the road.
fn is_dedicated_no_buffer(tag: &str) -> bool {
matches!(tag, "lane" | "designated" | "track")
}

/// NoDedicatedWithFacilities variant is when a
/// cycleway does not have a designated lane, but
/// has some facilities for cycling awareness such as
/// signage for sharing the road with vehicle traffic.
fn is_no_dedicated_with_facilities(tag: &str) -> bool {
matches!(tag, "crossing" | "shared" | "shared_lane")
}
}
4 changes: 4 additions & 0 deletions rust/bambam-osm/src/app/network/common/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub mod cycleway_tag;
pub mod ops;
pub mod way_rtree_entry;
pub const MIN_DISTANCE_RTREE_NEIGHBOR: f32 = 0.0001378;
58 changes: 58 additions & 0 deletions rust/bambam-osm/src/app/network/common/ops.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use super::MIN_DISTANCE_RTREE_NEIGHBOR;
use crate::app::network::common::way_rtree_entry::WayRTreeEntry;
Comment thread
admrtin marked this conversation as resolved.
use crate::model::osm::graph::OsmNodeDataSerializable;
use crate::model::osm::graph::OsmWayDataSerializable;
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<WayRTreeEntry>,
) -> 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<Vec<WayRTreeEntry>, Box<dyn Error>> {
let mut edge_reader = csv::Reader::from_path(edges_file)?;
let mut way_entries = Vec::new();

for record in edge_reader.deserialize::<OsmWayDataSerializable>() {
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)
}
59 changes: 59 additions & 0 deletions rust/bambam-osm/src/app/network/common/way_rtree_entry.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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
/// 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`,
/// 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 {
bbox: AABB<[f32; 2]>,
pub centroid: geo::Point<f32>,
pub way: OsmWayDataSerializable,
}

impl WayRTreeEntry {
pub fn new(way: OsmWayDataSerializable) -> Option<Self> {
// Grab the bounding rectangle of the linestring. If it doesn't exist, return None.
Comment thread
admrtin marked this conversation as resolved.
let rect = way.linestring.bounding_rect()?;

// Compute the centroid of the linestring. If it doesn't exist, return None.
let centroid = way.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,
})
}
}

impl RTreeObject for WayRTreeEntry {
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.
// 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 {
let query_point = geo::Point::new(point[0], point[1]);
let distance = Euclidean.distance(self.centroid, query_point);
distance * distance
}
}
1 change: 1 addition & 0 deletions rust/bambam-osm/src/app/network/lts/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

3 changes: 3 additions & 0 deletions rust/bambam-osm/src/app/network/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod common;
pub mod lts;
pub mod wci;
97 changes: 97 additions & 0 deletions rust/bambam-osm/src/app/network/wci/bulk_compute_wci.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
use crate::app::network::wci::compute_wci::compute_wci;
use crate::app::network::wci::wci_score::WciError;
use crate::app::network::{
common::ops::load_way_rtree_entries, wci::compute_wci::WciComponentScores,
};
use crate::model::osm::graph::OsmNodeDataSerializable;
use kdam::{Bar, BarBuilder, BarExt};
use rayon::prelude::*;
use routee_compass_core::util::fs::read_utils;
use rstar::RTree;
use std::sync::{Arc, Mutex};
use std::{
error::Error,
fs::File,
io::{BufWriter, Write},
};

/// Bulk compute WCI scores for an OSM network by taking in a vertices-complete.csv
/// and edges-complete.csv.
///
/// Reads the verticies and edges into memory, constructs an R-tree for spatial queries,
/// computes the WCI score for each way in parallel, and writes the scores to an output file.
///
/// WCI scores are computed in parallel and written line-by-line to `output_file`.
/// Authors: EG (2025 original), AM (2026 refactor)
pub fn bulk_compute_wci(
edges_file: &str,
vertices_file: &str,
output_file: &str,
) -> Result<(), Box<dyn Error>> {
println!("Reading:\n\t- vertex set @ {vertices_file}\n\t- edge set @ {edges_file}");
// load all vertices into memory
let vertices: Box<[OsmNodeDataSerializable]> =
read_utils::from_csv(&vertices_file, true, None, None)?;
// load all ways into memory as type WayRTreeEntry for insertion into the R-tree
let way_rtree_entries = load_way_rtree_entries(edges_file, &vertices)?;
println!("Edges and vertices read successfully.");
// bulk-load a copy of the entries into the r-tree; we keep `entries` around
// so each centroid can be paired with its own way during the WCI calculation
let rtree = RTree::bulk_load(way_rtree_entries.clone());

let bar: Arc<Mutex<Bar>> = Arc::new(Mutex::new(
BarBuilder::default()
.desc(format!("Computing WCI scores for the road network"))
.total(way_rtree_entries.len())
.build()?,
));

// calculate the WCI component scores for each way in parallel using the compute_wci function
let wci_vec_with_components: Vec<WciComponentScores> = way_rtree_entries
.par_iter()
.map(|way_entry| -> Result<WciComponentScores, WciError> {
// compute wci for this entry
let wci = compute_wci(
&rtree,
way_entry,
vertices
.get(way_entry.way.src_vertex_id.0)
.unwrap_or(&OsmNodeDataSerializable::default()),
);
// update pbar
if let Ok(mut bar) = bar.clone().lock() {
let _ = bar.update(1);
}
// return wci result
wci
})
.collect::<Result<Vec<WciComponentScores>, WciError>>()?;

let file = File::create(output_file)?;
let mut writer = BufWriter::new(file);
// write header
writeln!(writer, "wci_total,wci_walk,wci_speed,wci_cycle,wci_signal")?;
// write values
for wci in &wci_vec_with_components {
writeln!(
writer,
"{},{},{},{},{}",
wci.total_score,
wci.walkability_score
.as_ref()
.map_or(String::new(), |v| v.to_string()),
wci.traffic_speed_score
.as_ref()
.map_or(String::new(), |v| v.to_string()),
wci.cycleway_score
.as_ref()
.map_or(String::new(), |v| v.to_string()),
wci.traffic_signal_score
.as_ref()
.map_or(String::new(), |v| v.to_string()),
)?;
}
writer.flush()?;
println!("WCI scores computed successfully.\nWCI scores file saved @ {output_file}.");
Ok(())
}
Loading
Loading