use geo_types::{Coord, Geometry, Polygon, Rect};
use crate::geometry_adapter::{envelope_internal, is_geometry_empty};
pub(crate) struct InteriorPointArea {
interior_point: Option<Coord<f64>>,
max_width: f64,
}
impl InteriorPointArea {
pub(crate) fn new(g: &Geometry<f64>) -> Self {
let mut int_pt = Self {
interior_point: None,
max_width: -1.0,
};
int_pt.process(g);
int_pt
}
pub(crate) fn get_interior_point(&self) -> Option<Coord<f64>> {
self.interior_point
}
fn process(&mut self, geom: &Geometry<f64>) {
if is_geometry_empty(geom) {
return;
}
match geom {
Geometry::Polygon(p) => self.process_polygon(p),
Geometry::MultiPolygon(mp) => {
for p in &mp.0 {
self.process_polygon(p);
}
}
Geometry::GeometryCollection(gc) => {
for g in &gc.0 {
self.process(g);
}
}
_ => {}
}
}
fn process_polygon(&mut self, polygon: &Polygon<f64>) {
let mut int_pt_poly = InteriorPointPolygon::new(polygon);
int_pt_poly.process();
let width = int_pt_poly.get_width();
if width > self.max_width {
self.max_width = width;
self.interior_point = int_pt_poly.get_interior_point();
}
}
}
fn avg(a: f64, b: f64) -> f64 {
(a + b) / 2.0
}
pub(crate) fn interior_point_area(geom: &Geometry<f64>) -> Option<Coord<f64>> {
let int_pt = InteriorPointArea::new(geom);
int_pt.get_interior_point()
}
pub(crate) struct InteriorPointPolygon<'a> {
polygon: &'a Polygon<f64>,
shell_envelope: Option<Rect<f64>>,
interior_point_y: f64,
interior_section_width: f64,
interior_point: Option<Coord<f64>>,
}
impl<'a> InteriorPointPolygon<'a> {
pub(crate) fn new(polygon: &'a Polygon<f64>) -> Self {
let shell_envelope = envelope_internal(&polygon.exterior().0);
Self {
polygon,
shell_envelope,
interior_point_y: get_scan_line_y(polygon, shell_envelope),
interior_section_width: 0.0,
interior_point: None,
}
}
pub(crate) fn get_interior_point(&self) -> Option<Coord<f64>> {
self.interior_point
}
pub(crate) fn get_width(&self) -> f64 {
self.interior_section_width
}
pub(crate) fn process(&mut self) {
let Some(&first) = self.polygon.exterior().0.first() else {
return;
};
self.interior_point = Some(first);
let mut crossings: Vec<f64> = Vec::new();
self.scan_ring(
&self.polygon.exterior().0,
self.shell_envelope,
&mut crossings,
);
for ring in self.polygon.interiors() {
self.scan_ring(&ring.0, envelope_internal(&ring.0), &mut crossings);
}
self.find_best_midpoint(&mut crossings);
}
fn scan_ring(&self, ring: &[Coord<f64>], env: Option<Rect<f64>>, crossings: &mut Vec<f64>) {
let Some(env) = env else {
return;
};
if !Self::intersects_horizontal_line_envelope(&env, self.interior_point_y) {
return;
}
for i in 1..ring.len() {
let pt_prev = ring[i - 1];
let pt = ring[i];
Self::add_edge_crossing(pt_prev, pt, self.interior_point_y, crossings);
}
}
fn add_edge_crossing(p0: Coord<f64>, p1: Coord<f64>, scan_y: f64, crossings: &mut Vec<f64>) {
if !Self::intersects_horizontal_line_coordinate(p0, p1, scan_y) {
return;
}
if !Self::is_edge_crossing_counted(p0, p1, scan_y) {
return;
}
let x_int = Self::intersection(p0, p1, scan_y);
crossings.push(x_int);
}
fn find_best_midpoint(&mut self, crossings: &mut [f64]) {
if crossings.is_empty() {
return;
}
assert!(
crossings.len() % 2 == 0,
"Interior Point robustness failure: odd number of scanline crossings"
);
crossings.sort_by(|a, b| a.partial_cmp(b).unwrap());
for pair in crossings.chunks_exact(2) {
let x1 = pair[0];
let x2 = pair[1];
let width = x2 - x1;
if width > self.interior_section_width {
self.interior_section_width = width;
let interior_point_x = avg(x1, x2);
self.interior_point = Some(Coord {
x: interior_point_x,
y: self.interior_point_y,
});
}
}
}
fn is_edge_crossing_counted(p0: Coord<f64>, p1: Coord<f64>, scan_y: f64) -> bool {
let y0 = p0.y;
let y1 = p1.y;
if y0 == y1 {
return false;
}
if y0 == scan_y && y1 < scan_y {
return false;
}
if y1 == scan_y && y0 < scan_y {
return false;
}
true
}
fn intersection(p0: Coord<f64>, p1: Coord<f64>, y: f64) -> f64 {
let x0 = p0.x;
let x1 = p1.x;
if x0 == x1 {
return x0;
}
let seg_dx = x1 - x0;
let seg_dy = p1.y - p0.y;
let m = seg_dy / seg_dx;
x0 + (y - p0.y) / m
}
fn intersects_horizontal_line_envelope(env: &Rect<f64>, y: f64) -> bool {
if y < env.min().y {
return false;
}
if y > env.max().y {
return false;
}
true
}
fn intersects_horizontal_line_coordinate(p0: Coord<f64>, p1: Coord<f64>, y: f64) -> bool {
if p0.y > y && p1.y > y {
return false;
}
if p0.y < y && p1.y < y {
return false;
}
true
}
}
pub(crate) struct ScanLineYOrdinateFinder<'a> {
poly: &'a Polygon<f64>,
centre_y: f64,
hi_y: f64,
lo_y: f64,
}
impl<'a> ScanLineYOrdinateFinder<'a> {
pub(crate) fn new(poly: &'a Polygon<f64>, shell_envelope: Option<Rect<f64>>) -> Self {
let (lo_y, hi_y) = match shell_envelope {
Some(env) => (env.min().y, env.max().y),
None => (-f64::MAX, f64::MAX),
};
Self {
poly,
centre_y: avg(lo_y, hi_y),
hi_y,
lo_y,
}
}
pub(crate) fn get_scan_line_y(&mut self) -> f64 {
let poly = self.poly;
self.process(&poly.exterior().0);
for ring in poly.interiors() {
self.process(&ring.0);
}
avg(self.hi_y, self.lo_y)
}
fn process(&mut self, line: &[Coord<f64>]) {
for pt in line {
let y = pt.y;
self.update_interval(y);
}
}
fn update_interval(&mut self, y: f64) {
if y <= self.centre_y {
if y > self.lo_y {
self.lo_y = y;
}
} else if y > self.centre_y && y < self.hi_y {
self.hi_y = y;
}
}
}
fn get_scan_line_y(poly: &Polygon<f64>, shell_envelope: Option<Rect<f64>>) -> f64 {
let mut finder = ScanLineYOrdinateFinder::new(poly, shell_envelope);
finder.get_scan_line_y()
}
#[cfg(test)]
mod tests {
use super::{InteriorPointPolygon, avg};
use geo_types::{LineString, Polygon};
fn unit_square() -> Polygon<f64> {
Polygon::new(
LineString::from(vec![
(0.0, 0.0),
(10.0, 0.0),
(10.0, 10.0),
(0.0, 10.0),
(0.0, 0.0),
]),
vec![],
)
}
#[test]
#[should_panic(
expected = "Interior Point robustness failure: odd number of scanline crossings"
)]
fn rejects_an_odd_number_of_crossings() {
let poly = unit_square();
let mut int_pt = InteriorPointPolygon::new(&poly);
int_pt.find_best_midpoint(&mut [1.0, 2.0, 3.0]);
}
#[test]
fn accepts_an_even_number_of_crossings() {
let poly = unit_square();
let mut int_pt = InteriorPointPolygon::new(&poly);
int_pt.find_best_midpoint(&mut [1.0, 2.0, 10.0, 30.0]);
assert_eq!(int_pt.get_width(), 20.0);
assert_eq!(int_pt.get_interior_point().map(|c| c.x), Some(20.0));
}
#[test]
fn treats_an_empty_crossing_list_as_a_zero_area_polygon() {
let poly = unit_square();
let mut int_pt = InteriorPointPolygon::new(&poly);
int_pt.find_best_midpoint(&mut []);
assert_eq!(int_pt.get_width(), 0.0);
}
#[test]
fn averages_two_ordinates() {
assert_eq!(avg(1.0, 3.0), 2.0);
assert_eq!(avg(-4.0, 4.0), 0.0);
}
}