added current code

This commit is contained in:
Johannes Erwerle 2022-09-13 17:59:30 +02:00
parent 557558acc6
commit 41e5e9b032
39 changed files with 2431 additions and 215 deletions

160
src/alt.rs Normal file
View file

@ -0,0 +1,160 @@
use crate::gridgraph::{EdgeCost, GraphNode, GridGraph, NodeId};
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::collections::BinaryHeap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Landmark {
pub node: GraphNode,
pub distances: Vec<EdgeCost>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct LandmarkSet {
pub landmarks: Vec<Landmark>,
pub best_size: usize,
best_landmarks: Vec<usize>,
}
impl Landmark {
pub fn generate(node: GraphNode, graph: &GridGraph) -> Landmark {
let mut landmark = Landmark {
node,
distances: vec![EdgeCost::MAX; graph.nodes.len()],
};
landmark.node = node;
#[derive(Eq)]
struct DijkstraElement {
index: u32,
cost: EdgeCost,
}
impl Ord for DijkstraElement {
// inverted cmp function, such that the Max-Heap provided by Rust
// can be used as a Min-Heap
fn cmp(&self, other: &Self) -> Ordering {
other.cost.cmp(&self.cost)
}
}
impl PartialOrd for DijkstraElement {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for DijkstraElement {
fn eq(&self, other: &Self) -> bool {
self.cost == other.cost
}
}
let mut heap = BinaryHeap::new();
heap.push(DijkstraElement {
cost: 0,
index: landmark.node.index,
});
let mut counter = 0;
while let Some(DijkstraElement { index, cost }) = heap.pop() {
// the heap does not support "update" operations, so we
// insert elements again and if they come out of the heap but have
// been processed earlier we simply skip them.
if landmark.distances[index as usize] <= cost {
continue;
};
counter += 1;
if counter % 1000 == 0 {
println!("Finished {} nodes", counter);
}
landmark.distances[index as usize] = cost;
let edge_start = graph.edge_offsets[index as usize] as usize;
let edge_end = graph.edge_offsets[(index + 1) as usize] as usize;
for edge in graph.edges[edge_start..edge_end].iter() {
let new_cost = cost + edge.cost;
if new_cost < landmark.distances[edge.neighbor as usize] {
//println!("adding new element to heap");
heap.push(DijkstraElement {
index: edge.neighbor,
cost: new_cost,
});
}
}
}
// now the shortest paths to all reachable nodes is calculated.
landmark
}
/// calculates the lower-bounding distance estimate between the 2 nodes
/// via the landmark.
/// If one or more of the nodes are not reachable from the landmark
/// an estimate of `0` is returned.
pub fn estimate(&self, from: NodeId, to: NodeId) -> EdgeCost {
let l_to = self.distances[to];
let l_from = self.distances[from];
if l_to == EdgeCost::MAX {
0
} else {
l_to.saturating_sub(l_from)
}
}
}
impl LandmarkSet {
pub fn random_set(size: usize, best_size: usize, graph: &GridGraph) -> Self {
let mut set = LandmarkSet::default();
set.best_size = best_size;
let nodes = graph.get_random_nodes(size);
for node in nodes.into_iter() {
let landmark = Landmark::generate(*node, graph);
set.landmarks.push(landmark)
}
set
}
pub fn select_best(&mut self, from: NodeId, to: NodeId) {
let mut results = vec![];
for (index, landmark) in self.landmarks.iter().enumerate() {
results.push((index, landmark.estimate(from, to)));
}
results.sort_by_key(|k| k.1);
results.reverse();
self.best_landmarks.clear();
for result in results[..self.best_size].iter() {
self.best_landmarks.push(result.0);
}
}
pub fn estimate(&self, from: NodeId, to: NodeId) -> EdgeCost {
let mut distance = 0;
for index in &self.best_landmarks {
distance = distance.max(self.landmarks[*index].estimate(from, to));
};
if distance == 0 {
distance = EdgeCost::MAX;
}
distance
}
}

137
src/astar.rs Normal file
View file

@ -0,0 +1,137 @@
use crate::gridgraph::{EdgeCost, GraphNode, GridGraph, Route};
use crate::utils::EARTH_RADIUS;
use std::cmp::Ordering;
use std::collections::BinaryHeap;
pub struct AStar {
pub graph: GridGraph,
}
#[derive(Eq)]
struct HeapElement {
index: u32,
cost: EdgeCost, // the cost so far plus the estimated cost until we reach the
// destination
path_cost: EdgeCost, // the cost to reach this node from the start node
ancestor: Option<u32>,
}
impl Ord for HeapElement {
// inverted cmp function, such that the Max-Heap provided by Rust
// can be used as a Min-Heap
fn cmp(&self, other: &Self) -> Ordering {
other.cost.cmp(&self.cost)
}
}
impl PartialOrd for HeapElement {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for HeapElement {
fn eq(&self, other: &Self) -> bool {
self.cost == other.cost
}
}
pub fn estimate_haversine(node: &GraphNode, destination: &GraphNode) -> EdgeCost {
// simple haversine distance
(node.position.distance_to(&destination.position) * EARTH_RADIUS) as EdgeCost
// let lat_dist_a = (node.position.lat - destination.position.lat).abs();
// let lat_dist_b = (destination.position.lat - node.position.lat).abs();
// (lat_dist_a.min(lat_dist_b) * EARTH_RADIUS) as EdgeCost
}
pub fn estimate_latitude(node: &GraphNode, destination: &GraphNode) -> EdgeCost {
let lat_dist_a = (node.position.lat - destination.position.lat).abs();
let lat_dist_b = (destination.position.lat - node.position.lat).abs();
(lat_dist_a.min(lat_dist_b) * EARTH_RADIUS) as EdgeCost
}
impl AStar {
pub fn shortest_path<F>(&self, start: &GraphNode, end: &GraphNode, estimate: F) -> Option<Route>
where F: Fn(&GraphNode, &GraphNode) -> EdgeCost {
let mut heap = BinaryHeap::new();
heap.push(HeapElement {
cost: estimate(start, end),
path_cost: 0,
index: start.index,
ancestor: None,
});
let mut distance = vec![EdgeCost::MAX; self.graph.nodes.len()];
let mut ancestor: Vec<Option<u32>> = vec![None; self.graph.nodes.len()];
let mut popcount = 0;
while let Some(HeapElement {
index,
cost, // the cost value, no longer needed, because it is only important for the
// distance estimate
path_cost,
ancestor: prev,
}) = heap.pop()
{
// the heap does not support "update" operations, so we
// insert elements again and if they come out of the heap but have
// been processed earlier (which implies a shorter distance)
// we simply skip them.
if distance[index as usize] <= path_cost {
continue;
};
popcount += 1;
distance[index as usize] = path_cost;
ancestor[index as usize] = prev;
if index == end.index {
break;
}
for edge in self.graph.get_edges(index as usize).iter() {
let new_cost = path_cost + edge.cost;
if new_cost < distance[edge.neighbor as usize] {
//println!("adding new element to heap");
heap.push(HeapElement{
index: edge.neighbor,
cost: new_cost + estimate(&self.graph.nodes[edge.neighbor as usize], end),
path_cost: new_cost,
ancestor: Some(index),
});
}
}
}
println!("popped {} elements from the heap", popcount);
// now the route calculation is done. If a route exist we can construct
// it from the ancestors.
if ancestor[end.index as usize].is_some() {
let mut route = Route {
cost: distance[end.index as usize],
nodes: Vec::new(),
};
let mut current = end.index;
while current != start.index {
route.nodes.push(self.graph.nodes[current as usize].position);
current = ancestor[current as usize].unwrap();
}
route.nodes.push(self.graph.nodes[current as usize].position);
route.nodes.reverse();
return Some(route);
}
None
}
}

View file

@ -0,0 +1,74 @@
use fapra_osm_2::gridgraph::GridGraph;
use std::fs::File;
use clap::Parser;
use std::process::exit;
use rand::seq::SliceRandom;
use fapra_osm_2::utils::RoutingQuery;
use serde_json;
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about=None)]
struct Args {
/// name of the FMI file to read
#[clap(short, long)]
graph: String,
/// number of samples to generate
#[clap(short, long, default_value_t = 1000)]
samples: usize,
}
fn main() {
let args = Args::parse();
let file = match File::open(args.graph.clone()) {
Ok(f) => f,
Err(e) => {
println!("Error while opening the file {}: {}", args.graph, e);
exit(1)
}
};
let graph = match GridGraph::from_fmi_file(file) {
Ok(g) => g,
Err(e) => {
println!("Error while reading the graph: {:?}", e);
exit(1);
}
};
let mut rng = rand::thread_rng();
let mut targets = graph.nodes.choose_multiple(&mut rng, args.samples * 2);
let mut queries: Vec<RoutingQuery> = Vec::new();
for i in (0..targets.len()).step_by(2) {
let source = match targets.next() {
Some(t) => t.index as usize,
None => {
println!("Expected a source node with index {}, but there was None.", i);
exit(2);
},
};
let destination = match targets.next() {
Some(t) => t.index as usize,
None => {
println!("Expected a destination node with index {}, but there was None.", i+1);
exit(2);
},
};
queries.push(RoutingQuery{ source, destination})
}
match serde_json::to_string(&queries) {
Ok(s) => println!("{}", s),
Err(e) => {
println!("Error while serializeing: {}", e);
exit(2);
}
};
}

62
src/bin/generate_grid.rs Normal file
View file

@ -0,0 +1,62 @@
use clap::Parser;
use fapra_osm_2::gridgraph::GridGraph;
use fapra_osm_2::coordinates::RadianCoordinate;
use fapra_osm_2::polygonset::PolygonSet;
use std::fs::File;
use std::process::exit;
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about=None)]
struct Args {
/// the PBF file to load
#[clap(short, long)]
input: String,
/// the output file to write the grid to
#[clap(short, long)]
output: String,
}
fn main() {
let args = Args::parse();
// This is a set of "well-known" points that are outside of all coastline
// polygon. (They are somewhere in the middle of the ocean, far from any
// coast.)
let outside_points = vec![
RadianCoordinate::from_degrees(44.94924926661153, -42.4072265625),
RadianCoordinate::from_degrees(36.82687474287728, -45.19775390625),
RadianCoordinate::from_degrees(17.219511333785114, -35.244140625),
RadianCoordinate::from_degrees(21.779905342529645, 142.20703125),
RadianCoordinate::from_degrees(7.710991655433217, 135.87890625),
RadianCoordinate::from_degrees(34.161818161230386, 150.1171875),
RadianCoordinate::from_degrees(39.98448618198461, -9.136505126953125),
];
let coasts = PolygonSet::from_pbf(&args.input, outside_points);
println!("{} polygons loaded", coasts.polygons.len());
let output = match File::create(args.output.clone()) {
Ok(f) => f,
Err(e) => {
println!("Error while opening the file {}: {}", args.output, e);
exit(1)
}
};
println!("{:?}", coasts.polygons[0].bbox);
let grid = GridGraph::generate_regular_grid(10, 10, Some(&coasts));
// let grid = GridGraph::generate_regular_grid(3, 4, None);
match grid.write_fmi_file(output) {
Ok(_) => {
println!("Wrote graph to {}", args.output);
}
Err(e) => {
println!("Error while opening the file {}: {}", args.output, e);
exit(1);
}
}
}

