use std::cmp::Ordering;
use std::collections::BinaryHeap;
use super::{Coord, Geometry};
const MAX_STEPS: usize = 10_000;
pub fn polylabel(rings: &[Vec<Coord>], precision: f64) -> Option<Coord> {
polylabel_with_distance(rings, precision).map(|(p, _)| p)
}
pub fn polylabel_with_distance(rings: &[Vec<Coord>], precision: f64) -> Option<(Coord, f64)> {
let outer = rings.first().filter(|r| r.len() >= 3)?;
let (mut x0, mut y0, mut x1, mut y1) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
for &[x, y] in outer {
x0 = x0.min(x);
y0 = y0.min(y);
x1 = x1.max(x);
y1 = y1.max(y);
}
let (w, h) = (x1 - x0, y1 - y0);
let size = w.min(h);
if size.is_nan() || size <= 0.0 {
return Some(([x0, y0], 0.0));
}
let precision = if precision > 0.0 {
precision
} else {
size * 1e-3
};
let mut heap = BinaryHeap::new();
let mut half = size / 2.0;
let mut x = x0;
while x < x1 {
let mut y = y0;
while y < y1 {
heap.push(Cell::new(x + half, y + half, half, rings));
y += size;
}
x += size;
}
let [cx, cy] = ring_centroid(outer);
let mut best = Cell::new(cx, cy, 0.0, rings);
let bbox = Cell::new(x0 + w / 2.0, y0 + h / 2.0, 0.0, rings);
if bbox.d > best.d {
best = bbox;
}
let mut steps = 0;
while let Some(c) = heap.pop() {
let (cx, cy, ch, cmax) = (c.x, c.y, c.h, c.max);
if c.d > best.d {
best = c;
}
if cmax - best.d <= precision {
continue;
}
steps += 1;
if steps > MAX_STEPS {
break;
}
half = ch / 2.0;
for (dx, dy) in [(-1.0, -1.0), (1.0, -1.0), (-1.0, 1.0), (1.0, 1.0)] {
heap.push(Cell::new(cx + dx * half, cy + dy * half, half, rings));
}
}
Some(([best.x, best.y], best.d))
}
impl Geometry {
pub fn label_point(&self, precision: f64) -> Option<Coord> {
if let Some(p) = first_point(self) {
return Some(p);
}
let mut best: Option<(f64, &Vec<Vec<Coord>>)> = None;
largest_polygon(self, &mut best);
best.and_then(|(_, rings)| polylabel(rings, precision))
}
}
fn first_point(g: &Geometry) -> Option<Coord> {
match g {
Geometry::Point(c) => Some(*c),
Geometry::MultiPoint(cs) => cs.first().copied(),
Geometry::Collection(gs) => gs.iter().find_map(first_point),
_ => None,
}
}
fn largest_polygon<'a>(g: &'a Geometry, best: &mut Option<(f64, &'a Vec<Vec<Coord>>)>) {
let mut consider = |rings: &'a Vec<Vec<Coord>>| {
let a = rings.first().map_or(0.0, |r| ring_area(r));
if best.as_ref().is_none_or(|b| a > b.0) {
*best = Some((a, rings));
}
};
match g {
Geometry::Polygon(rings) => consider(rings),
Geometry::MultiPolygon(ps) => ps.iter().for_each(consider),
Geometry::Collection(gs) => gs.iter().for_each(|g| largest_polygon(g, best)),
_ => {}
}
}
pub(crate) fn ring_area(r: &[Coord]) -> f64 {
let n = r.len();
if n < 3 {
return 0.0;
}
let mut s = 0.0;
let mut j = n - 1;
for i in 0..n {
s += (r[j][0] + r[i][0]) * (r[j][1] - r[i][1]);
j = i;
}
(s / 2.0).abs()
}
pub(crate) fn ring_centroid(r: &[Coord]) -> Coord {
let (mut a, mut cx, mut cy) = (0.0, 0.0, 0.0);
let n = r.len();
if n == 0 {
return [0.0, 0.0];
}
let mut j = n - 1;
for i in 0..n {
let (p, q) = (r[i], r[j]);
let f = p[0] * q[1] - q[0] * p[1];
cx += (p[0] + q[0]) * f;
cy += (p[1] + q[1]) * f;
a += f * 3.0;
j = i;
}
if a.abs() < 1e-12 {
return r[0];
}
[cx / a, cy / a]
}
fn signed_dist(p: Coord, rings: &[Vec<Coord>]) -> f64 {
let mut inside = false;
let mut best = f64::INFINITY;
for r in rings {
let n = r.len();
if n < 2 {
continue;
}
let mut j = n - 1;
for i in 0..n {
let (a, b) = (r[i], r[j]);
if (a[1] > p[1]) != (b[1] > p[1])
&& p[0] < (b[0] - a[0]) * (p[1] - a[1]) / (b[1] - a[1]) + a[0]
{
inside = !inside;
}
best = best.min(seg_dist2(p, a, b));
j = i;
}
}
let d = best.sqrt();
if inside {
d
} else {
-d
}
}
fn seg_dist2(p: Coord, a: Coord, b: Coord) -> f64 {
let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
let l2 = dx * dx + dy * dy;
let t = if l2 > 0.0 {
(((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / l2).clamp(0.0, 1.0)
} else {
0.0
};
let (ex, ey) = (a[0] + t * dx - p[0], a[1] + t * dy - p[1]);
ex * ex + ey * ey
}
struct Cell {
x: f64,
y: f64,
h: f64,
d: f64,
max: f64,
}
impl Cell {
fn new(x: f64, y: f64, h: f64, rings: &[Vec<Coord>]) -> Cell {
let d = signed_dist([x, y], rings);
Cell {
x,
y,
h,
d,
max: d + h * std::f64::consts::SQRT_2,
}
}
}
impl PartialEq for Cell {
fn eq(&self, o: &Self) -> bool {
self.max == o.max
}
}
impl Eq for Cell {}
impl PartialOrd for Cell {
fn partial_cmp(&self, o: &Self) -> Option<Ordering> {
Some(self.cmp(o))
}
}
impl Ord for Cell {
fn cmp(&self, o: &Self) -> Ordering {
self.max.total_cmp(&o.max)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::spatial::parse_wkt;
fn c_shape() -> Vec<Coord> {
vec![
[0.0, 0.0],
[10.0, 0.0],
[10.0, 2.0],
[2.0, 2.0],
[2.0, 8.0],
[10.0, 8.0],
[10.0, 10.0],
[0.0, 10.0],
]
}
#[test]
fn the_pole_of_a_c_shape_is_inside_it() {
let c = c_shape();
let ([x, y], d) = polylabel_with_distance(std::slice::from_ref(&c), 0.1).unwrap();
assert!(signed_dist([x, y], std::slice::from_ref(&c)) > 0.0);
assert!(x < 2.0 && d > 0.8, "({x},{y}) in the spine, d = {d}");
assert!(signed_dist(ring_centroid(&c), &[c]) < 0.0);
}
#[test]
fn a_squares_pole_is_its_centre() {
let sq = vec![[0.0, 0.0], [4.0, 0.0], [4.0, 4.0], [0.0, 4.0]];
let ([x, y], d) = polylabel_with_distance(&[sq], 0.01).unwrap();
assert!((x - 2.0).abs() < 0.05 && (y - 2.0).abs() < 0.05);
assert!((d - 2.0).abs() < 0.05);
}
#[test]
fn a_hole_is_avoided() {
let outer = vec![[0.0, 0.0], [8.0, 0.0], [8.0, 8.0], [0.0, 8.0]];
let hole = vec![[2.0, 2.0], [6.0, 2.0], [6.0, 6.0], [2.0, 6.0]];
let [x, y] = polylabel(&[outer.clone(), hole.clone()], 0.1).unwrap();
assert!(signed_dist([x, y], &[outer, hole]) > 0.0, "({x},{y})");
assert!(!((2.0..=6.0).contains(&x) && (2.0..=6.0).contains(&y)));
}
#[test]
fn degenerate_polygons_return_a_corner() {
let line = vec![[0.0, 0.0], [1.0, 0.0], [2.0, 0.0], [0.0, 0.0]];
assert_eq!(polylabel(&[line], 1.0), Some([0.0, 0.0]));
let spike = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [1.0, 0.0], [0.0, 0.0]];
assert_eq!(polylabel(&[spike], 1.0), Some([0.0, 0.0]));
}
#[test]
fn it_works_in_any_plane() {
let m: Vec<Coord> = c_shape()
.iter()
.map(|[x, y]| [x * 1e5 + 4e5, y * 1e5 + 5.8e6])
.collect();
let [x, _] = polylabel(&[m], 100.0).unwrap();
assert!(x < 2e5 + 4e5, "{x}");
}
#[test]
fn no_exterior_no_pole() {
assert_eq!(polylabel(&[], 1.0), None);
assert_eq!(polylabel(&[vec![[0.0, 0.0], [1.0, 1.0]]], 1.0), None);
}
#[test]
fn label_point_of_each_geometry_type() {
let p = |s: &str| parse_wkt(s).unwrap();
assert_eq!(p("POINT (3 4)").label_point(0.1), Some([3.0, 4.0]));
assert_eq!(
p("MULTIPOINT ((1 2), (5 6))").label_point(0.1),
Some([1.0, 2.0])
);
assert_eq!(p("LINESTRING (0 0, 1 1)").label_point(0.1), None);
let [x, y] =
p("MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)), ((10 10, 20 10, 20 20, 10 20, 10 10)))")
.label_point(0.01)
.unwrap();
assert!(
(x - 15.0).abs() < 0.1 && (y - 15.0).abs() < 0.1,
"({x},{y})"
);
assert_eq!(Geometry::Empty.label_point(0.1), None);
}
}