Implement routability more or less completely
This commit is contained in:
parent
4f36d2dce1
commit
bc56a1f8e6
17
tests/common/comparison.rs
Normal file
17
tests/common/comparison.rs
Normal 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);
|
||||||
|
// }
|
||||||
|
// }
|
@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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()]);
|
||||||
|
@ -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());
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
17
tests/common/osrm_error.rs
Normal file
17
tests/common/osrm_error.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
@ -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}"),
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
|
||||||
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
@ -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,36 +386,55 @@ 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],
|
||||||
};
|
};
|
||||||
|
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
|
let (_, response) = route_result
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("osrm-routed returned an unexpected error");
|
.expect("osrm-routed returned an unexpected error");
|
||||||
if expectation.is_empty() {
|
|
||||||
assert!(
|
assert!(
|
||||||
response.routes.is_empty()
|
!response.routes.is_empty()
|
||||||
|| response.routes.first().unwrap().distance == 0.,
|
&& response.routes.first().unwrap().distance >= 0.,
|
||||||
"no route expected when result column {title} is unset"
|
"no route expected when result column {title} is set to {expectation}"
|
||||||
);
|
);
|
||||||
} else if expectation.contains("km/h") {
|
}
|
||||||
|
_ if expectation.contains("km/h") => {
|
||||||
|
assert!(route_result.is_ok());
|
||||||
|
let (_, response) = route_result.as_ref().unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
!response.routes.is_empty(),
|
!response.routes.is_empty(),
|
||||||
"route expected when result column is set"
|
"route expected when result column is set"
|
||||||
@ -368,14 +445,26 @@ fn routability(world: &mut OSRMWorld, step: &Step) {
|
|||||||
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") {
|
}
|
||||||
|
_ => {
|
||||||
|
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());
|
assert!(!response.routes.is_empty());
|
||||||
let expected_rate = expectation
|
let expected_rate = expectation
|
||||||
.parse::<f64>()
|
.parse::<f64>()
|
||||||
@ -383,41 +472,148 @@ fn routability(world: &mut OSRMWorld, step: &Step) {
|
|||||||
let route = response.routes.first().unwrap();
|
let route = response.routes.first().unwrap();
|
||||||
let actual_rate = route.distance / route.weight;
|
let actual_rate = route.distance / route.weight;
|
||||||
assert!(
|
assert!(
|
||||||
aprox_equal_within_percentage_range(actual_rate, expected_rate, 1.),
|
approximate_within_range(actual_rate, expected_rate, &Offset::Percentage(1.)),
|
||||||
"{actual_rate} and {expected_rate} differ by more than 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 {
|
} else {
|
||||||
unimplemented!("{title} = {expectation}");
|
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,12 +832,13 @@ 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 {
|
||||||
|
Ok((_, response)) => {
|
||||||
let route = response.routes.first().expect("no route returned");
|
let route = response.routes.first().expect("no route returned");
|
||||||
let actual_modes = route
|
let actual_modes = route
|
||||||
.legs
|
.legs
|
||||||
@ -652,7 +852,12 @@ fn request_route(world: &mut OSRMWorld, step: &Step, state: String) {
|
|||||||
})
|
})
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.join(",");
|
.join(",");
|
||||||
assert_eq!(actual_modes, expectation);
|
actual_modes
|
||||||
|
},
|
||||||
|
Err(_) => String::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
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,27 +956,33 @@ 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 {
|
||||||
|
Ok((_, response)) => {
|
||||||
let actual_distance = response.routes.first().expect("no route returned").distance;
|
let actual_distance = response.routes.first().expect("no route returned").distance;
|
||||||
let (expected_distance, offset) = extract_number_and_offset("m", expectation);
|
let (expected_distance, offset) = extract_number_and_offset("m", expectation);
|
||||||
assert!(
|
assert!(
|
||||||
approx_equal_within_offset_range(
|
approximate_within_range(
|
||||||
actual_distance,
|
actual_distance,
|
||||||
expected_distance,
|
expected_distance,
|
||||||
offset as f64
|
&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");
|
||||||
let actual_summary = response.routes.first().expect("no route returned").legs.iter().map(|leg| {
|
let actual_summary = response.routes.first().expect("no route returned").legs.iter().map(|leg| {
|
||||||
@ -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")
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user