View file

@ -0,0 +1,28 @@
use fapra_osm_2::gridgraph::GridGraph;
use clap::Parser;
use std::fs::File;
use std::process::exit;
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about=None)]
struct Args {
/// the FMI file to load
#[clap(short, long)]
input: String,
}
fn main() {
let args = Args::parse();
let file = match File::open(args.input.clone()) {
Ok(f) => f,
Err(e) => {
println!("Error while opening file: {}", e);
exit(1);
}
};
let grid = GridGraph::from_fmi_file(file).unwrap();
println!("{}", grid.to_geojson().to_string());
}

149
src/bin/performance.rs Normal file
View file

@ -0,0 +1,149 @@
use clap::Parser;
use fapra_osm_2::alt::LandmarkSet;
use fapra_osm_2::astar::{estimate_haversine, estimate_latitude, AStar};
use fapra_osm_2::gridgraph::{GridGraph, NodeId};
use fapra_osm_2::utils::RoutingQuery;
use serde_json;
use std::fs::File;
use std::io::BufReader;
use std::process::exit;
use std::time::Instant;
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about=None)]
struct Args {
/// name of the FMI file to read
#[clap(short, long)]
graph: String,
/// sample target file
#[clap(short, long)]
targets: String,
/// landmark file
#[clap(short, long)]
landmarks: String,
/// run dijkstra
#[clap(long, action)]
dijkstra: bool,
/// run astar
#[clap(long, action)]
astar: bool,
/// run ALT
#[clap(long, action)]
alt: bool,
}
fn main() {
let args = Args::parse();
let file = match File::open(args.graph.clone()) {
Ok(f) => f,
Err(e) => {
println!("Error while opening the file {}: {}", args.graph, e);
exit(1)
}
};
let graph = match GridGraph::from_fmi_file(file) {
Ok(g) => g,
Err(e) => {
println!("Error while reading the graph: {:?}", e);
exit(1);
}
};
let targets = match File::open(args.targets.clone()) {
Ok(f) => f,
Err(e) => {
println!("Error while opening target file {}: {:?}", args.targets, e);
exit(1);
}
};
let targets: Vec<RoutingQuery> = serde_json::from_reader(BufReader::new(targets)).unwrap();
let landmarks = match File::open(args.landmarks.clone()) {
Ok(f) => f,
Err(e) => {
println!(
"Error while opening landmark file {}: {:?}",
args.landmarks, e
);
exit(1);
}
};
let mut landmarks: LandmarkSet = bincode::deserialize_from(BufReader::new(landmarks)).unwrap();
landmarks.best_size = 4;
let astar = AStar { graph: *graph };
println!("{:?}", args);
if args.astar {
println!("running A*");
let start = Instant::now();
for query in targets.iter() {
let source = astar.graph.nodes[query.source];
let destination = astar.graph.nodes[query.destination];
let _result = astar.shortest_path(&source, &destination, estimate_haversine);
}
let elapsed = start.elapsed();
let time_per_route = elapsed.as_secs_f64() / (targets.len() as f64);
println!("It took {} seconds per route for AStar.", time_per_route);
};
if args.dijkstra {
println!("running Dijkstra");
let start = Instant::now();
for query in targets.iter() {
println!("working on {:?}", query);
let source = astar.graph.nodes[query.source];
let destination = astar.graph.nodes[query.destination];
let result = astar.graph.shortest_path(&source, &destination);
println!("{}", result.unwrap().to_geojson());
}
let elapsed = start.elapsed();
let time_per_route = elapsed.as_secs_f64() / (targets.len() as f64);
println!("It took {} seconds per round for Dijkstra.", time_per_route);
}
if args.alt {
println!("running ALT");
// Landmarks
let start = Instant::now();
for query in targets.iter() {
let source = astar.graph.nodes[query.source];
let destination = astar.graph.nodes[query.destination];
landmarks.select_best(source.index as NodeId, destination.index as NodeId);
let _result = astar.shortest_path(&source, &destination, |src, dest| {
landmarks.estimate(src.index as NodeId, dest.index as NodeId)
});
}
let elapsed = start.elapsed();
let time_per_route = elapsed.as_secs_f64() / (targets.len() as f64);
println!("It took {} seconds per route for ALT.", time_per_route);
}
}

