Implement routability more or less completely

This commit is contained in:
Dennis 2024-07-09 19:08:04 +02:00
parent 4f36d2dce1
commit bc56a1f8e6
No known key found for this signature in database
GPG Key ID: 6937EAEA33A3FA5D
12 changed files with 565 additions and 241 deletions

View File

@ -113,12 +113,12 @@ Feature: Car - Destination only, no passing through
Scenario: Car - Routing around a way that becomes destination only Scenario: Car - Routing around a way that becomes destination only
Given the node map Given the node map
""" """
a---c---b a---c---b
+ \ + \
+ | + |
d | d |
1 | 1 |
\___e \___e
""" """
And the ways And the ways
@ -136,12 +136,12 @@ Feature: Car - Destination only, no passing through
Scenario: Car - Routing through a parking lot tagged access=destination,service Scenario: Car - Routing through a parking lot tagged access=destination,service
Given the node map Given the node map
""" """
a----c++++b+++g------h---i a----c++++b+++g------h---i
| + + + / | + + + /
| + + + / | + + + /
| + + + / | + + + /
| d++++e+f / | d++++e+f /
z--------------y z--------------y
""" """
And the ways And the ways
@ -165,12 +165,12 @@ Feature: Car - Destination only, no passing through
Given a grid size of 20 meters Given a grid size of 20 meters
Given the node map Given the node map
""" """
a---c---b a---c---b
: :
x x
: :
d d
\__e \__e
""" """
And the ways And the ways

View File

@ -0,0 +1,17 @@
#[derive(Debug)]
pub enum Offset {
Absolute(f64),
Percentage(f64),
}
// #[cfg(test)]
// mod tests {
// use crate::extract_number_and_offset;
// #[test]
// fn extract_number_and_offset() {
// let (value, result) = extract_number_and_offset("m", "300 +- 1m");
// assert_eq!(value, 300.);
// assert_eq!(offset, 1);
// }
// }

View File

@ -1,17 +1,24 @@
pub fn approx_equal(a: f32, b: f32, dp: u8) -> bool { use super::comparison::Offset;
let p = 10f32.powi(-(dp as i32));
pub fn approx_equal(a: f64, b: f64, dp: u8) -> bool {
let p = 10f64.powi(-(dp as i32));
(a - b).abs() < p (a - b).abs() < p
} }
pub fn aprox_equal_within_percentage_range(actual: f64, expectation: f64, percentage: f64) -> bool { fn aprox_equal_within_percentage_range(actual: f64, expectation: f64, percentage: f64) -> bool {
assert!(percentage.is_sign_positive() && percentage <= 100.); assert!(percentage.is_sign_positive() && percentage <= 100.);
let factor = 0.01 * percentage as f64; let factor = 0.01 * percentage as f64;
actual >= expectation - (factor * expectation) && actual <= expectation + (factor * expectation) actual >= expectation - (factor * expectation) && actual <= expectation + (factor * expectation)
} }
pub fn approx_equal_within_offset_range(actual: f64, expectation: f64, offset: f64) -> bool { fn approx_equal_within_offset_range(actual: f64, expectation: f64, offset: f64) -> bool {
assert!(offset >= 0., "offset must be positive"); assert!(offset >= 0., "offset must be positive");
actual >= expectation - offset && actual <= expectation + offset actual >= expectation - offset && actual <= expectation + offset
} }
// TODO: test coverage pub fn approximate_within_range(actual: f64, expectation: f64, offset: &Offset) -> bool{
match offset {
Offset::Absolute(a) => approx_equal_within_offset_range(actual, expectation, *a),
Offset::Percentage(p) => aprox_equal_within_percentage_range(actual, expectation, *p),
}
}

View File

@ -3,6 +3,6 @@ use serde::Deserialize;
#[derive(Clone, Copy, Debug, Default, Deserialize)] #[derive(Clone, Copy, Debug, Default, Deserialize)]
pub struct Location { pub struct Location {
// Note: The order is important since we derive Deserialize // Note: The order is important since we derive Deserialize
pub longitude: f32, pub longitude: f64,
pub latitude: f32, pub latitude: f64,
} }

View File

@ -2,6 +2,7 @@
extern crate flatbuffers; extern crate flatbuffers;
pub mod cli_arguments; pub mod cli_arguments;
pub mod comparison;
pub mod dot_writer; pub mod dot_writer;
pub mod f64_utils; pub mod f64_utils;
pub mod file_util; pub mod file_util;

View File

