Skip to content

Commit

Permalink
Move OpenStreetMap logic to a separate mod and make it available in b…
Browse files Browse the repository at this point in the history
…uilder
  • Loading branch information
Tristramg committed Jul 11, 2024
1 parent 238c4fc commit abcd17c
Show file tree
Hide file tree
Showing 4 changed files with 294 additions and 266 deletions.
72 changes: 72 additions & 0 deletions src/builder.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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<String, String>;
Expand Down Expand Up @@ -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::<String, Vec<_>>::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],
);
});
}
}
283 changes: 17 additions & 266 deletions src/geometry_from_osm.rs
Original file line number Diff line number Diff line change
@@ -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<osm4routing::Node>, Vec<osm4routing::Edge>) {
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)]
Expand All @@ -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<Edge>,
position: usize,
reversed: bool,
at_start: bool,
mut sorted: Vec<(Edge, bool)>,
) -> (Vec<Edge>, 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<Edge>,
sorted: Vec<(Edge, bool)>,
) -> (Vec<Edge>, 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<Edge>, 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::<String, Vec<_>>::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);
}
}
Loading

0 comments on commit abcd17c

Please sign in to comment.