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
Given the node map
"""
a---c---b
+ \
+ |
d |
1 |
\___e
a---c---b
+ \
+ |
d |
1 |
\___e
"""
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
Given the node map
"""
a----c++++b+++g------h---i
| + + + /
| + + + /
| + + + /
| d++++e+f /
z--------------y
a----c++++b+++g------h---i
| + + + /
| + + + /
| + + + /
| d++++e+f /
z--------------y
"""
And the ways
@ -165,12 +165,12 @@ Feature: Car - Destination only, no passing through
Given a grid size of 20 meters
Given the node map
"""
a---c---b
:
x
:
d
\__e
a---c---b
:
x
:
d
\__e
"""
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 {
let p = 10f32.powi(-(dp as i32));
use super::comparison::Offset;
pub fn approx_equal(a: f64, b: f64, dp: u8) -> bool {
let p = 10f64.powi(-(dp as i32));
(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.);
let factor = 0.01 * percentage as f64;
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");
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)]
pub struct Location {
// Note: The order is important since we derive Deserialize
pub longitude: f32,
pub latitude: f32,
pub longitude: f64,
pub latitude: f64,
}

View File

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

View File

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

View File

@ -16,9 +16,12 @@ pub struct OSMNode {
}
impl OSMNode {
// pub fn add_tag(&mut self, key: &str, value: &str) {
// self.tags.insert(key.into(), value.into());
// }
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 set_id_(&mut self, id: u64) {
// self.id = id;
@ -68,6 +71,13 @@ impl OSMWay {
// 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 {
let mut way = XMLElement::new("way");
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
#[derive(Debug, Default)]
pub struct OSMDb {
nodes: Vec<(char, OSMNode)>,
nodes: Vec<(String, OSMNode)>,
ways: Vec<OSMWay>,
// relations: Vec<OSMRelation>,
}
@ -12,14 +12,14 @@ pub struct OSMDb {
impl OSMDb {
pub fn add_node(&mut self, node: OSMNode) {
let name = node.tags.get("name").unwrap();
assert!(
name.len() == 1,
"name needs to be of length 1, but was \"{name}\""
);
self.nodes.push((name.chars().next().unwrap(), node));
// assert!(
// name.len() == 1,
// "name needs to be of length 1, but was \"{name}\""
// );
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.
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::{
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,
};
use crate::{common::local_task::LocalTask, Location};
@ -10,18 +10,18 @@ use reqwest::StatusCode;
use std::{
collections::HashMap,
fs::{create_dir_all, File},
io::{Read, Write},
io::Write,
path::PathBuf,
time::Duration,
};
// use ureq::Error;
const DEFAULT_ORIGIN: Location = Location {
longitude: 1.0f32,
latitude: 1.0f32,
longitude: 1.0f64,
latitude: 1.0f64,
};
const DEFAULT_GRID_SIZE: f32 = 100.;
const WAY_SPACING: f32 = 100.;
const DEFAULT_GRID_SIZE: f64 = 100.;
const WAY_SPACING: f64 = 100.;
const DEFAULT_PROFILE: &str = "bicycle";
#[derive(Debug, World)]
pub struct OSRMWorld {
@ -42,13 +42,12 @@ pub struct OSRMWorld {
pub query_options: HashMap<String, String>,
pub request_string: Option<String>,
pub grid_size: f32,
pub grid_size: f64,
pub origin: Location,
pub way_spacing: f32,
pub way_spacing: f64,
task: LocalTask,
client: reqwest::blocking::Client,
// agent: ureq::Agent,
}
impl Default for OSRMWorld {
@ -59,7 +58,7 @@ impl Default for OSRMWorld {
feature_digest: Default::default(),
osrm_digest: Default::default(),
osm_id: Default::default(),
profile: Default::default(),
profile: DEFAULT_PROFILE.into(),
known_osm_nodes: Default::default(),
known_locations: Default::default(),
osm_db: Default::default(),
@ -69,7 +68,7 @@ impl Default for OSRMWorld {
// default parameters // TODO: check if necessary
("steps".into(), "true".into()),
("alternatives".into(), "false".into()),
("annotations".into(), "true".into()),
// ("annotations".into(), "true".into()),
]),
request_string: Default::default(),
@ -77,10 +76,6 @@ impl Default for OSRMWorld {
origin: DEFAULT_ORIGIN,
way_spacing: WAY_SPACING,
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()
.connect_timeout(Duration::from_secs(5))
.no_proxy()
@ -166,6 +161,14 @@ impl OSRMWorld {
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 {
*match name {
// TODO: move lookup to world
@ -264,20 +267,6 @@ impl OSRMWorld {
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(
@ -295,7 +284,7 @@ impl OSRMWorld {
let url = match &self.request_string {
None => {
let mut url = format!(
"http://localhost:5000/route/v1/{}/{waypoint_string}",
"http://127.0.0.1:5000/route/v1/{}/{waypoint_string}",
self.profile,
);
if self.request_with_flatbuffers {
@ -314,19 +303,11 @@ impl OSRMWorld {
url
}
Some(request_string) => {
let temp = format!("http://localhost:5000/{}", request_string);
// if request_string == "?" {
// panic!("s: {temp}");
// }
let temp = format!("http://127.0.0.1:5000/{}", request_string);
temp
}
};
// 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() {
Ok(response) => response,
Err(e) => panic!("http error: {e}"),
@ -347,25 +328,5 @@ impl OSRMWorld {
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 serde::Deserialize;
@ -6,11 +7,12 @@ use super::{location::Location, nearest_response::Waypoint};
#[derive(Deserialize, Default, Debug)]
pub struct Maneuver {
pub bearing_after: f64,
pub bearing_before: f64,
pub bearing_after: u64,
pub bearing_before: u64,
pub location: Location,
pub modifier: Option<String>, // TODO: should be an enum
pub r#type: String, // TODO: should be an enum
pub exit: Option<u64>
}
#[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)]
pub struct Step {
pub geometry: Geometry,
@ -36,9 +47,11 @@ pub struct Step {
pub maneuver: Maneuver,
pub name: String,
pub pronunciation: Option<String>,
pub rotary_name: Option<String>,
pub r#ref: Option<String>,
pub duration: f64,
pub distance: f64,
pub intersections: Vec<Intersection>,
}
// #[derive(Deserialize, Debug)]
@ -92,15 +105,3 @@ impl RouteResponse {
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 common::{
cli_arguments::Args,
comparison::Offset,
dot_writer::DotWriter,
f64_utils::{
approx_equal, approx_equal_within_offset_range, aprox_equal_within_percentage_range,
},
f64_utils::{approx_equal, approximate_within_range},
hash_util::md5_of_osrm_executables,
location::Location,
osm::OSMWay,
osm::{OSMNode, OSMWay},
osrm_world::OSRMWorld,
route_response::{self, RouteResponse},
};
use core::panic;
use cucumber::{
codegen::ParametersProvider,
gherkin::{Step, Table},
given, then, when,
writer::summarize,
@ -25,17 +25,16 @@ use futures::{future, FutureExt};
use geo_types::Point;
use log::debug;
use std::{
collections::{HashMap, HashSet},
iter::zip,
collections::{HashMap, HashSet}, fmt::format, iter::zip, process::ExitCode, result
};
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 loc = ruler.offset(
&Point::new(origin.longitude, origin.latitude),
dx * grid_size,
dy * grid_size,
); //TODO: needs to be world's gridSize, not the local one
);
Location {
latitude: loc.y(),
longitude: loc.x(),
@ -89,8 +88,12 @@ fn set_node_locations(world: &mut OSRMWorld, step: &Step) {
let lon = &row[header_lookup["lon"]];
let lat = &row[header_lookup["lat"]];
let location = Location {
latitude: lat.parse::<f32>().expect("lat {lat} needs to be a f64"),
longitude: lon.parse::<f32>().expect("lon {lon} needs to be a f64"),
latitude: lat
.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") {
Some(index) => {
@ -117,22 +120,25 @@ fn set_node_map(world: &mut OSRMWorld, step: &Step) {
.split('\n')
.enumerate()
.for_each(|(row_index, row)| {
let row_index = row_index - 1;
row.chars()
.enumerate()
.filter(|(_column_index, charater)| *charater != ' ')
.filter(|(_column_index, character)| {
*character >= '0' && *character <= '9'
|| *character >= 'a' && *character <= 'z'
})
.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(
column_index as f32 * 0.5,
-(row_index as f32 - 1.),
column_index as f64 * 0.5,
-(row_index as f64),
world.origin,
world.grid_size,
);
match name {
'0'...'9' => world.add_location(name, location),
'a'...'z' => world.add_osm_node(name, location, None),
_ => {} // TODO: unreachable!("node name not in [0..9][a..z]: {docstring}"),
// tests contain random characters.
_ => unreachable!("node name not in [0..9][a..z]: {docstring}"),
}
});
});
@ -147,7 +153,7 @@ fn extra_parameters(world: &mut OSRMWorld, parameters: String) {
}
#[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;
}
@ -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
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());
} else {
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")]
fn routability(world: &mut OSRMWorld, step: &Step) {
world.write_osm_file();
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([
let tested_headers = HashSet::<_>::from([
"forw",
"backw",
"bothw",
@ -313,9 +313,67 @@ fn routability(world: &mut OSRMWorld, step: &Step) {
"backw_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()
.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()
{
panic!(
@ -328,96 +386,234 @@ fn routability(world: &mut OSRMWorld, step: &Step) {
.enumerate()
.for_each(|(index, test_case)| {
let source = offset_origin_by(
1. + world.way_spacing * index as f32,
1. + world.way_spacing * index as f64,
0.,
world.origin,
world.grid_size,
);
let target = offset_origin_by(
3. + world.way_spacing * index as f32,
3. + world.way_spacing * index as f64,
0.,
world.origin,
world.grid_size,
);
let expected_summary = format!("w{index}");
test_case
.iter()
.filter(|(title, _)| supported_headers.contains(title.as_str()))
.filter(|(title, _)| tested_headers.contains(title.as_str()))
.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 route_result = match forward {
true => world.route(&vec![source, target]),
false => world.route(&vec![target, source]),
true => &route_results[0],
false => &route_results[1],
};
let (_, response) = route_result
.as_ref()
.expect("osrm-routed returned an unexpected error");
if expectation.is_empty() {
assert!(
response.routes.is_empty()
|| 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(),
"route expected when result column is set"
);
match title.as_str() {
"forw" | "backw" => {
match expectation.as_str() {
"" => {
assert!(
route_result.is_err() ||
extract_summary_from_route_response(&route_result.as_ref().unwrap().1) != expected_summary,
// || response.routes.first().unwrap().distance == 0.,
"no route expected when result column {title} is unset"
);
}
"x" => {
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) =
extract_number_and_offset("km/h", expectation);
let route = response.routes.first().unwrap();
let actual_speed = route.distance / route.duration * 3.6;
assert!(
aprox_equal_within_percentage_range(
actual_speed,
expected_speed,
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
.parse::<f64>()
.expect("rate needs to be a number");
let route = response.routes.first().unwrap();
let actual_rate = route.distance / route.weight;
assert!(
aprox_equal_within_percentage_range(actual_rate, expected_rate, 1.),
"{actual_rate} and {expected_rate} differ by more than 1%"
);
} else {
unimplemented!("{title} = {expectation}");
let (expected_speed, offset) =
extract_number_and_offset("km/h", expectation);
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:?}"
);
}
_ => {
let (_, response) = route_result
.as_ref()
.expect("osrm-routed returned an unexpected error");
let mode = extract_mode_string_from_route_response(response);
assert_eq!(&mode, expectation, "failed: {test_case:?}");
}
}
}
"forw_rate" | "backw_rate" => {
assert!(route_result.is_ok());
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
.split(unit)
.split(delimiter)
.map(|token| token.trim())
.filter(|token| !token.is_empty())
.collect();
// println!("{tokens:?}");
let number = tokens[0]
.parse::<f64>()
.expect("{expectation} needs to define a speed");
.expect(&format!("'{}' needs to denote a parseablespeed", tokens[0]));
let offset = match tokens.len() {
1 => 5., // TODO: the JS fuzzy matcher has a default margin of 5% for absolute comparsions. This is imprecise
2 => tokens[1]
.replace("~", "")
.replace("+-", "")
.replace("~", "")
.replace("+-", "")
.replace("%", "")
.trim()
.parse()
.expect(&format!("{} needs to specify a number", tokens[1])),
_ => 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 tokens: Vec<_> = expectation
.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])),
// _ => 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 {
@ -538,7 +737,7 @@ fn request_route(world: &mut OSRMWorld, step: &Step, state: String) {
.expect("to node name is one char long"),
);
vec![from_location, to_location]
},
}
WaypointsOrLocation::Undefined => {
world.request_string = test_case.get("request").cloned();
// 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);
test_case
.iter()
.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(),
};
let route = if expectation.is_empty() {
assert!(route_result.is_err());
assert!(response.routes.is_empty());
String::new()
} else {
response
.routes
.first()
.expect("no route returned")
.expect("no route returned when checking 'route' column")
.legs
.iter()
.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);
// println!("{actual_speed} == {expected_speed} +- {offset}");
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}"
);
},
"modes" => {
let (_, response) = route_result.as_ref().expect("osrm-routed returned an unexpected error");
let route = response.routes.first().expect("no route returned");
let actual_modes = route
.legs
.iter()
.map(|leg| {
leg.steps
let actual = match &route_result {
Ok((_, response)) => {
let route = response.routes.first().expect("no route returned");
let actual_modes = route
.legs
.iter()
.map(|step| step.mode.clone())
.map(|leg| {
leg.steps
.iter()
.map(|step| step.mode.clone())
.collect::<Vec<String>>()
.join(",")
})
.collect::<Vec<String>>()
.join(",")
})
.collect::<Vec<String>>()
.join(",");
assert_eq!(actual_modes, expectation);
.join(",");
actual_modes
},
Err(_) => String::new(),
};
assert_eq!(actual, expectation);
},
"turns" => {
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
.iter()
.map(|step| {
let prefix = step.maneuver.r#type.clone();
if prefix == "depart" || prefix == "arrive" {
// TODO: this reimplements the behavior that depart and arrive are not checked for their modifier
// check if tests shall be adapted, since this is reported by the engine
return prefix;
}
let suffix = match &step.maneuver.modifier {
Some(modifier) => " ".to_string() + &modifier,
_ => "".into(),
// NOTE: this is port of JS logic as is. Arguably, this should be replace by a simple join over all type/modifier pairs
let r#type = step.maneuver.r#type.clone();
let modifier = match &step.maneuver.modifier {
Some(modifier) => modifier.as_str(),
_ => "",
};
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>>()
.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);
// println!("{actual_time} == {expected_time} +- {offset}");
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}"
);
},
@ -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>>()
}).flatten().collect();
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)| {
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}");
});
},
@ -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>>()
}).flatten().collect::<Vec<f64>>();
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)| {
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}");
});
},
@ -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 (expected_weight, offset) = extract_number_and_offset("s", expectation);
assert!(
approx_equal_within_offset_range(
approximate_within_range(
actual_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" => {
let (_, response) = route_result.as_ref().expect("osrm-routed returned an unexpected error");
let actual_distance = response.routes.first().expect("no route returned").distance;
let (expected_distance, offset) = extract_number_and_offset("m", expectation);
assert!(
approx_equal_within_offset_range(
actual_distance,
expected_distance,
offset as f64
),
"actual time {actual_distance} not equal to expected value {expected_distance}"
);
match &route_result {
Ok((_, response)) => {
let actual_distance = response.routes.first().expect("no route returned").distance;
let (expected_distance, offset) = extract_number_and_offset("m", expectation);
assert!(
approximate_within_range(
actual_distance,
expected_distance,
&offset
),
"actual distance {actual_distance} not equal to expected value {expected_distance} +- {offset:?}"
);
},
Err(_) => {
assert_eq!("", expectation);
}
};
},
"summary" => {
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:?}");
},
"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
_ => {
let msg = format!("case {case} = {expectation} not implemented");
@ -843,9 +1153,9 @@ fn main() {
future::ready(()).boxed()
})
// .with_writer(DotWriter::default().normalized())
// .filter_run("features", |_, _, sc| {
.filter_run("features/testbot/oneway_phantom.feature", |_, _, sc| {
!sc.tags.iter().any(|t| t == "todo")
// .filter_run("features/", |fe, _, sc| {
.filter_run("features/guidance/anticipate-lanes.feature", |fe, _, sc| {
!sc.tags.iter().any(|t| t == "todo") && !fe.tags.iter().any(|t| t == "todo")
}),
);
}