@ -62,8 +62,8 @@ impl NearestResponse {
let hint = wp.hint().expect("hint is missing").to_string(); let hint = wp.hint().expect("hint is missing").to_string();
let location = wp.location().expect("waypoint must have a location"); let location = wp.location().expect("waypoint must have a location");
let location = Location { let location = Location {
latitude: location.latitude(), latitude: location.latitude() as f64,
longitude: location.longitude(), longitude: location.longitude() as f64,
}; };
let nodes = wp.nodes().expect("waypoint mus have nodes"); let nodes = wp.nodes().expect("waypoint mus have nodes");
let nodes = Some(vec![nodes.first(), nodes.second()]); let nodes = Some(vec![nodes.first(), nodes.second()]);

View File

@ -16,9 +16,12 @@ pub struct OSMNode {
} }
impl OSMNode { impl OSMNode {
// pub fn add_tag(&mut self, key: &str, value: &str) { pub fn add_tag(&mut self, key: &str, value: &str) {
// self.tags.insert(key.into(), value.into()); if key.is_empty() || value.is_empty() {
// } return;
}
self.tags.insert(key.into(), value.into());
}
// pub fn set_id_(&mut self, id: u64) { // pub fn set_id_(&mut self, id: u64) {
// self.id = id; // self.id = id;
@ -68,6 +71,13 @@ impl OSMWay {
// self.tags = tags; // self.tags = tags;
// } // }
pub fn add_tag(&mut self, key: &str, value: &str) {
if key.is_empty() || value.is_empty() {
return;
}
self.tags.insert(key.into(), value.into());
}
pub fn to_xml(&self) -> XMLElement { pub fn to_xml(&self) -> XMLElement {
let mut way = XMLElement::new("way"); let mut way = XMLElement::new("way");
way.add_attribute("id", &self.id.to_string()); way.add_attribute("id", &self.id.to_string());

View File

@ -4,7 +4,7 @@ use xml_builder::{XMLBuilder, XMLElement, XMLVersion};
// TODO: better error handling in XML creation // TODO: better error handling in XML creation
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct OSMDb { pub struct OSMDb {
nodes: Vec<(char, OSMNode)>, nodes: Vec<(String, OSMNode)>,
ways: Vec<OSMWay>, ways: Vec<OSMWay>,
// relations: Vec<OSMRelation>, // relations: Vec<OSMRelation>,
} }
@ -12,14 +12,14 @@ pub struct OSMDb {
impl OSMDb { impl OSMDb {
pub fn add_node(&mut self, node: OSMNode) { pub fn add_node(&mut self, node: OSMNode) {
let name = node.tags.get("name").unwrap(); let name = node.tags.get("name").unwrap();
assert!( // assert!(
name.len() == 1, // name.len() == 1,
"name needs to be of length 1, but was \"{name}\"" // "name needs to be of length 1, but was \"{name}\""
); // );
self.nodes.push((name.chars().next().unwrap(), node)); self.nodes.push((name.clone(), node));
} }
pub fn find_node(&self, search_name: char) -> Option<&(char, OSMNode)> { pub fn find_node(&self, search_name: String) -> Option<&(String, OSMNode)> {
// TODO: this is a linear search. // TODO: this is a linear search.
self.nodes.iter().find(|(name, _node)| search_name == *name) self.nodes.iter().find(|(name, _node)| search_name == *name)
} }

View File

@ -0,0 +1,17 @@
use serde::Deserialize;
#[derive(Debug, Default, Deserialize)]
pub struct OSRMError {
pub code: String,
pub message: String,
}
impl OSRMError {
pub fn from_json_reader(reader: impl std::io::Read) -> Self {
let response = match serde_json::from_reader::<_, Self>(reader) {
Ok(response) => response,
Err(e) => panic!("parsing error {e}"),
};
response
}
}

View File

@ -1,5 +1,5 @@
use super::{ use super::{
nearest_response::NearestResponse, osm::OSMNode, osm_db::OSMDb, osrm_error::OSRMError, nearest_response::NearestResponse, osm::{OSMNode, OSMWay}, osm_db::OSMDb, osrm_error::OSRMError,
route_response::RouteResponse, route_response::RouteResponse,
}; };
use crate::{common::local_task::LocalTask, Location}; use crate::{common::local_task::LocalTask, Location};
@ -10,18 +10,18 @@ use reqwest::StatusCode;
use std::{ use std::{
collections::HashMap, collections::HashMap,
fs::{create_dir_all, File}, fs::{create_dir_all, File},
io::{Read, Write}, io::Write,
path::PathBuf, path::PathBuf,
time::Duration, time::Duration,
}; };
// use ureq::Error;
const DEFAULT_ORIGIN: Location = Location { const DEFAULT_ORIGIN: Location = Location {
longitude: 1.0f32, longitude: 1.0f64,
latitude: 1.0f32, latitude: 1.0f64,
}; };
const DEFAULT_GRID_SIZE: f32 = 100.; const DEFAULT_GRID_SIZE: f64 = 100.;
const WAY_SPACING: f32 = 100.; const WAY_SPACING: f64 = 100.;
const DEFAULT_PROFILE: &str = "bicycle";
#[derive(Debug, World)] #[derive(Debug, World)]
pub struct OSRMWorld { pub struct OSRMWorld {
@ -42,13 +42,12 @@ pub struct OSRMWorld {
pub query_options: HashMap<String, String>, pub query_options: HashMap<String, String>,
pub request_string: Option<String>, pub request_string: Option<String>,
pub grid_size: f32, pub grid_size: f64,
pub origin: Location, pub origin: Location,
pub way_spacing: f32, pub way_spacing: f64,
task: LocalTask, task: LocalTask,
client: reqwest::blocking::Client, client: reqwest::blocking::Client,
// agent: ureq::Agent,
} }
impl Default for OSRMWorld { impl Default for OSRMWorld {
@ -59,7 +58,7 @@ impl Default for OSRMWorld {
feature_digest: Default::default(), feature_digest: Default::default(),
osrm_digest: Default::default(), osrm_digest: Default::default(),
osm_id: Default::default(), osm_id: Default::default(),
profile: Default::default(), profile: DEFAULT_PROFILE.into(),
known_osm_nodes: Default::default(), known_osm_nodes: Default::default(),
known_locations: Default::default(), known_locations: Default::default(),
osm_db: Default::default(), osm_db: Default::default(),
@ -69,7 +68,7 @@ impl Default for OSRMWorld {
// default parameters // TODO: check if necessary // default parameters // TODO: check if necessary
("steps".into(), "true".into()), ("steps".into(), "true".into()),
("alternatives".into(), "false".into()), ("alternatives".into(), "false".into()),
("annotations".into(), "true".into()), // ("annotations".into(), "true".into()),
]), ]),
request_string: Default::default(), request_string: Default::default(),
@ -77,10 +76,6 @@ impl Default for OSRMWorld {
origin: DEFAULT_ORIGIN, origin: DEFAULT_ORIGIN,
way_spacing: WAY_SPACING, way_spacing: WAY_SPACING,
task: LocalTask::default(), task: LocalTask::default(),
// agent: ureq::AgentBuilder::new()
// .timeout_read(Duration::from_secs(5))
// .timeout_write(Duration::from_secs(5))
// .build(),
client: reqwest::blocking::Client::builder() client: reqwest::blocking::Client::builder()
.connect_timeout(Duration::from_secs(5)) .connect_timeout(Duration::from_secs(5))
.no_proxy() .no_proxy()
@ -166,6 +161,14 @@ impl OSRMWorld {
self.osm_db.add_node(node); self.osm_db.add_node(node);
} }
pub fn add_osm_way(&mut self, way: OSMWay) {
way.nodes.iter().for_each(|node| {
self.osm_db.add_node(node.clone());
});
self.osm_db.add_way(way);
}
pub fn get_location(&self, name: char) -> Location { pub fn get_location(&self, name: char) -> Location {
*match name { *match name {
// TODO: move lookup to world // TODO: move lookup to world
@ -264,20 +267,6 @@ impl OSRMWorld {
return Err((status.as_u16(), OSRMError::from_json_reader(bytes))); return Err((status.as_u16(), OSRMError::from_json_reader(bytes)));
} }
} }
// match call {
// Ok(response) => {
// let response = match self.request_with_flatbuffers {
// true => NearestResponse::from_flatbuffer(response.into_reader()),
// false => NearestResponse::from_json_reader(response.into_reader()),
// };
// Ok((200u16, response))
// }
// Err(Error::Status(code, response)) => {
// return Err((code, OSRMError::from_json_reader(response.into_reader())));
// }
// Err(e) => panic!("http error: {e}"),
// }
} }
pub fn route( pub fn route(
@ -295,7 +284,7 @@ impl OSRMWorld {
let url = match &self.request_string { let url = match &self.request_string {
None => { None => {
let mut url = format!( let mut url = format!(
"http://localhost:5000/route/v1/{}/{waypoint_string}", "http://127.0.0.1:5000/route/v1/{}/{waypoint_string}",
self.profile, self.profile,
); );
if self.request_with_flatbuffers { if self.request_with_flatbuffers {
@ -314,19 +303,11 @@ impl OSRMWorld {
url url
} }
Some(request_string) => { Some(request_string) => {
let temp = format!("http://localhost:5000/{}", request_string); let temp = format!("http://127.0.0.1:5000/{}", request_string);
// if request_string == "?" {
// panic!("s: {temp}");
// }
temp temp
} }
}; };
// println!("url: {url}"); // println!("url: {url}");
// let request = self.agent.get(&url);
// if url.ends_with("?") {
// // request = request.query("", "");
// }
// let call = request.call();
let response = match self.client.get(url).send() { let response = match self.client.get(url).send() {
Ok(response) => response, Ok(response) => response,
Err(e) => panic!("http error: {e}"), Err(e) => panic!("http error: {e}"),
@ -347,25 +328,5 @@ impl OSRMWorld {
return Err((status.as_u16(), OSRMError::from_json_reader(bytes))); return Err((status.as_u16(), OSRMError::from_json_reader(bytes)));
} }
} }
// match call {
// Ok(response) => {
// let text = std::io::read_to_string(response.into_reader()).unwrap();
// let response = match self.request_with_flatbuffers {
// true => unimplemented!("RouteResponse::from_flatbuffer(body)"),
// false => RouteResponse::from_string(&text),
// };
// Ok((200u16, response))
// }
// Err(Error::Status(code, response)) => {
// let result = Err((code, OSRMError::from_json_reader(response.into_reader())));
// if url.ends_with("?") {
// panic!("{url} {result:?}");
// }
// return result;
// }
// Err(e) => panic!("http error: {e}"),
// }
} }
} }

View File

@ -1,3 +1,4 @@
use std::default; use std::default;
use serde::Deserialize; use serde::Deserialize;
@ -6,11 +7,12 @@ use super::{location::Location, nearest_response::Waypoint};
#[derive(Deserialize, Default, Debug)] #[derive(Deserialize, Default, Debug)]
pub struct Maneuver { pub struct Maneuver {
pub bearing_after: f64, pub bearing_after: u64,
pub bearing_before: f64, pub bearing_before: u64,
pub location: Location, pub location: Location,
pub modifier: Option<String>, // TODO: should be an enum pub modifier: Option<String>, // TODO: should be an enum
pub r#type: String, // TODO: should be an enum pub r#type: String, // TODO: should be an enum
pub exit: Option<u64>
} }
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
@ -29,6 +31,15 @@ impl Default for Geometry {
} }
} }
#[derive(Debug, Default, Clone, Deserialize)]
pub struct Intersection {
pub r#in: Option<u64>,
pub out: Option<u64>,
pub entry: Vec<bool>,
pub bearings: Vec<u64>,
pub location: Location,
}
#[derive(Deserialize, Default, Debug)] #[derive(Deserialize, Default, Debug)]
pub struct Step { pub struct Step {
pub geometry: Geometry, pub geometry: Geometry,
@ -36,9 +47,11 @@ pub struct Step {
pub maneuver: Maneuver, pub maneuver: Maneuver,
pub name: String, pub name: String,
pub pronunciation: Option<String>, pub pronunciation: Option<String>,
pub rotary_name: Option<String>,
pub r#ref: Option<String>, pub r#ref: Option<String>,
pub duration: f64, pub duration: f64,
pub distance: f64, pub distance: f64,
pub intersections: Vec<Intersection>,
} }
// #[derive(Deserialize, Debug)] // #[derive(Deserialize, Debug)]
@ -92,15 +105,3 @@ impl RouteResponse {
response response
} }
} }
// #[cfg(test)]
// mod tests {
// use super::RouteResponse;
// #[test]
// fn parse_geojson() {
// let input = r#"{"code":"Ok","routes":[{"geometry":{"coordinates":[[1.00009,1],[1.000269,1]],"type":"LineString"},"weight":1.9,"duration":1.9,"legs":[{"annotation":{"speed":[10.5],"weight":[1.9],"nodes":[1,2],"duration":[1.9],"distance":[19.92332315]},"summary":"abc","weight":1.9,"duration":1.9,"steps":[{"geometry":{"coordinates":[[1.00009,1],[1.000269,1]],"type":"LineString"},"maneuver":{"location":[1.00009,1],"bearing_after":90,"bearing_before":0,"modifier":"right","type":"depart"},"mode":"driving","name":"abc","intersections":[{"out":0,"entry":[true],"bearings":[90],"location":[1.00009,1]}],"driving_side":"right","weight":1.9,"duration":1.9,"distance":19.9},{"geometry":{"coordinates":[[1.000269,1],[1.000269,1]],"type":"LineString"},"maneuver":{"location":[1.000269,1],"bearing_after":0,"bearing_before":90,"modifier":"right","type":"arrive"},"mode":"driving","name":"abc","intersections":[{"in":0,"entry":[true],"bearings":[270],"location":[1.000269,1]}],"driving_side":"right","weight":0,"duration":0,"distance":0}],"distance":19.9}],"weight_name":"duration","distance":19.9}],"waypoints":[{"name":"abc","hint":"AAAAgAEAAIAKAAAAHgAAAAAAAAAoAAAA6kYgQWyG70EAAAAA6kYgQgoAAAAeAAAAAAAAACgAAAABAACAmkIPAEBCDwCaQg8Ai0EPAAAArwUAAAAA","distance":20.01400211,"location":[1.00009,1]},{"name":"abc","hint":"AAAAgAEAAIAdAAAACwAAAAAAAAAoAAAAbIbvQepGIEEAAAAA6kYgQh0AAAALAAAAAAAAACgAAAABAACATUMPAEBCDwBNQw8Ai0EPAAAArwUAAAAA","distance":20.01400211,"location":[1.000269,1]}]} "#;
// let result = RouteResponse::from_string(&input);
// }
// }

View File

@ -4,18 +4,18 @@ use cheap_ruler::CheapRuler;
use clap::Parser; use clap::Parser;
use common::{ use common::{
cli_arguments::Args, cli_arguments::Args,
comparison::Offset,
dot_writer::DotWriter, dot_writer::DotWriter,
f64_utils::{ f64_utils::{approx_equal, approximate_within_range},
approx_equal, approx_equal_within_offset_range, aprox_equal_within_percentage_range,
},
hash_util::md5_of_osrm_executables, hash_util::md5_of_osrm_executables,
location::Location, location::Location,
osm::OSMWay, osm::{OSMNode, OSMWay},
osrm_world::OSRMWorld, osrm_world::OSRMWorld,
route_response::{self, RouteResponse}, route_response::{self, RouteResponse},
}; };
use core::panic; use core::panic;
use cucumber::{ use cucumber::{
codegen::ParametersProvider,
gherkin::{Step, Table}, gherkin::{Step, Table},
given, then, when, given, then, when,
writer::summarize, writer::summarize,
@ -25,17 +25,16 @@ use futures::{future, FutureExt};
use geo_types::Point; use geo_types::Point;
use log::debug; use log::debug;
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet}, fmt::format, iter::zip, process::ExitCode, result
iter::zip,
}; };
fn offset_origin_by(dx: f32, dy: f32, origin: Location, grid_size: f32) -> Location { fn offset_origin_by(dx: f64, dy: f64, origin: Location, grid_size: f64) -> Location {
let ruler = CheapRuler::new(origin.latitude, cheap_ruler::DistanceUnit::Meters); let ruler = CheapRuler::new(origin.latitude, cheap_ruler::DistanceUnit::Meters);
let loc = ruler.offset( let loc = ruler.offset(
&Point::new(origin.longitude, origin.latitude), &Point::new(origin.longitude, origin.latitude),
dx * grid_size, dx * grid_size,
dy * grid_size, dy * grid_size,
); //TODO: needs to be world's gridSize, not the local one );
Location { Location {
latitude: loc.y(), latitude: loc.y(),
longitude: loc.x(), longitude: loc.x(),
@ -89,8 +88,12 @@ fn set_node_locations(world: &mut OSRMWorld, step: &Step) {
let lon = &row[header_lookup["lon"]]; let lon = &row[header_lookup["lon"]];
let lat = &row[header_lookup["lat"]]; let lat = &row[header_lookup["lat"]];
let location = Location { let location = Location {
latitude: lat.parse::<f32>().expect("lat {lat} needs to be a f64"), latitude: lat
longitude: lon.parse::<f32>().expect("lon {lon} needs to be a f64"), .parse()
.expect("lat {lat} needs to be a floating point number"),
longitude: lon
.parse()
.expect("lon {lon} needs to be a floating point number"),
}; };
let id = match header_lookup.get("id") { let id = match header_lookup.get("id") {
Some(index) => { Some(index) => {
@ -117,22 +120,25 @@ fn set_node_map(world: &mut OSRMWorld, step: &Step) {
.split('\n') .split('\n')
.enumerate() .enumerate()
.for_each(|(row_index, row)| { .for_each(|(row_index, row)| {
let row_index = row_index - 1;
row.chars() row.chars()
.enumerate() .enumerate()
.filter(|(_column_index, charater)| *charater != ' ') .filter(|(_column_index, character)| {
*character >= '0' && *character <= '9'
|| *character >= 'a' && *character <= 'z'
})
.for_each(|(column_index, name)| { .for_each(|(column_index, name)| {
// This ports the logic from previous implementations. // This ports the logic from previous JS/Ruby implementations.
let location = offset_origin_by( let location = offset_origin_by(
column_index as f32 * 0.5, column_index as f64 * 0.5,
-(row_index as f32 - 1.), -(row_index as f64),
world.origin, world.origin,
world.grid_size, world.grid_size,
); );
match name { match name {
'0'...'9' => world.add_location(name, location), '0'...'9' => world.add_location(name, location),
'a'...'z' => world.add_osm_node(name, location, None), 'a'...'z' => world.add_osm_node(name, location, None),
_ => {} // TODO: unreachable!("node name not in [0..9][a..z]: {docstring}"), _ => unreachable!("node name not in [0..9][a..z]: {docstring}"),
// tests contain random characters.
} }
}); });
}); });
@ -147,7 +153,7 @@ fn extra_parameters(world: &mut OSRMWorld, parameters: String) {
} }
#[given(expr = "a grid size of {float} meters")] #[given(expr = "a grid size of {float} meters")]
fn set_grid_size(world: &mut OSRMWorld, meters: f32) { fn set_grid_size(world: &mut OSRMWorld, meters: f64) {
world.grid_size = meters; world.grid_size = meters;
} }
@ -181,7 +187,7 @@ fn set_ways(world: &mut OSRMWorld, step: &Step) {
// TODO: this check is probably not necessary since it is also checked below implicitly // TODO: this check is probably not necessary since it is also checked below implicitly
panic!("referenced unknown node {name} in way {token}"); panic!("referenced unknown node {name} in way {token}");
} }
if let Some((_, node)) = world.osm_db.find_node(name) { if let Some((_, node)) = world.osm_db.find_node(name.to_string()) {
way.add_node(node.clone()); way.add_node(node.clone());
} else { } else {
panic!("node is known, but not found in osm_db"); panic!("node is known, but not found in osm_db");
@ -299,13 +305,7 @@ fn request_nearest(world: &mut OSRMWorld, step: &Step, state: String) {
#[then(expr = "routability should be")] #[then(expr = "routability should be")]
fn routability(world: &mut OSRMWorld, step: &Step) { fn routability(world: &mut OSRMWorld, step: &Step) {
world.write_osm_file(); let tested_headers = HashSet::<_>::from([
world.extract_osm_file();
// TODO: preprocess
let (header, test_cases) = parse_table_from_steps(&step.table.as_ref());
// TODO: rename forw/backw to forw/backw_speed
let supported_headers = HashSet::<_>::from([
"forw", "forw",
"backw", "backw",
"bothw", "bothw",
@ -313,9 +313,67 @@ fn routability(world: &mut OSRMWorld, step: &Step) {
"backw_rate", "backw_rate",
"bothw_rate", "bothw_rate",
]); ]);
if 0 == header let (headers, test_cases) = parse_table_from_steps(&step.table.as_ref());
// add all non-empty, non-test headers as key=value pairs to OSM data
test_cases
.iter() .iter()
.filter(|title| supported_headers.contains(title.as_str())) .enumerate()
.for_each(|(index, test_case)| {
let mut way = OSMWay {
id: world.make_osm_id(),
..Default::default()
};
way.add_tag("highway", "primary");
way.add_tag("name", &format!("w{index}"));
let source = offset_origin_by(
world.way_spacing * index as f64,
0.,
world.origin,
world.grid_size,
);
let mut source_node = OSMNode {
id: world.make_osm_id(),
location: source,
..Default::default()
};
source_node.add_tag("name", &format!("a{index}"));
way.add_node(source_node);
let target = offset_origin_by(
4. + world.way_spacing * index as f64,
0.,
world.origin,
world.grid_size,
);
let mut target_node = OSMNode {
id: world.make_osm_id(),
location: target,
..Default::default()
};
target_node.add_tag("name", &format!("e{index}"));
way.add_node(target_node);
test_case
.iter()
.filter(|(key, _value)| !tested_headers.iter().any(|header| header == key))
.for_each(|(key, value)| {
if key != "#" {
// ignore comments
way.add_tag(key, value);
}
});
world.add_osm_way(way);
});
world.write_osm_file();
world.extract_osm_file();
// TODO: preprocess
// TODO: rename forw/backw to forw/backw_speed (comment from JS implementation)
if 0 == headers
.iter()
.filter(|title| tested_headers.contains(title.as_str()))
.count() .count()
{ {
panic!( panic!(
@ -328,96 +386,234 @@ fn routability(world: &mut OSRMWorld, step: &Step) {
.enumerate() .enumerate()
.for_each(|(index, test_case)| { .for_each(|(index, test_case)| {
let source = offset_origin_by( let source = offset_origin_by(
1. + world.way_spacing * index as f32, 1. + world.way_spacing * index as f64,
0., 0.,
world.origin, world.origin,
world.grid_size, world.grid_size,
); );
let target = offset_origin_by( let target = offset_origin_by(
3. + world.way_spacing * index as f32, 3. + world.way_spacing * index as f64,
0., 0.,
world.origin, world.origin,
world.grid_size, world.grid_size,
); );
let expected_summary = format!("w{index}");
test_case test_case
.iter() .iter()
.filter(|(title, _)| supported_headers.contains(title.as_str())) .filter(|(title, _)| tested_headers.contains(title.as_str()))
.for_each(|(title, expectation)| { .for_each(|(title, expectation)| {
let route_results = vec![world.route(&vec![source, target])
,world.route(&vec![target, source])];
let forward = title.starts_with("forw"); let forward = title.starts_with("forw");
let route_result = match forward { let route_result = match forward {
true => world.route(&vec![source, target]), true => &route_results[0],
false => world.route(&vec![target, source]), false => &route_results[1],
}; };
let (_, response) = route_result match title.as_str() {
.as_ref() "forw" | "backw" => {
.expect("osrm-routed returned an unexpected error"); match expectation.as_str() {
if expectation.is_empty() { "" => {
assert!( assert!(
response.routes.is_empty() route_result.is_err() ||
|| response.routes.first().unwrap().distance == 0., extract_summary_from_route_response(&route_result.as_ref().unwrap().1) != expected_summary,
"no route expected when result column {title} is unset" // || response.routes.first().unwrap().distance == 0.,
); "no route expected when result column {title} is unset"
} else if expectation.contains("km/h") { );
assert!( }
!response.routes.is_empty(), "x" => {
"route expected when result column is set" let (_, response) = route_result
); .as_ref()
.expect("osrm-routed returned an unexpected error");
assert!(
!response.routes.is_empty()
&& response.routes.first().unwrap().distance >= 0.,
"no route expected when result column {title} is set to {expectation}"
);
}
_ if expectation.contains("km/h") => {
assert!(route_result.is_ok());
let (_, response) = route_result.as_ref().unwrap();
assert!(
!response.routes.is_empty(),
"route expected when result column is set"
);
let (expected_speed, offset) = let (expected_speed, offset) =
extract_number_and_offset("km/h", expectation); extract_number_and_offset("km/h", expectation);
let route = response.routes.first().unwrap(); let route = response.routes.first().unwrap();
let actual_speed = route.distance / route.duration * 3.6; let actual_speed = route.distance / route.duration * 3.6;
assert!( assert!(
aprox_equal_within_percentage_range( approximate_within_range(
actual_speed, actual_speed,
expected_speed, expected_speed,
offset &offset
), ),
"{actual_speed} and {expected_speed} differ by more than {offset}" "{actual_speed} and {expected_speed} differ by more than {offset:?}"
); );
} else if title.ends_with("_rate") { }
assert!(!response.routes.is_empty()); _ => {
let expected_rate = expectation let (_, response) = route_result
.parse::<f64>() .as_ref()
.expect("rate needs to be a number"); .expect("osrm-routed returned an unexpected error");
let route = response.routes.first().unwrap(); let mode = extract_mode_string_from_route_response(response);
let actual_rate = route.distance / route.weight; assert_eq!(&mode, expectation, "failed: {test_case:?}");
assert!( }
aprox_equal_within_percentage_range(actual_rate, expected_rate, 1.), }
"{actual_rate} and {expected_rate} differ by more than 1%" }
); "forw_rate" | "backw_rate" => {
} else { assert!(route_result.is_ok());
unimplemented!("{title} = {expectation}"); let (_, response) = route_result.as_ref().unwrap();
assert!(!response.routes.is_empty());
let expected_rate = expectation
.parse::<f64>()
.expect("rate needs to be a number");
let route = response.routes.first().unwrap();
let actual_rate = route.distance / route.weight;
assert!(
approximate_within_range(actual_rate, expected_rate, &Offset::Percentage(1.)),
"{actual_rate} and {expected_rate} differ by more than 1%"
);
}
"bothw" => {
match expectation.as_str() {
"x" => {
for result in &route_results {
let (_, response) = result
.as_ref()
.expect("osrm-routed returned an unexpected error");
assert!(
!response.routes.is_empty()
&& response.routes.first().unwrap().distance >= 0.,
"no forward route when result column {title}={expectation}"
);
}
}
_ if expectation.contains("km/h") => {
let (expected_speed, offset) =
extract_number_and_offset("km/h", expectation);
for result in &route_results {
assert!(result.is_ok());
let (_, response) = result.as_ref().unwrap();
assert!(
!response.routes.is_empty(),
"route expected when result column is set"
);
let route = response.routes.first().unwrap();
let actual_speed = route.distance / route.duration * 3.6;
assert!(
approximate_within_range(
actual_speed,
expected_speed,
&offset
),
"{actual_speed} and {expected_speed} differ by more than {offset:?}"
);
}
}
_ => {
// match expectation against mode
for result in &route_results {
match result {
Ok((_,response)) => {
let mode = extract_mode_string_from_route_response(response);
let summary = extract_summary_from_route_response(response);
if summary != expected_summary {
assert!(expectation.is_empty());
} else {
assert_eq!(&mode, expectation, "failed: {source:?} -> {target:?}");
}
},
Err(_) => {
assert!(expectation.is_empty());
},
}
}
}
}
} }
}); _ => {
unreachable!("{title} = {expectation}");
}
}
});
}); });
// unimplemented!("{test_cases:#?}");
} }
fn extract_number_and_offset(unit: &str, expectation: &str) -> (f64, f64) { fn extract_summary_from_route_response(response: &RouteResponse) -> String {
response
.routes
.first()
.unwrap()
.legs
.first()
.unwrap()
.summary
.clone()
}
fn extract_mode_string_from_route_response(response: &RouteResponse) -> String {
// From JS test suite:
// use the mode of the first step of the route
// for routability table test, we can assume the mode is the same throughout the route,
// since the route is just a single way
assert!(
!response.routes.is_empty(),
"route expected when extracting mode"
);
response
.routes
.first()
.unwrap()
.legs
.first()
.unwrap()
.steps
.first()
.unwrap()
.mode
.clone()
}
fn extract_number_and_offset(unit: &str, expectation: &str) -> (f64, Offset) {
let expectation = expectation.replace(unit, "");
let delimiter = if expectation.contains("+-") {
"+-"
} else if expectation.contains("~") {
"~"
} else {
"unknown"
};
let tokens: Vec<_> = expectation let tokens: Vec<_> = expectation
.split(unit) .split(delimiter)
.map(|token| token.trim()) .map(|token| token.trim())
.filter(|token| !token.is_empty()) .filter(|token| !token.is_empty())
.collect(); .collect();
// println!("{tokens:?}"); // println!("{tokens:?}");
let number = tokens[0] let number = tokens[0]
.parse::<f64>() .parse::<f64>()
.expect("{expectation} needs to define a speed"); .expect(&format!("'{}' needs to denote a parseablespeed", tokens[0]));
let offset = match tokens.len() { let offset = match tokens.len() {
1 => 5., // TODO: the JS fuzzy matcher has a default margin of 5% for absolute comparsions. This is imprecise 1 => 5., // TODO: the JS fuzzy matcher has a default margin of 5% for absolute comparsions. This is imprecise
2 => tokens[1] 2 => tokens[1]
.replace("~", "") .replace("~", "")
.replace("+-", "") .replace("+-", "")
.replace("%", "")
.trim() .trim()
.parse() .parse()
.expect(&format!("{} needs to specify a number", tokens[1])), .expect(&format!("{} needs to specify a number", tokens[1])),
_ => unreachable!("expectations can't be parsed"), _ => unreachable!("expectations can't be parsed"),
}; };
(number, offset) if expectation.ends_with("%") {
return (number, Offset::Percentage(offset));
}
(number, Offset::Absolute(offset))
} }
fn extract_number_vector_and_offset(unit: &str, expectation: &str) -> (Vec<f64>, u8) { fn extract_number_vector_and_offset(unit: &str, expectation: &str) -> (Vec<f64>, Offset) {
let expectation = expectation.replace(",", ""); let expectation = expectation.replace(",", "");
let tokens: Vec<_> = expectation let tokens: Vec<_> = expectation
.split(unit) .split(unit)
@ -446,7 +642,10 @@ fn extract_number_vector_and_offset(unit: &str, expectation: &str) -> (Vec<f64>,
.expect(&format!("{} needs to specify a number", tokens[1])), .expect(&format!("{} needs to specify a number", tokens[1])),
// _ => unreachable!("expectations can't be parsed"), // _ => unreachable!("expectations can't be parsed"),
}; };
(numbers, offset) if expectation.ends_with("%") {
return (numbers, Offset::Percentage(offset.into()));
}
(numbers, Offset::Absolute(offset.into()))
} }
pub enum WaypointsOrLocation { pub enum WaypointsOrLocation {
@ -538,7 +737,7 @@ fn request_route(world: &mut OSRMWorld, step: &Step, state: String) {
.expect("to node name is one char long"), .expect("to node name is one char long"),
); );
vec![from_location, to_location] vec![from_location, to_location]
}, }
WaypointsOrLocation::Undefined => { WaypointsOrLocation::Undefined => {
world.request_string = test_case.get("request").cloned(); world.request_string = test_case.get("request").cloned();
// println!("setting request to: {:?}", world.request_string); // println!("setting request to: {:?}", world.request_string);
@ -554,7 +753,6 @@ fn request_route(world: &mut OSRMWorld, step: &Step, state: String) {
} }
let route_result = world.route(&waypoints); let route_result = world.route(&waypoints);
test_case test_case
.iter() .iter()
.map(|(column_title, expectation)| (column_title.as_str(), expectation.as_str())) .map(|(column_title, expectation)| (column_title.as_str(), expectation.as_str()))
@ -566,13 +764,14 @@ fn request_route(world: &mut OSRMWorld, step: &Step, state: String) {
Err(_) => &RouteResponse::default(), Err(_) => &RouteResponse::default(),
}; };
let route = if expectation.is_empty() { let route = if expectation.is_empty() {
assert!(route_result.is_err());
assert!(response.routes.is_empty()); assert!(response.routes.is_empty());
String::new() String::new()
} else { } else {
response response
.routes .routes
.first() .first()
.expect("no route returned") .expect("no route returned when checking 'route' column")
.legs .legs
.iter() .iter()
.map(|leg| { .map(|leg| {
@ -633,26 +832,32 @@ fn request_route(world: &mut OSRMWorld, step: &Step, state: String) {
let (expected_speed, offset) = extract_number_and_offset("km/h", expectation); let (expected_speed, offset) = extract_number_and_offset("km/h", expectation);
// println!("{actual_speed} == {expected_speed} +- {offset}"); // println!("{actual_speed} == {expected_speed} +- {offset}");
assert!( assert!(
aprox_equal_within_percentage_range(actual_speed, expected_speed, offset), approximate_within_range(actual_speed, expected_speed, &offset),
"actual time {actual_speed} not equal to expected value {expected_speed}" "actual time {actual_speed} not equal to expected value {expected_speed}"
); );
}, },
"modes" => { "modes" => {
let (_, response) = route_result.as_ref().expect("osrm-routed returned an unexpected error"); let actual = match &route_result {
let route = response.routes.first().expect("no route returned"); Ok((_, response)) => {
let actual_modes = route let route = response.routes.first().expect("no route returned");
.legs let actual_modes = route
.iter() .legs
.map(|leg| {
leg.steps
.iter() .iter()
.map(|step| step.mode.clone()) .map(|leg| {
leg.steps
.iter()
.map(|step| step.mode.clone())
.collect::<Vec<String>>()
.join(",")
})
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join(",") .join(",");
}) actual_modes
.collect::<Vec<String>>() },
.join(","); Err(_) => String::new(),
assert_eq!(actual_modes, expectation); };
assert_eq!(actual, expectation);
}, },
"turns" => { "turns" => {
let (_, response) = route_result.as_ref().expect("osrm-routed returned an unexpected error"); let (_, response) = route_result.as_ref().expect("osrm-routed returned an unexpected error");
@ -664,17 +869,43 @@ fn request_route(world: &mut OSRMWorld, step: &Step, state: String) {
leg.steps leg.steps
.iter() .iter()
.map(|step| { .map(|step| {
let prefix = step.maneuver.r#type.clone(); // NOTE: this is port of JS logic as is. Arguably, this should be replace by a simple join over all type/modifier pairs
if prefix == "depart" || prefix == "arrive" { let r#type = step.maneuver.r#type.clone();
// TODO: this reimplements the behavior that depart and arrive are not checked for their modifier let modifier = match &step.maneuver.modifier {
// check if tests shall be adapted, since this is reported by the engine Some(modifier) => modifier.as_str(),
return prefix; _ => "",
}
let suffix = match &step.maneuver.modifier {
Some(modifier) => " ".to_string() + &modifier,
_ => "".into(),
}; };
prefix + &suffix match r#type.as_str() {
"depart" | "arrive" => {
r#type
}
"roundabout" => {
let exit = match step.maneuver.exit {
Some(x) => x,
None => unreachable!("roundabout maneuver must come with an exit number"),
};
format!("roundabout-exit-{}", exit)
},
"rotary" => {
let exit = match step.maneuver.exit {
Some(x) => x,
None => unreachable!("roundabout maneuver must come with an exit number"),
};
if let Some(rotary_name) = &step.rotary_name {
format!("{rotary_name}-exit-{exit}")
} else {
format!("rotary-exit-{exit}")
}
},
"roundabout turn" => {
let exit = match step.maneuver.exit {
Some(x) => x,
None => unreachable!("roundabout maneuver must come with an exit number"),
};
format!("{} {} exit-{}", r#type, modifier, exit)
}
_ => format!("{} {}", r#type, &modifier),
}
}) })
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join(",") .join(",")
@ -689,7 +920,7 @@ fn request_route(world: &mut OSRMWorld, step: &Step, state: String) {
let (expected_time, offset) = extract_number_and_offset("s", expectation); let (expected_time, offset) = extract_number_and_offset("s", expectation);
// println!("{actual_time} == {expected_time} +- {offset}"); // println!("{actual_time} == {expected_time} +- {offset}");
assert!( assert!(
approx_equal_within_offset_range(actual_time, expected_time, offset as f64), approximate_within_range(actual_time, expected_time, &offset),
"actual time {actual_time} not equal to expected value {expected_time}" "actual time {actual_time} not equal to expected value {expected_time}"
); );
}, },
@ -700,10 +931,10 @@ fn request_route(world: &mut OSRMWorld, step: &Step, state: String) {
leg.steps.iter().filter(|step| step.duration > 0.).map(|step| step.duration).collect::<Vec<f64>>() leg.steps.iter().filter(|step| step.duration > 0.).map(|step| step.duration).collect::<Vec<f64>>()
}).flatten().collect(); }).flatten().collect();
let (expected_times, offset) = extract_number_vector_and_offset("s", expectation); let (expected_times, offset) = extract_number_vector_and_offset("s", expectation);
assert_eq!(actual_times.len(), expected_times.len(), "times mismatch: {actual_times:?} != {expected_times:?} +- {offset}"); assert_eq!(actual_times.len(), expected_times.len(), "times mismatch: {actual_times:?} != {expected_times:?} +- {offset:?}");
zip(actual_times, expected_times).for_each(|(actual_time, expected_time)| { zip(actual_times, expected_times).for_each(|(actual_time, expected_time)| {
assert!(approx_equal_within_offset_range(actual_time, expected_time, offset as f64), assert!(approximate_within_range(actual_time, expected_time, &offset),
"actual time {actual_time} not equal to expected value {expected_time}"); "actual time {actual_time} not equal to expected value {expected_time}");
}); });
}, },
@ -713,10 +944,10 @@ fn request_route(world: &mut OSRMWorld, step: &Step, state: String) {
leg.steps.iter().filter(|step| step.distance > 0.).map(|step| step.distance).collect::<Vec<f64>>() leg.steps.iter().filter(|step| step.distance > 0.).map(|step| step.distance).collect::<Vec<f64>>()
}).flatten().collect::<Vec<f64>>(); }).flatten().collect::<Vec<f64>>();
let (expected_distances, offset) = extract_number_vector_and_offset("m", expectation); let (expected_distances, offset) = extract_number_vector_and_offset("m", expectation);
assert_eq!(expected_distances.len(), actual_distances.len(), "distances mismatch {expected_distances:?} != {actual_distances:?} +- {offset}"); assert_eq!(expected_distances.len(), actual_distances.len(), "distances mismatch {expected_distances:?} != {actual_distances:?} +- {offset:?}");
zip(actual_distances, expected_distances).for_each(|(actual_distance, expected_distance)| { zip(actual_distances, expected_distances).for_each(|(actual_distance, expected_distance)| {
assert!(approx_equal_within_offset_range(actual_distance, expected_distance, offset as f64), assert!(approximate_within_range(actual_distance, expected_distance, &offset),
"actual distance {actual_distance} not equal to expected value {expected_distance}"); "actual distance {actual_distance} not equal to expected value {expected_distance}");
}); });
}, },
@ -725,26 +956,32 @@ fn request_route(world: &mut OSRMWorld, step: &Step, state: String) {
let actual_weight = response.routes.first().expect("no route returned").weight; let actual_weight = response.routes.first().expect("no route returned").weight;
let (expected_weight, offset) = extract_number_and_offset("s", expectation); let (expected_weight, offset) = extract_number_and_offset("s", expectation);
assert!( assert!(
approx_equal_within_offset_range( approximate_within_range(
actual_weight, actual_weight,
expected_weight, expected_weight,
offset as f64 &offset
), ),
"actual time {actual_weight} not equal to expected value {expected_weight}" "actual weight {actual_weight} not equal to expected value {expected_weight}"
); );
}, },
"distance" => { "distance" => {
let (_, response) = route_result.as_ref().expect("osrm-routed returned an unexpected error"); match &route_result {
let actual_distance = response.routes.first().expect("no route returned").distance; Ok((_, response)) => {
let (expected_distance, offset) = extract_number_and_offset("m", expectation); let actual_distance = response.routes.first().expect("no route returned").distance;
assert!( let (expected_distance, offset) = extract_number_and_offset("m", expectation);
approx_equal_within_offset_range( assert!(
actual_distance, approximate_within_range(
expected_distance, actual_distance,
offset as f64 expected_distance,
), &offset
"actual time {actual_distance} not equal to expected value {expected_distance}" ),
); "actual distance {actual_distance} not equal to expected value {expected_distance} +- {offset:?}"
);
},
Err(_) => {
assert_eq!("", expectation);
}
};
}, },
"summary" => { "summary" => {
let (_, response) = route_result.as_ref().expect("osrm-routed returned an unexpected error"); let (_, response) = route_result.as_ref().expect("osrm-routed returned an unexpected error");
@ -817,6 +1054,79 @@ fn request_route(world: &mut OSRMWorld, step: &Step, state: String) {
assert_eq!(&actual_message, expected_message, "message does not match {test_case:?}"); assert_eq!(&actual_message, expected_message, "message does not match {test_case:?}");
}, },
"bearing" => {
let reverse_bearing = | bearing: u64 | -> u64 {
assert!(bearing <= 360);
if bearing >= 180 {
return bearing - 180;
}
bearing + 180
};
let actual_bearing = match &route_result {
Ok((_,r)) => {
r.routes.first().expect("no routes found in 'bearing' check").legs.iter().map(|leg| {
leg.steps.iter().map(|step| {
let intersection = step.intersections.first().expect("could not find intersection when checking bearing");
let prefix = if let Some(r#in) = intersection.r#in {
reverse_bearing(intersection.bearings[r#in as usize])
} else {
0
};
let suffix = if let Some(out) = intersection.out {
intersection.bearings[out as usize]
} else {
0
};
format!("{}->{}", prefix, suffix)
}).collect::<Vec<String>>().join(",")
}).collect::<Vec<_>>().join(",")
},
Err(_) => String::new(),
};
let expected_bearing = test_case.get("bearing").expect("test case doesn't have bearing");
assert_eq!(&actual_bearing, expected_bearing, "bearings don't match");
},
"lanes" => {
let actual = match &route_result {
Ok((_, response)) => {
let route = response.routes.first().expect("no route returned");
/* if(i.lanes)
{
return i.lanes.map( l => {
let indications = l.indications.join(';');
return indications + ':' + (l.valid ? 'true' : 'false');
}).join(' ');
}
else
{
return '';
}
}).join(';');
*/
let actual_lanes = route
.legs
.iter()
.map(|leg| {
leg.steps
.iter()
.map(|step| {//step.intersections.iter().map(|i|{
"-".to_string()
})
.collect::<Vec<String>>()
.join(" ")
})
.collect::<Vec<String>>()
.join(";");
actual_lanes
},
Err(_) => String::new(),
};
assert_eq!(actual, expectation);
}
// TODO: more checks need to be implemented // TODO: more checks need to be implemented
_ => { _ => {
let msg = format!("case {case} = {expectation} not implemented"); let msg = format!("case {case} = {expectation} not implemented");
@ -843,9 +1153,9 @@ fn main() {
future::ready(()).boxed() future::ready(()).boxed()
}) })
// .with_writer(DotWriter::default().normalized()) // .with_writer(DotWriter::default().normalized())
// .filter_run("features", |_, _, sc| { // .filter_run("features/", |fe, _, sc| {
.filter_run("features/testbot/oneway_phantom.feature", |_, _, sc| { .filter_run("features/guidance/anticipate-lanes.feature", |fe, _, sc| {
!sc.tags.iter().any(|t| t == "todo") !sc.tags.iter().any(|t| t == "todo") && !fe.tags.iter().any(|t| t == "todo")
}), }),
); );
} }