From 1d4611056cf8a357b6c1259d772bf9766cca227f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristram=20Gr=C3=A4bener?= Date: Thu, 25 Jan 2024 17:36:26 +0100 Subject: [PATCH] Allow to contract two graph edges from two osm ways --- Cargo.toml | 2 +- readme.md | 6 ++ src/main.rs | 10 +- src/osm4routing/categorize.rs | 2 +- src/osm4routing/models.rs | 50 ++++++++++ src/osm4routing/reader.rs | 89 +++++++++++++++++- .../test_data/ways_to_merge.osm.pbf | Bin 0 -> 343 bytes 7 files changed, 154 insertions(+), 5 deletions(-) create mode 100644 src/osm4routing/test_data/ways_to_merge.osm.pbf diff --git a/Cargo.toml b/Cargo.toml index f8b0f75..04f89da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "osm4routing" edition = "2021" -version = "0.5.9" +version = "0.6.0" authors = ["Tristram Gräbener "] description = "Convert OpenStreetMap data into routing friendly CSV" homepage = "https://github.com/Tristramg/osm4routing2" diff --git a/readme.md b/readme.md index d9bc98b..4d4565a 100644 --- a/readme.md +++ b/readme.md @@ -55,3 +55,9 @@ If you need to read some tags, pass them to the `reader`: let (nodes, edges) = osm4routing::Reader::new().read_tag("highway").read("some_data.osm.pbf")?; ``` + +If want to contract edges that come from different OpenStreetMap ways, but where there is no intersection (that can happen when the tags change, e.g. a tunnel): + +``` +let (nodes, edges) = osm4routing::Reader::new().merge_ways().read("some_data.osm.pbf")?; +``` \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index cb9f87a..9137544 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,11 +11,19 @@ struct Cli { /// Output path of the csv file that will contain the edges #[arg(short, long, default_value = "nodes.csv")] edges_file: String, + /// Merge two edges from different OSM ways into a single edge when there is no intersection + #[arg(short, long)] + merge_edges: bool, } fn main() { let cli = Cli::parse(); + let mut reader = if cli.merge_edges { + osm4routing::Reader::new().merge_ways() + } else { + osm4routing::Reader::new() + }; - match osm4routing::read(&cli.source_pbf) { + match reader.read(&cli.source_pbf) { Ok((nodes, edges)) => { osm4routing::writers::csv(nodes, edges, &cli.nodes_file, &cli.edges_file) } diff --git a/src/osm4routing/categorize.rs b/src/osm4routing/categorize.rs index 953f988..62b24c0 100644 --- a/src/osm4routing/categorize.rs +++ b/src/osm4routing/categorize.rs @@ -36,7 +36,7 @@ pub enum TrainAccessibility { } // Edgeself contains what mode can use the edge in each direction -#[derive(Clone, Copy)] +#[derive(Clone, Copy, PartialEq)] pub struct EdgeProperties { pub foot: FootAccessibility, pub car_forward: CarAccessibility, diff --git a/src/osm4routing/models.rs b/src/osm4routing/models.rs index 0015b04..c637383 100644 --- a/src/osm4routing/models.rs +++ b/src/osm4routing/models.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::hash::{Hash, Hasher}; use super::categorize::EdgeProperties; pub use osmpbfreader::objects::{NodeId, WayId}; @@ -46,6 +47,7 @@ impl Default for Node { } // Edge is a topological representation with only two extremities and no geometry +#[derive(Clone)] pub struct Edge { pub id: String, pub osm_id: WayId, @@ -57,6 +59,20 @@ pub struct Edge { pub tags: std::collections::HashMap, } +impl Hash for Edge { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + +impl PartialEq for Edge { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for Edge {} + impl Default for Edge { fn default() -> Self { Self { @@ -103,6 +119,40 @@ impl Edge { } 0. } + + // Changes the direction of the geometry, source and target + // Returns a new Edge + pub fn reverse(mut self) -> Self { + self.nodes.reverse(); + self.geometry.reverse(); + std::mem::swap(&mut self.target, &mut self.source); + self + } + + // Merges two edges together. It supposes that self.target == e2.source and will panic otherwise + fn unsafe_merge(mut self, other: Self) -> Self { + assert!(self.target == other.source); + self.id = format!("{}-{}", self.id, other.id); + self.target = other.target; + self.nodes = [&self.nodes, &other.nodes[1..]].concat(); + self.geometry = [&self.geometry, &other.geometry[1..]].concat(); + self + } + + // Creates a new edges by stiching together two edges at node `node` + // Will panic if the node is not a common extremity for both + pub fn merge(edge1: &Self, edge2: &Self, node: NodeId) -> Self { + let edge1 = edge1.clone(); + let edge2 = edge2.clone(); + assert!(edge1.source == node || edge1.target == node); + assert!(edge2.source == node || edge2.target == node); + match (edge1.target == node, edge2.source == node) { + (true, true) => edge1.unsafe_merge(edge2), + (false, true) => edge1.reverse().unsafe_merge(edge2), + (true, false) => edge1.unsafe_merge(edge2.reverse()), + (false, false) => edge1.reverse().unsafe_merge(edge2.reverse()), + } + } } #[test] diff --git a/src/osm4routing/reader.rs b/src/osm4routing/reader.rs index d19938f..b3e6ceb 100644 --- a/src/osm4routing/reader.rs +++ b/src/osm4routing/reader.rs @@ -30,6 +30,7 @@ pub struct Reader { forbidden_tags: HashMap>, required_tags: HashMap>, tags_to_read: HashSet, + should_merge_ways: bool, } impl Reader { @@ -58,6 +59,11 @@ impl Reader { self } + pub fn merge_ways(mut self) -> Self { + self.should_merge_ways = true; + self + } + fn count_nodes_uses(&mut self) { for way in &self.ways { for (i, node_id) in way.nodes.iter().enumerate() { @@ -65,7 +71,7 @@ impl Reader { .nodes .get_mut(node_id) .expect("Missing node, id: {node_id}"); - // Count double extremities nodes + // Count double extremities nodes to be sure to include dead-ends if i == 0 || i == way.nodes.len() - 1 { node.uses += 2; } else { @@ -107,6 +113,65 @@ impl Reader { result } + // An OSM way can be split even if it’s — in a topologicial sense — the same edge + // For instance a road crossing a river, will be split to allow a tag bridge=yes + // Even if there was no crossing + fn do_merge_edges(&mut self, edges: Vec) -> Vec { + let initial_edges_count = edges.len(); + + // We build an adjacency map for every node that might have exactly two edges + let mut neighbors: HashMap> = HashMap::new(); + for edge in edges.iter() { + // Extremities of a way in `count_nodes_uses` are counted twice to avoid pruning deadends. + // We want to look at nodes with at two extremities, hence 4 uses + if self.nodes.get(&edge.source).is_none() { + println!("Problem with node {}, edge {}", &edge.source.0, edge.id); + } + if self.nodes.get(&edge.source).unwrap().uses == 4 { + neighbors.entry(edge.source).or_default().push(edge); + } + if self.nodes.get(&edge.target).unwrap().uses == 4 { + neighbors.entry(edge.target).or_default().push(edge); + } + } + + let mut result = Vec::new(); + let mut already_merged = HashSet::new(); + for (node, edges) in neighbors.drain() { + // We merge two edges at the node if there are only two edges + // The edges must have the same accessibility properties + // The edges must be from different ways (no surface) + // The edges must not have been merged this iteration (they might be re-merged through a recurive call) + if edges.len() == 2 + && edges[0].properties == edges[1].properties + && edges[0].id != edges[1].id + && !already_merged.contains(&edges[0].id) + && !already_merged.contains(&edges[1].id) + { + let edge1 = edges[0]; + let edge2 = edges[1]; + result.push(Edge::merge(edge1, edge2, node)); + already_merged.insert(edge1.id.clone()); + already_merged.insert(edge2.id.clone()); + self.nodes.remove(&node); + } + } + + for edge in edges.into_iter() { + if !already_merged.contains(&edge.id) { + result.push(edge); + } + } + + // If we reduced the number of edges, that means that we merged edges + // They might need to be merged again, recursively + if initial_edges_count > result.len() { + self.do_merge_edges(result) + } else { + result + } + } + fn is_user_rejected(&self, way: &osmpbfreader::Way) -> bool { let meet_required_tags = self.required_tags.is_empty() || way.tags.iter().any(|(key, val)| { @@ -199,7 +264,13 @@ impl Reader { let file_nodes = std::fs::File::open(path).map_err(|e| e.to_string())?; self.read_nodes(file_nodes); self.count_nodes_uses(); - Ok((self.nodes(), self.edges())) + + let edges = if self.should_merge_ways { + self.do_merge_edges(self.edges()) + } else { + self.edges() + }; + Ok((self.nodes(), edges)) } } @@ -344,3 +415,17 @@ fn require_multiple_tags() { .unwrap(); assert_eq!(1, ways.len()); } + +#[test] +fn merging_edges() { + let (_nodes, edges) = Reader::new() + .read("src/osm4routing/test_data/ways_to_merge.osm.pbf") + .unwrap(); + assert_eq!(2, edges.len()); + + let (_nodes, edges) = Reader::new() + .merge_ways() + .read("src/osm4routing/test_data/ways_to_merge.osm.pbf") + .unwrap(); + assert_eq!(1, edges.len()); +} diff --git a/src/osm4routing/test_data/ways_to_merge.osm.pbf b/src/osm4routing/test_data/ways_to_merge.osm.pbf new file mode 100644 index 0000000000000000000000000000000000000000..5b294cd39a56a17a5ef54b10862557feb53539b0 GIT binary patch literal 343 zcmZQzVBqEA^bhv+NKH&hEs`h^$dW3km=pZyv;H|=zjGdEeXsBuUoZ}SROES9+wbdX ze^1?OLOkd6wJvh6)bZ%mKk0F{`+G@dlE@kTbH3-!YkTWz1sYt~e8Je{MN-H~4Nc7r zm#*vU>t8>u&+tlL)5}-)q_56})B0Z5{dG=UVV}jr;9{v<%D}+D&BYFMgiB&cqC~Pl zx>S6{oX1HIlO7}qbcuI8O`3n}@R>6w&aGN{=IAL7CZ{z%JPr!13#UXlsx*tWJXhKA z}m)Uyy58Kc8ZT|D}=82LAA7YMao-Ypnx8tw`%SBd(G!4ll zR2u^Y(xiNWHVQpXVsQG_bLLCWrz2i^-