From 4cd4a72e11e7b04226130a30c9170f9d481989cf Mon Sep 17 00:00:00 2001 From: Rob Fitzgerald Date: Fri, 28 Aug 2026 09:44:21 -0600 Subject: [PATCH 1/4] support for global or mode-specific configurations --- configuration/test_denver.toml | 12 ++++-- configuration/test_gtfs_config_boulder.toml | 12 ++++-- .../test_gtfs_config_denver_rtd.toml | 12 ++++-- .../model/output_plugin/global_or_modal.rs | 37 +++++++++++++++++++ .../src/model/output_plugin/mod.rs | 2 + .../src/model/output_plugin/output_config.rs | 9 +++-- .../src/util/bambam-config-omf.toml | 12 ++++-- .../src/model/output_plugin/bambam/plugin.rs | 15 +++++--- 8 files changed, 90 insertions(+), 21 deletions(-) create mode 100644 rust/bambam-core/src/model/output_plugin/global_or_modal.rs diff --git a/configuration/test_denver.toml b/configuration/test_denver.toml index 750ef27d..39afe6ba 100644 --- a/configuration/test_denver.toml +++ b/configuration/test_denver.toml @@ -175,11 +175,17 @@ tree = "geo_json" [[plugin.output_plugins]] type = "bambam" format = "aggregate" -binning = { type = "time", feature = "trip_time", values = [10,20,30,40], unit = "minutes" } -geometry_model = { type = "destination_point" } -isochrone_algorithm = { type = "k_nearest_concave_hull", k = 3 } isochrone_format = "wkb" +binning.type = "global" +binning.value = { type = "time", feature = "trip_time", values = [10,20,30,40], unit = "minutes" } + +geometry_model.type = "global" +geometry_model.value = { type = "destination_point" } + +isochrone_algorithm.type = "global" +isochrone_algorithm.value = { type = "k_nearest_concave_hull", k = 3 } + [[plugin.output_plugins]] type = "isochrone" diff --git a/configuration/test_gtfs_config_boulder.toml b/configuration/test_gtfs_config_boulder.toml index e1be6f07..39bccb5e 100644 --- a/configuration/test_gtfs_config_boulder.toml +++ b/configuration/test_gtfs_config_boulder.toml @@ -102,11 +102,17 @@ tree = "geo_json" [[plugin.output_plugins]] type = "bambam" format = "aggregate" -binning = { type = "time", feature = "trip_time", values = [10,20,30,40], unit = "minutes" } -geometry_model = { type = "destination_point" } -isochrone_algorithm = { type = "k_nearest_concave_hull", k = 3 } isochrone_format = "wkb" +binning.type = "global" +binning.value = { type = "time", feature = "trip_time", values = [10,20,30,40], unit = "minutes" } + +geometry_model.type = "global" +geometry_model.value = { type = "destination_point" } + +isochrone_algorithm.type = "global" +isochrone_algorithm.value = { type = "k_nearest_concave_hull", k = 3 } + [[plugin.output_plugins]] type = "isochrone" diff --git a/configuration/test_gtfs_config_denver_rtd.toml b/configuration/test_gtfs_config_denver_rtd.toml index b8fff548..5c536394 100644 --- a/configuration/test_gtfs_config_denver_rtd.toml +++ b/configuration/test_gtfs_config_denver_rtd.toml @@ -95,11 +95,17 @@ tree = "geo_json" [[plugin.output_plugins]] type = "bambam" format = "aggregate" -binning = { type = "time", feature = "trip_time", values = [10,20,30,40], unit = "minutes" } -geometry_model = { type = "destination_point" } -isochrone_algorithm = { type = "k_nearest_concave_hull", k = 3 } isochrone_format = "wkb" +binning.type = "global" +binning.value = { type = "time", feature = "trip_time", values = [10,20,30,40], unit = "minutes" } + +geometry_model.type = "global" +geometry_model.value = { type = "destination_point" } + +isochrone_algorithm.type = "global" +isochrone_algorithm.value = { type = "k_nearest_concave_hull", k = 3 } + [[plugin.output_plugins]] type = "isochrone" diff --git a/rust/bambam-core/src/model/output_plugin/global_or_modal.rs b/rust/bambam-core/src/model/output_plugin/global_or_modal.rs new file mode 100644 index 00000000..67c00259 --- /dev/null +++ b/rust/bambam-core/src/model/output_plugin/global_or_modal.rs @@ -0,0 +1,37 @@ +use std::collections::HashMap; + +use routee_compass::plugin::output::OutputPluginError; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "snake_case", tag = "type")] +pub enum GlobalOrModal { + Global { + value: T, + }, + Modal { + values: HashMap, + fallback: Option, + }, +} + +impl GlobalOrModal { + /// gets the configuration value. if the user has provided a global value, we are done. + /// if the user has provided modal values, we first check if this mode has a matching config. + /// if not, we attempt to draw from the fallback value. + pub fn get_value(&self, mode: &str) -> Result<&T, OutputPluginError> { + match self { + GlobalOrModal::Global { value } => Ok(value), + GlobalOrModal::Modal { values, fallback } => { + if let Some(value) = values.get(mode) { + return Ok(value); + } + fallback.as_ref().ok_or_else(|| { + let name = std::any::type_name::(); + let msg = format!("user specified modal configuration for {name} but mode '{mode}' not found and no fallback specified."); + OutputPluginError::OutputPluginFailed(msg) + }) + } + } + } +} diff --git a/rust/bambam-core/src/model/output_plugin/mod.rs b/rust/bambam-core/src/model/output_plugin/mod.rs index fbc89fc3..d176fbca 100644 --- a/rust/bambam-core/src/model/output_plugin/mod.rs +++ b/rust/bambam-core/src/model/output_plugin/mod.rs @@ -1,5 +1,7 @@ +mod global_or_modal; pub mod isochrone; pub mod opportunity; mod output_config; +pub use global_or_modal::GlobalOrModal; pub use output_config::BambamOutputConfig; diff --git a/rust/bambam-core/src/model/output_plugin/output_config.rs b/rust/bambam-core/src/model/output_plugin/output_config.rs index 2ac3b542..1d56a4a9 100644 --- a/rust/bambam-core/src/model/output_plugin/output_config.rs +++ b/rust/bambam-core/src/model/output_plugin/output_config.rs @@ -3,6 +3,7 @@ use crate::model::{ output_plugin::{ isochrone::{GeometryModelConfig, IsochroneAlgorithm, IsochroneOutputFormat}, opportunity::OpportunityOrientation, + GlobalOrModal, }, }; use serde::{Deserialize, Serialize}; @@ -14,15 +15,15 @@ use serde::{Deserialize, Serialize}; pub enum BambamOutputConfig { Aggregate { /// the method for binning the output. required when opportunity_format == Aggregate. - binning: BinningConfig, + binning: GlobalOrModal, /// any additional filters to apply when selecting destinations. optional for both /// opportunity_formats. - destination_filter: Option>, + destination_filter: Option>>, /// algorithm for assigning physical destination locations from a search tree branch. /// used in the isochrone drawing procedure. - geometry_model: GeometryModelConfig, + geometry_model: GlobalOrModal, /// algorithm used to draw isochrones from the destination points. - isochrone_algorithm: IsochroneAlgorithm, + isochrone_algorithm: GlobalOrModal, /// geometry format to use when writing isochrones. isochrone_format: IsochroneOutputFormat, /// location along a roadway where the opportunity is map matched. by default, diff --git a/rust/bambam-omf/src/util/bambam-config-omf.toml b/rust/bambam-omf/src/util/bambam-config-omf.toml index aa29465b..f30a3d64 100644 --- a/rust/bambam-omf/src/util/bambam-config-omf.toml +++ b/rust/bambam-omf/src/util/bambam-config-omf.toml @@ -155,11 +155,17 @@ type = "traversal" [[plugin.output_plugins]] type = "bambam" format = "aggregate" -binning = { type = "time", feature = "trip_time", values = [10,20,30,40], unit = "minutes" } -geometry_model = { type = "destination_point" } -isochrone_algorithm = { type = "k_nearest_concave_hull", k = 3 } isochrone_format = "wkb" +binning.type = "global" +binning.value = { type = "time", feature = "trip_time", values = [10,20,30,40], unit = "minutes" } + +geometry_model.type = "global" +geometry_model.value = { type = "destination_point" } + +isochrone_algorithm.type = "global" +isochrone_algorithm.value = { type = "k_nearest_concave_hull", k = 3 } + [[plugin.output_plugins]] type = "isochrone" diff --git a/rust/bambam/src/model/output_plugin/bambam/plugin.rs b/rust/bambam/src/model/output_plugin/bambam/plugin.rs index 0def1db2..28e56267 100644 --- a/rust/bambam/src/model/output_plugin/bambam/plugin.rs +++ b/rust/bambam/src/model/output_plugin/bambam/plugin.rs @@ -19,6 +19,8 @@ impl OutputPlugin for BambamOutputPlugin { _result: &Result<(SearchAppResult, SearchInstance), CompassAppError>, ) -> Result<(), OutputPluginError> { let mut row = bambam_typed::BambamOutputRow::new(output); + let req = row.request()?; + let mode = req.get_mode()?; let mut info = row.info_mut()?; match &self.0 { @@ -30,15 +32,18 @@ impl OutputPlugin for BambamOutputPlugin { isochrone_format, opportunity_orientation, } => { + // set values provided by configuration info.set_opportunity_format(OpportunityFormat::Aggregate)?; info.set_opportunity_orientation(*opportunity_orientation)?; - info.set_bin_range(binning)?; + info.set_isochrone_format(isochrone_format)?; + + // set values that may vary based on the travel mode. + info.set_bin_range(binning.get_value(&mode)?)?; if let Some(f) = destination_filter { - info.set_destination_filter(f)?; + info.set_destination_filter(f.get_value(&mode)?)?; } - info.set_geometry_model(geometry_model)?; - info.set_isochrone_algorithm(isochrone_algorithm)?; - info.set_isochrone_format(isochrone_format)?; + info.set_geometry_model(geometry_model.get_value(&mode)?)?; + info.set_isochrone_algorithm(isochrone_algorithm.get_value(&mode)?)?; } BambamOutputConfig::Disaggregate { destination_filter, From 64847572b7699b80da85a625af0f5c6ec8835264 Mon Sep 17 00:00:00 2001 From: Rob Fitzgerald Date: Fri, 28 Aug 2026 14:12:17 -0600 Subject: [PATCH 2/4] rename request.mode field to trip_mode --- rust/bambam-core/src/model/bambam_field.rs | 4 ++-- rust/bambam-core/src/model/bambam_typed.rs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/rust/bambam-core/src/model/bambam_field.rs b/rust/bambam-core/src/model/bambam_field.rs index 9c62be51..aa244808 100644 --- a/rust/bambam-core/src/model/bambam_field.rs +++ b/rust/bambam-core/src/model/bambam_field.rs @@ -10,7 +10,7 @@ use serde_json::{json, Value}; pub const TIME_BINS: &str = "bin"; pub const TIME_BIN: &str = "time_bin"; pub const INFO: &str = "info"; -pub const MODE: &str = "mode"; +pub const TRIP_MODE: &str = "trip_mode"; pub const OUTPUT_CONFIG: &str = "output_config"; pub const ISOCHRONE: &str = "isochrone"; pub const ISOCHRONE_ALGORITHM: &str = "isochrone_algorithm"; @@ -54,7 +54,7 @@ pub mod get { }; pub fn mode(value: &Value) -> Result { - let path = ["request", super::MODE]; + let path = ["request", super::TRIP_MODE]; super::get_nested(value, &path).map_err(|e| { let dot_path = path.join("."); OutputPluginError::OutputPluginFailed(format!( diff --git a/rust/bambam-core/src/model/bambam_typed.rs b/rust/bambam-core/src/model/bambam_typed.rs index e6c11749..b86a6eef 100644 --- a/rust/bambam-core/src/model/bambam_typed.rs +++ b/rust/bambam-core/src/model/bambam_typed.rs @@ -146,8 +146,8 @@ pub struct RequestSection<'a>(&'a Value); impl<'a> RequestSection<'a> { /// Returns the transport `mode` string (e.g. `"car"`, `"transit"`). - pub fn get_mode(&self) -> Result { - get_field(self.0, bambam_field::MODE) + pub fn get_trip_mode(&self) -> Result { + get_field(self.0, bambam_field::TRIP_MODE) } } @@ -576,7 +576,7 @@ mod tests { fn request_section_get_mode() { let mut value = json!({"request": {"mode": "transit"}}); let row = BambamOutputRow::new(&mut value); - assert_eq!(row.request().unwrap().get_mode().unwrap(), "transit"); + assert_eq!(row.request().unwrap().get_trip_mode().unwrap(), "transit"); } /// Request section: missing request key returns error. From fdb1388c707ab17b302b2574f9514db85a4520ab Mon Sep 17 00:00:00 2001 From: Rob Fitzgerald Date: Fri, 28 Aug 2026 14:12:21 -0600 Subject: [PATCH 3/4] rename request.mode field to trip_mode --- rust/bambam/src/model/output_plugin/bambam/plugin.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/bambam/src/model/output_plugin/bambam/plugin.rs b/rust/bambam/src/model/output_plugin/bambam/plugin.rs index 28e56267..3c0ab52d 100644 --- a/rust/bambam/src/model/output_plugin/bambam/plugin.rs +++ b/rust/bambam/src/model/output_plugin/bambam/plugin.rs @@ -20,7 +20,7 @@ impl OutputPlugin for BambamOutputPlugin { ) -> Result<(), OutputPluginError> { let mut row = bambam_typed::BambamOutputRow::new(output); let req = row.request()?; - let mode = req.get_mode()?; + let mode = req.get_trip_mode()?; let mut info = row.info_mut()?; match &self.0 { From 7d31f773150a78d42319815737467a609144ab10 Mon Sep 17 00:00:00 2001 From: Rob Fitzgerald Date: Fri, 25 Sep 2026 11:48:43 -0600 Subject: [PATCH 4/4] doc comment for GlobalOrModal --- .../model/output_plugin/global_or_modal.rs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/rust/bambam-core/src/model/output_plugin/global_or_modal.rs b/rust/bambam-core/src/model/output_plugin/global_or_modal.rs index 67c00259..6c774bb1 100644 --- a/rust/bambam-core/src/model/output_plugin/global_or_modal.rs +++ b/rust/bambam-core/src/model/output_plugin/global_or_modal.rs @@ -3,6 +3,33 @@ use std::collections::HashMap; use routee_compass::plugin::output::OutputPluginError; use serde::{Deserialize, Serialize}; +/// a configuration that can either be defined for all modes or defined +/// to vary by mode. +/// +/// Note: if modal is selected, then all modes must be represented in the `values`, +/// unless a fallback is provided. +/// +/// # Global Example +/// +/// All modes using the same time binning configuration: +/// +/// ```toml +/// binning.type = "global" +/// binning.value = { type = "time", feature = "trip_time", values = [10,20,30,40], unit = "minutes" } +/// ``` +/// +/// # Modal Example +/// +/// Geometry Model linestring sampling density varying by mode: +/// +/// ```toml +/// [plugin.output_plugins.geometry_model] +/// type = "modal" +/// values.walk = { type = "linestring_stride", stride = 20.0, distance_unit = "meters" } +/// values.bike = { type = "linestring_stride", stride = 50.0, distance_unit = "meters" } +/// values.transit = { type = "linestring_stride", stride = 50.0, distance_unit = "meters" } +/// values.drive = { type = "linestring_stride", stride = 150.0, distance_unit = "meters" } +/// ``` #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "snake_case", tag = "type")] pub enum GlobalOrModal {