use core::fmt::Debug;
use core::hash::BuildHasherDefault;
use geo::Point;
use petgraph::prelude::DiGraphMap;
use routers_network::{
Direction, DirectionAwareEdgeId, Discovery, Edge, Entry, Metadata, Network, Node, Route, Scan,
edge::Weight, network::GraphEdge,
};
use rstar::{AABB, RTree};
use rustc_hash::{FxHashMap, FxHasher};
use serde::Serialize;
#[derive(Default, Serialize, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct MockEntryId(pub i64);
impl Entry for MockEntryId {
#[inline]
fn identifier(&self) -> i64 {
self.0
}
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct MockMetadata;
impl Metadata for MockMetadata {
type Raw<'a> = ();
type Runtime = ();
type TripContext = ();
fn pick(_raw: ()) -> Self {
MockMetadata
}
fn runtime(_ctx: Option<()>) -> () {}
fn accessible(&self, _access: &(), _direction: Direction) -> bool {
true
}
}
type GraphStructure = DiGraphMap<
MockEntryId,
(Weight, DirectionAwareEdgeId<MockEntryId>),
BuildHasherDefault<FxHasher>,
>;
pub struct MockNetwork {
graph: GraphStructure,
nodes: FxHashMap<MockEntryId, Node<MockEntryId>>,
metadata: FxHashMap<MockEntryId, MockMetadata>,
node_index: RTree<Node<MockEntryId>>,
edge_index: RTree<Edge<Node<MockEntryId>>>,
}
impl Debug for MockNetwork {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("mock network")
}
}
impl Discovery<MockEntryId> for MockNetwork {
fn edges_in_box<'a>(
&'a self,
aabb: AABB<Point>,
) -> Box<dyn Iterator<Item = &'a Edge<Node<MockEntryId>>> + Send + 'a>
where
MockEntryId: 'a,
{
Box::new(self.edge_index.locate_in_envelope_intersecting(&aabb))
}
fn nodes_in_box<'a>(
&'a self,
aabb: AABB<Point>,
) -> Box<dyn Iterator<Item = &'a Node<MockEntryId>> + Send + 'a>
where
MockEntryId: 'a,
{
Box::new(self.node_index.locate_in_envelope(&aabb))
}
fn node(&self, id: &MockEntryId) -> Option<&Node<MockEntryId>> {
self.nodes.get(id)
}
fn edge(&self, source: &MockEntryId, target: &MockEntryId) -> Option<Edge<MockEntryId>> {
self.graph
.edge_weight(*source, *target)
.map(|&(weight, id)| Edge {
source: *source,
target: *target,
weight,
id,
})
}
}
impl Scan<MockEntryId> for MockNetwork {
fn nearest_node<'a>(&'a self, point: &Point) -> Option<&'a Node<MockEntryId>>
where
MockEntryId: 'a,
{
self.node_index.nearest_neighbor(point)
}
}
impl Route<MockEntryId> for MockNetwork {
fn route_nodes(
&self,
start_node: MockEntryId,
finish_node: MockEntryId,
) -> Option<(Weight, Vec<Node<MockEntryId>>)> {
let (score, path) = petgraph::algo::astar(
&self.graph,
start_node,
|finish| finish == finish_node,
|(_, _, w)| w.0,
|_| 0,
)?;
let route = path
.iter()
.filter_map(|v| self.nodes.get(v).copied())
.collect();
Some((score, route))
}
}
impl Network<MockEntryId, MockMetadata> for MockNetwork {
fn metadata(&self, id: &MockEntryId) -> Option<&MockMetadata> {
self.metadata.get(id)
}
fn point(&self, id: &MockEntryId) -> Option<Point> {
self.nodes.get(id).map(|n| n.position)
}
fn edges_outof<'a>(
&'a self,
id: MockEntryId,
) -> Box<dyn Iterator<Item = GraphEdge<MockEntryId>> + 'a> {
Box::new(
self.graph
.edges_directed(id, petgraph::Direction::Outgoing)
.map(|(src, dst, &data)| (src, dst, data)),
)
}
fn edges_into<'a>(
&'a self,
id: MockEntryId,
) -> Box<dyn Iterator<Item = GraphEdge<MockEntryId>> + 'a> {
Box::new(
self.graph
.edges_directed(id, petgraph::Direction::Incoming)
.map(|(src, dst, &data)| (src, dst, data)),
)
}
fn fatten(
&self,
Edge {
source,
target,
weight,
id,
}: &Edge<MockEntryId>,
) -> Option<Edge<Node<MockEntryId>>> {
Some(Edge {
source: *self.nodes.get(source)?,
target: *self.nodes.get(target)?,
id: DirectionAwareEdgeId::new(Node::new(dummy_point(), id.index()))
.with_direction(id.direction()),
weight: *weight,
})
}
}
const DEFAULT_WEIGHT: Weight = 1;
#[inline(always)]
fn dummy_point() -> Point {
Point::new(0., 0.)
}
struct NodeDef {
id: MockEntryId,
position: Point,
}
struct EdgeDef {
source: MockEntryId,
target: MockEntryId,
weight: Weight,
edge_id: MockEntryId,
}
pub struct MockNetworkBuilder {
nodes: Vec<NodeDef>,
edges: Vec<EdgeDef>,
next_edge_id: i64,
}
impl Default for MockNetworkBuilder {
fn default() -> Self {
Self::new()
}
}
impl MockNetworkBuilder {
pub fn new() -> Self {
Self {
nodes: Vec::new(),
edges: Vec::new(),
next_edge_id: 1,
}
}
pub fn node(mut self, id: i64, position: Point) -> Self {
self.nodes.push(NodeDef {
id: MockEntryId(id),
position,
});
self
}
pub fn edge(self, source: i64, target: i64) -> Self {
self.weighted_edge(source, target, DEFAULT_WEIGHT)
}
pub fn weighted_edge(mut self, source: i64, target: i64, weight: Weight) -> Self {
let edge_id = MockEntryId(self.next_edge_id);
self.next_edge_id += 1;
self.edges.push(EdgeDef {
source: MockEntryId(source),
target: MockEntryId(target),
weight,
edge_id,
});
self
}
pub fn bidirectional_edge(self, a: i64, b: i64) -> Self {
self.bidirectional_weighted_edge(a, b, DEFAULT_WEIGHT)
}
pub fn bidirectional_weighted_edge(mut self, a: i64, b: i64, weight: Weight) -> Self {
let edge_id = MockEntryId(self.next_edge_id);
self.next_edge_id += 1;
self.edges.push(EdgeDef {
source: MockEntryId(a),
target: MockEntryId(b),
weight,
edge_id,
});
self.edges.push(EdgeDef {
source: MockEntryId(b),
target: MockEntryId(a),
weight,
edge_id,
});
self
}
pub fn build(self) -> MockNetwork {
let mut graph = GraphStructure::new();
let mut nodes: FxHashMap<MockEntryId, Node<MockEntryId>> = FxHashMap::default();
let mut metadata: FxHashMap<MockEntryId, MockMetadata> = FxHashMap::default();
for NodeDef { id, position } in &self.nodes {
nodes.insert(*id, Node::new(*position, *id));
}
for EdgeDef {
source,
target,
weight,
edge_id,
} in &self.edges
{
let direction_aware = DirectionAwareEdgeId::new(*edge_id);
graph.add_edge(*source, *target, (*weight, direction_aware));
metadata.entry(*edge_id).or_default();
}
let fat_edges: Vec<Edge<Node<MockEntryId>>> = self
.edges
.iter()
.filter_map(|e| {
let src_node = *nodes.get(&e.source)?;
let tgt_node = *nodes.get(&e.target)?;
let direction_aware =
DirectionAwareEdgeId::new(Node::new(dummy_point(), e.edge_id));
Some(Edge {
source: src_node,
target: tgt_node,
weight: e.weight,
id: direction_aware,
})
})
.collect();
let node_index = RTree::bulk_load(nodes.values().copied().collect());
let edge_index = RTree::bulk_load(fat_edges);
MockNetwork {
graph,
nodes,
metadata,
node_index,
edge_index,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::r#match::MatchSimpleExt;
use geo::{LineString, point, wkt};
fn straight_road() -> MockNetwork {
MockNetworkBuilder::new()
.node(1, point!(x: -118.15, y: 34.15))
.node(2, point!(x: -118.16, y: 34.15))
.node(3, point!(x: -118.17, y: 34.15))
.edge(1, 2)
.edge(2, 3)
.build()
}
#[test]
fn builder_registers_nodes() {
let net = straight_road();
assert!(net.node(&MockEntryId(1)).is_some());
assert!(net.node(&MockEntryId(2)).is_some());
assert!(net.node(&MockEntryId(3)).is_some());
assert!(net.node(&MockEntryId(99)).is_none());
}
#[test]
fn builder_registers_edges() {
let net = straight_road();
assert!(net.edge(&MockEntryId(1), &MockEntryId(2)).is_some());
assert!(net.edge(&MockEntryId(2), &MockEntryId(3)).is_some());
assert!(net.edge(&MockEntryId(2), &MockEntryId(1)).is_none());
}
#[test]
fn bidirectional_edge_creates_both_directions() {
let net = MockNetworkBuilder::new()
.node(1, point!(x: -118.15, y: 34.15))
.node(2, point!(x: -118.16, y: 34.15))
.bidirectional_edge(1, 2)
.build();
assert!(net.edge(&MockEntryId(1), &MockEntryId(2)).is_some());
assert!(net.edge(&MockEntryId(2), &MockEntryId(1)).is_some());
}
#[test]
fn nearest_node_returns_closest() {
let net = straight_road();
let query = point!(x: -118.161, y: 34.151);
let nearest = net.nearest_node(&query).expect("nearest node must exist");
assert_eq!(nearest.id, MockEntryId(2));
}
#[test]
fn metadata_present_for_all_edges() {
let net = straight_road();
assert!(net.metadata(&MockEntryId(1)).is_some());
assert!(net.metadata(&MockEntryId(2)).is_some());
}
#[test]
fn mock_metadata_always_accessible() {
let meta = MockMetadata;
assert!(meta.accessible(&(), Direction::Outgoing));
assert!(meta.accessible(&(), Direction::Incoming));
}
#[test]
fn route_nodes_finds_direct_path() {
let net = straight_road();
let (_, path) = net
.route_nodes(MockEntryId(1), MockEntryId(3))
.expect("route must exist");
let ids: Vec<i64> = path.iter().map(|n| n.id.0).collect();
assert_eq!(ids, vec![1, 2, 3]);
}
#[test]
fn route_nodes_returns_none_for_unreachable() {
let net = straight_road();
assert!(net.route_nodes(MockEntryId(3), MockEntryId(1)).is_none());
}
#[test]
fn map_match_straight_road() {
let net = straight_road();
let linestring: LineString = wkt! {
LINESTRING(
-118.151 34.1503,
-118.155 34.1503,
-118.160 34.1503,
-118.165 34.1503
)
};
let result = net
.match_simple(linestring)
.expect("map match must succeed on a reachable network");
for element in &result.discretized.elements {
assert!(
net.metadata(element.edge.id()).is_some(),
"metadata must be present for every matched edge"
);
}
assert_eq!(
result.discretized.elements.len(),
4,
"discretized path must have one element per GPS input point"
);
}
#[test]
fn map_match_interpolated_path_crosses_intermediate_edge() {
let net = MockNetworkBuilder::new()
.node(1, point!(x: -118.14, y: 34.15))
.node(2, point!(x: -118.15, y: 34.15))
.node(3, point!(x: -118.16, y: 34.15))
.node(4, point!(x: -118.17, y: 34.15))
.edge(1, 2)
.edge(2, 3)
.edge(3, 4)
.build();
let linestring: LineString = wkt! {
LINESTRING(
-118.141 34.1503,
-118.169 34.1503
)
};
let result = net
.match_simple(linestring)
.expect("map match must succeed");
assert!(
!result.interpolated.elements.is_empty(),
"interpolated path must be non-empty when candidates span non-adjacent edges"
);
for element in &result.interpolated.elements {
assert!(
net.metadata(element.edge.id()).is_some(),
"every interpolated edge must have metadata"
);
}
}
#[test]
fn map_match_prefers_straight_over_turn() {
let net = MockNetworkBuilder::new()
.node(1, point!(x: -118.10, y: 34.15))
.node(2, point!(x: -118.13, y: 34.15))
.node(3, point!(x: -118.16, y: 34.15))
.node(4, point!(x: -118.13, y: 34.12))
.bidirectional_edge(1, 2)
.bidirectional_edge(2, 3)
.bidirectional_edge(2, 4)
.build();
let linestring: LineString = wkt! {
LINESTRING(
-118.101 34.1503,
-118.111 34.1503,
-118.121 34.1503,
-118.131 34.1503,
-118.141 34.1503,
-118.151 34.1503,
-118.158 34.1503
)
};
let result = net
.match_simple(linestring)
.expect("map match must succeed");
assert!(
!result.discretized.elements.is_empty(),
"discretized path must be non-empty: a real match was made"
);
let matched_node_ids: Vec<i64> = result
.discretized
.elements
.iter()
.flat_map(|e| [e.edge.source.id.0, e.edge.target.id.0])
.collect();
assert!(
!matched_node_ids.contains(&4),
"the south-branch node (4) must not appear in a straight-west trajectory match"
);
}
#[test]
fn map_match_highway_preferred_over_offramp() {
let net = MockNetworkBuilder::new()
.node(1, point!(x: -118.100, y: 34.150))
.node(2, point!(x: -118.105, y: 34.150)) .node(3, point!(x: -118.109, y: 34.149)) .node(4, point!(x: -118.113, y: 34.148)) .node(5, point!(x: -118.107, y: 34.146)) .bidirectional_edge(1, 2)
.bidirectional_edge(2, 3)
.bidirectional_edge(3, 4)
.edge(2, 5) .edge(5, 4) .build();
let linestring: LineString = wkt! {
LINESTRING(
-118.102 34.1503,
-118.111 34.1488
)
};
let result = net
.match_simple(linestring)
.expect("map match must succeed");
assert!(
!result.interpolated.elements.is_empty(),
"interpolated path must be non-empty: routing spanned non-adjacent edges"
);
let interpolated_node_ids: Vec<i64> = result
.interpolated
.elements
.iter()
.flat_map(|e| [e.edge.source.id.0, e.edge.target.id.0])
.collect();
assert!(
!interpolated_node_ids.contains(&5),
"offramp detour node (5) must not appear: the shorter highway route is preferred"
);
}
#[test]
fn map_match_follows_turn_at_junction() {
let net = MockNetworkBuilder::new()
.node(1, point!(x: -118.10, y: 34.15)) .node(2, point!(x: -118.13, y: 34.15)) .node(3, point!(x: -118.13, y: 34.18)) .node(4, point!(x: -118.16, y: 34.15)) .bidirectional_edge(1, 2)
.bidirectional_edge(2, 3)
.bidirectional_edge(2, 4)
.build();
let linestring: LineString = wkt! {
LINESTRING(
-118.101 34.1503,
-118.111 34.1503,
-118.121 34.1503,
-118.1297 34.1503,
-118.1297 34.153,
-118.1297 34.163
)
};
let result = net
.match_simple(linestring)
.expect("map match must succeed");
assert!(
!result.discretized.elements.is_empty(),
"discretized path must be non-empty"
);
let matched_node_ids: Vec<i64> = result
.discretized
.elements
.iter()
.flat_map(|e| [e.edge.source.id.0, e.edge.target.id.0])
.collect();
assert!(
!matched_node_ids.contains(&4),
"east-continuation node (4) must not appear when the GPS turns north at the junction"
);
}
#[test]
fn map_match_interpolated_path_includes_candidate_edges() {
let net = MockNetworkBuilder::new()
.node(1, point!(x: -118.14, y: 34.15))
.node(2, point!(x: -118.15, y: 34.15))
.node(3, point!(x: -118.16, y: 34.15))
.node(4, point!(x: -118.17, y: 34.15))
.edge(1, 2)
.edge(2, 3)
.edge(3, 4)
.build();
let linestring: LineString = wkt! {
LINESTRING(
-118.141 34.1503,
-118.169 34.1503
)
};
let result = net
.match_simple(linestring)
.expect("map match must succeed");
let interpolated_node_ids: Vec<i64> = result
.interpolated
.elements
.iter()
.flat_map(|e| [e.edge.source.id.0, e.edge.target.id.0])
.collect();
assert!(
interpolated_node_ids.contains(&1),
"node 1 (start of first candidate edge e1) must appear in the interpolated path"
);
assert!(
interpolated_node_ids.contains(&4),
"node 4 (end of last candidate edge e3) must appear in the interpolated path"
);
assert!(
interpolated_node_ids.contains(&2) && interpolated_node_ids.contains(&3),
"nodes 2 and 3 (intermediate bridging edge e2) must appear in the interpolated path"
);
for element in &result.interpolated.elements {
assert!(
net.metadata(element.edge.id()).is_some(),
"every interpolated edge must have metadata"
);
}
}
#[test]
fn map_match_prefers_motorway_over_motorway_link() {
let net = MockNetworkBuilder::new()
.node(1, point!(x: -118.140, y: 34.15006))
.node(2, point!(x: -118.141, y: 34.15006))
.node(3, point!(x: -118.142, y: 34.15006))
.node(4, point!(x: -118.143, y: 34.15006))
.node(5, point!(x: -118.140, y: 34.14996))
.node(6, point!(x: -118.141, y: 34.14996))
.node(7, point!(x: -118.142, y: 34.14996))
.node(8, point!(x: -118.143, y: 34.14996))
.weighted_edge(1, 2, 1)
.weighted_edge(2, 3, 1)
.weighted_edge(3, 4, 1)
.weighted_edge(5, 6, 2)
.weighted_edge(6, 7, 2)
.weighted_edge(7, 8, 2)
.build();
let linestring: LineString = wkt! {
LINESTRING(
-118.1405 34.150000,
-118.1415 34.150000,
-118.1425 34.150000
)
};
let result = net
.match_simple(linestring)
.expect("map match must succeed");
for element in &result.discretized.elements {
assert_eq!(
element.edge.weight, 1,
"matched edge must be the motorway (weight=1), not the motorway_link (weight=2)"
);
}
}
}