119
src/bin/task6-rocket.rs Normal file
View file

@ -0,0 +1,119 @@
#[macro_use] extern crate rocket;
use clap::Parser;
use std::process::exit;
use std::fs::File;
use fapra_osm_2::gridgraph::{GridGraph, Route};
use fapra_osm_2::coordinates::{RadianCoordinate, DegreeCoordinate};
use rand::seq::SliceRandom;
use serde::Serialize;
use rocket::State;
use rocket::response::status::BadRequest;
use rocket::form::Form;
use rocket::fs::FileServer;
use rocket::serde::json::Json;
use rocket_dyn_templates::{Template, context, Engines};
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about=None)]
struct Args {
/// name of the FMI file to read
#[clap(short, long)]
filename: String,
}
#[get("/")]
fn index() -> Template {
Template::render("index", context! {})
}
#[get("/random")]
fn random_route(graphwrapper: &State<GraphWrapper>) -> String {
let graph = &graphwrapper.graph;
let mut rng = rand::thread_rng();
let mut route = None;
while route.is_none() {
let start = graph.nodes.choose(&mut rng).unwrap();
let end = graph.nodes.choose(&mut rng).unwrap();
route = graph.shortest_path(&start, &end);
}
format!("{}", route.unwrap().to_geojson().to_string())
}
#[derive(FromForm)]
struct RouteQuery<'r> {
r#from: &'r str,
r#to: &'r str,
}
#[derive(Debug)]
#[derive(Serialize)]
pub struct RouteResponse {
success: bool,
route: Option<Route>,
}
#[post("/route", data = "<routequery>")]
fn route(routequery: Form<RouteQuery<'_>>, graphwrapper: &State<GraphWrapper>) -> Result<Json<RouteResponse>, BadRequest<String>> {
let from = match DegreeCoordinate::from_string_tuple(routequery.from) {
Ok(from) => RadianCoordinate::from(from),
Err(e) => {return Result::Err(BadRequest(Some(format!("Error while parsing from field: {:?}", e))));}
};
let to = match DegreeCoordinate::from_string_tuple(routequery.to) {
Ok(to) => RadianCoordinate::from(to),
Err(e) => {return Result::Err(BadRequest(Some(format!("Error while parsing to field: {:?}", e))));}
};
let from = graphwrapper.graph.get_nearest_node(from).unwrap();
let to = graphwrapper.graph.get_nearest_node(to).unwrap();
let route = graphwrapper.graph.shortest_path(from, to);
println!("from: {:?}, to: {:?}", from, to);
let response = RouteResponse{success: route.is_some(), route};
Ok(Json(response))
}
struct GraphWrapper {
graph: Box<GridGraph>
}
#[launch]
fn rocket() -> _ {
let args = Args::parse();
println!("Loading file from {}", args.filename);
let file = match File::open(args.filename.clone()) {
Ok(f) => f,
Err(e) => {
println!("Error while opening the file {}: {}", args.filename, e);
exit(1)
}
};
let graph = match GridGraph::from_fmi_file(file) {
Ok(g) => g,
Err(e) => {
println!("Error while reading the graph: {:?}", e);
exit(1);
}
};
println!("Loaded graph file");
// let graph = GridGraph::generate_regular_grid(10,10);
rocket::build()
.manage(GraphWrapper{graph: graph})
.mount("/", routes![index])
.mount("/", routes![random_route])
.mount("/", routes![route])
.mount("/static", FileServer::from("static"))
.attach(Template::custom(|engines: &mut Engines| {engines.handlebars.set_dev_mode(true);}))
}

52
src/bin/test_alt_gen.rs Normal file
View file

@ -0,0 +1,52 @@
use bincode;
use clap::Parser;
use fapra_osm_2::alt::LandmarkSet;
use fapra_osm_2::gridgraph::GridGraph;
use std::fs::File;
use std::io::prelude::*;
use std::process::exit;
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about=None)]
struct Args {
/// the FMI file to load
#[clap(short, long)]
input: String,
/// the file to which to write the landmarks
#[clap(short, long)]
output: String,
/// the amount of landmarks to generate
#[clap(short, long)]
amount: usize,
}
fn main() {
let args = Args::parse();
let file = match File::open(args.input.clone()) {
Ok(f) => f,
Err(e) => {
println!("Error while opening file: {}", e);
exit(1);
}
};
let grid = GridGraph::from_fmi_file(file).unwrap();
println!("finished loading grid from file");
let mut output = match File::create(args.output.clone()) {
Ok(f) => f,
Err(e) => {
println!("Error while creating the file {}: {}", args.output, e);
exit(2)
}
};
let set = LandmarkSet::random_set(args.amount, 4, &grid);
let encoded = bincode::serialize(&set).unwrap();
output.write_all(&encoded).expect("Error while writing LandmarkSet data");
}

View file

