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::{
record::SegmentHeading, OvertureMapsCollectionError, SegmentFullType, SegmentSpeedLimit,
TransportationSegmentRecord,
},
graph::{connector_in_segment::ConnectorInSegment, consts},
};
pub struct SegmentSplit {
pub src: ConnectorInSegment,
pub dst: ConnectorInSegment,
pub heading: SegmentHeading,
}
impl SegmentSplit {
pub fn new(src: ConnectorInSegment, dst: ConnectorInSegment, heading: SegmentHeading) -> Self {
Self { src, dst, heading }
}
pub fn create_edge_from_split(
&self,
edge_id: EdgeId,
edge_list_id: EdgeListId,
segments: &[&TransportationSegmentRecord],
segment_lookup: &HashMap<String, usize>,
_vertices: &[Vertex],
vertex_lookup: &HashMap<String, usize>,
) -> Result<Edge, OvertureMapsCollectionError> {
use OvertureMapsCollectionError as E;
let SegmentSplit { src, dst, heading } = self;
let segment_id = if src.segment_id != dst.segment_id {
let msg = format!(
"attempting to create edge where src segment != dst segment ('{}' != '{}')",
src.segment_id, dst.segment_id
);
return Err(E::InvalidSegmentConnectors(msg));
} else {
&src.segment_id
};
let src_id = vertex_lookup
.get(&src.connector_id)
.ok_or(E::InvalidSegmentConnectors(format!(
"segment references unknown connector {}",
src.connector_id
)))?;
let dst_id = vertex_lookup
.get(&dst.connector_id)
.ok_or(E::InvalidSegmentConnectors(format!(
"segment references unknown connector {}",
dst.connector_id
)))?;
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)),
};
if dst.linear_reference < src.linear_reference {
return Err(E::InvalidSegmentConnectors(format!(
"SimpleConnectorSplit: at_dst ({}) < at_src ({}) for connectors {} -> {}",
dst.linear_reference, src.linear_reference, src.connector_id, dst.connector_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)
})?;
let segment = segments.get(*segment_idx).ok_or_else(|| {
let msg =
format!("missing lookup entry for segment {segment_id} with index {segment_idx}");
E::InvalidSegmentConnectors(msg)
})?;
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::<uom::si::length::meter>(distance_f32 as f64);
let edge = Edge {
edge_list_id,
edge_id,
src_vertex_id,
dst_vertex_id,
distance,
};
Ok(edge)
}
pub fn create_geometry_from_split(
&self,
segments: &[&TransportationSegmentRecord],
segment_lookup: &HashMap<String, usize>,
) -> Result<LineString<f32>, OvertureMapsCollectionError> {
use OvertureMapsCollectionError as E;
let SegmentSplit { src, dst, heading } = self;
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)
})?;
let segment = segments.get(*segment_idx).ok_or_else(|| {
let msg =
format!("missing lookup entry for segment {segment_id} with index {segment_idx}");
E::InvalidSegmentConnectors(msg)
})?;
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![];
out_coords.push(segment.get_coord_at(src.linear_reference.0)?);
let mut total_distance = 0.;
for line in segment_geometry.lines() {
let line_distance = Haversine.length(&line);
total_distance += line_distance;
if total_distance <= distance_to_src + consts::F32_DISTANCE_TOLERANCE {
continue;
}
if total_distance + consts::F32_DISTANCE_TOLERANCE >= distance_to_dst {
break;
}
out_coords.push(line.end);
}
out_coords.push(segment.get_coord_at(dst.linear_reference.0)?);
if *heading == SegmentHeading::Backward {
out_coords.reverse();
}
Ok(LineString::new(out_coords))
}
pub fn get_split_speed(
&self,
segments: &[&TransportationSegmentRecord],
segment_lookup: &HashMap<String, usize>,
) -> Result<Option<f64>, OvertureMapsCollectionError> {
use OvertureMapsCollectionError as E;
let segment = self.get_segment(segments, segment_lookup)?;
let SegmentSplit { src, dst, heading } = self;
let speed_limits = match segment.speed_limits.as_ref() {
Some(limits) => limits,
None => return Ok(None),
};
let speed_limits_with_heading = speed_limits
.iter()
.filter(|s| has_max_speed_for_heading(s, heading))
.collect_vec();
let start = src.linear_reference.0;
let end = dst.linear_reference.0;
let intersecting_portions: Vec<f64> = speed_limits_with_heading
.iter()
.map(|speed_limit| speed_limit.get_linear_reference_portion(start, end))
.collect::<Result<_, E>>()?;
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::<uom::si::velocity::mile_per_hour>()
* weight)
})
.collect::<Result<Vec<f64>, E>>()?;
Ok(Some(weighted_mph.iter().sum()))
}
pub fn get_split_segment_full_type(
&self,
segments: &[&TransportationSegmentRecord],
segment_lookup: &HashMap<String, usize>,
) -> Result<SegmentFullType, OvertureMapsCollectionError> {
let segment = self.get_segment(segments, segment_lookup)?;
let SegmentSplit { src, dst, .. } = self;
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);
};
let opt_first_matching_sublcass = segment.subclass_rules.as_ref().and_then(|rules| {
rules
.iter()
.find(|rule| matches!(rule.check_open_intersection(start, end), Ok(true)))
});
let subclass =
opt_first_matching_sublcass.and_then(|value_between| value_between.value.clone());
match subclass {
Some(value) => Ok(segment_class.with_subclass(value)),
None => Ok(segment_class),
}
}
pub fn get_split_length_meters(
&self,
segments: &[&TransportationSegmentRecord],
segment_lookup: &HashMap<String, usize>,
) -> Result<f32, OvertureMapsCollectionError> {
let segment = self.get_segment(segments, segment_lookup)?;
let start = self.src.linear_reference.0;
let end = self.dst.linear_reference.0;
Ok(segment.get_distance_at_meters(end)? - segment.get_distance_at_meters(start)?)
}
pub fn get_omf_segment_id_and_linear_ref(
&self,
segments: &[&TransportationSegmentRecord],
segment_lookup: &HashMap<String, usize>,
) -> Result<(String, f64), OvertureMapsCollectionError> {
let segment = self.get_segment(segments, segment_lookup)?;
Ok((segment.id.clone(), self.src.linear_reference.0))
}
fn get_segment<'a>(
&self,
segments: &'a [&TransportationSegmentRecord],
segment_lookup: &HashMap<String, usize>,
) -> Result<&'a TransportationSegmentRecord, OvertureMapsCollectionError> {
use OvertureMapsCollectionError as E;
let segment_id = &self.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)
})?)
}
}
fn has_max_speed_for_heading(s: &SegmentSpeedLimit, heading: &SegmentHeading) -> bool {
if s.max_speed.as_ref().is_none() {
return false;
}
let when = match s.when.as_ref() {
Some(w) => w,
None => return true, };
match when.heading.as_ref() {
None => true, Some(h) if h == heading => true,
_ => false,
}
}
#[cfg(test)]
mod test {
use crate::collection::{
record::{SegmentHeading, SpeedLimitWithUnit},
SegmentAccessRestrictionWhen, SegmentSpeedLimit, SegmentSpeedUnit,
};
#[test]
fn no_maxspeed_entry() {
let record = SegmentSpeedLimit {
min_speed: Some(SpeedLimitWithUnit {
value: 35,
unit: SegmentSpeedUnit::Mph,
}),
max_speed: None,
is_max_speed_variable: None,
when: Some(SegmentAccessRestrictionWhen {
during: None,
heading: Some(SegmentHeading::Backward),
using: None,
recognized: None,
mode: None,
vehicle: None,
}),
between: Some(vec![0.0, 0.418244116]),
};
let heading = SegmentHeading::Forward;
let result = super::has_max_speed_for_heading(&record, &heading);
assert!(!result)
}
}