2024-06-08 11:21:54 -04:00
|
|
|
// extern crate clap;
|
2024-05-30 10:14:58 -04:00
|
|
|
|
2024-05-30 10:39:51 -04:00
|
|
|
mod common;
|
2024-05-30 08:35:52 -04:00
|
|
|
|
|
|
|
use cheap_ruler::CheapRuler;
|
2024-05-30 10:19:03 -04:00
|
|
|
use clap::Parser;
|
2024-06-05 04:31:07 -04:00
|
|
|
use common::{
|
2024-06-05 08:59:19 -04:00
|
|
|
cli_arguments::Args, dot_writer::DotWriter, f64_utils::approx_equal,
|
2024-06-08 11:21:54 -04:00
|
|
|
hash_util::md5_of_osrm_executables, location::Location, nearest_response::NearestResponse,
|
|
|
|
osm::OSMWay, osrm_world::OSRMWorld, task_starter::TaskStarter,
|
2024-06-05 04:31:07 -04:00
|
|
|
};
|
2024-05-30 12:23:03 -04:00
|
|
|
use core::panic;
|
2024-06-05 08:58:41 -04:00
|
|
|
use cucumber::{gherkin::Step, given, when, World, WriterExt};
|
2024-05-30 08:35:52 -04:00
|
|
|
use futures::{future, FutureExt};
|
2024-06-08 11:21:54 -04:00
|
|
|
use geo_types::point;
|
2024-06-05 04:31:07 -04:00
|
|
|
use log::debug;
|
2024-06-05 08:59:19 -04:00
|
|
|
use std::{collections::HashMap, fs::File, io::Write, time::Duration};
|
2024-05-31 15:07:10 -04:00
|
|
|
use ureq::Agent;
|
2024-05-30 08:35:52 -04:00
|
|
|
|
|
|
|
const DEFAULT_ORIGIN: [f64; 2] = [1., 1.]; // TODO: move to world?
|
|
|
|
const DEFAULT_GRID_SIZE: f64 = 100.; // TODO: move to world?
|
|
|
|
|
2024-06-08 11:21:54 -04:00
|
|
|
fn offset_origin_by(dx: f64, dy: f64) -> Location {
|
2024-05-30 08:35:52 -04:00
|
|
|
let ruler = CheapRuler::new(DEFAULT_ORIGIN[1], cheap_ruler::DistanceUnit::Meters);
|
2024-06-08 11:21:54 -04:00
|
|
|
let loc = ruler.offset(
|
2024-05-30 08:35:52 -04:00
|
|
|
&point!(DEFAULT_ORIGIN),
|
|
|
|
dx * DEFAULT_GRID_SIZE,
|
|
|
|
dy * DEFAULT_GRID_SIZE,
|
2024-06-08 11:21:54 -04:00
|
|
|
); //TODO: needs to be world's gridSize, not the local one
|
|
|
|
Location {
|
|
|
|
latitude: loc.y() as f32,
|
|
|
|
longitude: loc.x() as f32,
|
|
|
|
}
|
2024-05-30 08:35:52 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
#[given(expr = "the profile \"{word}\"")]
|
|
|
|
fn set_profile(world: &mut OSRMWorld, profile: String) {
|
2024-06-05 04:31:07 -04:00
|
|
|
debug!(
|
2024-05-30 08:35:52 -04:00
|
|
|
"using profile: {profile} on scenario: {}",
|
|
|
|
world.scenario_id
|
|
|
|
);
|
|
|
|
world.profile = profile;
|
|
|
|
}
|
2024-06-05 04:31:07 -04:00
|
|
|
|
|
|
|
#[given(expr = "the node locations")]
|
|
|
|
fn set_node_locations(world: &mut OSRMWorld, step: &Step) {
|
|
|
|
let table = step.table().expect("cannot get table");
|
|
|
|
let header = table.rows.first().expect("node locations table empty");
|
|
|
|
assert_eq!(header.len(), 3, "header needs to define three columns");
|
|
|
|
assert_eq!(
|
|
|
|
header[0], "node",
|
|
|
|
"first column needs to be 'node' indicating the one-letter name"
|
|
|
|
);
|
|
|
|
// the following lookup allows to define lat lon columns in any order
|
2024-06-05 12:26:39 -04:00
|
|
|
let header_lookup: HashMap<&str, usize> = header
|
2024-06-05 13:19:36 -04:00
|
|
|
.iter()
|
2024-06-05 12:26:39 -04:00
|
|
|
.enumerate()
|
|
|
|
.map(|(index, name)| (name.as_str(), index))
|
|
|
|
.collect();
|
2024-06-05 04:31:07 -04:00
|
|
|
["lat", "lon"].iter().for_each(|dim| {
|
|
|
|
assert!(
|
2024-06-05 12:26:39 -04:00
|
|
|
header_lookup.contains_key(*dim),
|
2024-06-05 04:31:07 -04:00
|
|
|
"table must define a {dim} column"
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2024-06-08 11:21:54 -04:00
|
|
|
table.rows.iter().skip(1).for_each(|row| {
|
2024-06-05 04:31:07 -04:00
|
|
|
assert_eq!(3, row.len());
|
|
|
|
assert_eq!(row[0].len(), 1, "node name not in [0..9][a..z]");
|
|
|
|
let name = &row[0].chars().next().expect("node name cannot be empty"); // the error is unreachable
|
2024-06-05 12:26:39 -04:00
|
|
|
let lon = &row[header_lookup["lon"]];
|
|
|
|
let lat = &row[header_lookup["lat"]];
|
2024-06-08 11:21:54 -04:00
|
|
|
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"),
|
|
|
|
};
|
2024-06-05 04:31:07 -04:00
|
|
|
match name {
|
|
|
|
'0'...'9' => world.add_location(*name, location),
|
|
|
|
'a'...'z' => world.add_osm_node(*name, location, None),
|
|
|
|
_ => unreachable!("node name not in [0..9][a..z]"),
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-05-30 08:35:52 -04:00
|
|
|
#[given(expr = "the node map")]
|
|
|
|
fn set_node_map(world: &mut OSRMWorld, step: &Step) {
|
|
|
|
if let Some(docstring) = step.docstring() {
|
|
|
|
// TODO: refactor into a function
|
|
|
|
docstring
|
|
|
|
.split('\n')
|
|
|
|
.enumerate()
|
|
|
|
.for_each(|(row_index, row)| {
|
|
|
|
row.chars()
|
|
|
|
.enumerate()
|
|
|
|
.filter(|(_column_index, charater)| *charater != ' ')
|
|
|
|
.for_each(|(column_index, name)| {
|
|
|
|
// This ports the logic from previous implementations.
|
|
|
|
let location =
|
|
|
|
offset_origin_by(column_index as f64 * 0.5, -(row_index as f64 - 1.));
|
|
|
|
match name {
|
|
|
|
'0'...'9' => world.add_location(name, location),
|
|
|
|
'a'...'z' => world.add_osm_node(name, location, None),
|
|
|
|
_ => unreachable!("node name not in [0..9][a..z]"),
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
panic!("node map not found");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-05 04:31:07 -04:00
|
|
|
#[given(expr = r#"the extract extra arguments {string}"#)]
|
|
|
|
fn extra_parameters(world: &mut OSRMWorld, parameters: String) {
|
|
|
|
world.extraction_parameters.push(parameters);
|
|
|
|
}
|
|
|
|
|
2024-05-30 08:35:52 -04:00
|
|
|
#[given(regex = "the ways")]
|
|
|
|
fn set_ways(world: &mut OSRMWorld, step: &Step) {
|
2024-06-05 04:31:07 -04:00
|
|
|
// debug!("using profile: {profile}");
|
2024-05-30 08:35:52 -04:00
|
|
|
if let Some(table) = step.table.as_ref() {
|
|
|
|
if table.rows.is_empty() {
|
|
|
|
panic!("empty way table provided")
|
|
|
|
}
|
|
|
|
// store a reference to the headers for convenient lookup
|
2024-06-08 11:21:54 -04:00
|
|
|
let headers = table.rows.first().expect("table has a first row");
|
2024-05-30 08:35:52 -04:00
|
|
|
|
|
|
|
// iterate over the following rows and build ways one by one
|
|
|
|
table.rows.iter().skip(1).for_each(|row| {
|
|
|
|
let mut way = OSMWay {
|
|
|
|
id: world.make_osm_id(),
|
|
|
|
..Default::default()
|
|
|
|
};
|
|
|
|
way.tags.insert("highway".into(), "primary".into()); // default may get overwritten below
|
|
|
|
row.iter().enumerate().for_each(|(column_index, token)| {
|
|
|
|
let header = headers[column_index].as_str();
|
|
|
|
if header == "nodes" {
|
|
|
|
assert!(
|
|
|
|
token.len() >= 2,
|
|
|
|
"ways must be defined by token of at least length two giving"
|
|
|
|
);
|
|
|
|
way.tags.insert("name".into(), token.clone());
|
|
|
|
token.chars().for_each(|name| {
|
2024-05-30 12:23:03 -04:00
|
|
|
if !world.known_osm_nodes.contains_key(&name) {
|
|
|
|
// TODO: this check is probably not necessary since it is also checked below implicitly
|
2024-05-30 08:35:52 -04:00
|
|
|
panic!("referenced unknown node {name} in way {token}");
|
|
|
|
}
|
|
|
|
if let Some((_, node)) = world.osm_db.find_node(name) {
|
|
|
|
way.add_node(node.clone());
|
|
|
|
} else {
|
|
|
|
panic!("node is known, but not found in osm_db");
|
|
|
|
}
|
|
|
|
})
|
|
|
|
} else if !token.is_empty() {
|
|
|
|
way.tags.insert(header.into(), token.clone());
|
|
|
|
}
|
|
|
|
});
|
|
|
|
world.osm_db.add_way(way);
|
|
|
|
});
|
|
|
|
} else {
|
2024-06-05 04:31:07 -04:00
|
|
|
debug!("no table found {step:#?}");
|
2024-05-30 08:35:52 -04:00
|
|
|
}
|
|
|
|
|
2024-06-05 04:31:07 -04:00
|
|
|
// debug!("{}", world.osm_db.to_xml())
|
2024-05-30 08:35:52 -04:00
|
|
|
}
|
|
|
|
|
2024-06-08 11:21:54 -04:00
|
|
|
// #[when("I request nearest I should get")]
|
|
|
|
#[when(regex = r"^I request nearest( with flatbuffers|) I should get$")]
|
|
|
|
fn request_nearest(world: &mut OSRMWorld, step: &Step, state: String) {
|
|
|
|
let request_with_flatbuffers = state == " with flatbuffers";
|
2024-05-30 08:35:52 -04:00
|
|
|
// if .osm file does not exist
|
|
|
|
// write osm file
|
|
|
|
|
2024-06-05 04:31:07 -04:00
|
|
|
// TODO: the OSRMWorld instance should have a function to write the .osm file
|
|
|
|
let osm_file = world
|
|
|
|
.feature_cache_path()
|
|
|
|
.join(world.scenario_id.clone() + ".osm");
|
2024-05-30 08:35:52 -04:00
|
|
|
if !osm_file.exists() {
|
2024-06-05 04:31:07 -04:00
|
|
|
debug!("writing to osm file: {osm_file:?}");
|
2024-05-30 08:35:52 -04:00
|
|
|
let mut file = File::create(osm_file).expect("could not create OSM file");
|
|
|
|
file.write_all(world.osm_db.to_xml().as_bytes())
|
|
|
|
.expect("could not write OSM file");
|
|
|
|
} else {
|
2024-06-05 04:31:07 -04:00
|
|
|
debug!("not writing to OSM file {osm_file:?}");
|
2024-05-30 08:35:52 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// if extracted file does not exist
|
2024-06-05 04:31:07 -04:00
|
|
|
let cache_path = world.feature_cache_path().join(&world.osrm_digest);
|
2024-05-30 08:46:45 -04:00
|
|
|
if cache_path.exists() {
|
2024-06-05 04:31:07 -04:00
|
|
|
debug!("{cache_path:?} exists");
|
2024-05-30 08:46:45 -04:00
|
|
|
} else {
|
2024-06-05 04:31:07 -04:00
|
|
|
debug!("{cache_path:?} does not exist");
|
2024-05-30 08:46:45 -04:00
|
|
|
}
|
2024-05-30 12:23:03 -04:00
|
|
|
|
|
|
|
// parse query data
|
2024-06-05 12:26:39 -04:00
|
|
|
let table = &step.table.as_ref().expect("no query table specified");
|
|
|
|
// the following lookup allows to define lat lon columns in any order
|
|
|
|
let header = table.rows.first().expect("node locations table empty");
|
|
|
|
// TODO: move to common functionality
|
|
|
|
let test_cases: Vec<_> = table
|
2024-05-30 12:32:06 -04:00
|
|
|
.rows
|
|
|
|
.iter()
|
|
|
|
.skip(1)
|
|
|
|
.map(|row| {
|
2024-06-05 12:26:39 -04:00
|
|
|
let row_map: HashMap<String, String> = row
|
|
|
|
.iter()
|
|
|
|
.enumerate()
|
|
|
|
.map(|(column_index, value)| {
|
|
|
|
let key = header[column_index].clone();
|
|
|
|
(key, value.clone())
|
|
|
|
})
|
|
|
|
.collect();
|
|
|
|
row_map
|
2024-05-30 12:32:06 -04:00
|
|
|
})
|
|
|
|
.collect();
|
2024-05-30 12:23:03 -04:00
|
|
|
|
|
|
|
let data_path = cache_path.join(world.scenario_id.to_owned() + ".osrm");
|
2024-05-31 15:07:10 -04:00
|
|
|
|
2024-06-02 10:34:05 -04:00
|
|
|
// TODO: this should not require a temporary and behave like the API of std::process
|
2024-06-08 11:21:54 -04:00
|
|
|
let mut task = TaskStarter::new(world.routed_path().to_str().expect("task can be started"));
|
|
|
|
task.arg(data_path.to_str().expect("data path unwrappable"));
|
2024-06-02 10:34:05 -04:00
|
|
|
task.spawn_wait_till_ready("running and waiting for requests");
|
|
|
|
assert!(task.is_ready());
|
|
|
|
|
2024-05-31 15:11:00 -04:00
|
|
|
// TODO: move to generic http request handling struct
|
2024-05-30 12:59:00 -04:00
|
|
|
let agent: Agent = ureq::AgentBuilder::new()
|
2024-05-31 15:07:10 -04:00
|
|
|
.timeout_read(Duration::from_secs(5))
|
|
|
|
.timeout_write(Duration::from_secs(5))
|
|
|
|
.build();
|
2024-05-30 12:59:00 -04:00
|
|
|
|
2024-05-30 12:32:06 -04:00
|
|
|
// parse and run test cases
|
2024-06-05 12:26:39 -04:00
|
|
|
for test_case in test_cases {
|
|
|
|
let query_location = world.get_location(
|
|
|
|
test_case
|
|
|
|
.get("in")
|
|
|
|
.expect("node name is one char long")
|
|
|
|
.chars()
|
|
|
|
.next()
|
|
|
|
.expect("node name is one char long"),
|
|
|
|
);
|
|
|
|
let expected_location = world.get_location(
|
|
|
|
test_case
|
|
|
|
.get("out")
|
|
|
|
.expect("node name is one char long")
|
|
|
|
.chars()
|
|
|
|
.next()
|
|
|
|
.expect("node name is one char long"),
|
|
|
|
);
|
2024-05-30 12:23:03 -04:00
|
|
|
|
2024-06-05 04:31:07 -04:00
|
|
|
// debug!("{query_location:?} => {expected_location:?}");
|
2024-05-30 12:32:06 -04:00
|
|
|
// run queries
|
2024-06-08 11:21:54 -04:00
|
|
|
let mut url = format!(
|
|
|
|
"http://localhost:5000/nearest/v1/{}/{:?},{:?}",
|
|
|
|
world.profile, query_location.longitude, query_location.latitude
|
2024-05-31 15:07:10 -04:00
|
|
|
);
|
2024-06-08 11:21:54 -04:00
|
|
|
if request_with_flatbuffers {
|
|
|
|
url += ".flatbuffers";
|
|
|
|
}
|
2024-05-31 15:07:10 -04:00
|
|
|
let call = agent.get(&url).call();
|
|
|
|
|
|
|
|
let body = match call {
|
2024-06-08 11:21:54 -04:00
|
|
|
Ok(response) => response.into_reader(),
|
2024-05-31 15:07:10 -04:00
|
|
|
Err(e) => panic!("http error: {e}"),
|
|
|
|
};
|
|
|
|
|
2024-06-08 11:21:54 -04:00
|
|
|
let response = match request_with_flatbuffers {
|
|
|
|
true => NearestResponse::from_flatbuffer(body),
|
|
|
|
false => NearestResponse::from_json_reader(body),
|
2024-05-31 15:07:10 -04:00
|
|
|
};
|
|
|
|
|
2024-06-05 12:26:39 -04:00
|
|
|
if test_case.contains_key("out") {
|
|
|
|
// check that result node is (approximately) equivalent
|
|
|
|
let result_location = response.waypoints[0].location();
|
2024-06-08 11:21:54 -04:00
|
|
|
assert!(approx_equal(
|
|
|
|
result_location.longitude,
|
|
|
|
expected_location.longitude,
|
|
|
|
5
|
|
|
|
));
|
|
|
|
assert!(approx_equal(
|
|
|
|
result_location.latitude,
|
|
|
|
expected_location.latitude,
|
|
|
|
5
|
|
|
|
));
|
2024-06-05 12:26:39 -04:00
|
|
|
}
|
|
|
|
if test_case.contains_key("data_version") {
|
2024-06-08 11:21:54 -04:00
|
|
|
assert_eq!(
|
|
|
|
test_case.get("data_version"),
|
|
|
|
response.data_version.as_ref()
|
|
|
|
);
|
2024-06-05 12:26:39 -04:00
|
|
|
}
|
2024-05-30 12:32:06 -04:00
|
|
|
}
|
2024-05-30 08:35:52 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
fn main() {
|
2024-05-30 10:14:58 -04:00
|
|
|
let args = Args::parse();
|
2024-06-05 04:31:07 -04:00
|
|
|
debug!("arguments: {:?}", args);
|
2024-05-30 08:35:52 -04:00
|
|
|
|
|
|
|
futures::executor::block_on(
|
|
|
|
OSRMWorld::cucumber()
|
2024-05-31 03:43:46 -04:00
|
|
|
.max_concurrent_scenarios(1)
|
2024-05-30 12:23:03 -04:00
|
|
|
.before(move |feature, _rule, scenario, world| {
|
2024-06-02 09:17:12 -04:00
|
|
|
world.scenario_id = common::scenario_id::scenario_id(scenario);
|
2024-05-30 12:23:03 -04:00
|
|
|
world.set_scenario_specific_paths_and_digests(feature.path.clone());
|
2024-06-05 04:31:07 -04:00
|
|
|
world.osrm_digest = md5_of_osrm_executables().digest().to_hex_lowercase();
|
2024-05-30 08:35:52 -04:00
|
|
|
|
2024-06-05 04:31:07 -04:00
|
|
|
// TODO: clean up cache if needed? Or do in scenarios?
|
2024-05-30 08:35:52 -04:00
|
|
|
|
|
|
|
future::ready(()).boxed()
|
|
|
|
})
|
2024-06-06 07:57:19 -04:00
|
|
|
.with_writer(DotWriter::default().normalized())
|
|
|
|
.run("features/nearest/"),
|
2024-05-30 08:35:52 -04:00
|
|
|
);
|
|
|
|
}
|