@ -1,17 +1,19 @@
use crate::ACCURACY_BOUNDARY;
use geojson::{Position, Value};
use std::convert::From;
use std::f64::consts::{FRAC_PI_2, PI, TAU};
use crate::ACCURACY_BOUNDARY;
use serde::{Serialize, Deserialize};
/// Spherical coordinates in radians.
#[derive(Clone, Copy, Debug, PartialEq, Default)]
#[derive(Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize)]
pub struct RadianCoordinate {
pub lat: f64,
pub lon: f64,
}
/// Spherical coordinates in degrees.
#[derive(Clone, Copy, Debug, PartialEq, Default)]
#[derive(Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize)]
pub struct DegreeCoordinate {
pub lat: f64,
pub lon: f64,
@ -25,14 +27,14 @@ impl From<DegreeCoordinate> for RadianCoordinate {
impl From<DegreeCoordinate> for geojson::Value {
fn from(coordinate: DegreeCoordinate) -> Self {
Value::Point(vec![coordinate.lat, coordinate.lon])
Value::Point(vec![coordinate.lon, coordinate.lat])
}
}
impl From<RadianCoordinate> for Position {
fn from(coordinate: RadianCoordinate) -> Self {
let coordinate = DegreeCoordinate::from(coordinate);
vec![coordinate.lat, coordinate.lon]
vec![coordinate.lon, coordinate.lat]
}
}
@ -70,12 +72,11 @@ impl RadianCoordinate {
/// returns the longitude of a point when the coordinate system is
/// transformed, such that `north_pole` is the north pole of that system.
pub fn get_transformed_longitude(&self, north_pole: &RadianCoordinate) -> f64 {
if self.lat == FRAC_PI_2 {
return self.lon
return self.lon;
};
let top = (self.lon- north_pole.lon).sin() * self.lat.cos();
let top = (self.lon - north_pole.lon).sin() * self.lat.cos();
let bottom = (self.lat.sin() * north_pole.lat.cos())
- (self.lat.cos() * north_pole.lat.sin() * (self.lon - north_pole.lon).cos());
@ -118,40 +119,6 @@ impl RadianCoordinate {
2.0 * a.sqrt().atan2((1.0_f64 - a).sqrt())
}
/// checks whether the strike is between the shorter angle between the points
/// `a` and `b` while using `pole` as the north pole.
pub fn strike_between(
&self,
pole: &RadianCoordinate,
a: &RadianCoordinate,
b: &RadianCoordinate,
) -> bool {
let mut lon = self.get_transformed_longitude(pole).rem_euclid(2.0 * PI);
let a_lon = a.get_transformed_longitude(pole).rem_euclid(2.0 * PI);
let b_lon = b.get_transformed_longitude(pole).rem_euclid(2.0 * PI);
// select the start boundary of the smaller circle segment in the
// positive direction
let start;
let mut end;
if (b_lon - a_lon) > PI {
start = b_lon;
end = a_lon;
} else {
start = a_lon;
end = b_lon;
}
// rotate the values such that the start is interpreted as 0 and
// handle the wrap around.
// The start of the interval is now at 0 and the end should be smaller
// than PI.
end = (end - start).rem_euclid(2.0 * PI);
lon = (lon - start).rem_euclid(2.0 * PI);
0_f64 <= lon && lon <= end
}
}
impl DegreeCoordinate {
@ -163,6 +130,30 @@ impl DegreeCoordinate {
DegreeCoordinate { lat, lon }
}
/// returns a SphericalCoordinate parsed from the given string.
pub fn from_string_tuple(input: &str) -> Result<DegreeCoordinate, CoordinateParsingError> {
let splits: Vec<&str> = input.split(",").collect();
if splits.len() != 2 {
return Err(CoordinateParsingError::NoSemicolon);
}
let lat = splits[0];
let lon = splits[1];
let lat = match lat.parse::<f64>() {
Ok(lat) => lat,
Err(_) => { return Err(CoordinateParsingError::NotAFloat); }
};
let lon = match lon.parse::<f64>() {
Ok(lon) => lon,
Err(_) => { return Err(CoordinateParsingError::NotAFloat); }
};
Ok(DegreeCoordinate{lat, lon})
}
}
/// normalizes longitude values given in radians to the range (-PI, PI]
@ -190,3 +181,45 @@ fn test_normalize_lon() {
assert!((normalize_lon(PI + FRAC_PI_2) - (-FRAC_PI_2)).abs() < 10e-12);
assert!((normalize_lon(-PI - FRAC_PI_2) - (FRAC_PI_2)).abs() < 10e-12);
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum LongitudeDirection {
East,
West,
None,
}
pub fn east_or_west(from: f64, to: f64) -> LongitudeDirection {
let delta = normalize_lon(from - to);
if delta > 0.0 && delta < PI {
LongitudeDirection::West
} else if delta < 0.0 && delta > -PI {
LongitudeDirection::East
} else {
LongitudeDirection::None
}
}
#[derive(Clone, Debug)]
pub enum CoordinateParsingError {
NoSemicolon,
NotAFloat,
}
#[test]
fn test_east_or_west() {
assert_eq!(east_or_west(0.0, 0.0), LongitudeDirection::None);
assert_eq!(east_or_west(0.0, 1.0), LongitudeDirection::East);
assert_eq!(east_or_west(0.0, -1.0), LongitudeDirection::West);
assert_eq!(east_or_west(1.0, -1.0), LongitudeDirection::West);
assert_eq!(east_or_west(-1.0, 1.0), LongitudeDirection::East);
assert_eq!(east_or_west(0.5, 1.0), LongitudeDirection::East);
assert_eq!(east_or_west(-1.0, -0.5), LongitudeDirection::East);
// wrap around tests
assert_eq!(east_or_west(3.0, -3.0), LongitudeDirection::East);
assert_eq!(east_or_west(-3.0, 3.0), LongitudeDirection::West);
}

682
src/gridgraph.rs Normal file
View file

@ -0,0 +1,682 @@
use crate::coordinates::{DegreeCoordinate, RadianCoordinate};
use crate::polygonset::PolygonSet;
use geojson::{Feature, FeatureCollection, Geometry, Position, Value};
use serde::ser::{SerializeStruct, Serializer};
use std::cmp::Ordering;
use std::collections::{BinaryHeap, HashMap};
use std::f64::consts::{FRAC_PI_2, PI, TAU};
use std::fs::File;
use std::io::{BufRead, BufReader, Write};
use std::vec::Vec;
use crate::utils::EARTH_RADIUS;
use rand::seq::SliceRandom;
use serde::{Serialize, Deserialize};
/// Type for all edge costs
/// Allows for easy adjustments for bigger/smaller number types if that is
/// neccessary/sufficient.
/// All distance measurements on the grid should use this type.
pub type EdgeCost = u64;
pub type NodeId = usize;
#[derive(Debug, Clone, Default)]
pub struct GridGraph {
pub nodes: Vec<GraphNode>,
pub edges: Vec<GraphEdge>,
pub edge_offsets: Vec<usize>,
lookup_grid: LookupGrid,
}
#[derive(Debug, Clone, Default)]
pub struct LookupGrid {
lat_size: usize,
lon_size: usize,
grid: Vec<Vec<Vec<NodeId>>>,
}
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct GraphNode {
pub index: u32,
pub position: RadianCoordinate,
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct GraphEdge {
// index of the neighbor in the data structure
pub neighbor: u32,
pub cost: EdgeCost,
}
#[derive(Debug)]
pub enum FmiParsingError {
FormatError(String),
NotAnInteger(String),
NotAFloat(String),
WrongNodeAmount,
WrongEdgeAmount,
}
#[derive(Debug)]
pub struct Route {
pub nodes: Vec<RadianCoordinate>,
pub cost: EdgeCost,
}
impl Serialize for Route {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_struct("Route", 2)?;
state.serialize_field("geojson", &self.to_geojson())?;
state.serialize_field("cost", &self.cost)?;
state.end()
}
}
impl LookupGrid {
/// returns the latitude index and longitude index of a position
pub fn get_grid_cell(&self, position: RadianCoordinate) -> (usize, usize) {
let lat_index = ((position.lat + 90.0) / 180.0 * self.lat_size as f64).floor() as usize;
let lon_index = ((position.lon + 180.0) / 360.0 * self.lon_size as f64).floor() as usize;
(lat_index, lon_index)
}
/// inserts the nodes in to the grid
pub fn insert_nodes(&mut self, nodes: &[GraphNode]) {
for node in nodes.iter() {
let (lat, lon) = self.get_grid_cell(node.position);
self.grid[lat][lon].push(node.index as usize);
}
}
/// creates a new lookup grid with the given size and adds the given nodes
pub fn new(lat_size: usize, lon_size: usize, nodes: &[GraphNode]) -> LookupGrid {
let mut instance = LookupGrid {
lat_size,
lon_size,
grid: vec![vec!(Vec::new(); lon_size); lat_size],
};
instance.insert_nodes(nodes);
instance
}
}
impl Route {
pub fn to_geojson(&self) -> Box<FeatureCollection> {
let mut features: Box<FeatureCollection> = Box::new(FeatureCollection {
bbox: None,
features: vec![],
foreign_members: None,
});
let mut points = geojson::LineStringType::new();
for node in self.nodes.iter() {
let position: Position = (*node).into();
points.push(position);
}
let geometry = Geometry::new(Value::LineString(points));
features.features.push(Feature {
bbox: None,
geometry: Some(geometry),
id: None,
properties: None,
foreign_members: None,
});
features
}
}
impl GridGraph {
/// selects a single random graph node.
pub fn get_random_node(&self) -> Option<&GraphNode> {
let mut rng = rand::thread_rng();
self.nodes.choose(&mut rng)
}
/// selects up to `amount` random but distinct nodes from the graph.
pub fn get_random_nodes(&self, amount: usize) -> Vec<&GraphNode>{
let mut rng = rand::thread_rng();
self.nodes.choose_multiple(&mut rng, amount).collect()
}
/// calculates the shortest path from the start node to the end node.
/// start and end nodes have to be references to the nodes contained in the
/// grid graph.
/// `start` and `end` have to be different nodes.
/// Returns the route.
pub fn shortest_path(&self, start: &GraphNode, end: &GraphNode) -> Option<Route> {
#[derive(Eq, PartialEq, Debug)]
struct DijkstraElement {
index: u32,
cost: EdgeCost,
}
impl Ord for DijkstraElement {
// inverted cmp function, such that the Max-Heap provided by Rust
// can be used as a Min-Heap
fn cmp(&self, other: &Self) -> Ordering {
other.cost.cmp(&self.cost).then_with(|| self.index.cmp(&other.index))
}
}
impl PartialOrd for DijkstraElement {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
let mut heap = BinaryHeap::new();
heap.push(DijkstraElement {
cost: 0,
index: start.index,
});
let mut distance = vec![EdgeCost::MAX; self.nodes.len()];
let mut ancestor: Vec<Option<u32>> = vec![None; self.nodes.len()];
let mut popcount = 0;
while let Some(DijkstraElement {
index,
cost,
}) = heap.pop()
{
popcount += 1;
if index == end.index {
// println!("found the end");
break;
}
// the heap does not support "update" operations, so we
// insert elements again and if they come out of the heap but have
// been processed earlier we simply skip them.
if cost > distance[index as usize] {
//println!("skipping {} because of cost {}, it can be reached with {}", index, cost, distance[index as usize]);
continue;
};
// println!("working on node {} with cost {}", index, cost);
let edge_start = self.edge_offsets[index as usize] as usize;
let edge_end = self.edge_offsets[(index + 1) as usize] as usize;
for edge in self.edges[edge_start..edge_end].iter() {
let new_cost = cost + edge.cost;
if new_cost < distance[edge.neighbor as usize] {
// println!("found cheaper edge {:?} with cost {} than previous best {}", edge, new_cost, distance[edge.neighbor as usize]);
distance[edge.neighbor as usize] = new_cost;
ancestor[edge.neighbor as usize] = Some(index);
//println!("adding new element to heap");
heap.push(DijkstraElement {
index: edge.neighbor,
cost: new_cost,
});
} else {
// println!("edge {:?} is more expensive ({}) than the previous best {}", edge, new_cost, distance[edge.neighbor as usize]);
}
}
}
println!("popped {} elements from the heap", popcount);
// now the route calculation is done. If a route exist we can construct
// it from the ancestors.
if ancestor[end.index as usize].is_some() {
let mut route = Route {
cost: distance[end.index as usize],
nodes: Vec::new(),
};
let mut current = end.index;
while current != start.index {
route.nodes.push(self.nodes[current as usize].position);
current = ancestor[current as usize].unwrap();
}
route.nodes.push(self.nodes[current as usize].position);
route.nodes.reverse();
return Some(route);
}
None
}
/// returns the GraphNode nearest to that positon.
/// There might be cornercases where other nodes are actually closer due
/// to the spherical projection.
/// There also might be cases where no neares position is found because
/// the search only works on a 3x3 grid around the position.
pub fn get_nearest_node(&self, position: RadianCoordinate) -> Option<&GraphNode> {
let (lat, lon) = self.lookup_grid.get_grid_cell(position);
let lat = lat as isize;
let lon = lon as isize;
let lookup_distance = 1;
let mut best_node: Option<&GraphNode> = None;
let mut best_distance = f64::MAX;
// iterate over the grid and handle the wrap-aroung at 180 degrees
for lat_index in (lat - lookup_distance)..(lat + lookup_distance + 1) {
let lat_index = lat_index.rem_euclid(self.lookup_grid.lat_size as isize) as usize;
for lon_index in (lon - lookup_distance)..(lon + lookup_distance + 1) {
let lon_index = lon_index.rem_euclid(self.lookup_grid.lon_size as isize) as usize;
// actual check of the grid nodes in the cell
for node_id in self.lookup_grid.grid[lat_index][lon_index].iter() {
let node = &(self.nodes[*node_id as usize]);
let dist = node.position.distance_to(&position);
if dist < best_distance {
best_node = Some(node);
best_distance = dist;
}
}
}
}
best_node
}
/// reads a graph from an FMI file.
pub fn from_fmi_file(file: File) -> Result<Box<GridGraph>, FmiParsingError> {
let mut gridgraph = Box::new(GridGraph::default());
let reader = BufReader::new(file);
let lines = reader.lines();
let mut total_node_count: u32 = 0;
let mut total_edge_count: u32 = 0;
let mut node_count: u32 = 0;
let mut edge_count: u32 = 0;
let mut nodes: HashMap<u32, GraphNode> = HashMap::new();
let mut edges: HashMap<u32, Vec<TemporaryGraphEdge>> = HashMap::new();
enum ParserState {
NodeCount,
EdgeCount,
Nodes,
Edges,
Done,
}
struct TemporaryGraphEdge {
src: u32,
dst: u32,
cost: EdgeCost,
}
fn parse_int(line: &str) -> Result<u32, FmiParsingError> {
match line.parse::<u32>() {
Ok(result) => Ok(result),
Err(_) => Err(FmiParsingError::NotAnInteger(format!(
"Line is not an integer: '{}'",
line
))),
}
}
fn parse_float(line: &str) -> Result<f64, FmiParsingError> {
match line.parse::<f64>() {
Ok(result) => Ok(result),
Err(_) => Err(FmiParsingError::NotAFloat(format!(
"Line is not an float: '{}'",
line
))),
}
}
fn parse_node(line: &str) -> Result<GraphNode, FmiParsingError> {
let splits = line.split(" ").collect::<Vec<&str>>();
if splits.len() < 3 {
return Err(FmiParsingError::FormatError(format!(
"Line '{}' does not have at least 3 parts",
line
)));
}
let index = parse_int(splits[0])?;
let lat = parse_float(splits[1])?;
let lon = parse_float(splits[2])?;
Ok(GraphNode {
index,
position: RadianCoordinate::from_degrees(lat, lon),
})
}
fn parse_edge(line: &str) -> Result<TemporaryGraphEdge, FmiParsingError> {
let splits = line.split(" ").collect::<Vec<&str>>();
if splits.len() < 3 {
return Err(FmiParsingError::FormatError(format!(
"Line '{}' does not have at least 3 parts",
line
)));
}
let src = parse_int(splits[0])?;
let dst = parse_int(splits[1])?;
let cost = parse_int(splits[2])? as EdgeCost;
Ok(TemporaryGraphEdge { src, dst, cost })
}
let mut state = ParserState::NodeCount {};
for line in lines {
let line = line.unwrap();
let line = line.trim();
if line.is_empty() || line.starts_with("#") {
continue;
}
match state {
ParserState::NodeCount => {
total_node_count = parse_int(line)?;
state = ParserState::EdgeCount {};
}
ParserState::EdgeCount => {
total_edge_count = parse_int(line)?;
state = ParserState::Nodes {};
}
ParserState::Nodes => {
let node = parse_node(line)?;
nodes.entry(node.index).or_insert(node);
node_count += 1;
if node_count == total_node_count {
state = ParserState::Edges {};
}
}
ParserState::Edges => {
let edge = parse_edge(line)?;
edges.entry(edge.src).or_default().push(edge);
edge_count += 1;
if edge_count == total_edge_count {
state = ParserState::Done;
}
}
ParserState::Done => {
break;
}
}
}
if node_count != total_node_count {
return Err(FmiParsingError::WrongNodeAmount);
}
if edge_count != total_edge_count {
return Err(FmiParsingError::WrongEdgeAmount);
}
// add the nodes to the gridgraph
for (_, mut item) in nodes.iter_mut() {
item.index = gridgraph.nodes.len() as u32;
gridgraph.nodes.push(item.clone());
}
// add the edges
gridgraph.edge_offsets.push(0);
for index in nodes.keys() {
for edge in edges.entry(*index).or_default() {
if !nodes.contains_key(&edge.dst) {
println!(
"An edge refers to the non-existing destination node {} skipping.",
edge.dst
);
continue;
}
let dst_index = nodes[&edge.dst].index;
gridgraph.edges.push(GraphEdge {
cost: edge.cost,
neighbor: dst_index,
});
}
gridgraph.edge_offsets.push(gridgraph.edges.len());
}
gridgraph.lookup_grid = LookupGrid::new(100, 100, &gridgraph.nodes);
Ok(gridgraph)
}
pub fn write_fmi_file(&self, mut file: File) -> std::io::Result<()> {
writeln!(file, "{}", self.nodes.len())?;
writeln!(file, "{}", self.edges.len())?;
for node in self.nodes.iter() {
let deg_pos = DegreeCoordinate::from(node.position);
writeln!(file, "{} {} {}", node.index, deg_pos.lat, deg_pos.lon,)?;
}
for node in self.nodes.iter() {
for edge in self.get_edges(node.index as NodeId) {
let neighbor = self.nodes[edge.neighbor as usize];
writeln!(file, "{} {} {}", node.index, neighbor.index, edge.cost)?;
}
}
Ok(())
}
pub fn get_edges(&self, node: NodeId) -> &[GraphEdge] {
let start = self.edge_offsets[node];
let end = self.edge_offsets[node + 1];
&self.edges[start..end]
}
/// generates a regular grid over the globe with the given grid size.
pub fn generate_regular_grid(
lat_resolution: usize,
lon_resolution: usize,
polygons: Option<&PolygonSet>,
) -> Box<GridGraph> {
#[derive(Debug)]
struct TemporaryGraphNode {
final_index: u32,
position: RadianCoordinate,
lat_index: usize,
lon_index: usize,
// specifies whether or not the node will be in the final graph or
// not, because it might be on land.
used: bool,
}
impl TemporaryGraphNode {
pub fn left_neighbor_index(&self, lon_resolution: usize) -> usize {
let left_lon_index =
(self.lon_index as isize - 1).rem_euclid(lon_resolution as isize) as usize;
left_lon_index + (self.lat_index * lon_resolution)
}
pub fn right_neighbor_index(&self, lon_resolution: usize) -> usize {
let left_lon_index = (self.lon_index + 1).rem_euclid(lon_resolution);
left_lon_index + (self.lat_index * lon_resolution)
}
pub fn bottom_neighbor_index(&self, lon_resolution: usize) -> Option<usize> {
if self.lat_index <= 0 {
return None;
};
Some(self.lon_index + (self.lat_index - 1) * lon_resolution)
}
pub fn top_neighbor_index(&self, lat_resolution: usize, lon_resolution: usize) -> Option<usize> {
if self.lat_index >= lat_resolution - 1 {
return None;
};
Some(self.lon_index + (self.lat_index + 1) * lon_resolution)
}
}
let mut temp_nodes = Vec::new();
for lat_index in 0..lat_resolution {
let lat = ((lat_index as f64 / lat_resolution as f64) * PI) - FRAC_PI_2;
for lon_index in 0..lon_resolution {
let lon = ((lon_index as f64 / lon_resolution as f64) * TAU) - PI;
let position = RadianCoordinate { lat, lon };
let used = match polygons {
None => true,
Some(p) => !p.in_any(position),
};
temp_nodes.push(TemporaryGraphNode {
final_index: 0,
position,
used,
lat_index,
lon_index,
})
}
}
let mut final_graph: Box<GridGraph> = Box::new(GridGraph::default());
// add all the nodes to the final graph
for mut node in temp_nodes.iter_mut() {
if !node.used {
continue;
};
let new_node = GraphNode {
index: final_graph.nodes.len() as u32,
position: node.position,
};
node.final_index = new_node.index;
final_graph.nodes.push(new_node);
}
// add all edges to the final graph
for node in temp_nodes.iter() {
println!("working on edges for node {:?}", node);
if !node.used {
continue;
};
final_graph.edge_offsets.push(final_graph.edges.len());
fn add_neighbor(
node: &TemporaryGraphNode,
neighbor: &TemporaryGraphNode,
grid: &mut GridGraph,
) {
if neighbor.used {
grid.edges.push(GraphEdge {
neighbor: neighbor.final_index,
cost: (node.position.distance_to(&(neighbor.position)) * EARTH_RADIUS)
as EdgeCost,
});
}
}
// add neighbors
let left_neighbor = &temp_nodes[node.left_neighbor_index(lon_resolution)];
add_neighbor(&node, &left_neighbor, &mut final_graph);
let right_neighbor = &temp_nodes[node.right_neighbor_index(lon_resolution)];
add_neighbor(&node, &right_neighbor, &mut final_graph);
let top_index = node.top_neighbor_index(lat_resolution, lon_resolution);
match top_index {
Some(index) => {
println!("top neighbor is {}", index);
add_neighbor(&node, &temp_nodes[index], &mut final_graph);
}
None => (),
}
let bottom_index = node.bottom_neighbor_index(lon_resolution);
match bottom_index {
Some(index) => {
add_neighbor(&node, &temp_nodes[index], &mut final_graph);
}
None => (),
}
}
// pushing the end offset
final_graph.edge_offsets.push(final_graph.edges.len());
// making the cells of the lookup grid 4 times the size of the regular
// grid is a heuristic that worked well.
final_graph.lookup_grid = LookupGrid::new(
(lat_resolution / 4).max(1),
(lon_resolution / 4).max(1),
&final_graph.nodes,
);
final_graph
}
/// Returns a GeoJSON representation of the grid.
/// This gets very large very soon and might be hard to render.
pub fn to_geojson(&self) -> Box<FeatureCollection> {
let mut features: Box<FeatureCollection> = Box::new(FeatureCollection {
bbox: None,
features: vec![],
foreign_members: None,
});
for node in self.nodes.iter() {
let value = Value::from(node.position);
features.features.push(Feature::from(value));
}
for node in self.nodes.iter() {
for edge_index in
self.edge_offsets[node.index as usize]..self.edge_offsets[(node.index + 1) as usize]
{
let edge = self.edges[edge_index as usize];
// the edges are stored as directed edges, but we only want to
// draw one of them
if node.index < edge.neighbor {
continue;
}
let neighbor = self.nodes[edge.neighbor as usize];
let mut points = geojson::LineStringType::new();
points.push(Position::from(node.position));
points.push(Position::from(neighbor.position));
let geometry = Geometry::new(Value::LineString(points));
features.features.push(Feature {
bbox: None,
geometry: Some(geometry),
id: None,
properties: None,
foreign_members: None,
})
}
}
features
}
}

View file

@ -1,5 +1,11 @@
pub mod coordinates;
pub mod polygon;
pub mod polygonset;
pub mod gridgraph;
pub mod utils;
pub mod astar;
pub mod alt;
#[cfg(test)]
pub mod tests;

View file

@ -1,8 +1,7 @@
use crate::coordinates::{RadianCoordinate, normalize_lon};
use crate::coordinates::{east_or_west, RadianCoordinate};
use geojson::{Feature, FeatureCollection, Position, Value};
use std::convert::From;
use std::f64;
use std::f64::consts::{PI, TAU};
/// A spherical polygon
/// the first and last node have to be identical.
@ -139,6 +138,7 @@ impl Polygon {
point1: RadianCoordinate,
point2: RadianCoordinate,
) -> Result<Polygon, PolygonVerificationError> {
println!("build_polygon");
let bbox = BoundingBox::from_nodes(&nodes);
let polygon = Polygon {
@ -178,6 +178,11 @@ impl Polygon {
/// If the polygon does not have enough nodes, nothing can be inside
/// the polygon, so false is returned.
pub fn contains(&self, point: &RadianCoordinate) -> bool {
if !self.bbox.contains(*point) {
return false;
};
let first_check = self.contains_internal(point, &(self.outside_point_1));
match first_check {
@ -203,77 +208,80 @@ impl Polygon {
let mut crossings = 0;
for i in 0..(self.nodes.len()-1) {
let mut trans_lons = vec![];
let point_trans_lon = point.get_transformed_longitude(outside);
// precalculate all the transformed longitudes with the outside point
// as their north pole
for node in self.nodes.iter() {
trans_lons.push(node.get_transformed_longitude(outside));
}
for i in 0..(self.nodes.len() - 1) {
let mut strike_match = false;
// strike check
let a_trans_lon = trans_lons[i];
// this first check, checks whether the line from outside to point
// might go through A
if a_trans_lon == point_trans_lon {
strike_match = true;
} else {
// to test whether we are between A and B we check if we
// move into the same direction from A to B, from A to P and from
// P to B.
let b_trans_lon = trans_lons[i + 1];
let dir_ab = east_or_west(a_trans_lon, b_trans_lon);
let dir_ap = east_or_west(a_trans_lon, point_trans_lon);
let dir_pb = east_or_west(point_trans_lon, b_trans_lon);
if dir_ab == dir_ap && dir_ap == dir_pb {
strike_match = true;
}
}
if !strike_match {
continue;
}
let point_a = self.nodes[i];
// check whether the point is the vertex
// check whether we are on the vertex.
if *point == point_a {
return Ok(true);
}
// now check whether outside and the query point are on different
// sides of the arc AB.
// This is done by transforming the longitudes and checking the
// walking directions
//
let point_b = self.nodes[i + 1];
let b_trans_lon = point_b.get_transformed_longitude(&point_a);
let outside_trans_lon = outside.get_transformed_longitude(&point_a);
let point_trans_lon = point.get_transformed_longitude(&point_a);
println!("working on arc from {:?} to {:?}", point_a, point_b);
// is the great circle from outside to point crossing through
// the current vertex?
if point.on_great_circle(outside, &point_a) {
println!("we are on a great circle");
// point c is the previous vertex.
// the second -1 is needed, because the polygon has explict
// start/end points
let point_c = if i == 0 {
self.nodes[self.nodes.len() -2]
} else {
self.nodes[i - 1]
};
let lon_p = point.get_transformed_longitude(&point_a);
let mut lon_b = point_b.get_transformed_longitude(&point_a);
let mut lon_c = point_c.get_transformed_longitude(&point_a);
// the sign tell us on which side of the great circle from
// a to point we are
lon_b = (lon_b - lon_p) % (TAU);
lon_c = (lon_c - lon_p) % (TAU);
if lon_b.signum() != lon_c.signum() {
crossings += 1;
continue;
};
// the queried point is exactly on the arc
if point_trans_lon == b_trans_lon {
return Ok(true);
}
if point.strike_between(outside, &point_a, &point_b) {
println!("we have a matching strike");
let lon_b = point_b.get_transformed_longitude(&point_a);
let mut lon_point = point.get_transformed_longitude(&point_a);
let mut lon_outside = outside.get_transformed_longitude(&point_a);
let dir_bpoint = east_or_west(b_trans_lon, point_trans_lon);
let dir_boutside = east_or_west(b_trans_lon, outside_trans_lon);
println!("lon_b: {}", lon_b);
println!("initial lon_point: {}", lon_point);
println!("initial lon_outside: {}", lon_outside);
// the sign tells us on which side of the great circle from
// a to b we are
lon_point = lon_point - lon_b;
lon_outside = lon_outside - lon_b;
println!("lon_point: {}", lon_point);
println!("lon_outside: {}", lon_outside);
lon_point = normalize_lon(lon_point);
lon_outside = normalize_lon(lon_outside);
println!("lon_point: {}", lon_point);
println!("lon_outside: {}", lon_outside);
if lon_point.signum() != lon_outside.signum() {
crossings += 1;
};
// are we walking into different directions from the arc AB to
// the other 2 points?
// being on the arc has been excluded for the queried point
// and may not happen for the outside point because otherwise
// it would not be outside after a successful strike check.
if dir_bpoint != dir_boutside {
crossings += 1;
}
}
Ok(crossings % 2 == 1)
}
}
@ -357,18 +365,8 @@ mod tests {
println!("{}", features.to_string());
let in_point = RadianCoordinate::from_degrees(5.0, 5.0);
let out_point = RadianCoordinate {
lat: -0.1,
lon: 0.1,
};
let out_point = RadianCoordinate::from_degrees(-17.0, 15.0);
let antipodal_point = RadianCoordinate {
lat: -0.1,
lon: -PI + 0.1,
};
let antipodal_point = RadianCoordinate::from_degrees(-30.0, -160.0);
let outside_vertex_intersect = RadianCoordinate {
lat: -0.1,
@ -380,7 +378,7 @@ mod tests {
lon: -0.1,
};
//assert_eq!(polygon.contains(&in_point), true);
assert_eq!(polygon.contains(&in_point), true);
assert_eq!(polygon.contains(&out_point), false);
assert_eq!(polygon.contains(&antipodal_point), false);
assert_eq!(
@ -404,7 +402,7 @@ mod tests {
],
RadianCoordinate::from_degrees(1.0, -1.0),
RadianCoordinate::from_degrees(2.0, -1.0),
);
);
assert!(polygon.is_ok());
let polygon = polygon.unwrap();

284
src/polygonset.rs Normal file
View file

@ -0,0 +1,284 @@
use crate::coordinates::RadianCoordinate;
use crate::polygon::{Polygon, PolygonVerificationError, BoundingBox};
use osmpbfreader::objects::{NodeId, OsmId, OsmObj, Way, WayId};
use std::collections::btree_map::Entry;
use std::collections::{BTreeMap, BTreeSet};
use std::fs::File;
use std::process::exit;
pub type OsmObjectMap = BTreeMap<OsmId, OsmObj>;
/// A Set of Polygons
/// used to run a point in polygon check on many polygons at once.
#[derive(Debug, Default)]
pub struct PolygonSet {
pub polygons: Vec<Polygon>,
}
impl PolygonSet {
/// reads the given input file and generates a PolygonSet.
/// The `outside_points` have to be outside of all polygons.
/// There must be at least 2 outside points, but depending on their
/// placement relative to the edges of the polygons, more might be needed.
pub fn from_pbf(input: &str, outside_points: Vec<RadianCoordinate>) -> PolygonSet {
let file = match File::open(input.clone()) {
Ok(f) => f,
Err(e) => {
println!("Error while opening the file {}: {}", input, e);
exit(1)
}
};
let mut pbf = osmpbfreader::OsmPbfReader::new(file);
let objects: OsmObjectMap = pbf
.get_objs_and_deps(|obj| obj.is_way() && obj.tags().contains("natural", "coastline"))
.unwrap();
println!("Done unwrapping the OSM objects");
// aggregate all coast segments WayIDs into a Set
let coast_segments = aggregate_coasts(&objects);
println!(
"Done aggregating coast segments: {} segments",
coast_segments.len()
);
// build maps from start/end points of a segment to the segment ID
let (starts, _ends) = collects_start_end(&coast_segments, &objects);
// Put all WayIds into a Set that is used as a work queue of unprocessed
// segments
let mut work_queue = BTreeSet::new();
for segment_id in coast_segments.iter() {
let segment_id = segment_id.way().unwrap();
work_queue.insert(segment_id);
}
println!("Done setting up the work queue");
// the PolygonSet in which we will collect our Polygons.
let mut polyset = PolygonSet::default();
while work_queue.is_empty() == false {
let mut assembly = vec![];
// dirty way to pop an element from the set.
// currently (2022-04-18) there is a feature in nightly
// https://github.com/rust-lang/rust/issues/62924
let current = work_queue.iter().next().cloned().unwrap();
work_queue.take(&current).unwrap();
let segment = objects.get(&OsmId::Way(current)).unwrap().way().unwrap();
assembly.push(segment);
// println!("starting to work on a segment {:?}", segment);
let (mut start, mut end) = find_start_end(&assembly);
while start != end {
let next_ids = starts.get(&end).unwrap();
for next_id in next_ids {
let next_segment = objects.get(&OsmId::Way(*next_id)).unwrap().way().unwrap();
assembly.push(next_segment);
work_queue.remove(next_id);
let (start_new, end_new) = find_start_end(&assembly);
start = start_new;
end = end_new;
if start == end {
break;
}
}
}
polyset
.polygons
.push(build_polygon(&assembly, &objects, &outside_points));
}
polyset
}
/// Returns `true` when the `input` is in any of the polygons.
pub fn in_any(&self, input: RadianCoordinate) -> bool {
for polygon in self.polygons.iter() {
if polygon.contains(&input) {
return true;
}
}
return false;
}
}
/// aggregate all coast segment WayIDs into a Set
fn aggregate_coasts(objects: &OsmObjectMap) -> BTreeSet<&OsmId> {
let mut coast_segments = BTreeSet::new();
for (key, value) in objects.iter() {
match value {
OsmObj::Way(_) => {
coast_segments.insert(key);
}
OsmObj::Node(_) => {}
OsmObj::Relation(r) => {
println!("Found unexpected relation: {:?}", r);
}
}
}
coast_segments
}
fn collects_start_end(
coast_segments: &BTreeSet<&OsmId>,
objects: &OsmObjectMap,
) -> (BTreeMap<NodeId, Vec<WayId>>, BTreeMap<NodeId, Vec<WayId>>) {
// Create a Map from NodeIDs of the start and ends of a way to WayIDs
let mut starts: BTreeMap<NodeId, Vec<WayId>> = BTreeMap::new();
let mut ends: BTreeMap<NodeId, Vec<WayId>> = BTreeMap::new();
for segment_id in coast_segments.iter() {
let segment = objects.get(&segment_id).unwrap().way().unwrap();
let segment_id = segment_id.way().unwrap();
let start = segment.nodes.first().unwrap();
let end = segment.nodes.last().unwrap();
// TODO cleanup
match starts.entry(*start) {
Entry::Vacant(e) => {
e.insert(vec![segment_id]);
}
Entry::Occupied(mut e) => {
e.get_mut().push(segment_id);
}
};
match ends.entry(*end) {
Entry::Vacant(e) => {
e.insert(vec![segment_id]);
}
Entry::Occupied(mut e) => {
e.get_mut().push(segment_id);
}
};
}
(starts, ends)
}
fn find_start_end(assembly: &Vec<&Way>) -> (NodeId, NodeId) {
let start = *assembly.first().unwrap().nodes.first().unwrap();
let end = *assembly.last().unwrap().nodes.last().unwrap();
(start, end)
}
/// builds a polygon from the given vector of ways.
fn build_polygon(
assembly: &Vec<&Way>,
objects: &OsmObjectMap,
outside_points: &Vec<RadianCoordinate>,
) -> Polygon {
let mut polygon = Polygon::default();
let first_way = assembly.first().unwrap();
add_points(first_way.nodes.iter(), &mut polygon.nodes, objects);
for way in assembly.iter().skip(1) {
add_points(way.nodes.iter().skip(1), &mut polygon.nodes, objects);
}
polygon.outside_point_1 = outside_points[0];
polygon.outside_point_2 = outside_points[1];
let iterator = outside_points.iter().skip(2);
polygon.bbox = BoundingBox::from_nodes(&polygon.nodes);
let mut verification = polygon.verify();
for new_point in iterator {
match verification {
Result::Ok(_) => {
return polygon;
}
Err(PolygonVerificationError::Antipodal) => {
polygon.outside_point_1 = *new_point;
}
Err(PolygonVerificationError::OutSide1SameCircle) => {
polygon.outside_point_1 = *new_point;
}
Err(PolygonVerificationError::OutSide2SameCircle) => {
polygon.outside_point_2 = *new_point;
}
}
verification = polygon.verify();
}
println!(
"No valid outside points found for a polygon with {:?} edges",
polygon.nodes.len()
);
polygon
}
fn add_points<'a>(
nodes: impl Iterator<Item = &'a NodeId>,
target: &mut Vec<RadianCoordinate>,
objects: &OsmObjectMap,
) {
for node in nodes {
let point = match objects.get(&OsmId::Node(*node)) {
Some(p) => p.node().unwrap(),
None => {
println!("No node found with id {:?}", node);
continue;
}
};
let lat = point.lat();
let lon = point.lon();
target.push(RadianCoordinate::from_degrees(lat, lon));
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::polygon::Polygon;
#[test]
fn test_in_any() {
let inside = RadianCoordinate::from_degrees(0.0, 10.0);
let outside = RadianCoordinate::from_degrees(60.0, 30.0);
let polygon = Polygon::build_polygon(
vec![
RadianCoordinate::from_degrees(45.0, 40.0),
RadianCoordinate::from_degrees(45.0, -45.0),
RadianCoordinate::from_degrees(-45.0, -40.0),
RadianCoordinate::from_degrees(-45.0, 45.0),
RadianCoordinate::from_degrees(45.0, 40.0),
],
RadianCoordinate::from_degrees(30.0, 60.0),
RadianCoordinate::from_degrees(-30.0, 60.0),
);
assert!(polygon.is_ok());
let polygon = polygon.unwrap();
assert!(polygon.contains(&inside));
let polyset = PolygonSet {
polygons: vec![polygon],
};
assert!(polyset.in_any(inside));
assert!(!polyset.in_any(outside));
}
}

View file

@ -1,5 +1,5 @@
use crate::coordinates::RadianCoordinate;
use std::f64::consts::{PI, FRAC_PI_2, FRAC_PI_4, FRAC_PI_8};
use std::f64::consts::{PI, FRAC_PI_2};
#[test]
fn on_great_circle() {
@ -48,75 +48,3 @@ fn antipodal() {
assert!(!point1.antipodal(&point4));
assert!(point6.antipodal(&point7));
}
#[test]
fn transformed_longitude() {
// test with regular north pole
let point0 = RadianCoordinate { lat: 0.0, lon: 0.0 };
let north_pole = RadianCoordinate {
lat: FRAC_PI_2,
lon: 0.0,
};
let difference = (point0.get_transformed_longitude(&north_pole) - point0.lon).abs() % PI;
assert!(difference < 1e-10);
// example with 4 points, where the X->P is between X->A and X->B
let point_x = RadianCoordinate {
lat: 0.0,
lon: -0.1,
};
let point_a = RadianCoordinate { lat: 0.1, lon: 0.0 };
let point_p = RadianCoordinate { lat: 0.0, lon: 0.1 };
let point_b = RadianCoordinate {
lat: -0.1,
lon: 0.0,
};
assert!(
point_a.get_transformed_longitude(&point_x)
< point_p.get_transformed_longitude(&point_x)
);
assert!(
point_p.get_transformed_longitude(&point_x)
< point_b.get_transformed_longitude(&point_x)
);
}
#[test]
fn strike_check() {
let pole = RadianCoordinate {
lat: FRAC_PI_4,
lon: 0.0,
};
let a = RadianCoordinate { lat: 0.0, lon: 0.1 };
let b = RadianCoordinate {
lat: 0.0,
lon: -0.1,
};
// Point that is between a and b and crosses the line between a and b
let p1 = RadianCoordinate {
lat: -FRAC_PI_8,
lon: 0.0,
};
// Point that is between a and b and does not cross the line between a and b
let p2 = RadianCoordinate {
lat: FRAC_PI_8,
lon: 0.0,
};
// Point that is left of a and b
let p3 = RadianCoordinate {
lat: 0.0,
lon: -0.2,
};
// Point that is right of a and b
let p4 = RadianCoordinate { lat: 0.0, lon: 0.2 };
assert!(p1.strike_between(&pole, &a, &b));
assert!(p2.strike_between(&pole, &a, &b));
assert!(!p3.strike_between(&pole, &a, &b));
assert!(!p4.strike_between(&pole, &a, &b));
}

9
src/utils.rs Normal file
View file

@ -0,0 +1,9 @@
use serde::{Deserialize, Serialize};
pub const EARTH_RADIUS: f64 = 6_371_000.0; // meters
#[derive(Serialize, Deserialize, Debug, Copy, Clone)]
pub struct RoutingQuery{
pub source: usize,
pub destination: usize,
}