initial commit
This commit is contained in:
commit
32e68ee635
12 changed files with 3012 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/target
|
||||||
2185
Cargo.lock
generated
Normal file
2185
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
16
Cargo.toml
Normal file
16
Cargo.toml
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
[package]
|
||||||
|
name = "fapra_osm_2"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
geojson = { version = "0.22.3", features = ["geo-types"]}
|
||||||
|
handlebars = { version = "4.2", features = ["dir_source"] }
|
||||||
|
clap = { version = "3.1", features = ["derive"] }
|
||||||
|
rand = {version = "0.8", features = ["alloc"] }
|
||||||
|
rocket = {version="0.5.0-rc", features=["json"]}
|
||||||
|
rocket_dyn_templates = {version="0.1.0-rc", features=["handlebars"]}
|
||||||
|
serde = {version="1.0", features=["derive"]}
|
||||||
|
osmpbfreader = "0.15.2"
|
||||||
24
src/bin/polygon_print.rs
Normal file
24
src/bin/polygon_print.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
use fapra_osm_2::coordinates::RadianCoordinate;
|
||||||
|
use fapra_osm_2::polygon::Polygon;
|
||||||
|
use geojson::FeatureCollection;
|
||||||
|
|
||||||
|
|
||||||
|
/// a simple program to show the geojson export of polygons
|
||||||
|
fn main() {
|
||||||
|
let nodes = vec![
|
||||||
|
RadianCoordinate::from_degrees(10.0, 15.0),
|
||||||
|
RadianCoordinate::from_degrees(20.0, 10.0),
|
||||||
|
RadianCoordinate::from_degrees(20.0, 25.0),
|
||||||
|
RadianCoordinate::from_degrees(10.0, 15.0),
|
||||||
|
];
|
||||||
|
|
||||||
|
let outside1 = RadianCoordinate::from_degrees(30.0, 15.0);
|
||||||
|
let outside2 = RadianCoordinate::from_degrees(-10.0, 15.0);
|
||||||
|
|
||||||
|
let polygon =
|
||||||
|
Polygon::build_polygon(nodes, outside1, outside2).expect("error while building polygon");
|
||||||
|
|
||||||
|
let fc: FeatureCollection = polygon.into();
|
||||||
|
|
||||||
|
println!("{}", fc.to_string());
|
||||||
|
}
|
||||||
192
src/coordinates.rs
Normal file
192
src/coordinates.rs
Normal file
|
|
@ -0,0 +1,192 @@
|
||||||
|
use geojson::{Position, Value};
|
||||||
|
use std::convert::From;
|
||||||
|
use std::f64::consts::{FRAC_PI_2, PI, TAU};
|
||||||
|
use crate::ACCURACY_BOUNDARY;
|
||||||
|
|
||||||
|
/// Spherical coordinates in radians.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Default)]
|
||||||
|
pub struct RadianCoordinate {
|
||||||
|
pub lat: f64,
|
||||||
|
pub lon: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spherical coordinates in degrees.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Default)]
|
||||||
|
pub struct DegreeCoordinate {
|
||||||
|
pub lat: f64,
|
||||||
|
pub lon: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DegreeCoordinate> for RadianCoordinate {
|
||||||
|
fn from(other: DegreeCoordinate) -> Self {
|
||||||
|
RadianCoordinate::from_degrees(other.lat, other.lon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DegreeCoordinate> for geojson::Value {
|
||||||
|
fn from(coordinate: DegreeCoordinate) -> Self {
|
||||||
|
Value::Point(vec![coordinate.lat, coordinate.lon])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RadianCoordinate> for Position {
|
||||||
|
fn from(coordinate: RadianCoordinate) -> Self {
|
||||||
|
let coordinate = DegreeCoordinate::from(coordinate);
|
||||||
|
vec![coordinate.lat, coordinate.lon]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RadianCoordinate> for geojson::Value {
|
||||||
|
fn from(coordinate: RadianCoordinate) -> Self {
|
||||||
|
let degrees: DegreeCoordinate = coordinate.into();
|
||||||
|
degrees.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RadianCoordinate> for DegreeCoordinate {
|
||||||
|
fn from(other: RadianCoordinate) -> Self {
|
||||||
|
DegreeCoordinate::from_radians(other.lat, other.lon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RadianCoordinate {
|
||||||
|
/// Builds a RadianCoordinate from latitude and longitude given in
|
||||||
|
/// degrees.
|
||||||
|
pub fn from_degrees(lat: f64, lon: f64) -> Self {
|
||||||
|
let lat = lat / 90.0 * FRAC_PI_2;
|
||||||
|
let lon = normalize_lon(lon / 180.0 * PI);
|
||||||
|
|
||||||
|
RadianCoordinate { lat, lon }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// gives a normalizes version of the Coordinate
|
||||||
|
pub fn normalize(&self) -> RadianCoordinate {
|
||||||
|
RadianCoordinate {
|
||||||
|
lat: self.lat,
|
||||||
|
lon: normalize_lon(self.lon),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
};
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
bottom.atan2(top)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns `true` when the point is on a great circle with point `a` and `b`
|
||||||
|
/// or numerically very close to that
|
||||||
|
pub fn on_great_circle(&self, a: &RadianCoordinate, b: &RadianCoordinate) -> bool {
|
||||||
|
let lat_a = a.get_transformed_longitude(&self);
|
||||||
|
let lat_b = b.get_transformed_longitude(&self);
|
||||||
|
|
||||||
|
(lat_a - lat_b).abs().rem_euclid(PI) < ACCURACY_BOUNDARY
|
||||||
|
}
|
||||||
|
|
||||||
|
/// calculates whether `other` is antipodal to this point.
|
||||||
|
pub fn antipodal(&self, other: &RadianCoordinate) -> bool {
|
||||||
|
// if the distance between both points is very close to PI, they are
|
||||||
|
// antipodal
|
||||||
|
|
||||||
|
let c = self.distance_to(other);
|
||||||
|
|
||||||
|
let diff = (c - PI).abs();
|
||||||
|
|
||||||
|
diff < ACCURACY_BOUNDARY
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns the shortest distance to an other RadianCoordinate along
|
||||||
|
/// the surface of the unit-sphere in radians.
|
||||||
|
pub fn distance_to(&self, other: &RadianCoordinate) -> f64 {
|
||||||
|
// using the haversine formula
|
||||||
|
// if the distance between both points is very close to PI, they are
|
||||||
|
// antipodal
|
||||||
|
|
||||||
|
let delta_lat = other.lat - self.lat;
|
||||||
|
let delta_lon = other.lon - self.lon;
|
||||||
|
|
||||||
|
let a = (delta_lat / 2.0).sin().powi(2)
|
||||||
|
+ self.lat.cos() * other.lat.cos() * (delta_lon / 2.0).sin().powi(2);
|
||||||
|
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 {
|
||||||
|
/// Builds a DegreeCoordinate from latitude and longitude given in
|
||||||
|
/// radians.
|
||||||
|
pub fn from_radians(lat: f64, lon: f64) -> Self {
|
||||||
|
let lat = lat * 90.0 / FRAC_PI_2;
|
||||||
|
let lon = normalize_lon(lon) * 180.0 / PI;
|
||||||
|
|
||||||
|
DegreeCoordinate { lat, lon }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// normalizes longitude values given in radians to the range (-PI, PI]
|
||||||
|
pub fn normalize_lon(lon: f64) -> f64 {
|
||||||
|
// restrict values to -/+ TAU
|
||||||
|
let mut lon = lon % (TAU);
|
||||||
|
|
||||||
|
if lon <= -PI {
|
||||||
|
lon = TAU + lon;
|
||||||
|
}
|
||||||
|
|
||||||
|
if lon > PI {
|
||||||
|
lon = -TAU + lon;
|
||||||
|
}
|
||||||
|
|
||||||
|
lon
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_normalize_lon() {
|
||||||
|
assert!((normalize_lon(0.0) - 0.0).abs() < 10e-12);
|
||||||
|
assert!((normalize_lon(PI) - PI).abs() < 10e-12);
|
||||||
|
assert!((normalize_lon(-PI) - PI).abs() < 10e-12);
|
||||||
|
assert!((normalize_lon(TAU) - 0.0).abs() < 10e-12);
|
||||||
|
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);
|
||||||
|
}
|
||||||
6
src/lib.rs
Normal file
6
src/lib.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
pub mod coordinates;
|
||||||
|
pub mod polygon;
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod tests;
|
||||||
|
|
||||||
|
const ACCURACY_BOUNDARY: f64 = 1e-15;
|
||||||
415
src/polygon.rs
Normal file
415
src/polygon.rs
Normal file
|
|
@ -0,0 +1,415 @@
|
||||||
|
use crate::coordinates::{RadianCoordinate, normalize_lon};
|
||||||
|
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.
|
||||||
|
#[derive(Debug, Default, PartialEq, Clone)]
|
||||||
|
pub struct Polygon {
|
||||||
|
pub nodes: Vec<RadianCoordinate>,
|
||||||
|
pub outside_point_1: RadianCoordinate,
|
||||||
|
pub outside_point_2: RadianCoordinate,
|
||||||
|
pub bbox: BoundingBox,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A simple bounding box.
|
||||||
|
#[derive(Debug, Default, PartialEq, Clone)]
|
||||||
|
pub struct BoundingBox {
|
||||||
|
pub lat_min: f64,
|
||||||
|
pub lat_max: f64,
|
||||||
|
pub lon_min: f64,
|
||||||
|
pub lon_max: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Debug)]
|
||||||
|
pub enum PolygonVerificationError {
|
||||||
|
Antipodal,
|
||||||
|
OutSide1SameCircle,
|
||||||
|
OutSide2SameCircle,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Debug)]
|
||||||
|
pub enum ContainmentError {
|
||||||
|
Antipodal,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BoundingBox {
|
||||||
|
/// checks whether a given point is in the bounding box.
|
||||||
|
/// being on the border counts as being inside.
|
||||||
|
pub fn contains(&self, point: RadianCoordinate) -> bool {
|
||||||
|
let point = point.normalize();
|
||||||
|
|
||||||
|
self.lat_min <= point.lat
|
||||||
|
&& point.lat <= self.lat_max
|
||||||
|
&& self.lon_min <= point.lon
|
||||||
|
&& point.lon <= self.lon_max
|
||||||
|
}
|
||||||
|
|
||||||
|
/// builds a bounding box around the given coordinates.
|
||||||
|
pub fn from_nodes(nodes: &Vec<RadianCoordinate>) -> BoundingBox {
|
||||||
|
let mut bbox = BoundingBox {
|
||||||
|
lat_min: f64::MAX,
|
||||||
|
lat_max: f64::MIN,
|
||||||
|
lon_min: f64::MAX,
|
||||||
|
lon_max: f64::MIN,
|
||||||
|
};
|
||||||
|
|
||||||
|
for node in nodes.iter() {
|
||||||
|
bbox.lat_min = bbox.lat_min.min(node.lat);
|
||||||
|
bbox.lat_max = bbox.lat_max.max(node.lat);
|
||||||
|
bbox.lon_min = bbox.lon_min.min(node.lon);
|
||||||
|
bbox.lon_max = bbox.lon_max.max(node.lon);
|
||||||
|
}
|
||||||
|
|
||||||
|
bbox
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<BoundingBox> for Value {
|
||||||
|
fn from(bbox: BoundingBox) -> Self {
|
||||||
|
Value::Polygon(vec![vec![
|
||||||
|
RadianCoordinate {
|
||||||
|
lon: bbox.lon_min,
|
||||||
|
lat: bbox.lat_min,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
RadianCoordinate {
|
||||||
|
lon: bbox.lon_min,
|
||||||
|
lat: bbox.lat_max,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
RadianCoordinate {
|
||||||
|
lon: bbox.lon_max,
|
||||||
|
lat: bbox.lat_max,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
RadianCoordinate {
|
||||||
|
lon: bbox.lon_max,
|
||||||
|
lat: bbox.lat_min,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
RadianCoordinate {
|
||||||
|
lon: bbox.lon_min,
|
||||||
|
lat: bbox.lat_min,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
]])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Polygon> for FeatureCollection {
|
||||||
|
fn from(polygon: Polygon) -> Self {
|
||||||
|
let mut features = FeatureCollection {
|
||||||
|
bbox: None,
|
||||||
|
features: vec![],
|
||||||
|
foreign_members: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut values: Vec<Value> = Vec::default();
|
||||||
|
|
||||||
|
values.push(polygon.bbox.into());
|
||||||
|
values.push(polygon.outside_point_1.into());
|
||||||
|
values.push(polygon.outside_point_2.into());
|
||||||
|
|
||||||
|
// build the polygon
|
||||||
|
let mut nodes: Vec<Position> = Vec::default();
|
||||||
|
|
||||||
|
for node in polygon.nodes.into_iter() {
|
||||||
|
nodes.push(node.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
values.push(Value::Polygon(vec![nodes]));
|
||||||
|
|
||||||
|
for value in values.into_iter() {
|
||||||
|
features.features.push(Feature::from(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
features
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Polygon {
|
||||||
|
/// Builds a Polygon from the given nodes and 2 points that are outside of
|
||||||
|
/// the polygon.
|
||||||
|
///
|
||||||
|
pub fn build_polygon(
|
||||||
|
nodes: Vec<RadianCoordinate>,
|
||||||
|
point1: RadianCoordinate,
|
||||||
|
point2: RadianCoordinate,
|
||||||
|
) -> Result<Polygon, PolygonVerificationError> {
|
||||||
|
let bbox = BoundingBox::from_nodes(&nodes);
|
||||||
|
|
||||||
|
let polygon = Polygon {
|
||||||
|
bbox,
|
||||||
|
nodes,
|
||||||
|
outside_point_1: point1,
|
||||||
|
outside_point_2: point2,
|
||||||
|
};
|
||||||
|
|
||||||
|
polygon.verify()?;
|
||||||
|
Ok(polygon)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify(&self) -> Result<(), PolygonVerificationError> {
|
||||||
|
if self.outside_point_1.antipodal(&(self.outside_point_2)) {
|
||||||
|
return Err(PolygonVerificationError::Antipodal);
|
||||||
|
}
|
||||||
|
|
||||||
|
for point in 0..(self.nodes.len() - 1) {
|
||||||
|
let a = self.nodes[point];
|
||||||
|
let b = self.nodes[point + 1];
|
||||||
|
|
||||||
|
if self.outside_point_1.on_great_circle(&a, &b) {
|
||||||
|
return Err(PolygonVerificationError::OutSide1SameCircle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.outside_point_2.on_great_circle(&a, &b) {
|
||||||
|
return Err(PolygonVerificationError::OutSide2SameCircle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if `point` is inside the polygon.
|
||||||
|
/// Being on the border of the polygon counts as being inside.
|
||||||
|
/// 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 {
|
||||||
|
let first_check = self.contains_internal(point, &(self.outside_point_1));
|
||||||
|
|
||||||
|
match first_check {
|
||||||
|
Ok(result) => result,
|
||||||
|
Err(ContainmentError::Antipodal) => self
|
||||||
|
.contains_internal(point, &(self.outside_point_2))
|
||||||
|
.unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contains_internal(
|
||||||
|
&self,
|
||||||
|
point: &RadianCoordinate,
|
||||||
|
outside: &RadianCoordinate,
|
||||||
|
) -> Result<bool, ContainmentError> {
|
||||||
|
if point.antipodal(outside) {
|
||||||
|
return Err(ContainmentError::Antipodal);
|
||||||
|
}
|
||||||
|
|
||||||
|
if point == outside {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut crossings = 0;
|
||||||
|
|
||||||
|
for i in 0..(self.nodes.len()-1) {
|
||||||
|
let point_a = self.nodes[i];
|
||||||
|
|
||||||
|
// check whether the point is the vertex
|
||||||
|
if *point == point_a {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
let point_b = self.nodes[i + 1];
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(crossings % 2 == 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use crate::coordinates::RadianCoordinate;
|
||||||
|
use crate::polygon::{Polygon, PolygonVerificationError};
|
||||||
|
use std::f64::consts::{FRAC_PI_2, FRAC_PI_4, FRAC_PI_8, PI};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verify() {
|
||||||
|
let nodes = vec![
|
||||||
|
RadianCoordinate { lat: 0.0, lon: 0.0 },
|
||||||
|
RadianCoordinate {
|
||||||
|
lat: FRAC_PI_8,
|
||||||
|
lon: 0.0,
|
||||||
|
},
|
||||||
|
RadianCoordinate {
|
||||||
|
lat: 0.0,
|
||||||
|
lon: FRAC_PI_8,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
//p1 and p2 are antipodal
|
||||||
|
//both are on a great circle with the first segment
|
||||||
|
let p1 = RadianCoordinate { lat: 0.5, lon: 0.0 };
|
||||||
|
let p2 = RadianCoordinate { lat: -0.5, lon: PI };
|
||||||
|
|
||||||
|
// p3 is neither antipodal nor on a great circle with any of the segments
|
||||||
|
let p3 = RadianCoordinate {
|
||||||
|
lat: 0.2,
|
||||||
|
lon: -0.2,
|
||||||
|
};
|
||||||
|
let p4 = RadianCoordinate {
|
||||||
|
lat: 0.3,
|
||||||
|
lon: -0.3,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
Polygon::build_polygon(nodes.clone(), p1, p2),
|
||||||
|
Err(PolygonVerificationError::Antipodal)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Polygon::build_polygon(nodes.clone(), p1, p3),
|
||||||
|
Err(PolygonVerificationError::OutSide1SameCircle)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Polygon::build_polygon(nodes.clone(), p3, p2),
|
||||||
|
Err(PolygonVerificationError::OutSide2SameCircle)
|
||||||
|
);
|
||||||
|
assert!(Polygon::build_polygon(nodes.clone(), p3, p4).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_degrees() {
|
||||||
|
let test = RadianCoordinate::from_degrees(45.0, 90.0);
|
||||||
|
|
||||||
|
assert!((test.lat - FRAC_PI_4).abs() < 1e-10);
|
||||||
|
assert!((test.lon - FRAC_PI_2).abs() < 1e-10);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn point_in_polygon() {
|
||||||
|
let polygon = Polygon::build_polygon(
|
||||||
|
vec![
|
||||||
|
RadianCoordinate::from_degrees(0.0, 0.0),
|
||||||
|
RadianCoordinate::from_degrees(22.5, 0.0),
|
||||||
|
RadianCoordinate::from_degrees(0.0, 22.5),
|
||||||
|
RadianCoordinate::from_degrees(0.0, 0.0),
|
||||||
|
],
|
||||||
|
RadianCoordinate::from_degrees(60.0, 20.0),
|
||||||
|
RadianCoordinate::from_degrees(18.104087015773956, -39.08935546875),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(polygon.is_ok());
|
||||||
|
let polygon = polygon.unwrap();
|
||||||
|
|
||||||
|
use geojson::FeatureCollection;
|
||||||
|
let features = FeatureCollection::from(polygon.clone());
|
||||||
|
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 outside_vertex_intersect = RadianCoordinate {
|
||||||
|
lat: -0.1,
|
||||||
|
lon: 0.1,
|
||||||
|
};
|
||||||
|
|
||||||
|
let outside_vertex_intersect_2 = RadianCoordinate {
|
||||||
|
lat: -0.1,
|
||||||
|
lon: -0.1,
|
||||||
|
};
|
||||||
|
|
||||||
|
//assert_eq!(polygon.contains(&in_point), true);
|
||||||
|
assert_eq!(polygon.contains(&out_point), false);
|
||||||
|
assert_eq!(polygon.contains(&antipodal_point), false);
|
||||||
|
assert_eq!(
|
||||||
|
polygon.contains_internal(&outside_vertex_intersect, &(polygon.outside_point_1)),
|
||||||
|
Ok(false)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
polygon.contains_internal(&in_point, &outside_vertex_intersect_2),
|
||||||
|
Ok(true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn point_in_polygon2() {
|
||||||
|
let polygon = Polygon::build_polygon(
|
||||||
|
vec![
|
||||||
|
RadianCoordinate::from_degrees(0.0, 180.0),
|
||||||
|
RadianCoordinate::from_degrees(45.0, 180.),
|
||||||
|
RadianCoordinate::from_degrees(45.0, 90.0),
|
||||||
|
RadianCoordinate::from_degrees(0.0, 180.0),
|
||||||
|
],
|
||||||
|
RadianCoordinate::from_degrees(1.0, -1.0),
|
||||||
|
RadianCoordinate::from_degrees(2.0, -1.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(polygon.is_ok());
|
||||||
|
let polygon = polygon.unwrap();
|
||||||
|
|
||||||
|
let in_point = RadianCoordinate::from_degrees(40.0, 135.0);
|
||||||
|
assert_eq!(polygon.contains(&in_point), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/tests/bounding_box.rs
Normal file
43
src/tests/bounding_box.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
use crate::coordinates::RadianCoordinate;
|
||||||
|
use crate::polygon::BoundingBox;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_contains() {
|
||||||
|
let bbox = BoundingBox {
|
||||||
|
lat_min: -1.0,
|
||||||
|
lat_max: 1.0,
|
||||||
|
lon_min: -1.0,
|
||||||
|
lon_max: 1.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(bbox.contains(RadianCoordinate { lat: 0.0, lon: 0.0 }));
|
||||||
|
assert!(bbox.contains(RadianCoordinate { lat: 1.0, lon: 0.0 }));
|
||||||
|
assert!(!bbox.contains(RadianCoordinate { lat: 2.0, lon: 0.0 }));
|
||||||
|
assert!(!bbox.contains(RadianCoordinate { lat: 0.0, lon: 2.0 }));
|
||||||
|
assert!(!bbox.contains(RadianCoordinate {
|
||||||
|
lat: -2.0,
|
||||||
|
lon: 2.0
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn create_bbox() {
|
||||||
|
let nodes = vec![
|
||||||
|
RadianCoordinate { lat: 1.0, lon: 1.0 },
|
||||||
|
RadianCoordinate {
|
||||||
|
lat: -1.5,
|
||||||
|
lon: 0.0,
|
||||||
|
},
|
||||||
|
RadianCoordinate {
|
||||||
|
lat: -0.5,
|
||||||
|
lon: -3.0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let bbox = BoundingBox::from_nodes(&nodes);
|
||||||
|
|
||||||
|
assert_eq!(bbox.lat_max, 1.0);
|
||||||
|
assert_eq!(bbox.lat_min, -1.5);
|
||||||
|
assert_eq!(bbox.lon_max, 1.0);
|
||||||
|
assert_eq!(bbox.lon_min, -3.0);
|
||||||
|
}
|
||||||
122
src/tests/coordinates.rs
Normal file
122
src/tests/coordinates.rs
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
use crate::coordinates::RadianCoordinate;
|
||||||
|
use std::f64::consts::{PI, FRAC_PI_2, FRAC_PI_4, FRAC_PI_8};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn on_great_circle() {
|
||||||
|
let a = RadianCoordinate { lat: 0.0, lon: 0.0 };
|
||||||
|
let b = RadianCoordinate { lat: 0.0, lon: 1.0 };
|
||||||
|
|
||||||
|
// on the circle
|
||||||
|
let p1 = RadianCoordinate { lat: 0.0, lon: 0.5 };
|
||||||
|
let p2 = RadianCoordinate {
|
||||||
|
lat: 0.0,
|
||||||
|
lon: -3.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// not on the circle
|
||||||
|
let p3 = RadianCoordinate { lat: 0.4, lon: 0.1 };
|
||||||
|
|
||||||
|
assert!(p1.on_great_circle(&a, &b), "p1 not on great circle");
|
||||||
|
assert!(p2.on_great_circle(&a, &b), "p2 not on great circle");
|
||||||
|
assert!(!p3.on_great_circle(&a, &b), "p3 on great circle");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn antipodal() {
|
||||||
|
let point1 = RadianCoordinate { lat: 0.0, lon: 0.0 };
|
||||||
|
let point2 = RadianCoordinate { lat: 0.0, lon: PI };
|
||||||
|
let point3 = RadianCoordinate {
|
||||||
|
lat: FRAC_PI_2,
|
||||||
|
lon: 0.0,
|
||||||
|
};
|
||||||
|
let point4 = RadianCoordinate {
|
||||||
|
lat: -FRAC_PI_2,
|
||||||
|
lon: 0.0,
|
||||||
|
};
|
||||||
|
let point5 = RadianCoordinate {
|
||||||
|
lat: -FRAC_PI_2,
|
||||||
|
lon: 42.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let point6 = RadianCoordinate{lat: -0.1, lon: 0.1};
|
||||||
|
let point7 = RadianCoordinate{lat: 0.1, lon: -PI+0.1};
|
||||||
|
|
||||||
|
assert!(point1.antipodal(&point2));
|
||||||
|
assert!(point3.antipodal(&point4));
|
||||||
|
assert!(point3.antipodal(&point5));
|
||||||
|
assert!(!point1.antipodal(&point3));
|
||||||
|
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));
|
||||||
|
}
|
||||||
2
src/tests/mod.rs
Normal file
2
src/tests/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod bounding_box;
|
||||||
|
pub mod coordinates;
|
||||||
6
src/tests/point_in_polygon.rs
Normal file
6
src/tests/point_in_polygon.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
use crate::polygon::Polygon;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_point_in_polygon() {
|
||||||
|
|
||||||
|
}
|
||||||
0
src/tests/polygon.rs
Normal file
0
src/tests/polygon.rs
Normal file
Loading…
Add table
Add a link
Reference in a new issue