use rstar::{PointDistance, RTree, RTreeObject, AABB};
use crate::config::BBox;
use crate::geo::Laea;
pub const CROP_MARGIN_DEG: f64 = 5.0;
pub type Rings = Vec<Vec<(f64, f64)>>;
pub fn point_seg_dist2(px: f64, py: f64, ax: f64, ay: f64, bx: f64, by: f64) -> f64 {
let (dx, dy) = (bx - ax, by - ay);
let len2 = dx * dx + dy * dy;
let (cx, cy) = if len2 <= 0.0 {
(ax, ay) } else {
let t = (((px - ax) * dx + (py - ay) * dy) / len2).clamp(0.0, 1.0);
(ax + t * dx, ay + t * dy)
};
let (ex, ey) = (px - cx, py - cy);
ex * ex + ey * ey
}
pub fn expand(b: &BBox, m: f64) -> BBox {
BBox {
min_lon: b.min_lon - m,
max_lon: b.max_lon + m,
min_lat: b.min_lat - m,
max_lat: b.max_lat + m,
}
}
pub struct TaggedSegment {
pub ax: f64,
pub ay: f64,
pub bx: f64,
pub by: f64,
pub tag: usize,
}
impl RTreeObject for TaggedSegment {
type Envelope = AABB<[f64; 2]>;
fn envelope(&self) -> Self::Envelope {
AABB::from_corners(
[self.ax.min(self.bx), self.ay.min(self.by)],
[self.ax.max(self.bx), self.ay.max(self.by)],
)
}
}
impl PointDistance for TaggedSegment {
fn distance_2(&self, point: &[f64; 2]) -> f64 {
point_seg_dist2(point[0], point[1], self.ax, self.ay, self.bx, self.by)
}
}
pub fn point_in_rings(lon: f64, lat: f64, rings: &[Vec<(f64, f64)>]) -> bool {
let mut inside = false;
for ring in rings {
let n = ring.len();
if n < 3 {
continue;
}
let mut j = n - 1;
for i in 0..n {
let (xi, yi) = ring[i];
let (xj, yj) = ring[j];
if (yi > lat) != (yj > lat) && lon < (xj - xi) * (lat - yi) / (yj - yi) + xi {
inside = !inside;
}
j = i;
}
}
inside
}
fn rings_bbox(rings: &Rings) -> Option<BBox> {
let mut b: Option<BBox> = None;
for &(lon, lat) in rings.iter().flatten() {
b = Some(match b {
None => BBox { min_lon: lon, max_lon: lon, min_lat: lat, max_lat: lat },
Some(b) => BBox {
min_lon: b.min_lon.min(lon),
max_lon: b.max_lon.max(lon),
min_lat: b.min_lat.min(lat),
max_lat: b.max_lat.max(lat),
},
});
}
b
}
struct PolyBBox {
aabb: AABB<[f64; 2]>,
idx: usize,
}
impl RTreeObject for PolyBBox {
type Envelope = AABB<[f64; 2]>;
fn envelope(&self) -> Self::Envelope {
self.aabb
}
}
pub struct PolygonIndex<A> {
feats: Vec<(Rings, A)>,
bbox_tree: RTree<PolyBBox>,
seg_tree: RTree<TaggedSegment>,
proj: Laea,
}
impl<A> PolygonIndex<A> {
pub fn build(feats: Vec<(Rings, A)>, region: BBox, margin_deg: f64, proj: Laea) -> Self {
let crop = expand(®ion, margin_deg);
let kept: Vec<(Rings, A)> = feats
.into_iter()
.filter(|(rings, _)| match rings_bbox(rings) {
Some(b) => {
b.max_lon >= crop.min_lon
&& b.min_lon <= crop.max_lon
&& b.max_lat >= crop.min_lat
&& b.min_lat <= crop.max_lat
}
None => false,
})
.collect();
let mut boxes = Vec::with_capacity(kept.len());
let mut segs = Vec::new();
for (idx, (rings, _)) in kept.iter().enumerate() {
let b = rings_bbox(rings).expect("kept features have vertices");
boxes.push(PolyBBox {
aabb: AABB::from_corners([b.min_lon, b.min_lat], [b.max_lon, b.max_lat]),
idx,
});
for ring in rings {
for w in ring.windows(2) {
let (ax, ay) = proj.forward(w[0].0, w[0].1);
let (bx, by) = proj.forward(w[1].0, w[1].1);
segs.push(TaggedSegment { ax, ay, bx, by, tag: idx });
}
}
}
PolygonIndex {
feats: kept,
bbox_tree: RTree::bulk_load(boxes),
seg_tree: RTree::bulk_load(segs),
proj,
}
}
pub fn locate(&self, lon: f64, lat: f64) -> Option<&A> {
let q = AABB::from_point([lon, lat]);
for cand in self.bbox_tree.locate_in_envelope_intersecting(q) {
if point_in_rings(lon, lat, &self.feats[cand.idx].0) {
return Some(&self.feats[cand.idx].1);
}
}
let (x, y) = self.proj.forward(lon, lat);
self.seg_tree
.nearest_neighbor([x, y])
.map(|s| &self.feats[s.tag].1)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn even_odd_handles_holes() {
let rings: Rings = vec![
vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0), (0.0, 0.0)],
vec![(4.0, 4.0), (6.0, 4.0), (6.0, 6.0), (4.0, 6.0), (4.0, 4.0)],
];
assert!(point_in_rings(2.0, 2.0, &rings)); assert!(!point_in_rings(5.0, 5.0, &rings)); assert!(!point_in_rings(20.0, 5.0, &rings)); }
#[test]
fn point_seg_dist2_clamps_to_endpoints() {
assert!((point_seg_dist2(0.0, 1.0, -1.0, 0.0, 1.0, 0.0) - 1.0).abs() < 1e-12);
assert!((point_seg_dist2(3.0, 0.0, -1.0, 0.0, 1.0, 0.0) - 4.0).abs() < 1e-12);
assert!((point_seg_dist2(1.0, 1.0, 0.0, 0.0, 0.0, 0.0) - 2.0).abs() < 1e-12);
}
}