From abcd17cec3f4462b7a9e28b1dab67ce6b6b2e1a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristram=20Gr=C3=A4bener?= Date: Wed, 10 Jul 2024 18:00:12 +0200 Subject: [PATCH] Move OpenStreetMap logic to a separate mod and make it available in builder --- src/builder.rs | 72 ++++++++++ src/geometry_from_osm.rs | 283 +++------------------------------------ src/lib.rs | 3 + src/osm_helpers.rs | 202 ++++++++++++++++++++++++++++ 4 files changed, 294 insertions(+), 266 deletions(-) create mode 100644 src/osm_helpers.rs diff --git a/src/builder.rs b/src/builder.rs index 7d37dc7..85737da 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1,6 +1,7 @@ //! Tools to make it easier to build an LRS //! It also avoids the need to manipulate flatbuffer data +use std::collections::HashMap; use std::path::Path; use flatbuffers::{ForwardsUOffset, Vector, WIPOffset}; @@ -9,6 +10,7 @@ use geo::Coord; use crate::curves::{Curve, SphericalLineStringCurve}; use crate::lrs_generated::{self, *}; +use crate::osm_helpers::sort_edges; /// A key-value `HashMap` to add metadata to the objects. pub type Properties = std::collections::HashMap; @@ -264,4 +266,74 @@ impl<'fbb> Builder<'fbb> { self.fbb.finish(lrs, None); self.fbb.finished_data() } + + /// Read the topology from an OpenStreetMap source. + /// + /// It will read [`Node`]s, [`Segment`] and create the [`Traversal`]s + /// + pub fn read_from_osm( + &mut self, + input_file: &str, + lrm_tag: &str, + required: Vec<(String, String)>, + to_reject: Vec<(String, String)>, + ) { + let mut reader = osm4routing::Reader::new().merge_ways().read_tag(lrm_tag); + + for (key, value) in required.iter() { + reader = reader.require(key, value) + } + + for (key, value) in to_reject.iter() { + reader = reader.reject(key, value) + } + + let (nodes, edges) = reader + .read(input_file) + .expect("could not read the osm file"); + + let mut edges_map = HashMap::<_, _>::new(); + let mut traversals = HashMap::>::new(); + let nodes_index: HashMap<_, _> = nodes + .iter() + .map(|n| (n.id, self.add_node(&n.id.0.to_string(), properties!()))) + .collect(); + + let mut edge_index = 0; + for edge in edges.iter() { + if let Some(srv_ref) = edge.tags.get(lrm_tag) { + edges_map.insert(edge.id.clone(), edge_index); + traversals + .entry(srv_ref.clone()) + .or_default() + .push(edge.clone()); + + let start_node_index = nodes_index[&edge.source]; + let end_node_index = nodes_index[&edge.target]; + self.add_segment(srv_ref, &edge.geometry, start_node_index, end_node_index); + edge_index += 1; + } + } + + // Sort the traversals + for (srv_ref, edges) in traversals.into_iter() { + let segments: Vec<_> = sort_edges(edges, &srv_ref) + .into_iter() + .map(|(edge, reversed)| SegmentOfTraversal { + segment_index: edges_map[&edge.id], + reversed, + }) + .collect(); + self.add_traversal(&srv_ref, &segments); + } + + edges.iter().for_each(|e| { + self.add_segment( + &e.id, + &e.geometry, + nodes_index[&e.source], + nodes_index[&e.target], + ); + }); + } } diff --git a/src/geometry_from_osm.rs b/src/geometry_from_osm.rs index 763a205..0f1f9ed 100644 --- a/src/geometry_from_osm.rs +++ b/src/geometry_from_osm.rs @@ -1,27 +1,6 @@ -use std::collections::HashMap; - use clap::Parser; -use liblrs::{ - builder::{Builder, SegmentOfTraversal}, - properties, -}; -use osm4routing::Edge; -fn read_osm(input_file: &str, lrm_tag: &str) -> (Vec, Vec) { - osm4routing::Reader::new() - .merge_ways() - .require("railway", "rail") - .reject("service", "siding") - .reject("service", "spur") - .reject("building", "*") - .reject("area", "yes") - .reject("gauge", "600") - .reject("roller_coaster", "*") - .reject("construction", "*") - .read_tag(lrm_tag) - .read(input_file) - .expect("could not read the osm file") -} +use liblrs::{builder::Builder, properties}; #[derive(Parser, Debug)] #[command(version, about, long_about = None)] @@ -40,261 +19,33 @@ struct Args { lrm_tag: String, } -// When sorting the edges, each candidate is tested to see if they match and if they need to be reversed. -#[derive(PartialEq, Eq, Debug)] -enum Candidate { - Source, - Target, - Impossible, -} - -// If we have a string of edges that ends with `end_edge`, -// the candidate `Edge(s, t)` can be joined at the `end_edge(source, target)`. -// Returns Forward if the `Edge` can be append in the same direction `(target == s)`, -// Backward if it must be reversed `(target == t)` or -// And NotCandidate if it can’t be appended. -// `consider_source` means that the source (or target if false) of the `last_edge` is considered. -fn can_be_appended(candidate: &Edge, last_edge: &Edge, consider_source: bool) -> Candidate { - let last_node = if consider_source { - last_edge.source - } else { - last_edge.target - }; - if candidate.source == last_node { - Candidate::Source - } else if candidate.target == last_node { - Candidate::Target - } else { - Candidate::Impossible - } -} - -fn insert( - mut to_insert: Vec, - position: usize, - reversed: bool, - at_start: bool, - mut sorted: Vec<(Edge, bool)>, -) -> (Vec, Vec<(Edge, bool)>) { - let to_insert_value = (to_insert.remove(position), reversed); - if at_start { - sorted.insert(0, to_insert_value); - } else { - sorted.push(to_insert_value) - } - sort_iteration(to_insert, sorted) -} - -fn sort_iteration( - to_insert: Vec, - sorted: Vec<(Edge, bool)>, -) -> (Vec, Vec<(Edge, bool)>) { - if sorted.is_empty() { - insert(to_insert, 0, false, false, vec![]) - } else { - for i in 0..to_insert.len() { - let (begin_edge, begin_direction) = sorted.first().unwrap(); - let (end_edge, end_direction) = sorted.last().unwrap(); - - let at_begin = can_be_appended(&to_insert[i], begin_edge, !begin_direction); - let at_end = can_be_appended(&to_insert[i], end_edge, *end_direction); - - match (at_begin, at_end) { - (Candidate::Target, _) => return insert(to_insert, i, false, true, sorted), - (Candidate::Source, _) => return insert(to_insert, i, true, true, sorted), - (_, Candidate::Source) => return insert(to_insert, i, false, false, sorted), - (_, Candidate::Target) => return insert(to_insert, i, true, false, sorted), - (Candidate::Impossible, Candidate::Impossible) => continue, - } - } - (to_insert, sorted) - } -} - -fn sort_edges(edges: Vec, traversal_ref: &str) -> Vec<(Edge, bool)> { - let (to_insert, sorted) = sort_iteration(edges, vec![]); - - // Print some stats about edges that could not be matched - if !to_insert.is_empty() { - let ignored = to_insert.len(); - let total = ignored + sorted.len(); - let first = if sorted[0].1 { - sorted[0].0.target - } else { - sorted[0].0.source - }; - - let last_edge = sorted.last().unwrap(); - let last = if last_edge.1 { - last_edge.0.source - } else { - last_edge.0.target - }; - println!("[WARN] on traversal {traversal_ref}, ignoring {ignored} edges out of {total}"); - println!( - " Sorted traversal from osm node: {} to: {}", - first.0, last.0 - ); - } - - sorted -} - /// Example: to generate an LRS from an OpenStreetMap dump /// /// `$ cargo run --release --bin geometry_from_osm -- -i france.osm.pbf -o osm_83000.lrs.bin --lrm-tag=ref:fr:SNCF_Reseau` fn main() { let cli_args = Args::parse(); - let (nodes, edges) = read_osm(&cli_args.input_osm_file, &cli_args.lrm_tag); - let (nodes_len, edges_len) = (nodes.len(), edges.len()); - println!("In OpenStreetMap, we have {nodes_len} nodes and {edges_len} edges."); - let mut edges_map = HashMap::<_, _>::new(); - let mut traversals = HashMap::>::new(); + let required = properties!("railway" => "rail"); + let to_reject = properties!( + "service"=> "siding", + "service"=> "spur", + "building"=> "*", + "area"=> "yes", + "gauge"=> "600", + "roller_coaster"=> "*", + "construction"=> "*" + ); let mut builder = Builder::new(); - - let nodes_index: HashMap<_, _> = nodes - .iter() - .map(|n| (n.id, builder.add_node(&n.id.0.to_string(), properties!()))) - .collect(); - - let mut edge_index = 0; - for edge in edges.iter() { - if let Some(srv_ref) = edge.tags.get(&cli_args.lrm_tag) { - edges_map.insert(edge.id.clone(), edge_index); - traversals - .entry(srv_ref.clone()) - .or_default() - .push(edge.clone()); - - let start_node_index = nodes_index[&edge.source]; - let end_node_index = nodes_index[&edge.target]; - builder.add_segment(srv_ref, &edge.geometry, start_node_index, end_node_index); - edge_index += 1; - } - } - - println!("In the LRS, we have: "); - println!(" {} nodes,", nodes_index.len()); - println!(" {edge_index} segments,"); - println!(" {} traversals.", traversals.len()); - // Sort the traversals - for (srv_ref, edges) in traversals.into_iter() { - let segments: Vec<_> = sort_edges(edges.clone(), &srv_ref) - .into_iter() - .map(|(edge, reversed)| SegmentOfTraversal { - segment_index: edges_map[&edge.id], - reversed, - }) - .collect(); - builder.add_traversal(&srv_ref, &segments); - } - - edges.iter().for_each(|e| { - builder.add_segment( - &e.id, - &e.geometry, - nodes_index[&e.source], - nodes_index[&e.target], - ); - }); + builder.read_from_osm( + &cli_args.input_osm_file, + &cli_args.lrm_tag, + required, + to_reject, + ); builder.save( &cli_args.output_lrs, properties!("source" => "OpenStreetMap", "licence" => "OdBL"), ); } - -#[cfg(test)] -pub mod tests { - use osm4routing::{Edge, NodeId}; - - use crate::{can_be_appended, sort_edges, Candidate}; - - fn edge(source: i64, target: i64) -> Edge { - Edge { - source: NodeId(source), - target: NodeId(target), - ..Default::default() - } - } - - #[test] - fn test_is_candidate() { - // last_edge is constant - assert_eq!( - can_be_appended(&edge(0, 1), &edge(1, 2), true), - Candidate::Target - ); - - assert_eq!( - can_be_appended(&edge(0, 1), &edge(1, 2), false), - Candidate::Impossible - ); - - assert_eq!( - can_be_appended(&edge(1, 0), &edge(1, 2), true), - Candidate::Source - ); - - assert_eq!( - can_be_appended(&edge(1, 0), &edge(1, 2), false), - Candidate::Impossible - ); - - // last_edge in opposite directions - assert_eq!( - can_be_appended(&edge(0, 1), &edge(2, 1), true), - Candidate::Impossible - ); - - assert_eq!( - can_be_appended(&edge(0, 1), &edge(2, 1), false), - Candidate::Target - ); - - assert_eq!( - can_be_appended(&edge(1, 0), &edge(2, 1), true), - Candidate::Impossible - ); - - assert_eq!( - can_be_appended(&edge(1, 0), &edge(2, 1), false), - Candidate::Source - ); - } - - #[test] - fn sort_edges_simple() { - let e = edge(0, 1); - - let sorted = sort_edges(vec![e.clone()], ""); - assert_eq!(sorted[0].0, e); - assert!(!sorted[0].1); - } - - #[test] - fn sort_edges_two_edges_in_order() { - let e1 = edge(0, 1); - let e2 = edge(1, 2); - - let sorted = sort_edges(vec![e1.clone(), e2.clone()], ""); - assert_eq!(sorted[0].0, e1); - assert_eq!(sorted[1].0, e2); - assert!(!sorted[0].1); - assert!(!sorted[1].1); - } - - #[test] - fn sort_edges_two_edges_first_reversed() { - let e1 = edge(1, 0); - let e2 = edge(1, 2); - - let sorted = sort_edges(vec![e1.clone(), e2.clone()], ""); - assert_eq!(sorted[0].0, e1); - assert_eq!(sorted[1].0, e2); - assert!(sorted[0].1); - assert!(!sorted[1].1); - } -} diff --git a/src/lib.rs b/src/lib.rs index 96759e9..541778b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,9 @@ #[rustfmt::skip] mod lrs_generated; +#[deny(missing_docs)] +mod osm_helpers; + #[deny(missing_docs)] pub mod curves; #[deny(missing_docs)] diff --git a/src/osm_helpers.rs b/src/osm_helpers.rs new file mode 100644 index 0000000..ea4dae6 --- /dev/null +++ b/src/osm_helpers.rs @@ -0,0 +1,202 @@ +//! Helper functions to manipulate OpenStreetMap data +//! and extract an ordered topology + +use osm4routing::Edge; + +/// When sorting the edges, each candidate is tested to see if they match and if they need to be reversed. +#[derive(PartialEq, Eq, Debug)] +enum Candidate { + Source, + Target, + Impossible, +} + +/// If we have a string of edges that ends with `end_edge`, +/// the candidate `Edge(s, t)` can be joined at the `end_edge(source, target)`. +/// Returns Forward if the `Edge` can be append in the same direction `(target == s)`, +/// Backward if it must be reversed `(target == t)` or +/// And NotCandidate if it can’t be appended. +/// `consider_source` means that the source (or target if false) of the `last_edge` is considered. +fn can_be_appended(candidate: &Edge, last_edge: &Edge, consider_source: bool) -> Candidate { + let last_node = if consider_source { + last_edge.source + } else { + last_edge.target + }; + if candidate.source == last_node { + Candidate::Source + } else if candidate.target == last_node { + Candidate::Target + } else { + Candidate::Impossible + } +} + +fn insert( + mut to_insert: Vec, + position: usize, + reversed: bool, + at_start: bool, + mut sorted: Vec<(Edge, bool)>, +) -> (Vec, Vec<(Edge, bool)>) { + let to_insert_value = (to_insert.remove(position), reversed); + if at_start { + sorted.insert(0, to_insert_value); + } else { + sorted.push(to_insert_value) + } + sort_iteration(to_insert, sorted) +} + +fn sort_iteration( + to_insert: Vec, + sorted: Vec<(Edge, bool)>, +) -> (Vec, Vec<(Edge, bool)>) { + if sorted.is_empty() { + insert(to_insert, 0, false, false, vec![]) + } else { + for i in 0..to_insert.len() { + let (begin_edge, begin_direction) = sorted.first().unwrap(); + let (end_edge, end_direction) = sorted.last().unwrap(); + + let at_begin = can_be_appended(&to_insert[i], begin_edge, !begin_direction); + let at_end = can_be_appended(&to_insert[i], end_edge, *end_direction); + + match (at_begin, at_end) { + (Candidate::Target, _) => return insert(to_insert, i, false, true, sorted), + (Candidate::Source, _) => return insert(to_insert, i, true, true, sorted), + (_, Candidate::Source) => return insert(to_insert, i, false, false, sorted), + (_, Candidate::Target) => return insert(to_insert, i, true, false, sorted), + (Candidate::Impossible, Candidate::Impossible) => continue, + } + } + (to_insert, sorted) + } +} + +/// Sort edges from OpenStreetMap to build continous traversals. +/// +/// The traversals are identified by a tag that is used on many ways. +/// We try to build the longest continous chain of ways, but the is no guarantee to succeed. +/// The ways might not share nodes or they might represent a tree. +pub fn sort_edges(edges: Vec, traversal_ref: &str) -> Vec<(Edge, bool)> { + let (to_insert, sorted) = sort_iteration(edges, vec![]); + + // Print some stats about edges that could not be matched + if !to_insert.is_empty() { + let ignored = to_insert.len(); + let total = ignored + sorted.len(); + let first = if sorted[0].1 { + sorted[0].0.target + } else { + sorted[0].0.source + }; + + let last_edge = sorted.last().unwrap(); + let last = if last_edge.1 { + last_edge.0.source + } else { + last_edge.0.target + }; + println!("[WARN] on traversal {traversal_ref}, ignoring {ignored} edges out of {total}"); + println!( + " Sorted traversal from osm node: {} to: {}", + first.0, last.0 + ); + } + + sorted +} + +#[cfg(test)] +pub mod tests { + use osm4routing::{Edge, NodeId}; + + use super::*; + + fn edge(source: i64, target: i64) -> Edge { + Edge { + source: NodeId(source), + target: NodeId(target), + ..Default::default() + } + } + + #[test] + fn test_is_candidate() { + // last_edge is constant + assert_eq!( + can_be_appended(&edge(0, 1), &edge(1, 2), true), + Candidate::Target + ); + + assert_eq!( + can_be_appended(&edge(0, 1), &edge(1, 2), false), + Candidate::Impossible + ); + + assert_eq!( + can_be_appended(&edge(1, 0), &edge(1, 2), true), + Candidate::Source + ); + + assert_eq!( + can_be_appended(&edge(1, 0), &edge(1, 2), false), + Candidate::Impossible + ); + + // last_edge in opposite directions + assert_eq!( + can_be_appended(&edge(0, 1), &edge(2, 1), true), + Candidate::Impossible + ); + + assert_eq!( + can_be_appended(&edge(0, 1), &edge(2, 1), false), + Candidate::Target + ); + + assert_eq!( + can_be_appended(&edge(1, 0), &edge(2, 1), true), + Candidate::Impossible + ); + + assert_eq!( + can_be_appended(&edge(1, 0), &edge(2, 1), false), + Candidate::Source + ); + } + + #[test] + fn sort_edges_simple() { + let e = edge(0, 1); + + let sorted = sort_edges(vec![e.clone()], ""); + assert_eq!(sorted[0].0, e); + assert!(!sorted[0].1); + } + + #[test] + fn sort_edges_two_edges_in_order() { + let e1 = edge(0, 1); + let e2 = edge(1, 2); + + let sorted = sort_edges(vec![e1.clone(), e2.clone()], ""); + assert_eq!(sorted[0].0, e1); + assert_eq!(sorted[1].0, e2); + assert!(!sorted[0].1); + assert!(!sorted[1].1); + } + + #[test] + fn sort_edges_two_edges_first_reversed() { + let e1 = edge(1, 0); + let e2 = edge(1, 2); + + let sorted = sort_edges(vec![e1.clone(), e2.clone()], ""); + assert_eq!(sorted[0].0, e1); + assert_eq!(sorted[1].0, e2); + assert!(sorted[0].1); + assert!(!sorted[1].1); + } +}