336 lines
11 KiB
Rust
336 lines
11 KiB
Rust
use super::{
|
|
nearest_response::NearestResponse,
|
|
osm::{OSMNode, OSMWay},
|
|
osm_db::OSMDb,
|
|
osrm_error::OSRMError,
|
|
route_response::RouteResponse,
|
|
};
|
|
use crate::{common::local_task::LocalTask, Location};
|
|
use core::panic;
|
|
use cucumber::World;
|
|
use log::debug;
|
|
use reqwest::StatusCode;
|
|
use std::{
|
|
collections::HashMap,
|
|
fs::{create_dir_all, File},
|
|
io::Write,
|
|
path::PathBuf,
|
|
time::Duration,
|
|
};
|
|
|
|
const DEFAULT_ORIGIN: Location = Location {
|
|
longitude: 1.0f64,
|
|
latitude: 1.0f64,
|
|
};
|
|
const DEFAULT_GRID_SIZE: f64 = 100.;
|
|
const WAY_SPACING: f64 = 100.;
|
|
const DEFAULT_PROFILE: &str = "bicycle";
|
|
|
|
#[derive(Debug, World)]
|
|
pub struct OSRMWorld {
|
|
pub feature_path: Option<PathBuf>,
|
|
pub scenario_id: String,
|
|
pub feature_digest: String,
|
|
pub osrm_digest: String,
|
|
pub osm_id: u64,
|
|
pub profile: String,
|
|
|
|
pub known_osm_nodes: HashMap<char, Location>,
|
|
pub known_locations: HashMap<char, Location>,
|
|
|
|
pub osm_db: OSMDb,
|
|
pub extraction_parameters: Vec<String>,
|
|
|
|
pub request_with_flatbuffers: bool,
|
|
pub query_options: HashMap<String, String>,
|
|
pub request_string: Option<String>,
|
|
|
|
pub grid_size: f64,
|
|
pub origin: Location,
|
|
pub way_spacing: f64,
|
|
|
|
task: LocalTask,
|
|
client: reqwest::blocking::Client,
|
|
}
|
|
|
|
impl Default for OSRMWorld {
|
|
fn default() -> Self {
|
|
Self {
|
|
feature_path: Default::default(),
|
|
scenario_id: Default::default(),
|
|
feature_digest: Default::default(),
|
|
osrm_digest: Default::default(),
|
|
osm_id: Default::default(),
|
|
profile: DEFAULT_PROFILE.into(),
|
|
known_osm_nodes: Default::default(),
|
|
known_locations: Default::default(),
|
|
osm_db: Default::default(),
|
|
extraction_parameters: Default::default(),
|
|
request_with_flatbuffers: Default::default(),
|
|
query_options: HashMap::from([
|
|
// default parameters // TODO: check if necessary
|
|
("steps".into(), "true".into()),
|
|
("alternatives".into(), "false".into()),
|
|
// ("annotations".into(), "true".into()),
|
|
]),
|
|
request_string: Default::default(),
|
|
|
|
grid_size: DEFAULT_GRID_SIZE,
|
|
origin: DEFAULT_ORIGIN,
|
|
way_spacing: WAY_SPACING,
|
|
task: LocalTask::default(),
|
|
client: reqwest::blocking::Client::builder()
|
|
.connect_timeout(Duration::from_secs(5))
|
|
.no_proxy()
|
|
.http1_only()
|
|
.build()
|
|
.unwrap(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl OSRMWorld {
|
|
pub fn feature_cache_path(&self) -> PathBuf {
|
|
let full_path = self.feature_path.clone().unwrap();
|
|
let path = full_path
|
|
.ancestors()
|
|
.find(|p| p.ends_with("features"))
|
|
.expect(".feature files reside in a directory tree with the root name 'features'");
|
|
|
|
let suffix = full_path.strip_prefix(path).unwrap();
|
|
let path = path.parent().unwrap();
|
|
debug!("suffix: {suffix:?}");
|
|
let cache_path = path
|
|
.join("test")
|
|
.join("cache")
|
|
.join(suffix)
|
|
.join(&self.feature_digest);
|
|
|
|
debug!("{cache_path:?}");
|
|
if !cache_path.exists() {
|
|
create_dir_all(&cache_path).expect("cache path could not be created");
|
|
} else {
|
|
debug!("not creating cache dir");
|
|
}
|
|
cache_path
|
|
}
|
|
|
|
pub fn routed_path(&self) -> PathBuf {
|
|
let full_path = self.feature_path.clone().unwrap();
|
|
let path = full_path
|
|
.ancestors()
|
|
.find(|p| p.ends_with("features"))
|
|
.expect(".feature files reside in a directory tree with the root name 'features'");
|
|
let routed_path = path
|
|
.parent()
|
|
.expect("cannot get parent path")
|
|
.join("build")
|
|
.join("osrm-routed");
|
|
assert!(routed_path.exists(), "osrm-routed binary not found");
|
|
routed_path
|
|
}
|
|
|
|
pub fn set_scenario_specific_paths_and_digests(&mut self, path: Option<PathBuf>) {
|
|
self.feature_path.clone_from(&path);
|
|
|
|
let file = File::open(path.clone().unwrap())
|
|
.unwrap_or_else(|_| panic!("filesystem broken? can't open file {:?}", path));
|
|
self.feature_digest = chksum_md5::chksum(file)
|
|
.expect("md5 could not be computed")
|
|
.to_hex_lowercase();
|
|
}
|
|
|
|
pub fn make_osm_id(&mut self) -> u64 {
|
|
// number implicitly starts a 1. This is in line with previous implementations
|
|
self.osm_id += 1;
|
|
self.osm_id
|
|
}
|
|
|
|
pub fn add_osm_node(&mut self, name: char, location: Location, id: Option<u64>) {
|
|
if self.known_osm_nodes.contains_key(&name) {
|
|
panic!("duplicate node: {name}");
|
|
}
|
|
let id = match id {
|
|
Some(id) => id,
|
|
None => self.make_osm_id(),
|
|
};
|
|
let node = OSMNode {
|
|
id,
|
|
location,
|
|
tags: HashMap::from([("name".to_string(), name.to_string())]),
|
|
};
|
|
|
|
self.known_osm_nodes.insert(name, location);
|
|
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
|
|
'0'..='9' => self
|
|
.known_locations
|
|
.get(&name)
|
|
.expect("test case specifies unknown location: {name}"),
|
|
'a'..='z' => self
|
|
.known_osm_nodes
|
|
.get(&name)
|
|
.expect("test case specifies unknown osm node: {name}"),
|
|
_ => unreachable!("nodes have to be name in [0-9][a-z]"),
|
|
}
|
|
}
|
|
|
|
pub fn add_location(&mut self, name: char, location: Location) {
|
|
if self.known_locations.contains_key(&name) {
|
|
panic!("duplicate location: {name}")
|
|
}
|
|
self.known_locations.insert(name, location);
|
|
}
|
|
|
|
pub fn write_osm_file(&self) {
|
|
let osm_file = self
|
|
.feature_cache_path()
|
|
.join(self.scenario_id.clone() + ".osm");
|
|
if !osm_file.exists() {
|
|
debug!("writing to osm file: {osm_file:?}");
|
|
let mut file = File::create(osm_file).expect("could not create OSM file");
|
|
file.write_all(self.osm_db.to_xml().as_bytes())
|
|
.expect("could not write OSM file");
|
|
} else {
|
|
debug!("not writing to OSM file {osm_file:?}");
|
|
}
|
|
}
|
|
|
|
pub fn extract_osm_file(&self) {
|
|
let cache_path = self.artefact_cache_path();
|
|
if cache_path.exists() {
|
|
debug!("{cache_path:?} exists");
|
|
} else {
|
|
unimplemented!("{cache_path:?} does not exist");
|
|
}
|
|
}
|
|
|
|
pub fn artefact_cache_path(&self) -> PathBuf {
|
|
self.feature_cache_path().join(&self.osrm_digest)
|
|
}
|
|
|
|
fn start_routed(&mut self) {
|
|
if self.task.is_ready() {
|
|
// task running already
|
|
return;
|
|
}
|
|
let data_path = self
|
|
.artefact_cache_path()
|
|
.join(self.scenario_id.to_owned() + ".osrm");
|
|
|
|
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");
|
|
assert!(self.task.is_ready());
|
|
}
|
|
|
|
pub fn nearest(
|
|
&mut self,
|
|
query_location: &Location,
|
|
) -> Result<(u16, NearestResponse), (u16, OSRMError)> {
|
|
self.start_routed();
|
|
|
|
let mut url = format!(
|
|
"http://localhost:5000/nearest/v1/{}/{:?},{:?}",
|
|
self.profile, query_location.longitude, query_location.latitude
|
|
);
|
|
if self.request_with_flatbuffers {
|
|
url += ".flatbuffers";
|
|
}
|
|
|
|
let response = match self.client.get(url).send() {
|
|
Ok(response) => response,
|
|
Err(e) => panic!("http error: {e}"),
|
|
};
|
|
let status = response.status();
|
|
let bytes = &response.bytes().unwrap()[..];
|
|
|
|
match status {
|
|
StatusCode::OK => {
|
|
let response = match self.request_with_flatbuffers {
|
|
true => NearestResponse::from_flatbuffer(bytes),
|
|
false => NearestResponse::from_json_reader(bytes),
|
|
};
|
|
return Ok((status.as_u16(), response));
|
|
}
|
|
_ => {
|
|
return Err((status.as_u16(), OSRMError::from_json_reader(bytes)));
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn route(
|
|
&mut self,
|
|
waypoints: &[Location],
|
|
) -> Result<(u16, RouteResponse), (u16, OSRMError)> {
|
|
self.start_routed();
|
|
|
|
let waypoint_string = waypoints
|
|
.iter()
|
|
.map(|location| format!("{:?},{:?}", location.longitude, location.latitude))
|
|
.collect::<Vec<String>>()
|
|
.join(";");
|
|
|
|
let url = match &self.request_string {
|
|
None => {
|
|
let mut url = format!(
|
|
"http://127.0.0.1:5000/route/v1/{}/{waypoint_string}",
|
|
self.profile,
|
|
);
|
|
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;
|
|
}
|
|
url
|
|
}
|
|
Some(request_string) => {
|
|
let temp = format!("http://127.0.0.1:5000/{}", request_string);
|
|
temp
|
|
}
|
|
};
|
|
// println!("url: {url}");
|
|
let response = match self.client.get(url).send() {
|
|
Ok(response) => response,
|
|
Err(e) => panic!("http error: {e}"),
|
|
};
|
|
let status = &response.status();
|
|
|
|
match *status {
|
|
StatusCode::OK => {
|
|
let text = response.text().unwrap();
|
|
let response = match self.request_with_flatbuffers {
|
|
true => unimplemented!("RouteResponse::from_flatbuffer(body)"),
|
|
false => RouteResponse::from_string(&text),
|
|
};
|
|
Ok((status.as_u16(), response))
|
|
}
|
|
_ => {
|
|
let bytes = &response.bytes().unwrap()[..];
|
|
return Err((status.as_u16(), OSRMError::from_json_reader(bytes)));
|
|
}
|
|
}
|
|
}
|
|
}
|