use geo::{Coord, CoordsIter, Distance, Haversine, Line, Point};
use geohash::{decode_bbox, encode, neighbors};
use std::cmp::Ordering;
fn calculate_perpendicular_point(point: &Point<f64>, line: &Line<f64>) -> Point<f64> {
let start = line.start;
let end = line.end;
if (start.x - end.x).abs() < f64::EPSILON && (start.y - end.y).abs() < f64::EPSILON {
return Point::new(start.x, start.y);
}
let line_vector = Coord {
x: end.x - start.x,
y: end.y - start.y,
};
let point_vector = Coord {
x: point.x() - start.x,
y: point.y() - start.y,
};
let t = (point_vector.x * line_vector.x + point_vector.y * line_vector.y)
/ (line_vector.x * line_vector.x + line_vector.y * line_vector.y);
let foot_x = start.x + t * line_vector.x;
let foot_y = start.y + t * line_vector.y;
Point::new(foot_x, foot_y)
}
fn distance_to_line(point: &Point<f64>, line: &Line<f64>) -> f64 {
let destination = calculate_perpendicular_point(point, line);
Haversine::distance(point.clone(), destination)
}
fn find_nearest_point_distance(
geohash_str: &str,
point: &Point<f64>,
) -> Result<(Coord, f64), crate::GeohashRTreeError> {
let rec = decode_bbox(geohash_str)?;
let min = rec
.coords_iter()
.map(|c| {
let distance = Haversine::distance(point.clone(), Point(c.clone()));
(c, distance)
})
.min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal))
.unwrap();
Ok(min)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Direction {
C,
N,
NE,
E,
SE,
S,
SW,
W,
NW,
}
pub fn sort_geohash_neighbors(
point: Point<f64>,
geohash_precision: usize,
) -> Result<Vec<(String, f64, Direction)>, crate::GeohashRTreeError> {
let geohash_str = encode(Coord::from(point), geohash_precision)?;
let nbs = neighbors(&geohash_str)?;
let ne = find_nearest_point_distance(&nbs.ne, &point)?;
let se = find_nearest_point_distance(&nbs.se, &point)?;
let nw = find_nearest_point_distance(&nbs.nw, &point)?;
let sw: (Coord, f64) = find_nearest_point_distance(&nbs.sw, &point)?;
let e = distance_to_line(&point, &Line::new(ne.0.clone(), se.0.clone()));
let s = distance_to_line(&point, &Line::new(se.0.clone(), sw.0.clone()));
let w = distance_to_line(&point, &Line::new(nw.0.clone(), sw.0.clone()));
let n = distance_to_line(&point, &Line::new(nw.0.clone(), ne.0.clone()));
let mut sorted_neighbors = vec![
(geohash_str, 0., Direction::C),
(nbs.ne, ne.1, Direction::NE),
(nbs.se, se.1, Direction::SE),
(nbs.sw, sw.1, Direction::SW),
(nbs.nw, nw.1, Direction::NW),
(nbs.e, e, Direction::E),
(nbs.s, s, Direction::S),
(nbs.w, w, Direction::W),
(nbs.n, n, Direction::N),
];
sorted_neighbors.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
Ok(sorted_neighbors)
}
#[cfg(test)]
mod tests {
use super::*;
use geo::{Line, Point};
#[test]
fn test_perpendicular_point() {
let point = Point::new(1.0, 1.0);
let line = Line::new(Point::new(0.0, 0.0), Point::new(2.0, 0.0));
let foot = calculate_perpendicular_point(&point, &line);
assert!((foot.x() - 1.0).abs() < f64::EPSILON);
assert!(foot.y().abs() < f64::EPSILON);
let point2 = Point::new(1.0, 1.0);
let line2 = Line::new(Point::new(0.0, 0.0), Point::new(0.0, 2.0));
let foot2 = calculate_perpendicular_point(&point2, &line2);
assert!(foot2.x().abs() < f64::EPSILON);
assert!((foot2.y() - 1.0).abs() < f64::EPSILON);
let point3 = Point::new(0.0, 1.0);
let line3 = Line::new(Point::new(0.0, 0.0), Point::new(1.0, 1.0));
let foot3 = calculate_perpendicular_point(&point3, &line3);
assert!((foot3.x() - 0.5).abs() < f64::EPSILON);
assert!((foot3.y() - 0.5).abs() < f64::EPSILON);
}
#[test]
fn test_sort_geohash_neighbors() {
let point = Point::new(114.432292, 30.545003);
let geohash_precision = 5usize;
let neighbors = sort_geohash_neighbors(point, geohash_precision).unwrap();
assert_eq!(neighbors[0], ("wt3mg".into(), 0.0, Direction::C));
assert_eq!(neighbors.last().unwrap().0, "wt3q4");
assert_eq!(neighbors.last().unwrap().2, Direction::NW);
}
}