Skip to content

Commit

Permalink
Allow to contract two graph edges from two osm ways
Browse files Browse the repository at this point in the history
  • Loading branch information
Tristramg committed Jan 26, 2024
1 parent 4196cd5 commit 1d46110
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 5 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "osm4routing"
edition = "2021"
version = "0.5.9"
version = "0.6.0"
authors = ["Tristram Gräbener <tristramg@gmail.com>"]
description = "Convert OpenStreetMap data into routing friendly CSV"
homepage = "https://github.com/Tristramg/osm4routing2"
Expand Down
6 changes: 6 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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")?;
```
10 changes: 9 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion src/osm4routing/categorize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
50 changes: 50 additions & 0 deletions src/osm4routing/models.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::collections::HashMap;
use std::hash::{Hash, Hasher};

use super::categorize::EdgeProperties;
pub use osmpbfreader::objects::{NodeId, WayId};
Expand Down Expand Up @@ -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,
Expand All @@ -57,6 +59,20 @@ pub struct Edge {
pub tags: std::collections::HashMap<String, String>,
}

impl Hash for Edge {
fn hash<H: Hasher>(&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 {
Expand Down Expand Up @@ -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]
Expand Down
89 changes: 87 additions & 2 deletions src/osm4routing/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub struct Reader {
forbidden_tags: HashMap<String, HashSet<String>>,
required_tags: HashMap<String, HashSet<String>>,
tags_to_read: HashSet<String>,
should_merge_ways: bool,
}

impl Reader {
Expand Down Expand Up @@ -58,14 +59,19 @@ 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() {
let node = self
.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 {
Expand Down Expand Up @@ -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<Edge>) -> Vec<Edge> {
let initial_edges_count = edges.len();

// We build an adjacency map for every node that might have exactly two edges
let mut neighbors: HashMap<NodeId, Vec<_>> = 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)| {
Expand Down Expand Up @@ -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))
}
}

Expand Down Expand Up @@ -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());
}
Binary file added src/osm4routing/test_data/ways_to_merge.osm.pbf
Binary file not shown.

0 comments on commit 1d46110

Please sign in to comment.