Implement geometry comparison

This commit is contained in:
Dennis 2024-06-28 13:04:04 +02:00
parent 85eb7a9383
commit 772f9ccc21
No known key found for this signature in database
GPG Key ID: 6937EAEA33A3FA5D
6 changed files with 169 additions and 44 deletions

View File

@ -200,7 +200,7 @@ curl 'http://router.project-osrm.org/nearest/v1/driving/13.388860,52.517037?numb
Finds the fastest route between coordinates in the supplied order.
```endpoint
GET /route/v1/{profile}/{coordinates}?alternatives={true|false|number}&steps={true|false}&geometries={polyline|polyline6|geojson}&overview={full|simplified|false}&annotations={true|false}
GET /route/v1/{profile}/{coordinates}?alternatives={true|false|number}&steps={true|false}&geometries={polyline|polyline6|geojson}&overview={full|simplified|false}&annotations={true|false}&continue_straight={default|true|false}
```
In addition to the [general options](#general-options) the following options are supported for this service:

View File

@ -29,8 +29,6 @@ module.exports = function () {
var params = this.paramsToString(parameters);
this.query = baseUri + (params.length ? '/' + params : '');
console.log(this.query);
request(this.query, (err, res, body) => {
if (err && err.code === 'ECONNREFUSED') {
return cb(new Error('*** osrm-routed is not running.'));

View File

@ -24,7 +24,7 @@ impl LocalTask {
// TODO: also check that process is running
self.ready
}
pub fn arg(&mut self, argument: &str) -> &mut Self {
pub fn arg(mut self, argument: &str) -> Self {
self.arguments.push(argument.into());
self
}

View File

@ -36,7 +36,7 @@ pub struct OSRMWorld {
pub extraction_parameters: Vec<String>,
pub request_with_flatbuffers: bool,
pub bearings: Option<String>,
pub query_options: HashMap<String, String>,
pub grid_size: f32,
pub origin: Location,
@ -60,7 +60,13 @@ impl Default for OSRMWorld {
osm_db: Default::default(),
extraction_parameters: Default::default(),
request_with_flatbuffers: Default::default(),
bearings: None,
query_options: HashMap::from([
// default parameters // TODO: check if necessary
("steps".into(), "true".into()),
("alternatives".into(), "false".into()),
("annotations".into(), "true".into()),
]),
grid_size: DEFAULT_GRID_SIZE,
origin: DEFAULT_ORIGIN,
way_spacing: WAY_SPACING,
@ -206,9 +212,7 @@ impl OSRMWorld {
.artefact_cache_path()
.join(self.scenario_id.to_owned() + ".osrm");
// TODO: this should not require a temporary and behave like the API of std::process
self.task = LocalTask::new(self.routed_path().to_string_lossy().into());
self.task
self.task = LocalTask::new(self.routed_path().to_string_lossy().into())
.arg(data_path.to_str().expect("data path unwrappable"));
self.task
.spawn_wait_till_ready("running and waiting for requests");
@ -229,6 +233,19 @@ impl OSRMWorld {
if self.request_with_flatbuffers {
url += ".flatbuffers";
}
if !self.query_options.is_empty() {
let options = self
.query_options
.iter()
.map(|(key, value)| format!("{key}={value}"))
.collect::<Vec<String>>()
.join("&");
url += "?";
url += &options;
}
// panic!("url: {url}");
let call = self.agent.get(&url).call();
let body = match call {
@ -253,15 +270,21 @@ impl OSRMWorld {
.join(";");
let mut url = format!(
"http://localhost:5000/route/v1/{}/{waypoint_string}?steps=true&alternatives=false",
"http://localhost:5000/route/v1/{}/{waypoint_string}",
self.profile,
);
if self.request_with_flatbuffers {
url += ".flatbuffers";
}
if let Some(bearings) = &self.bearings {
url += "&bearings=";
url += bearings;
if !self.query_options.is_empty() {
let options = self
.query_options
.iter()
.map(|(key, value)| format!("{key}={value}"))
.collect::<Vec<String>>()
.join("&");
url += "?";
url += &options;
}
// println!("url: {url}");
let call = self.agent.get(&url).call();

View File

@ -1,3 +1,5 @@
use std::default;
use serde::Deserialize;
use super::{location::Location, nearest_response::Waypoint};
@ -11,9 +13,25 @@ pub struct Maneuver {
pub r#type: String, // TODO: should be an enum
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum Geometry {
A(String),
B {
coordinates: Vec<Location>,
r#type: String,
},
}
impl Default for Geometry {
fn default() -> Self {
Geometry::A("".to_string())
}
}
#[derive(Deserialize, Default, Debug)]
pub struct Step {
pub geometry: String,
pub geometry: Geometry,
pub mode: String,
pub maneuver: Maneuver,
pub name: String,
@ -38,9 +56,9 @@ pub struct Leg {
// pub annotation: Option<Vec<Annotation>>,
}
#[derive(Deserialize, Debug)]
#[derive(Deserialize, Debug, Default)]
pub struct Route {
pub geometry: String,
pub geometry: Geometry,
pub weight: f64,
pub duration: f64,
pub legs: Vec<Leg>,
@ -52,7 +70,7 @@ pub struct Route {
pub struct RouteResponse {
pub code: String,
pub routes: Vec<Route>,
pub waypoints: Vec<Waypoint>,
pub waypoints: Option<Vec<Waypoint>>,
pub data_version: Option<String>,
}
@ -66,7 +84,6 @@ impl RouteResponse {
}
pub fn from_string(input: &str) -> Self {
// println!("{input}");
let response = match serde_json::from_str(input) {
Ok(response) => response,
Err(e) => panic!("parsing error {e} => {input}"),
@ -74,3 +91,15 @@ 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

@ -12,11 +12,14 @@ use common::{
location::Location,
osm::OSMWay,
osrm_world::OSRMWorld,
route_response,
};
use core::panic;
use cucumber::{
gherkin::{Step, Table},
given, then, when, World, WriterExt,
given, then, when,
writer::summarize,
World, WriterExt,
};
use futures::{future, FutureExt};
use geo_types::Point;
@ -48,6 +51,12 @@ fn set_profile(world: &mut OSRMWorld, profile: String) {
world.profile = profile;
}
#[given(expr = "the query options")]
fn set_query_options(world: &mut OSRMWorld, step: &Step) {
let table = parse_option_table(&step.table.as_ref());
world.query_options.extend(table.into_iter());
}
#[given(expr = "the node locations")]
fn set_node_locations(world: &mut OSRMWorld, step: &Step) {
let table = step.table().expect("cannot get table");
@ -189,6 +198,19 @@ fn set_ways(world: &mut OSRMWorld, step: &Step) {
}
}
fn parse_option_table(table: &Option<&Table>) -> HashMap<String, String> {
let table = table.expect("no query table specified");
let result = table
.rows
.iter()
.map(|row| {
assert_eq!(2, row.len());
(row[0].clone(), row[1].clone())
})
.collect();
result
}
fn parse_table_from_steps(table: &Option<&Table>) -> (Vec<String>, Vec<HashMap<String, String>>) {
// parse query data
let table = table.expect("no query table specified");
@ -456,6 +478,15 @@ pub fn get_location_specification(test_case: &HashMap<String, String>) -> Waypoi
// WaypointsOrLocation::Undefined
}
#[given(expr = r"skip waypoints")]
fn skip_waypoints(world: &mut OSRMWorld, step: &Step) {
// TODO: adapt test to use query options
// only used in features/testbot/basic.feature
world
.query_options
.insert("skip_waypoints".into(), "true".into());
}
#[when(regex = r"^I route( with flatbuffers|) I should get$")]
fn request_route(world: &mut OSRMWorld, step: &Step, state: String) {
world.request_with_flatbuffers = state == " with flatbuffers";
@ -500,8 +531,11 @@ fn request_route(world: &mut OSRMWorld, step: &Step, state: String) {
}
};
if let Some(bearing) = test_case.get("bearings").cloned() {
world.bearings = Some(bearing.replace(" ", ";"));
if let Some(bearings) = test_case.get("bearings").cloned() {
// TODO: change test cases to provide proper query options
world
.query_options
.insert("bearings".into(), bearings.replace(" ", ";"));
}
let response = world.route(&waypoints);
@ -510,7 +544,7 @@ fn request_route(world: &mut OSRMWorld, step: &Step, state: String) {
.iter()
.map(|(column_title, expectation)| (column_title.as_str(), expectation.as_str()))
.for_each(|(case, expectation)| match case {
"from" | "to" | "bearings"=> {}, // ignore input columns
"from" | "to" | "bearings" | "waypoints" | "#" => {}, // ignore input and comment columns
"route" => {
let route = if expectation.is_empty() {
assert!(response.routes.is_empty());
@ -607,9 +641,14 @@ fn request_route(world: &mut OSRMWorld, step: &Step, state: String) {
.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,
None => "".into(),
_ => "".into(),
};
prefix + &suffix
})
@ -633,10 +672,10 @@ fn request_route(world: &mut OSRMWorld, step: &Step, state: String) {
// TODO: go over steps
let actual_times : Vec<f64>= response.routes.first().expect("no route returned").legs.iter().map(|leg| {
leg.steps.iter().map(|step| step.duration).collect::<Vec<f64>>()
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);
println!("{actual_times:?} == {expected_times:?} +- {offset}");
// println!("{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)| {
@ -645,29 +684,20 @@ fn request_route(world: &mut OSRMWorld, step: &Step, state: String) {
});
},
"distances" => {
println!("{:?}",response.routes.first().expect("no route returned"));
// TODO: go over steps
let actual_distances : Vec<f64> = response.routes.first().expect("no route returned").legs.iter().map(|leg| leg.distance).collect();
let actual_distances = response.routes.first().expect("no route returned").legs.iter().map(|leg| {
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);
println!("{expected_distances:?} == {actual_distances:?}");
println!("!");
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),
"actual distance {actual_distance} not equal to expected value {expected_distance}");
});
// // println!("{actual_time} == {expected_time} +- {offset}");
// assert!(
// approx_equal_within_offset_range(actual_time, expected_time, offset as f64),
// "actual time {actual_time} not equal to expected value {expected_time}"
// );
},
"weight" => {
let actual_weight = response.routes.first().expect("no route returned").weight;
let (expected_weight, offset) = extract_number_and_offset("s", expectation);
// println!("{actual_weight} == {expected_weight} +- {offset}");
assert!(
approx_equal_within_offset_range(
actual_weight,
@ -689,7 +719,54 @@ fn request_route(world: &mut OSRMWorld, step: &Step, state: String) {
"actual time {actual_distance} not equal to expected value {expected_distance}"
);
},
"waypoints" => {},
"summary" => {
let actual_summary = response.routes.first().expect("no route returned").legs.iter().map(|leg| {
leg.summary.clone()
}).collect::<Vec<String>>().join(",");
assert_eq!(actual_summary,expectation, "summary mismatch");
},
"data_version" => {
let expected_data_version = match test_case.get("data_version") {
Some(s) if !s.is_empty() => Some(s),
_ => None,
};
assert_eq!(
expected_data_version,
response.data_version.as_ref(),
"data_version does not match"
);
},
"waypoints_count" => {
let expected_waypoint_count = match test_case.get("waypoints_count") {
Some(s) if !s.is_empty() => s.parse::<usize>().expect("waypoint_count is a number"),
_ => 0,
};
let actual_waypoint_count = match &response.waypoints {
Some(w) => w.len(),
None => 0,
};
assert_eq!(
expected_waypoint_count,
actual_waypoint_count,
"waypoint_count does not match"
);
},
"geometry" => {
let expected_geometry = test_case.get("geometry").expect("no geometry found");
match &response.routes.first().expect("no route").geometry {
route_response::Geometry::A(actual_geometry) => {
assert_eq!(
expected_geometry,
actual_geometry,
"geometry does not match"
);
},
route_response::Geometry::B { coordinates: _, r#type: _ } => unimplemented!("geojson comparison"),
}
},
// "classes" => {},
// TODO: more checks need to be implemented
_ => {
let msg = format!("case {case} = {expectation} not implemented");
@ -700,8 +777,7 @@ fn request_route(world: &mut OSRMWorld, step: &Step, state: String) {
}
fn main() {
let args = Args::parse();
debug!("arguments: {:?}", args);
debug!("{args:?}");
let digest = md5_of_osrm_executables().digest().to_hex_lowercase();
futures::executor::block_on(
@ -717,8 +793,7 @@ fn main() {
future::ready(()).boxed()
})
// .with_writer(DotWriter::default().normalized())
.filter_run("features/testbot/time.feature", |_, _, sc| {
!sc.tags.iter().any(|t| t == "todo")
}),
// .filter_run("features/testbot/geometry.feature", |_, _, sc| {
.filter_run("features", |_, _, sc| !sc.tags.iter().any(|t| t == "todo")),
);
}