diff --git a/configuration/bambam-omf/travel-mode-filter.json b/configuration/bambam-omf/travel-mode-filter.json index 07462c17..720a123d 100644 --- a/configuration/bambam-omf/travel-mode-filter.json +++ b/configuration/bambam-omf/travel-mode-filter.json @@ -7,7 +7,7 @@ { "type": "class", "classes": ["secondary", "tertiary", "residential", "living_street", "unclassified", "service", "pedestrian", "footway", "steps", "path", "track", "bridleway", "unknown"], - "ignore_unset": true, + "allow_unset": true, "behavior": "include" }, { "type": "access_mode", "modes": ["foot"] } @@ -20,7 +20,7 @@ { "type": "class", "classes": ["primary", "secondary", "tertiary", "residential", "living_street", "unclassified", "service", "pedestrian", "path", "track", "cycleway", "bridleway", "unknown"], - "ignore_unset": true, + "allow_unset": true, "behavior": "include" }, { "type": "access_mode", "modes": ["bicycle"] } @@ -33,7 +33,7 @@ { "type": "class", "classes": ["motorway", "trunk", "primary", "secondary", "tertiary", "residential", "living_street", "unclassified", "service", "unknown"], - "ignore_unset": true, + "allow_unset": true, "behavior": "include" }, { "type": "access_mode", "modes": ["motor_vehicle", "car", "truck", "motorcycle"] } diff --git a/rust/Cargo.toml b/rust/Cargo.toml index dead6694..01dca801 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,22 +1,24 @@ [workspace] resolver = "2" -members = ["bambam", "bambam-osm", "bambam-gtfs", "bambam-omf", "bambam-gbfs", "bambam-py"] +members = [ + "bambam", + "bambam-gbfs", + "bambam-gtfs", + "bambam-omf", + "bambam-osm", + "bambam-py", +] [workspace.dependencies] +# third party +arrow = { version = "55.0.0" } +bamcensus = { version = "0.1.0" } +bamcensus-acs = { version = "0.1.0" } + # nrel bamcensus-core = { version = "0.1.0" } -bamcensus-acs = { version = "0.1.0" } -bamcensus = { version = "0.1.0" } bamcensus-lehd = { version = "0.1.0" } -routee-compass = "0.15.2" -routee-compass-core = "0.15.2" -routee-compass-powertrain = "0.15.2" -routee-compass-py = "0.15.2" -routee-compass-macros = "0.15.2" - -# third party -arrow = { version = "55.0.0" } chrono = { version = "0.4.41", features = ["serde"] } clap = { version = "4.3.19", features = ["derive"] } config = "0.15.11" @@ -51,10 +53,16 @@ rand = "0.9.1" rayon = "1.10.0" regex = { version = "1.11.1" } reqwest = { version = "0.12.12", features = ["blocking"] } +routee-compass = "0.15.2" +routee-compass-core = "0.15.2" +routee-compass-macros = "0.15.2" +routee-compass-powertrain = "0.15.2" +routee-compass-py = "0.15.2" rstar = { version = "0.12.0" } serde = { version = "1.0.160", features = ["derive"] } -serde_json = { version = "1.0" } serde_arrow = { version = "0.13.7", features = ["arrow-55"] } +serde_bytes = { version = "0.11.19" } +serde_json = { version = "1.0" } serde_with = { version = "3.0", features = ["chrono_0_4"] } shapefile = { version = "0.7.0", features = ["geo-types"] } skiplist = "0.5.1" diff --git a/rust/bambam-gbfs/Cargo.toml b/rust/bambam-gbfs/Cargo.toml index cceaf937..69b76611 100644 --- a/rust/bambam-gbfs/Cargo.toml +++ b/rust/bambam-gbfs/Cargo.toml @@ -6,15 +6,15 @@ license = "BSD-3-Clause" description = "GBFS Extensions for The Behavior and Advanced Mobility Big Access Model" [dependencies] +chrono = { workspace = true } +clap = { workspace = true } env_logger = { workspace = true } +geo = { workspace = true } +humantime = { workspace = true } +kdam = { workspace = true } +log = { workspace = true } +reqwest = { workspace = true } routee-compass-core = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } serde_with = { workspace = true } -chrono = { workspace = true } -humantime = { workspace = true } -log = { workspace = true } -geo = { workspace = true } -reqwest = { workspace = true } -clap = { workspace = true } -kdam = { workspace = true } \ No newline at end of file diff --git a/rust/bambam-omf/Cargo.toml b/rust/bambam-omf/Cargo.toml index 335f491a..b3da1932 100644 --- a/rust/bambam-omf/Cargo.toml +++ b/rust/bambam-omf/Cargo.toml @@ -18,15 +18,12 @@ keywords = [ categories = ["command-line-utilities", "science", "science::geo"] [dependencies] -bamcensus-core = { workspace = true } -bamcensus-acs = { workspace = true } -bamcensus-lehd = { workspace = true } -bamcensus = { workspace = true } -routee-compass = { workspace = true } -routee-compass-core = { workspace = true } -routee-compass-powertrain = { workspace = true } arrow = { workspace = true } +bamcensus = { workspace = true } +bamcensus-acs = { workspace = true } +bamcensus-core = { workspace = true } +bamcensus-lehd = { workspace = true } chrono = { workspace = true } clap = { workspace = true } config = { workspace = true } @@ -41,16 +38,20 @@ itertools = { workspace = true } kdam = { workspace = true } log = { workspace = true } object_store = { workspace = true } -parquet = { workspace = true } opening-hours-syntax = { workspace = true } ordered-float = { workspace = true } +parquet = { workspace = true } rayon = { workspace = true } reqwest = { workspace = true } +routee-compass = { workspace = true } +routee-compass-core = { workspace = true } +routee-compass-powertrain = { workspace = true } serde = { workspace = true } -serde_json = { workspace = true } serde_arrow = { workspace = true } +serde_bytes = { workspace = true } +serde_json = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } uom = { workspace = true } -wkt = { workspace = true } wkb = { workspace = true } +wkt = { workspace = true } diff --git a/rust/bambam-omf/src/app/network.rs b/rust/bambam-omf/src/app/network.rs index 31b310c5..46e9bacc 100644 --- a/rust/bambam-omf/src/app/network.rs +++ b/rust/bambam-omf/src/app/network.rs @@ -6,7 +6,8 @@ use crate::{ app::CliBoundingBox, collection::{ filter::TravelModeFilter, ObjectStoreSource, OvertureMapsCollectionError, - OvertureMapsCollectorConfig, ReleaseVersion, TransportationCollection, + OvertureMapsCollectorConfig, ReleaseVersion, SegmentAccessRestrictionWhen, + TransportationCollection, }, graph::OmfGraphVectorized, util, @@ -18,6 +19,20 @@ pub struct NetworkEdgeListConfiguration { pub filter: Vec, } +impl From<&NetworkEdgeListConfiguration> for SegmentAccessRestrictionWhen { + fn from(value: &NetworkEdgeListConfiguration) -> Self { + let user_modes_opt = value.filter.iter().find_map(|f| match f { + TravelModeFilter::MatchesModeAccess { modes } => Some(modes.clone()), + _ => None, + }); + let mut result = SegmentAccessRestrictionWhen::default(); + if let Some(modes) = user_modes_opt { + result.mode = Some(modes); + } + result + } +} + /// runs an OMF network import using the provided configuration. pub fn run( bbox: Option<&CliBoundingBox>, diff --git a/rust/bambam-omf/src/collection/error.rs b/rust/bambam-omf/src/collection/error.rs index c38d9f1d..f20892ef 100644 --- a/rust/bambam-omf/src/collection/error.rs +++ b/rust/bambam-omf/src/collection/error.rs @@ -48,6 +48,10 @@ pub enum OvertureMapsCollectionError { ReadError { path: PathBuf, message: String }, #[error("Error writing to '{path}': {message}")] WriteError { path: PathBuf, message: String }, + #[error("Invalid `between` vector: {0}")] + InvalidBetweenVector(String), + #[error("Required attribute is None: {0}")] + MissingAttribute(String), #[error("{0}")] InternalError(String), } diff --git a/rust/bambam-omf/src/collection/filter/travel_mode_filter.rs b/rust/bambam-omf/src/collection/filter/travel_mode_filter.rs index d89d2e11..208e71b0 100644 --- a/rust/bambam-omf/src/collection/filter/travel_mode_filter.rs +++ b/rust/bambam-omf/src/collection/filter/travel_mode_filter.rs @@ -24,7 +24,7 @@ pub enum TravelModeFilter { MatchesClasses { classes: HashSet, behavior: MatchBehavior, - ignore_unset: bool, + allow_unset: bool, }, /// filter a row based on a class with additional subclass(es). fails if not a match, /// and optionally, if 'class' or 'subclass' are unset. @@ -32,7 +32,7 @@ pub enum TravelModeFilter { MatchesClassesWithSubclasses { classes: HashMap>, behavior: MatchBehavior, - ignore_unset: bool, + allow_unset: bool, }, /// filter a row based on the [SegmentMode]. @@ -145,35 +145,39 @@ impl TravelModeFilter { /// returns false if there is no match. pub fn matches_filter(&self, segment: &TransportationSegmentRecord) -> bool { match self { + // subtype matching. default behavior is REJECT TravelModeFilter::MatchesSubtype { subtype } => segment .subtype .as_ref() .map(|s| s == subtype) .unwrap_or_default(), + // class matching. default behavior set by user (allow_unset). TravelModeFilter::MatchesClasses { classes, behavior, - ignore_unset, + allow_unset, } => segment .class .as_ref() .map(|c| behavior.apply(classes.contains(c))) - .unwrap_or(*ignore_unset), + .unwrap_or(*allow_unset), + // subclass matching. default behavior set by user (allow_unset). TravelModeFilter::MatchesClassesWithSubclasses { classes, behavior, - ignore_unset, + allow_unset, } => match (segment.class.as_ref(), segment.subclass.as_ref()) { (Some(cl), None) => behavior.apply(classes.contains_key(cl)), (Some(cl), Some(sc)) => match classes.get(cl) { - None => *ignore_unset, + None => *allow_unset, Some(subclasses) => behavior.apply(subclasses.contains(sc)), }, - _ => *ignore_unset, + _ => *allow_unset, }, + // mode matching. default behavior is ALLOW TravelModeFilter::MatchesModeAccess { modes } => { let restrictions = segment .access_restrictions diff --git a/rust/bambam-omf/src/collection/mod.rs b/rust/bambam-omf/src/collection/mod.rs index fd99abcc..8842630b 100644 --- a/rust/bambam-omf/src/collection/mod.rs +++ b/rust/bambam-omf/src/collection/mod.rs @@ -2,12 +2,12 @@ mod collector; mod collector_config; mod error; mod object_source; -mod record; mod taxonomy; mod version; pub mod constants; pub mod filter; +pub mod record; pub use collector::OvertureMapsCollector; pub use collector_config::OvertureMapsCollectorConfig; @@ -17,7 +17,9 @@ pub use filter::RowFilter; pub use filter::RowFilterConfig; pub use object_source::ObjectStoreSource; pub use record::{ - BuildingsRecord, OvertureRecord, OvertureRecordType, PlacesRecord, TransportationCollection, + BuildingsRecord, OvertureRecord, OvertureRecordType, PlacesRecord, + SegmentAccessRestrictionWhen, SegmentClass, SegmentFullType, SegmentSpeedLimit, + SegmentSpeedUnit, SegmentSubclass, SegmentSubtype, TransportationCollection, TransportationConnectorRecord, TransportationSegmentRecord, }; pub use taxonomy::{TaxonomyModel, TaxonomyModelBuilder}; diff --git a/rust/bambam-omf/src/collection/record/common.rs b/rust/bambam-omf/src/collection/record/common.rs index 23ec68cd..4c906f56 100644 --- a/rust/bambam-omf/src/collection/record/common.rs +++ b/rust/bambam-omf/src/collection/record/common.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct OvertureMapsBbox { xmin: Option, xmax: Option, @@ -9,7 +9,7 @@ pub struct OvertureMapsBbox { ymax: Option, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct OvertureMapsSource { property: Option, dataset: Option, @@ -18,14 +18,14 @@ pub struct OvertureMapsSource { confidence: Option, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct OvertureMapsNames { primary: Option, common: Option>>, rules: Option>, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, Default)] struct OvertureMapsNamesRule { variant: Option, language: Option, diff --git a/rust/bambam-omf/src/collection/record/geometry_wkb_codec.rs b/rust/bambam-omf/src/collection/record/geometry_wkb_codec.rs index b91bbd43..fec944ce 100644 --- a/rust/bambam-omf/src/collection/record/geometry_wkb_codec.rs +++ b/rust/bambam-omf/src/collection/record/geometry_wkb_codec.rs @@ -1,13 +1,16 @@ use geo::{Geometry, MapCoords, TryConvert}; use geozero::{error::GeozeroError, wkb::Wkb, ToGeo}; use serde::{Deserialize, Deserializer}; +use serde_bytes; -// Deserialize into an enum that can handle both Vec and String +/// Deserialize into an enum that can handle both String and Vec, in +/// that order. #[derive(Deserialize)] #[serde(untagged)] enum BytesOrString { - Bytes(Vec), String(String), + #[serde(with = "serde_bytes")] + Bytes(Vec), } impl std::fmt::Display for BytesOrString { diff --git a/rust/bambam-omf/src/collection/record/mod.rs b/rust/bambam-omf/src/collection/record/mod.rs index dc927823..4f9e6f18 100644 --- a/rust/bambam-omf/src/collection/record/mod.rs +++ b/rust/bambam-omf/src/collection/record/mod.rs @@ -14,12 +14,14 @@ pub use record_type::OvertureRecordType; pub use transportation_collection::TransportationCollection; pub use transportation_connector::TransportationConnectorRecord; pub use transportation_segment::{ - SegmentAccessRestriction, SegmentAccessType, SegmentClass, SegmentHeading, SegmentMode, - SegmentSubclass, SegmentSubtype, TransportationSegmentRecord, + SegmentAccessRestriction, SegmentAccessRestrictionWhen, SegmentAccessType, SegmentClass, + SegmentDestination, SegmentFullType, SegmentHeading, SegmentMode, SegmentRecognized, + SegmentSpeedLimit, SegmentSpeedUnit, SegmentSubclass, SegmentSubtype, SegmentUsing, + TransportationSegmentRecord, }; // Common structs and functions for many record types -use common::OvertureMapsBbox; -use common::OvertureMapsNames; -use common::OvertureMapsSource; +pub use common::OvertureMapsBbox; +pub use common::OvertureMapsNames; +pub use common::OvertureMapsSource; pub mod geometry_wkb_codec; diff --git a/rust/bambam-omf/src/collection/record/transportation_segment.rs b/rust/bambam-omf/src/collection/record/transportation_segment.rs index 78638f22..dcd1d565 100644 --- a/rust/bambam-omf/src/collection/record/transportation_segment.rs +++ b/rust/bambam-omf/src/collection/record/transportation_segment.rs @@ -1,6 +1,10 @@ +use std::fmt::{self, Debug}; + use geo::{Coord, Geometry, Haversine, InterpolatableLine, Length, LineString}; use opening_hours_syntax::rules::OpeningHoursExpression; +use routee_compass_core::model::unit::SpeedUnit; use serde::{Deserialize, Serialize}; +use uom::si::f64::Velocity; use super::{geometry_wkb_codec, OvertureMapsBbox, OvertureMapsNames, OvertureMapsSource}; use crate::collection::{OvertureMapsCollectionError, OvertureRecord}; @@ -11,7 +15,7 @@ use crate::collection::{OvertureMapsCollectionError, OvertureRecord}; /// and other attributes relevant to routing and mapping. /// /// see -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct TransportationSegmentRecord { /// GERS identifier for this segment record pub id: String, @@ -85,7 +89,7 @@ impl TransportationSegmentRecord { } } - pub fn get_distance_at(&self, at: f64) -> Result { + pub fn get_distance_at_meters(&self, at: f64) -> Result { if !(0.0..=1.0).contains(&at) { return Err(OvertureMapsCollectionError::InvalidLinearReference(at)); } @@ -110,6 +114,22 @@ impl TransportationSegmentRecord { } } } + + pub fn get_segment_full_type(&self) -> Result { + use OvertureMapsCollectionError as E; + + Ok(SegmentFullType( + self.subtype.clone().ok_or(E::MissingAttribute(format!( + "`subtype` not found in segment: {self:?}" + )))?, + self.class.clone().ok_or(E::MissingAttribute(format!( + "`class` not found in segment: {self:?}" + )))?, + self.subclass.clone(), + )) + } + + // pub fn first_matching_subclass } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] @@ -120,6 +140,17 @@ pub enum SegmentSubtype { Water, } +impl fmt::Display for SegmentSubtype { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + SegmentSubtype::Road => "road", + SegmentSubtype::Rail => "rail", + SegmentSubtype::Water => "water", + }; + f.write_str(s) + } +} + #[derive(Serialize, Debug, Clone, PartialEq, Eq, Hash)] #[serde(rename_all = "snake_case")] pub enum SegmentClass { @@ -144,6 +175,32 @@ pub enum SegmentClass { Custom(String), } +impl fmt::Display for SegmentClass { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + SegmentClass::Motorway => "motorway", + SegmentClass::Primary => "primary", + SegmentClass::Secondary => "secondary", + SegmentClass::Tertiary => "tertiary", + SegmentClass::Residential => "residential", + SegmentClass::LivingStreet => "living_street", + SegmentClass::Trunk => "trunk", + SegmentClass::Unclassified => "unclassified", + SegmentClass::Service => "service", + SegmentClass::Pedestrian => "pedestrian", + SegmentClass::Footway => "footway", + SegmentClass::Steps => "steps", + SegmentClass::Path => "path", + SegmentClass::Track => "track", + SegmentClass::Cycleway => "cycleway", + SegmentClass::Bridleway => "bridleway", + SegmentClass::Unknown => "unknown", + SegmentClass::Custom(s) => s.as_str(), + }; + f.write_str(s) + } +} + impl<'de> Deserialize<'de> for SegmentClass { fn deserialize(deserializer: D) -> Result where @@ -185,6 +242,42 @@ pub enum SegmentSubclass { CycleCrossing, } +impl fmt::Display for SegmentSubclass { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + SegmentSubclass::Link => "link", + SegmentSubclass::Sidewalk => "sidewalk", + SegmentSubclass::Crosswalk => "crosswalk", + SegmentSubclass::ParkingAisle => "parking_aisle", + SegmentSubclass::Driveway => "driveway", + SegmentSubclass::Alley => "alley", + SegmentSubclass::CycleCrossing => "cycle_crossing", + }; + f.write_str(s) + } +} + +/// Fully qualified segment type including type, class and subclass. E.g. road-service-driveway +#[derive(Eq, PartialEq, Hash)] +pub struct SegmentFullType(SegmentSubtype, SegmentClass, Option); + +impl SegmentFullType { + pub fn has_subclass(&self) -> bool { + self.2.is_some() + } + + pub fn with_subclass(&self, subclass: SegmentSubclass) -> Self { + Self(self.0.clone(), self.1.clone(), Some(subclass)) + } + + pub fn as_str(&self) -> String { + match self.2.as_ref() { + Some(subclass) => format!("{}-{}-{}", self.0, self.1, subclass), + None => format!("{}-{}", self.0, self.1), + } + } +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] #[serde(rename_all = "snake_case")] pub enum SegmentAccessType { @@ -391,6 +484,15 @@ pub enum SegmentSpeedUnit { Mph, } +impl SegmentSpeedUnit { + pub fn to_uom(&self, value: f64) -> Velocity { + match self { + SegmentSpeedUnit::Kmh => SpeedUnit::KPH.to_uom(value), + SegmentSpeedUnit::Mph => SpeedUnit::MPH.to_uom(value), + } + } +} + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ConnectorReference { pub connector_id: String, @@ -421,6 +523,46 @@ pub struct SegmentValueBetween { pub between: Option>, } +impl SegmentValueBetween { + /// Used to filter limits based on a linear reference segment. + /// Returns `true` if the open interval `(between[0], between[1])` + /// overlaps with the open interval `(start, end)`. + /// + /// # Examples + /// + /// ``` + /// # use bambam_omf::collection::SegmentSpeedLimit; + /// + /// let limit = SegmentSpeedLimit { + /// min_speed: None, + /// max_speed: None, + /// is_max_speed_variable: None, + /// when: None, + /// between: Some(vec![10.0, 20.0]), + /// }; + /// + /// // (15, 25) overlaps with (10, 20) + /// assert!(limit.check_open_intersection(15.0, 25.0).unwrap()); + /// // (20, 30) does not overlap with open interval (10, 20) + /// assert!(!limit.check_open_intersection(20.0, 30.0).unwrap()); + /// ``` + pub fn check_open_intersection( + &self, + start: f64, + end: f64, + ) -> Result { + let b_vector = + self.between + .as_ref() + .ok_or(OvertureMapsCollectionError::InvalidBetweenVector(format!( + "`between` vector is empty: {self:?}" + )))?; + let (low, high) = validate_between_vector(b_vector)?; + + Ok(start < *high && end > *low) + } +} + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct SegmentAccessRestriction { pub access_type: SegmentAccessType, @@ -444,7 +586,7 @@ fn default_none() -> Option { None } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct SegmentAccessRestrictionWhen { /// Time span or time spans during which something is open or active, specified /// in the OSM opening hours specification: @@ -519,36 +661,81 @@ pub struct SegmentAccessRestrictionWhenVehicle { unit: Option, } +/// Describes objects that can be reached by following a transportation +/// segment in the same way those objects are described on signposts or +/// ground writing that a traveller following the segment would observe +/// in the real world. This allows navigation systems to refer to signs +/// and observable writing that a traveller actually sees. #[derive(Debug, Serialize, Deserialize, Clone)] pub struct SegmentDestination { + /// Labeled destinations that can be reached by following the segment. #[serde(skip_serializing_if = "Option::is_none", default)] - labels: Option>, - #[serde(skip_serializing_if = "Option::is_none", default)] - symbols: Option>, - #[serde(skip_serializing_if = "Option::is_none", default)] - from_connector_id: Option, + pub labels: Option>, + /// Indicates what special symbol/icon is present on a signpost, visible as road marking or similar. #[serde(skip_serializing_if = "Option::is_none", default)] - to_segment_id: Option, + pub symbols: Option>, + /// Identifies the point of physical connection on this segment before which the destination sign or marking is visible. + pub from_connector_id: String, + /// Identifies the segment to transition to reach the destination(s) labeled on the sign or marking. + pub to_segment_id: String, + /// Identifies the point of physical connection on the segment identified by 'to_segment_id' to transition to for reaching the destination(s). + pub to_connector_id: String, + /// Properties defining travel headings that match a rule. #[serde(skip_serializing_if = "Option::is_none", default)] - to_connector_id: Option, - #[serde(skip_serializing_if = "Option::is_none", default)] - when: Option, - #[serde(skip_serializing_if = "Option::is_none", default)] - final_heading: Option, + pub when: Option, + /// Enumerates possible travel headings along segment geometry. + pub final_heading: SegmentHeading, +} + +/// Indicates what special symbol/icon is present on a signpost, visible as road marking or similar. +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "snake_case")] +pub enum SegmentSymbol { + Motorway, + Airport, + Hospital, + Center, + Industrial, + Parking, + Bus, + TrainStation, + RestArea, + Ferry, + Motorroad, + Fuel, + Viewpoint, + FuelDiesel, + Food, + Lodging, + Info, + CampSite, + Interchange, + Restrooms, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct SegmentDestinationLabel { - #[serde(skip_serializing_if = "Option::is_none", default)] - value: Option, - #[serde(rename = "type", skip_serializing_if = "Option::is_none", default)] - type_str: Option, + pub value: String, + pub r#type: SegmentDestinationLabelType, +} + +/// The type of object of the destination label. +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "snake_case")] +pub enum SegmentDestinationLabelType { + Street, + Country, + RouteRef, + TowardRouteRef, + Unknown, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct SegmentDestinationWhen { #[serde(skip_serializing_if = "Option::is_none", default)] - heading: Option, + pub heading: Option, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub mode: Option>, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -574,15 +761,101 @@ pub struct SegmentProhibitedTransitionsSequence { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct SegmentSpeedLimit { #[serde(skip_serializing_if = "Option::is_none", default)] - min_speed: Option, + pub min_speed: Option, #[serde(skip_serializing_if = "Option::is_none", default)] - max_speed: Option, + pub max_speed: Option, #[serde(skip_serializing_if = "Option::is_none", default)] - is_max_speed_variable: Option, + pub is_max_speed_variable: Option, #[serde(skip_serializing_if = "Option::is_none", default)] - when: Option, + pub when: Option, #[serde(skip_serializing_if = "Option::is_none", default)] - between: Option>, + pub between: Option>, +} + +impl SegmentSpeedLimit { + /// Used to filter limits based on a linear reference segment. + /// Returns `true` if the open interval `(between[0], between[1])` + /// overlaps with the open interval `(start, end)`. + /// + /// # Examples + /// + /// Basic overlap: + /// ``` + /// # use bambam_omf::collection::SegmentSpeedLimit; + /// + /// let limit = SegmentSpeedLimit { + /// min_speed: None, + /// max_speed: None, + /// is_max_speed_variable: None, + /// when: None, + /// between: Some(vec![10.0, 20.0]), + /// }; + /// + /// // (15, 25) overlaps with (10, 20) + /// assert!(limit.check_open_intersection(15.0, 25.0).unwrap()); + /// ``` + /// + /// No overlap: + /// ``` + /// # use bambam_omf::collection::SegmentSpeedLimit; + /// # let limit = SegmentSpeedLimit { + /// # min_speed: None, + /// # max_speed: None, + /// # is_max_speed_variable: None, + /// # when: None, + /// # between: Some(vec![10.0, 20.0]), + /// # }; + /// + /// // (20, 30) does not overlap with open interval (10, 20) + /// assert!(!limit.check_open_intersection(20.0, 30.0).unwrap()); + /// ``` + /// + /// No `between` restriction means always applicable: + /// ``` + /// # use bambam_omf::collection::SegmentSpeedLimit; + /// let limit = SegmentSpeedLimit { + /// min_speed: None, + /// max_speed: None, + /// is_max_speed_variable: None, + /// when: None, + /// between: None, + /// }; + /// + /// assert!(limit.check_open_intersection(100.0, 200.0).unwrap()); + /// ``` + pub fn check_open_intersection( + &self, + start: f64, + end: f64, + ) -> Result { + match self.between.as_ref() { + Some(b_vector) => { + let (low, high) = validate_between_vector(b_vector)?; + Ok(start < *high && end > *low) + } + None => Ok(true), + } + } + + pub fn get_max_speed(&self) -> Option { + self.max_speed.clone() + } + + /// given a sub-segment linear reference (start, end), compute the total overlapping portion + pub fn get_linear_reference_portion( + &self, + start: f64, + end: f64, + ) -> Result { + match self.between.as_ref() { + Some(b_vector) => { + let (low, high) = validate_between_vector(b_vector)?; + + Ok((high.min(end) - low.max(start)).max(0.)) + } + None => Ok(end - start), + } + } } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -590,3 +863,30 @@ pub struct SpeedLimitWithUnit { value: i32, unit: SegmentSpeedUnit, } + +impl SpeedLimitWithUnit { + pub fn to_uom_value(&self) -> Velocity { + self.unit.to_uom(self.value as f64) + } +} + +/// This function takes a [`Vec`]` and returns `a` and `b` if and only +/// if the vector has exactly two elements and the second one is higher than the +/// first one. Otherwise it returns an error. +fn validate_between_vector( + b_vector: &Vec, +) -> Result<(&f64, &f64), OvertureMapsCollectionError> { + let [low, high] = b_vector.as_slice() else { + return Err(OvertureMapsCollectionError::InvalidBetweenVector( + "Between vector has length != 2".to_string(), + )); + }; + + if high < low { + return Err(OvertureMapsCollectionError::InvalidBetweenVector(format!( + "`high` is lower than `low`: [{low}, {high}]" + ))); + } + + Ok((low, high)) +} diff --git a/rust/bambam-omf/src/graph/omf_graph.rs b/rust/bambam-omf/src/graph/omf_graph.rs index 99ebddac..13ceb34e 100644 --- a/rust/bambam-omf/src/graph/omf_graph.rs +++ b/rust/bambam-omf/src/graph/omf_graph.rs @@ -4,7 +4,8 @@ use super::serialize_ops as ops; use crate::{ app::network::NetworkEdgeListConfiguration, collection::{ - OvertureMapsCollectionError, TransportationCollection, TransportationSegmentRecord, + record::SegmentHeading, OvertureMapsCollectionError, SegmentAccessRestrictionWhen, + SegmentFullType, TransportationCollection, TransportationSegmentRecord, }, graph::{segment_ops, vertex_serializable::VertexSerializable}, }; @@ -27,6 +28,9 @@ pub struct OmfGraphVectorized { pub struct OmfEdgeList { pub edges: EdgeList, pub geometries: Vec>, + pub classes: Vec, + pub speeds: Vec, + pub speed_lookup: HashMap, } impl OmfGraphVectorized { @@ -43,6 +47,8 @@ impl OmfGraphVectorized { let mut edge_lists: Vec = vec![]; for (index, edge_list_config) in configuration.iter().enumerate() { let edge_list_id = EdgeListId(index); + + // create arguments for segment processing into edges let mut filter = edge_list_config.filter.clone(); filter.sort(); // sort for performance @@ -55,8 +61,20 @@ impl OmfGraphVectorized { let segment_lookup = ops::create_segment_lookup(&segments); // the splits are locations in each segment record where we want to define a vertex - // which may not yet exist on the graph - let splits = ops::find_splits(&segments, segment_ops::process_simple_connector_splits)?; + // which may not yet exist on the graph. this is where we begin to impose directivity + // in our records. + let mut splits = vec![]; + for heading in [SegmentHeading::Forward, SegmentHeading::Backward] { + let mut when: SegmentAccessRestrictionWhen = edge_list_config.into(); + when.heading = Some(heading); + + let directed_splits = ops::find_splits( + &segments, + Some(&when), + segment_ops::process_simple_connector_splits, + )?; + splits.extend(directed_splits); + } // depending on the split method, we may need to create additional vertices at locations // which are not OvertureMaps-defined connector types. @@ -78,9 +96,50 @@ impl OmfGraphVectorized { edge_list_id, )?; let geometries = ops::create_geometries(&segments, &segment_lookup, &splits)?; + + let classes = ops::create_segment_full_types(&segments, &segment_lookup, &splits)?; + + let speeds = ops::create_speeds(&segments, &segment_lookup, &splits)?; + let speed_lookup = ops::create_speed_by_segment_type_lookup( + &speeds, + &segments, + &segment_lookup, + &splits, + &classes, + )?; + + // insert global speed value for reference + let global_speed = + ops::get_global_average_speed(&speeds, &segments, &segment_lookup, &splits)?; + + // match speeds according to classes + let speeds = speeds + .into_par_iter() + .zip(&classes) + .map(|(opt_speed, class)| match opt_speed { + Some(speed) => Some(speed), + None => speed_lookup.get(class).copied(), + }) + // Fix the None with -1 for now + .map(|opt| match opt { + Some(v) => v, + None => global_speed, + }) + .collect::>(); + + // transform speed lookup into owned string + let mut speed_lookup = speed_lookup + .iter() + .map(|(&k, v)| (k.as_str(), *v)) + .collect::>(); + speed_lookup.insert(String::from("_global_"), global_speed); + let edge_list = OmfEdgeList { edges: EdgeList(edges.into_boxed_slice()), geometries, + classes, + speeds, + speed_lookup, }; edge_lists.push(edge_list); } @@ -165,6 +224,27 @@ impl OmfGraphVectorized { QuoteStyle::Never, overwrite, ); + let mut classes_writer = create_writer( + &mode_dir, + "edges-classes-enumerated.txt.gz", + false, + QuoteStyle::Never, + overwrite, + ); + let mut speeds_writer = create_writer( + &mode_dir, + "edges-speeds-mph-enumerated.txt.gz", + false, + QuoteStyle::Never, + overwrite, + ); + let mut speeds_mapping_writer = create_writer( + &mode_dir, + "edges-classes-speed-mapping.csv.gz", + true, + QuoteStyle::Necessary, + overwrite, + ); // Write Edges let e_iter = tqdm!( @@ -225,6 +305,84 @@ impl OmfGraphVectorized { )) })?; } + + // Write speeds + let s_iter = tqdm!( + edge_list.speeds.iter(), + total = edge_list.edges.len(), + desc = "speeds", + position = 1 + ); + for row in s_iter { + if let Some(ref mut writer) = speeds_writer { + writer.serialize(row).map_err(|e| { + OvertureMapsCollectionError::CsvWriteError(format!( + "Failed to write to edges-speeds-mph-enumerated.txt.gz: {e}" + )) + })?; + } + } + eprintln!(); + + if let Some(ref mut writer) = speeds_writer { + writer.flush().map_err(|e| { + OvertureMapsCollectionError::CsvWriteError(format!( + "Failed to flush edges-speeds-mph-enumerated.txt.gz: {e}" + )) + })?; + } + + // Write classes + let c_iter = tqdm!( + edge_list.classes.iter(), + total = edge_list.classes.len(), + desc = "classes", + position = 1 + ); + for row in c_iter { + if let Some(ref mut writer) = classes_writer { + writer.serialize(row.as_str()).map_err(|e| { + OvertureMapsCollectionError::CsvWriteError(format!( + "Failed to write to geometry file edges-classes-enumerated.txt.gz: {e}" + )) + })?; + } + } + eprintln!(); + + if let Some(ref mut writer) = classes_writer { + writer.flush().map_err(|e| { + OvertureMapsCollectionError::CsvWriteError(format!( + "Failed to flush edges-classes-enumerated.txt.gz: {e}" + )) + })?; + } + + // Write classes-speed mapping + let c_iter = tqdm!( + edge_list.speed_lookup.iter(), + total = edge_list.speed_lookup.len(), + desc = "classes-speed-mapping", + position = 1 + ); + for row in c_iter { + if let Some(ref mut writer) = speeds_mapping_writer { + writer.serialize(row).map_err(|e| { + OvertureMapsCollectionError::CsvWriteError(format!( + "Failed to write to geometry file edges-classes-speed-mapping.csv.gz: {e}" + )) + })?; + } + } + eprintln!(); + + if let Some(ref mut writer) = speeds_mapping_writer { + writer.flush().map_err(|e| { + OvertureMapsCollectionError::CsvWriteError(format!( + "Failed to flush edges-classes-speed-mapping.csv.gz: {e}" + )) + })?; + } } eprintln!(); diff --git a/rust/bambam-omf/src/graph/segment_ops.rs b/rust/bambam-omf/src/graph/segment_ops.rs index 993106c7..74b2bbd6 100644 --- a/rust/bambam-omf/src/graph/segment_ops.rs +++ b/rust/bambam-omf/src/graph/segment_ops.rs @@ -1,7 +1,10 @@ //! functions mapped onto [TransportationSegmentRecord] rows to create [SegmentSplit] values use crate::{ - collection::{OvertureMapsCollectionError, TransportationSegmentRecord}, + collection::{ + record::{SegmentAccessRestriction, SegmentHeading}, + OvertureMapsCollectionError, SegmentAccessRestrictionWhen, TransportationSegmentRecord, + }, graph::{segment_split::SegmentSplit, ConnectorInSegment}, }; use itertools::Itertools; @@ -9,7 +12,9 @@ use itertools::Itertools; /// creates simple connector splits from a record. pub fn process_simple_connector_splits( segment: &TransportationSegmentRecord, + when: Option<&SegmentAccessRestrictionWhen>, ) -> Result, OvertureMapsCollectionError> { + let headings = get_headings(segment, when)?; let result = segment .connectors .as_ref() @@ -18,11 +23,933 @@ pub fn process_simple_connector_splits( ))? .iter() .tuple_windows() - .map(|(src, dst)| { - let src = ConnectorInSegment::new(segment.id.clone(), src.connector_id.clone(), src.at); - let dst = ConnectorInSegment::new(segment.id.clone(), dst.connector_id.clone(), dst.at); - SegmentSplit::SimpleConnectorSplit { src, dst } + .flat_map(|(src, dst)| { + headings.iter().cloned().map(|heading| { + let src = + ConnectorInSegment::new(segment.id.clone(), src.connector_id.clone(), src.at); + let dst = + ConnectorInSegment::new(segment.id.clone(), dst.connector_id.clone(), dst.at); + SegmentSplit::SimpleConnectorSplit { src, dst, heading } + }) }) .collect::>(); Ok(result) } + +/// determines the headings over a segment that are supported. optionally matched to some +/// set of user-provided restrictions. +pub fn get_headings( + segment: &TransportationSegmentRecord, + when: Option<&SegmentAccessRestrictionWhen>, +) -> Result, OvertureMapsCollectionError> { + // If both when and access_restrictions are None/empty, return both headings + let access_restrictions = segment.access_restrictions.as_ref(); + + let no_restrictions = access_restrictions.map(|r| r.is_empty()).unwrap_or(true); + if when.is_none() && (access_restrictions.is_none() || no_restrictions) { + return Ok(vec![SegmentHeading::Forward, SegmentHeading::Backward]); + } + + // Collect valid headings based on access restrictions + let mut valid_headings = Vec::new(); + let when_heading = when.and_then(|w| w.heading.clone()); + let (test_fwd, test_bwd) = match when_heading { + None => (true, true), + Some(SegmentHeading::Forward) => (true, false), + Some(SegmentHeading::Backward) => (false, true), + }; + + // Check Forward heading + if test_fwd && is_heading_valid(SegmentHeading::Forward, when, access_restrictions) { + valid_headings.push(SegmentHeading::Forward); + } + + // Check Backward heading + if test_bwd && is_heading_valid(SegmentHeading::Backward, when, access_restrictions) { + valid_headings.push(SegmentHeading::Backward); + } + + Ok(valid_headings) +} + +/// Helper function to check if a heading is valid given the when constraint and access restrictions +/// +/// Access restrictions are evaluated in order, where: +/// - Multiple restrictions can combine (e.g., "Denied all" + "Allowed specific" = "allowed only for specific") +/// - A restriction applies if its heading and when conditions match the query +/// - The final decision is: allowed if any Allowed restriction applies AND no Denied restriction applies +fn is_heading_valid( + heading: SegmentHeading, + when: Option<&SegmentAccessRestrictionWhen>, + access_restrictions: Option<&Vec>, +) -> bool { + use crate::collection::record::SegmentAccessType as SAT; + + let Some(restrictions) = access_restrictions else { + return true; + }; + + let is_denied = |r: &&SegmentAccessRestriction| r.access_type == SAT::Denied; + + // Partition applicable restrictions by heading-specificity + let (heading_specific, general): (Vec<_>, Vec<_>) = restrictions + .iter() + .filter(|r| restriction_applies_to(r, &heading, when)) + .partition(|r| r.when.as_ref().and_then(|w| w.heading.as_ref()).is_some()); + + // Further partition each group by denied/allowed. each of these becomes a set of which + // emptiness proves a certain fact about the validity of this heading. + let (heading_denied, heading_allowed): (Vec<_>, Vec<_>) = + heading_specific.into_iter().partition(is_denied); + let (general_denied, general_allowed): (Vec<_>, Vec<_>) = + general.into_iter().partition(is_denied); + let has_heading_denied = !heading_denied.is_empty(); + let has_heading_allowed = !heading_allowed.is_empty(); + let no_general_denied = general_denied.is_empty(); + let has_general_allowed = !general_allowed.is_empty(); + + // Heading-specific denial takes priority - can only be overridden by heading-specific allowance + if has_heading_denied { + has_heading_allowed + } else { + // No heading-specific denial: allow if any allowance exists, or no denial exists + has_heading_allowed || has_general_allowed || no_general_denied + } +} + +/// Check if a restriction applies to the given heading and when conditions +/// +/// A restriction applies if: +/// 1. The heading matches (or restriction has no heading constraint) +/// 2. The when conditions match: +/// - If querying with when=None: restriction must have empty/minimal conditions (applies broadly) +/// - If querying with when=Some: the query conditions must be compatible with restriction +fn restriction_applies_to( + restriction: &SegmentAccessRestriction, + heading: &SegmentHeading, + when: Option<&SegmentAccessRestrictionWhen>, +) -> bool { + let restriction_when = restriction.when.as_ref(); + + // Check if the restriction's heading matches or is unrestricted + let heading_matches = restriction_when + .and_then(|w| w.heading.as_ref()) + .map(|h| h == heading) + .unwrap_or(true); // If no heading specified in restriction, it applies to all + + if !heading_matches { + return false; + } + + // If when is provided, check if the query conditions are compatible with the restriction + if let Some(when) = when { + when_is_compatible(when, restriction_when) + } else { + // No when constraint provided in query - we only match restrictions that apply + // broadly (without mode/using/recognized constraints), or have no when clause at all. + // This represents "what's allowed by default without specific conditions" + restriction_when.is_none() + || restriction_when.is_some_and(|rw| { + // A restriction with specific conditions (mode, using, recognized) doesn't + // apply to the "default" case + rw.mode.is_none() && rw.using.is_none() && rw.recognized.is_none() + }) + } +} + +/// Check if the when constraint is compatible with (contained by) the restriction when +/// +/// Returns true if the query 'when' is compatible with the restriction 'when'. +/// A restriction with None for a field means it applies broadly (to all values of that field). +/// A restriction with Some([values]) means it only applies to those specific values. +/// +/// # Arguments +/// * `when` - Query conditions (e.g., "Car mode") +/// * `segment_restrictions` - Restriction conditions (e.g., "Car and Bicycle modes" or None for all modes) +/// +/// Note: Heading compatibility is handled by `restriction_applies_to`, not here. +fn when_is_compatible( + when: &SegmentAccessRestrictionWhen, + segment_restrictions: Option<&SegmentAccessRestrictionWhen>, +) -> bool { + // return early if no restrictions on segment + let Some(restrictions) = segment_restrictions else { + return true; + }; + + // compatibility checks + // in the following blocks, for a given restriction: + // - if the restriction is not defined on the segment (None), we continue + // - if the restriction IS defined (Some), the "when" query must match it + // headings are NOT tested here as they have already been tested in get_headings + + // Check mode compatibility + if let Some(restriction_modes) = &restrictions.mode { + if let Some(when_modes) = &when.mode { + if !when_modes.iter().all(|m| restriction_modes.contains(m)) { + return false; + } + } else { + return false; + } + } + + // Check using compatibility + if let Some(restriction_using) = &restrictions.using { + if let Some(when_using) = &when.using { + if !when_using.iter().all(|u| restriction_using.contains(u)) { + return false; + } + } else { + return false; + } + } + + // Check recognized compatibility + if let Some(restriction_recognized) = &restrictions.recognized { + if let Some(when_recognized) = &when.recognized { + if !when_recognized + .iter() + .all(|r| restriction_recognized.contains(r)) + { + return false; + } + } else { + return false; + } + } + + // If we got here, all specified fields in when are compatible + true +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::collection::record::{ + OvertureMapsBbox, SegmentAccessType, SegmentMode, SegmentRecognized, SegmentUsing, + }; + + #[test] + fn test_segment_without_access_restrictions_both_headings() { + // Test: A segment without access restrictions should produce both headings + let segment = create_test_segment(None); + let result = get_headings(&segment, None).unwrap(); + + assert_eq!(result.len(), 2); + assert!(result.contains(&SegmentHeading::Forward)); + assert!(result.contains(&SegmentHeading::Backward)); + } + + #[test] + fn test_segment_with_empty_access_restrictions_both_headings() { + // Test: A segment with empty access restrictions should produce both headings + // when the user passes no constraints. + let segment = create_test_segment(Some(vec![])); + let result = get_headings(&segment, None).unwrap(); + + assert_eq!(result.len(), 2); + assert!(result.contains(&SegmentHeading::Forward)); + assert!(result.contains(&SegmentHeading::Backward)); + } + + #[test] + fn test_segment_with_forward_only_restriction() { + // Test: A segment with backward denied should only produce Forward heading + // when the user passes no constraints. + let segment = create_test_segment(Some(vec![create_restriction_heading_only( + SegmentAccessType::Denied, + SegmentHeading::Backward, + )])); + + let result = get_headings(&segment, None).unwrap(); + + assert_eq!(result, vec![SegmentHeading::Forward]); + } + + #[test] + fn test_segment_with_backward_only_restriction() { + // Test: A segment with forward denied should only produce Backward heading + // when the user passes no constraints. + let segment = create_test_segment(Some(vec![create_restriction_heading_only( + SegmentAccessType::Denied, + SegmentHeading::Forward, + )])); + + let result = get_headings(&segment, None).unwrap(); + + assert_eq!(result, vec![SegmentHeading::Backward]); + } + + #[test] + fn test_segment_with_mode_restriction_matching_when() { + // Test: Denied all modes for forward, then Allowed for Car/Bicycle + // Query with Car should allow Forward + let segment = create_test_segment(Some(create_denied_all_allowed_specific( + SegmentHeading::Forward, + Some(vec![SegmentMode::Car, SegmentMode::Bicycle]), + None, + None, + ))); + + let when = create_when( + SegmentHeading::Forward, + Some(vec![SegmentMode::Car]), + None, + None, + ); + let result = get_headings(&segment, Some(&when)).unwrap(); + + assert_eq!(result, vec![SegmentHeading::Forward]); + } + + #[test] + fn test_segment_with_mode_restriction_not_matching_when() { + // Test: Denied all for forward + Allowed only Car + // Query with Forward, Bicycle should deny both forward and backward + let segment = create_test_segment(Some(create_denied_all_allowed_specific( + SegmentHeading::Forward, + Some(vec![SegmentMode::Car]), + None, + None, + ))); + + let when = create_when( + SegmentHeading::Forward, + Some(vec![SegmentMode::Bicycle]), + None, + None, + ); + let result = get_headings(&segment, Some(&when)).unwrap(); + + assert_eq!(result.len(), 0); + } + + #[test] + fn test_segment_with_multiple_fields_matching() { + // Test: Denied all + Allowed with multiple field constraints, all matching + let segment = create_test_segment(Some(create_denied_all_allowed_specific( + SegmentHeading::Forward, + Some(vec![SegmentMode::Car, SegmentMode::Bicycle]), + Some(vec![SegmentUsing::AsCustomer]), + Some(vec![SegmentRecognized::AsEmployee]), + ))); + + let when = create_when( + SegmentHeading::Forward, + Some(vec![SegmentMode::Car]), + Some(vec![SegmentUsing::AsCustomer]), + Some(vec![SegmentRecognized::AsEmployee]), + ); + let result = get_headings(&segment, Some(&when)).unwrap(); + + assert_eq!(result, vec![SegmentHeading::Forward]); // Forward allowed with all conditions met + } + + #[test] + fn test_denied_all_then_allowed_specific() { + // Test: "Denied all" followed by "Allowed for cars" should allow only cars + // This is the classic "deny by default, allow exceptions" pattern + let segment = create_test_segment(Some(create_denied_all_allowed_specific( + SegmentHeading::Forward, + Some(vec![SegmentMode::Car]), + None, + None, + ))); + + // Query without when - should match the Denied restriction, accept any valid heading + let result_no_when = get_headings(&segment, None).unwrap(); + assert_eq!(result_no_when, vec![SegmentHeading::Backward]); // Only Backward is valid + + // Query with Car mode - should match the Allowed restriction + let when_car = create_when( + SegmentHeading::Forward, + Some(vec![SegmentMode::Car]), + None, + None, + ); + let result_car = get_headings(&segment, Some(&when_car)).unwrap(); + assert_eq!(result_car, vec![SegmentHeading::Forward]); + + // Query with Bicycle mode - should be denied + let when_bicycle = create_when( + SegmentHeading::Forward, + Some(vec![SegmentMode::Bicycle]), + None, + None, + ); + let result_bicycle = get_headings(&segment, Some(&when_bicycle)).unwrap(); + assert_eq!(result_bicycle.len(), 0); + } + + #[test] + fn test_allowed_overrides_denied_same_heading() { + // Test: When both Denied and Allowed apply to the same conditions, + // Allowed takes precedence (specific exception pattern) + let segment = create_test_segment(Some(vec![ + SegmentAccessRestriction { + access_type: SegmentAccessType::Denied, + when: Some(SegmentAccessRestrictionWhen { + during: None, + heading: Some(SegmentHeading::Forward), + using: None, + recognized: None, + mode: Some(vec![SegmentMode::Car, SegmentMode::Bicycle]), + vehicle: None, + }), + vehicle: None, + }, + SegmentAccessRestriction { + access_type: SegmentAccessType::Allowed, + when: Some(SegmentAccessRestrictionWhen { + during: None, + heading: Some(SegmentHeading::Forward), + using: None, + recognized: None, + mode: Some(vec![SegmentMode::Car]), + vehicle: None, + }), + vehicle: None, + }, + ])); + + // Car should be allowed (Allowed overrides Denied) + let when_car = create_when( + SegmentHeading::Forward, + Some(vec![SegmentMode::Car]), + None, + None, + ); + let result = get_headings(&segment, Some(&when_car)).unwrap(); + assert_eq!(result, vec![SegmentHeading::Forward]); + } + + #[test] + fn test_multiple_denied_restrictions() { + // Test: Multiple Denied restrictions - all should be respected + let segment = create_test_segment(Some(vec![ + create_restriction_heading_only(SegmentAccessType::Denied, SegmentHeading::Forward), + create_restriction_heading_only(SegmentAccessType::Denied, SegmentHeading::Backward), + ])); + + let result = get_headings(&segment, None).unwrap(); + + // Both directions denied + assert_eq!(result.len(), 0); + } + + #[test] + fn test_designated_treated_as_allowed() { + // Test: Blanket denial with Designated mode access type: should be treated like Allowed + let segment = create_test_segment(Some(vec![ + SegmentAccessRestriction { + access_type: SegmentAccessType::Denied, + when: Some(SegmentAccessRestrictionWhen { + during: None, + heading: Some(SegmentHeading::Forward), + using: None, + recognized: None, + mode: None, + vehicle: None, + }), + vehicle: None, + }, + SegmentAccessRestriction { + access_type: SegmentAccessType::Designated, + when: Some(SegmentAccessRestrictionWhen { + during: None, + heading: Some(SegmentHeading::Forward), + using: None, + recognized: None, + mode: Some(vec![SegmentMode::Bicycle]), + vehicle: None, + }), + vehicle: None, + }, + ])); + + let when_bicycle = create_when( + SegmentHeading::Forward, + Some(vec![SegmentMode::Bicycle]), + None, + None, + ); + let result = get_headings(&segment, Some(&when_bicycle)).unwrap(); + + assert_eq!(result, vec![SegmentHeading::Forward]); + } + + #[test] + fn test_restriction_with_mode_does_not_apply_when_query_has_no_mode() { + // Test: A restriction that specifies a mode constraint should NOT apply + // when the query doesn't specify a mode at all + // This exercises the fix: we check if restriction.mode is Some, not if when.mode is Some + let segment = create_test_segment(Some(vec![ + // Deny all forward traffic (no mode constraint) + create_restriction_heading_only(SegmentAccessType::Denied, SegmentHeading::Forward), + // Allow forward for bicycles only (mode constraint) + SegmentAccessRestriction { + access_type: SegmentAccessType::Allowed, + when: Some(SegmentAccessRestrictionWhen { + during: None, + heading: Some(SegmentHeading::Forward), + using: None, + recognized: None, + mode: Some(vec![SegmentMode::Bicycle]), + vehicle: None, + }), + vehicle: None, + }, + ])); + + // Query with when=None (no mode specified) + // The Denied restriction applies (no mode constraint, applies broadly) + // The Allowed restriction should NOT apply (has mode constraint, but query has none) + // Expected: Forward is denied because only Denied applies + let result = get_headings(&segment, None).unwrap(); + assert_eq!(result.len(), 1); + assert!(result.contains(&SegmentHeading::Backward)); + assert!(!result.contains(&SegmentHeading::Forward)); + } + + #[test] + fn test_from_data_1() { + // test case from OMF data, a segment with + // - blanket denial for heading backward + // - opts in designated HGV travel, no heading specified + // when + // - heading forward with motor_vehicle, car, truck, motorcycle: reject + // - heading backward with "": reject + // - heading forward with HGV: accept + let segment = create_test_segment(Some(vec![ + create_restriction_heading_only(SegmentAccessType::Denied, SegmentHeading::Backward), + create_restriction_mode(SegmentAccessType::Designated, vec![SegmentMode::Hgv]), + ])); + + let mode = vec![ + SegmentMode::MotorVehicle, + SegmentMode::Car, + SegmentMode::Truck, + SegmentMode::Motorcycle, + ]; + + // forward has no blanket denial, only optional designation, so we accept + let when1 = create_when(SegmentHeading::Forward, Some(mode.clone()), None, None); + let result1 = get_headings(&segment, Some(&when1)).unwrap(); + assert_eq!(result1.len(), 1); + + // blanket backward denial on a backward-oriented when query -> empty result + let when2 = create_when(SegmentHeading::Backward, Some(mode.clone()), None, None); + let result2 = get_headings(&segment, Some(&when2)).unwrap(); + assert_eq!(result2.len(), 0); + } + + #[test] + fn test_from_data_2() { + // a segment has a blanket denial of backward access, and a designation for bicycle. + // this should allow any mode traveling forward but no mode traveling backward. + + let segment = create_test_segment(Some(vec![ + create_restriction_heading_only(SegmentAccessType::Denied, SegmentHeading::Backward), + create_restriction_mode(SegmentAccessType::Designated, vec![SegmentMode::Bicycle]), + ])); + + // case 1: forward traversal should be accepted + let when1 = create_when( + SegmentHeading::Forward, + Some(vec![SegmentMode::Bicycle]), + None, + None, + ); + let result1 = get_headings(&segment, Some(&when1)).unwrap(); + assert_eq!(result1, vec![SegmentHeading::Forward]); + + // case 2: backward traversal should be denied + let when2 = create_when( + SegmentHeading::Backward, + Some(vec![SegmentMode::Bicycle]), + None, + None, + ); + let result2 = get_headings(&segment, Some(&when2)).unwrap(); + + assert_eq!(result2.len(), 0); + } + + /// Helper to create a minimal segment for testing + fn create_test_segment( + access_restrictions: Option>, + ) -> TransportationSegmentRecord { + // Create a minimal valid bbox using serde deserialization + let bbox: OvertureMapsBbox = + serde_json::from_str(r#"{"xmin": 0.0, "xmax": 1.0, "ymin": 0.0, "ymax": 1.0}"#) + .expect("test invariant failed, unable to mock bbox of record"); + let mut record = TransportationSegmentRecord::default(); + record.access_restrictions = access_restrictions; + record.bbox = bbox; + record + } + + /// Helper to create a simple access restriction with only heading constraint + fn create_restriction_heading_only( + access_type: SegmentAccessType, + heading: SegmentHeading, + ) -> SegmentAccessRestriction { + SegmentAccessRestriction { + access_type, + when: Some(SegmentAccessRestrictionWhen { + during: None, + heading: Some(heading), + using: None, + recognized: None, + mode: None, + vehicle: None, + }), + vehicle: None, + } + } + + /// Helper to create a simple access restriction with only mode constraint + fn create_restriction_mode( + access_type: SegmentAccessType, + modes: Vec, + ) -> SegmentAccessRestriction { + SegmentAccessRestriction { + access_type, + when: Some(SegmentAccessRestrictionWhen { + during: None, + heading: None, + using: None, + recognized: None, + mode: Some(modes), + vehicle: None, + }), + vehicle: None, + } + } + + /// Helper to create a simple access restriction with heading + modes constraint + fn create_restriction_heading_mode( + access_type: SegmentAccessType, + heading: SegmentHeading, + modes: Vec, + ) -> SegmentAccessRestriction { + SegmentAccessRestriction { + access_type, + when: Some(SegmentAccessRestrictionWhen { + during: None, + heading: Some(heading), + using: None, + recognized: None, + mode: Some(modes), + vehicle: None, + }), + vehicle: None, + } + } + + /// Helper to create "Denied all + Allowed specific" pattern for a heading + fn create_denied_all_allowed_specific( + heading: SegmentHeading, + allowed_modes: Option>, + allowed_using: Option>, + allowed_recognized: Option>, + ) -> Vec { + vec![ + SegmentAccessRestriction { + access_type: SegmentAccessType::Denied, + when: Some(SegmentAccessRestrictionWhen { + during: None, + heading: Some(heading.clone()), + using: None, + recognized: None, + mode: None, + vehicle: None, + }), + vehicle: None, + }, + SegmentAccessRestriction { + access_type: SegmentAccessType::Allowed, + when: Some(SegmentAccessRestrictionWhen { + during: None, + heading: Some(heading), + using: allowed_using, + recognized: allowed_recognized, + mode: allowed_modes, + vehicle: None, + }), + vehicle: None, + }, + ] + } + + /// Helper to create a query when object + fn create_when( + heading: SegmentHeading, + mode: Option>, + using: Option>, + recognized: Option>, + ) -> SegmentAccessRestrictionWhen { + SegmentAccessRestrictionWhen { + during: None, + heading: Some(heading), + using, + recognized, + mode, + vehicle: None, + } + } + + #[test] + fn test_general_denial_blocks_both_directions() { + // Test: A denial with no heading specified should block both directions + let segment = create_test_segment(Some(vec![SegmentAccessRestriction { + access_type: SegmentAccessType::Denied, + when: Some(SegmentAccessRestrictionWhen { + during: None, + heading: None, // No heading = applies to all + using: None, + recognized: None, + mode: None, + vehicle: None, + }), + vehicle: None, + }])); + + let result = get_headings(&segment, None).unwrap(); + assert_eq!( + result.len(), + 0, + "General denial should block both directions" + ); + } + + #[test] + fn test_general_allowance_overrides_general_denial() { + // Test: A general allowance (no heading) should override a general denial (no heading) + // for a specific mode + let segment = create_test_segment(Some(vec![ + // General denial for all + SegmentAccessRestriction { + access_type: SegmentAccessType::Denied, + when: Some(SegmentAccessRestrictionWhen { + during: None, + heading: None, + using: None, + recognized: None, + mode: None, + vehicle: None, + }), + vehicle: None, + }, + // General allowance for bicycles (no heading specified) + SegmentAccessRestriction { + access_type: SegmentAccessType::Allowed, + when: Some(SegmentAccessRestrictionWhen { + during: None, + heading: None, + using: None, + recognized: None, + mode: Some(vec![SegmentMode::Bicycle]), + vehicle: None, + }), + vehicle: None, + }, + ])); + + // Query with bicycle mode - should be allowed in both directions + let when_fwd = create_when( + SegmentHeading::Forward, + Some(vec![SegmentMode::Bicycle]), + None, + None, + ); + let result_fwd = get_headings(&segment, Some(&when_fwd)).unwrap(); + assert_eq!(result_fwd, vec![SegmentHeading::Forward]); + + let when_bwd = create_when( + SegmentHeading::Backward, + Some(vec![SegmentMode::Bicycle]), + None, + None, + ); + let result_bwd = get_headings(&segment, Some(&when_bwd)).unwrap(); + assert_eq!(result_bwd, vec![SegmentHeading::Backward]); + } + + #[test] + fn test_heading_specific_allowance_without_denial() { + // Test: A heading-specific allowance without any denial should allow that heading + // (and the other heading should default to allowed since no denial) + let segment = create_test_segment(Some(vec![SegmentAccessRestriction { + access_type: SegmentAccessType::Allowed, + when: Some(SegmentAccessRestrictionWhen { + during: None, + heading: Some(SegmentHeading::Forward), + using: None, + recognized: None, + mode: Some(vec![SegmentMode::Bicycle]), + vehicle: None, + }), + vehicle: None, + }])); + + // Query with bicycle forward - should be allowed (explicit allowance) + let when_fwd = create_when( + SegmentHeading::Forward, + Some(vec![SegmentMode::Bicycle]), + None, + None, + ); + let result_fwd = get_headings(&segment, Some(&when_fwd)).unwrap(); + assert_eq!(result_fwd, vec![SegmentHeading::Forward]); + + // Query with bicycle backward - should also be allowed (no denial, defaults to allowed) + let when_bwd = create_when( + SegmentHeading::Backward, + Some(vec![SegmentMode::Bicycle]), + None, + None, + ); + let result_bwd = get_headings(&segment, Some(&when_bwd)).unwrap(); + assert_eq!(result_bwd, vec![SegmentHeading::Backward]); + } + + #[test] + fn test_restrictions_exist_but_none_apply() { + // Test: Restrictions exist but none apply to the query (different mode) + // Should default to allowed + let segment = create_test_segment(Some(vec![create_restriction_heading_mode( + SegmentAccessType::Denied, + SegmentHeading::Forward, + vec![SegmentMode::Car], + )])); + + // Query with Bicycle - the Car denial shouldn't apply + let when_bicycle = create_when( + SegmentHeading::Forward, + Some(vec![SegmentMode::Bicycle]), + None, + None, + ); + let result = get_headings(&segment, Some(&when_bicycle)).unwrap(); + assert_eq!( + result, + vec![SegmentHeading::Forward], + "Denial for Car shouldn't affect Bicycle" + ); + } + + #[test] + fn test_heading_specific_allowance_overrides_general_denial() { + // Test: A heading-specific allowance should override a general (non-heading) denial + let segment = create_test_segment(Some(vec![ + // General denial (no heading) + SegmentAccessRestriction { + access_type: SegmentAccessType::Denied, + when: Some(SegmentAccessRestrictionWhen { + during: None, + heading: None, + using: None, + recognized: None, + mode: None, + vehicle: None, + }), + vehicle: None, + }, + // Heading-specific allowance for forward + bicycle + SegmentAccessRestriction { + access_type: SegmentAccessType::Allowed, + when: Some(SegmentAccessRestrictionWhen { + during: None, + heading: Some(SegmentHeading::Forward), + using: None, + recognized: None, + mode: Some(vec![SegmentMode::Bicycle]), + vehicle: None, + }), + vehicle: None, + }, + ])); + + // Forward bicycle should be allowed (heading-specific allowance) + let when_fwd = create_when( + SegmentHeading::Forward, + Some(vec![SegmentMode::Bicycle]), + None, + None, + ); + let result_fwd = get_headings(&segment, Some(&when_fwd)).unwrap(); + assert_eq!(result_fwd, vec![SegmentHeading::Forward]); + + // Backward bicycle should be denied (general denial, no allowance) + let when_bwd = create_when( + SegmentHeading::Backward, + Some(vec![SegmentMode::Bicycle]), + None, + None, + ); + let result_bwd = get_headings(&segment, Some(&when_bwd)).unwrap(); + assert_eq!(result_bwd.len(), 0); + } + + #[test] + fn test_general_denial_with_heading_specific_denial_same_heading() { + // Test: Both general denial and heading-specific denial for same heading + // The heading-specific denial takes priority, requires heading-specific allowance + let segment = create_test_segment(Some(vec![ + // General denial + SegmentAccessRestriction { + access_type: SegmentAccessType::Denied, + when: Some(SegmentAccessRestrictionWhen { + during: None, + heading: None, + using: None, + recognized: None, + mode: None, + vehicle: None, + }), + vehicle: None, + }, + // Heading-specific denial for forward + create_restriction_heading_only(SegmentAccessType::Denied, SegmentHeading::Forward), + // General allowance for bicycle (should NOT override heading-specific denial) + SegmentAccessRestriction { + access_type: SegmentAccessType::Allowed, + when: Some(SegmentAccessRestrictionWhen { + during: None, + heading: None, + using: None, + recognized: None, + mode: Some(vec![SegmentMode::Bicycle]), + vehicle: None, + }), + vehicle: None, + }, + ])); + + // Forward bicycle - general allowance should NOT override heading-specific denial + let when_fwd = create_when( + SegmentHeading::Forward, + Some(vec![SegmentMode::Bicycle]), + None, + None, + ); + let result_fwd = get_headings(&segment, Some(&when_fwd)).unwrap(); + assert_eq!( + result_fwd.len(), + 0, + "General allowance should not override heading-specific denial" + ); + + // Backward bicycle - only general denial, general allowance should override + let when_bwd = create_when( + SegmentHeading::Backward, + Some(vec![SegmentMode::Bicycle]), + None, + None, + ); + let result_bwd = get_headings(&segment, Some(&when_bwd)).unwrap(); + assert_eq!(result_bwd, vec![SegmentHeading::Backward]); + } +} diff --git a/rust/bambam-omf/src/graph/segment_split.rs b/rust/bambam-omf/src/graph/segment_split.rs index 603f0d93..dbcf2588 100644 --- a/rust/bambam-omf/src/graph/segment_split.rs +++ b/rust/bambam-omf/src/graph/segment_split.rs @@ -1,10 +1,14 @@ use std::collections::HashMap; use geo::{Haversine, Length, LineString}; +use itertools::Itertools; use routee_compass_core::model::network::{Edge, EdgeId, EdgeListId, Vertex, VertexId}; use crate::{ - collection::{OvertureMapsCollectionError, TransportationSegmentRecord}, + collection::{ + record::SegmentHeading, OvertureMapsCollectionError, SegmentFullType, + TransportationSegmentRecord, + }, graph::connector_in_segment::ConnectorInSegment, }; @@ -14,10 +18,21 @@ pub enum SegmentSplit { SimpleConnectorSplit { src: ConnectorInSegment, dst: ConnectorInSegment, + heading: SegmentHeading, }, } impl SegmentSplit { + /// constructs a new simple segment split based purely on the linear references between + /// connectors along with any heading information relevant to the active travel mode. + pub fn new_simple( + src: ConnectorInSegment, + dst: ConnectorInSegment, + heading: SegmentHeading, + ) -> Self { + Self::SimpleConnectorSplit { src, dst, heading } + } + /// identifies any locations where additional coordinates are needed. /// when creating any missing connectors, call [ConnectorInSegment::new_without_connector_id] @@ -44,7 +59,7 @@ impl SegmentSplit { ) -> Result { use OvertureMapsCollectionError as E; match self { - SegmentSplit::SimpleConnectorSplit { src, dst } => { + SegmentSplit::SimpleConnectorSplit { src, dst, heading } => { // get the shared segment id for src + dst let segment_id = if src.segment_id != dst.segment_id { let msg = format!( @@ -73,6 +88,11 @@ impl SegmentSplit { "segment references unknown connector {}", dst.connector_id )))?; + // reverse src/dst if heading is backward + let (src_vertex_id, dst_vertex_id) = match heading { + SegmentHeading::Forward => (VertexId(*src_id), VertexId(*dst_id)), + SegmentHeading::Backward => (VertexId(*dst_id), VertexId(*src_id)), + }; // create this edge, push onto edges if dst.linear_reference < src.linear_reference { @@ -94,15 +114,17 @@ impl SegmentSplit { ); E::InvalidSegmentConnectors(msg) })?; - let dst_distance = segment.get_distance_at(dst.linear_reference.0)?; - let src_distance = segment.get_distance_at(src.linear_reference.0)?; - let distance = dst_distance - src_distance; + let dst_distance = segment.get_distance_at_meters(dst.linear_reference.0)?; + let src_distance = segment.get_distance_at_meters(src.linear_reference.0)?; + let distance_f32 = dst_distance - src_distance; + let distance = + uom::si::length::Length::new::(distance_f32 as f64); let edge = Edge { edge_list_id, edge_id, - src_vertex_id: VertexId(*src_id), - dst_vertex_id: VertexId(*dst_id), - distance: uom::si::f64::Length::new::(distance as f64), + src_vertex_id, + dst_vertex_id, + distance, }; Ok(edge) @@ -110,6 +132,9 @@ impl SegmentSplit { } } + /// extracts the LineString geometry corresponding to this split based on linear reference. + /// All of the points of the original LineString that line strictly inside the `src` and `dst` + /// are considered, and new ones are created at the beginning and end if necessary. pub fn create_geometry_from_split( &self, segments: &[&TransportationSegmentRecord], @@ -117,8 +142,10 @@ impl SegmentSplit { ) -> Result, OvertureMapsCollectionError> { use OvertureMapsCollectionError as E; + // let segment = self.get_segment(segments, segment_lookup)?; + match self { - SegmentSplit::SimpleConnectorSplit { src, dst } => { + SegmentSplit::SimpleConnectorSplit { src, dst, heading } => { let segment_id = &src.segment_id; let segment_idx = segment_lookup.get(segment_id).ok_or_else(|| { let msg = format!("missing lookup entry for segment {segment_id}"); @@ -131,8 +158,8 @@ impl SegmentSplit { E::InvalidSegmentConnectors(msg) })?; - let distance_to_src = segment.get_distance_at(src.linear_reference.0)?; - let distance_to_dst = segment.get_distance_at(dst.linear_reference.0)?; + let distance_to_src = segment.get_distance_at_meters(src.linear_reference.0)?; + let distance_to_dst = segment.get_distance_at_meters(dst.linear_reference.0)?; let segment_geometry = segment.get_linestring()?; let mut out_coords = vec![]; @@ -159,8 +186,168 @@ impl SegmentSplit { // Add final point out_coords.push(segment.get_coord_at(dst.linear_reference.0)?); + // reverse coordinate sequence if heading is backward + if *heading == SegmentHeading::Backward { + out_coords.reverse(); + } Ok(LineString::new(out_coords)) } } } + + /// returns the average `max_speed` of this split according to the speed limits + /// that match linear reference. Each element in the matching set is averaged + /// based on relative length. + pub fn get_split_speed( + &self, + segments: &[&TransportationSegmentRecord], + segment_lookup: &HashMap, + ) -> Result, OvertureMapsCollectionError> { + use OvertureMapsCollectionError as E; + + let segment = self.get_segment(segments, segment_lookup)?; + + match self { + SegmentSplit::SimpleConnectorSplit { src, dst, heading } => { + let speed_limits = match segment.speed_limits.as_ref() { + Some(limits) => limits, + None => return Ok(None), + }; + + // retain speed limits with no heading or with a matching heading + let speed_limits_with_heading = speed_limits + .iter() + .filter_map(|s| match s.when.as_ref() { + Some(access) => match access.heading.as_ref() { + None => Some(s), + Some(h) if h == heading => Some(s), + _ => None, + }, + None => None, + }) + .collect_vec(); + + // Compute the intersecting portion of each limit + // e.g. if limit is [0.5, 0.8] and segment is defined as [0.45, 0.6] then this value is .6 - .5 = 0.1 + let start = src.linear_reference.0; + let end = dst.linear_reference.0; + let intersecting_portions: Vec = speed_limits_with_heading + .iter() + .map(|speed_limit| speed_limit.get_linear_reference_portion(start, end)) + .collect::>()?; + + // Compute mph max speeds weighted by intersecting_length / total_intersecting_length + let total_intersecting_length: f64 = intersecting_portions.iter().sum(); + + if total_intersecting_length < 1e-6 { + return Ok(None); + } + + let weighted_mph = speed_limits_with_heading + .iter() + .zip(intersecting_portions) + .map(|(speed_limit, portion)| { + let weight = portion / total_intersecting_length; + + let max_speed = speed_limit.get_max_speed().ok_or(E::InternalError( + format!("Expected a value for `max_speed`: {speed_limit:?}"), + ))?; + + Ok(max_speed + .to_uom_value() + .get::() + * weight) + }) + .collect::, E>>()?; + + Ok(Some(weighted_mph.iter().sum())) + } + } + } + + /// return a fully-qualified segment type for this split based on the segment type-class pair + /// and the `subclass_rules` attached to it + pub fn get_split_segment_full_type( + &self, + segments: &[&TransportationSegmentRecord], + segment_lookup: &HashMap, + ) -> Result { + // Initial testing suggests that either subclass is not null OR there are rules + // specific for linear references + + let segment = self.get_segment(segments, segment_lookup)?; + match self { + SegmentSplit::SimpleConnectorSplit { src, dst, .. } => { + let start = src.linear_reference.0; + let end = dst.linear_reference.0; + + let segment_class = segment.get_segment_full_type()?; + + if segment_class.has_subclass() { + return Ok(segment_class); + }; + + // This ignores errors in `check_open_intersection` coming from invalid between values + let opt_first_matching_sublcass = + segment.subclass_rules.as_ref().and_then(|rules| { + rules.iter().find_map(|rule| { + match rule.check_open_intersection(start, end) { + Ok(true) => Some(rule), + _ => None, + } + }) + }); + + // Get value from inside + let subclass = opt_first_matching_sublcass + .and_then(|value_between| value_between.value.clone()); + + // If found, return + match subclass { + Some(value) => Ok(segment_class.with_subclass(value)), + None => Ok(segment_class), + } + } + } + } + + /// get Haversine distance along the LineString of the segment between start and end of the split + pub fn get_split_length_meters( + &self, + segments: &[&TransportationSegmentRecord], + segment_lookup: &HashMap, + ) -> Result { + let segment = self.get_segment(segments, segment_lookup)?; + match self { + SegmentSplit::SimpleConnectorSplit { src, dst, .. } => { + let start = src.linear_reference.0; + let end = dst.linear_reference.0; + Ok(segment.get_distance_at_meters(end)? - segment.get_distance_at_meters(start)?) + } + } + } + + /// get a reference to the segment that contains this split + fn get_segment<'a>( + &self, + segments: &'a [&TransportationSegmentRecord], + segment_lookup: &HashMap, + ) -> Result<&'a TransportationSegmentRecord, OvertureMapsCollectionError> { + use OvertureMapsCollectionError as E; + match self { + SegmentSplit::SimpleConnectorSplit { src, .. } => { + let segment_id = &src.segment_id; + let segment_idx = segment_lookup.get(segment_id).ok_or_else(|| { + let msg = format!("missing lookup entry for segment {segment_id}"); + E::InvalidSegmentConnectors(msg) + })?; + Ok(*segments.get(*segment_idx).ok_or_else(|| { + let msg = format!( + "missing lookup entry for segment {segment_id} with index {segment_idx}" + ); + E::InvalidSegmentConnectors(msg) + })?) + } + } + } } diff --git a/rust/bambam-omf/src/graph/serialize_ops.rs b/rust/bambam-omf/src/graph/serialize_ops.rs index 24769eaf..5fa8488d 100644 --- a/rust/bambam-omf/src/graph/serialize_ops.rs +++ b/rust/bambam-omf/src/graph/serialize_ops.rs @@ -10,7 +10,8 @@ use std::{ use crate::{ collection::{ - OvertureMapsCollectionError, TransportationConnectorRecord, TransportationSegmentRecord, + OvertureMapsCollectionError, SegmentAccessRestrictionWhen, SegmentFullType, + TransportationConnectorRecord, TransportationSegmentRecord, }, graph::{segment_split::SegmentSplit, ConnectorInSegment}, }; @@ -54,16 +55,19 @@ pub fn create_segment_lookup(segments: &[&TransportationSegmentRecord]) -> HashM } /// collects all splits from all segment records, used to create edges. -/// the application of split ops is parallelized over the segment records. +/// the application of split ops is parallelized over the segment records, as splits are +/// not ordered. pub fn find_splits( segments: &[&TransportationSegmentRecord], + when: Option<&SegmentAccessRestrictionWhen>, split_op: fn( &TransportationSegmentRecord, + Option<&SegmentAccessRestrictionWhen>, ) -> Result, OvertureMapsCollectionError>, ) -> Result, OvertureMapsCollectionError> { let result = segments .par_iter() - .map(|s| split_op(s)) + .map(|s| split_op(s, when)) .collect::>, OvertureMapsCollectionError>>()? .into_iter() .flatten() @@ -189,3 +193,91 @@ pub fn create_geometries( .map(|split| split.create_geometry_from_split(segments, segment_lookup)) .collect::>, OvertureMapsCollectionError>>() } + +pub fn create_speeds( + segments: &[&TransportationSegmentRecord], + segment_lookup: &HashMap, + splits: &[SegmentSplit], +) -> Result>, OvertureMapsCollectionError> { + splits + .par_iter() + .map(|split| split.get_split_speed(segments, segment_lookup)) + .collect::>, OvertureMapsCollectionError>>() +} + +pub fn create_segment_full_types( + segments: &[&TransportationSegmentRecord], + segment_lookup: &HashMap, + splits: &[SegmentSplit], +) -> Result, OvertureMapsCollectionError> { + splits + .par_iter() + .map(|split| split.get_split_segment_full_type(segments, segment_lookup)) + .collect::, OvertureMapsCollectionError>>() +} + +pub fn create_speed_by_segment_type_lookup<'a>( + initial_speeds: &[Option], + segments: &[&TransportationSegmentRecord], + segment_lookup: &HashMap, + splits: &[SegmentSplit], + classes: &'a [SegmentFullType], +) -> Result, OvertureMapsCollectionError> { + let split_lenghts = splits + .iter() + .map(|split| { + split + .get_split_length_meters(segments, segment_lookup) + .map(|v_f32| v_f32 as f64) + }) + .collect::, OvertureMapsCollectionError>>()?; + + let mut speed_sum_lookup: HashMap<&SegmentFullType, (f64, f64)> = HashMap::new(); + + for ((class, w), speed) in classes.iter().zip(split_lenghts).zip(initial_speeds) { + let Some(x) = speed else { continue }; // skip missing speeds + + let element = speed_sum_lookup.entry(class).or_insert((0.0, 0.0)); + element.0 += w * x; + element.1 += w; + } + + Ok(speed_sum_lookup + .into_iter() + .filter(|&(_k, (_wx, w))| w != 0.0) + .map(|(k, (wx, w))| (k, wx / w)) + .collect::>()) +} + +pub fn get_global_average_speed( + initial_speeds: &[Option], + segments: &[&TransportationSegmentRecord], + segment_lookup: &HashMap, + splits: &[SegmentSplit], +) -> Result { + let split_lenghts = splits + .iter() + .map(|split| { + split + .get_split_length_meters(segments, segment_lookup) + .map(|v_f32| v_f32 as f64) + }) + .collect::, OvertureMapsCollectionError>>()?; + + let mut total_length = 0.; + let mut weighted_sum = 0.; + for (opt_speed, length) in initial_speeds.iter().zip(split_lenghts) { + let Some(speed) = opt_speed else { continue }; // skip missing speeds + + total_length += length; + weighted_sum += length * speed; + } + + if total_length < 1e-6 { + return Err(OvertureMapsCollectionError::InternalError(format!( + "internal division by zero when computing average speed: {initial_speeds:?}" + ))); + } + + Ok(weighted_sum / total_length) +} diff --git a/rust/bambam-osm/Cargo.toml b/rust/bambam-osm/Cargo.toml index 79a1baf2..b60810db 100644 --- a/rust/bambam-osm/Cargo.toml +++ b/rust/bambam-osm/Cargo.toml @@ -18,12 +18,9 @@ keywords = [ categories = ["command-line-utilities", "science", "science::geo"] [dependencies] -bamcensus-core = { workspace = true } -bamcensus-acs = { workspace = true } bamcensus = { workspace = true } -routee-compass = { workspace = true } -routee-compass-core = { workspace = true } -routee-compass-powertrain = { workspace = true } +bamcensus-acs = { workspace = true } +bamcensus-core = { workspace = true } clap = { workspace = true } config = { workspace = true } @@ -42,6 +39,9 @@ osmio = { workspace = true } osmpbf = { workspace = true } rayon = { workspace = true } regex = { workspace = true } +routee-compass = { workspace = true } +routee-compass-core = { workspace = true } +routee-compass-powertrain = { workspace = true } rstar = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/rust/bambam-py/Cargo.toml b/rust/bambam-py/Cargo.toml index 78ea2677..8de797b6 100644 --- a/rust/bambam-py/Cargo.toml +++ b/rust/bambam-py/Cargo.toml @@ -8,17 +8,17 @@ crate-type = ["cdylib"] [dependencies] bambam = { path = "../bambam", version = "0.2.1" } -routee-compass = { workspace = true } -routee-compass-core = { workspace = true } -routee-compass-py = { workspace = true } -routee-compass-macros = { workspace = true } +chrono = { workspace = true } +config = { workspace = true } inventory = { workspace = true } itertools = { workspace = true } + +pyo3 = { version = "0.27.1", features = ["extension-module", "serde"] } +routee-compass = { workspace = true } +routee-compass-core = { workspace = true } +routee-compass-macros = { workspace = true } +routee-compass-py = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -chrono = { workspace = true } -config = { workspace = true } thiserror = { workspace = true } - -pyo3 = { version = "0.27.1", features = ["extension-module", "serde"] } diff --git a/rust/bambam/Cargo.toml b/rust/bambam/Cargo.toml index 76082c39..d5066f0e 100644 --- a/rust/bambam/Cargo.toml +++ b/rust/bambam/Cargo.toml @@ -18,53 +18,53 @@ keywords = [ categories = ["command-line-utilities", "science", "science::geo"] [dependencies] -bamcensus-core = { workspace = true } +bambam-gbfs = { version = "0.2.3", path = "../bambam-gbfs" } +bambam-omf = { version = "0.2.3", path = "../bambam-omf" } +bambam-osm = { version = "0.2.3", path = "../bambam-osm" } +bamcensus = { workspace = true } bamcensus-acs = { workspace = true } +bamcensus-core = { workspace = true } bamcensus-lehd = { workspace = true } -bamcensus = { workspace = true } -routee-compass = { workspace = true } -routee-compass-core = { workspace = true } -routee-compass-powertrain = { workspace = true } -bambam-osm = { version = "0.2.3", path = "../bambam-osm" } -bambam-omf = { version = "0.2.3", path = "../bambam-omf" } -bambam-gbfs = { version = "0.2.3", path = "../bambam-gbfs" } - -uom = { workspace = true} -inventory = { workspace = true} -jsonpath-rust = { workspace = true } -log = { workspace = true } -itertools = { workspace = true} -chrono = { workspace = true} -skiplist = { workspace = true } -zip = { workspace = true } +chrono = { workspace = true } +clap = { workspace = true } +config = { workspace = true } +csv = { workspace = true } # rusqlite = { workspace = true} env_logger = { workspace = true } -config = { workspace = true } -thiserror = { workspace = true } -kdam = { workspace = true } -clap = { workspace = true } -regex = { workspace = true } - -serde = { workspace = true} -serde_json = { workspace = true} -csv = { workspace = true} flate2 = { workspace = true } -toml = { workspace = true } - -rstar = { workspace = true} -geo = { workspace = true} +geo = { workspace = true } geo-traits = { workspace = true } geo-types = { workspace = true } -wkt = { workspace = true} -wkb = { workspace = true } +geojson = { workspace = true } +gtfs-structures = { workspace = true } + +h3o = { workspace = true } hex = { workspace = true } -geojson = { workspace = true} +inventory = { workspace = true } +itertools = { workspace = true } +jsonpath-rust = { workspace = true } +kdam = { workspace = true } +log = { workspace = true } rand = { workspace = true } -shapefile = { workspace = true} - -h3o = { workspace = true} -gtfs-structures = { workspace = true } rayon = { workspace = true } +regex = { workspace = true } +routee-compass = { workspace = true } +routee-compass-core = { workspace = true } +routee-compass-powertrain = { workspace = true } + +rstar = { workspace = true } + +serde = { workspace = true } +serde_json = { workspace = true } +shapefile = { workspace = true } +skiplist = { workspace = true } +thiserror = { workspace = true } tokio = { workspace = true } +toml = { workspace = true } + +uom = { workspace = true } +wkb = { workspace = true } +wkt = { workspace = true } +zip = { workspace = true }