use super::mic::{
maximum_inscribed_circle, maximum_inscribed_circle_multipolygon, MicOptions, MicUsedEngine,
};
use geo_types::{Coord, LineString, Polygon};
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
type MicPolygonInput = (Vec<(f64, f64)>, Option<Vec<Vec<(f64, f64)>>>);
pub fn build_polygon(
exterior: Vec<(f64, f64)>,
holes: Option<Vec<Vec<(f64, f64)>>>,
) -> PyResult<Polygon<f64>> {
if exterior.len() < 3 {
return Err(PyValueError::new_err(
"polygon exterior must contain at least 3 points",
));
}
let coords: Vec<Coord<f64>> = exterior.into_iter().map(|(x, y)| Coord { x, y }).collect();
let exterior_ls = LineString::from(coords);
let interiors: Vec<LineString<f64>> = holes
.unwrap_or_default()
.into_iter()
.map(|ring| {
let ring_coords: Vec<Coord<f64>> =
ring.into_iter().map(|(x, y)| Coord { x, y }).collect();
LineString::from(ring_coords)
})
.collect();
Ok(Polygon::new(exterior_ls, interiors))
}
pub fn core_err<E: std::fmt::Display>(e: E) -> PyErr {
PyValueError::new_err(format!("solve failed: {e}"))
}
pub fn guard<T>(f: impl FnOnce() -> PyResult<T> + std::panic::UnwindSafe) -> PyResult<T> {
match std::panic::catch_unwind(f) {
Ok(r) => r,
Err(_) => Err(PyValueError::new_err(
"solver panicked; input rejected without crashing host",
)),
}
}
#[pyclass(skip_from_py_object)]
#[derive(Clone)]
pub struct PyMicResult {
#[pyo3(get)]
pub center_x: f64,
#[pyo3(get)]
pub center_y: f64,
#[pyo3(get)]
pub radius: f64,
#[pyo3(get)]
pub radius_sq: f64,
#[pyo3(get)]
pub used_engine: String,
#[pyo3(get)]
pub candidate_count: usize,
#[pyo3(get)]
pub component_index: Option<usize>,
}
fn used_engine_name(v: MicUsedEngine) -> &'static str {
match v {
MicUsedEngine::Grid => "grid",
MicUsedEngine::Geos => "geos",
}
}
#[pyfunction(signature = (exterior, holes=None))]
pub fn solve_mic_py(
exterior: Vec<(f64, f64)>,
holes: Option<Vec<Vec<(f64, f64)>>>,
) -> PyResult<PyMicResult> {
guard(|| {
let polygon = build_polygon(exterior, holes)?;
let opts = MicOptions::default();
let r = maximum_inscribed_circle(&polygon, &opts).map_err(core_err)?;
Ok(PyMicResult {
center_x: r.center.x(),
center_y: r.center.y(),
radius: r.radius,
radius_sq: r.radius_sq,
used_engine: used_engine_name(r.used_engine).to_string(),
candidate_count: r.candidate_count,
component_index: r.component_index,
})
})
}
#[pyfunction(signature = (polygons))]
pub fn solve_mic_multipolygon_py(polygons: Vec<MicPolygonInput>) -> PyResult<PyMicResult> {
guard(|| {
let mut polys = Vec::with_capacity(polygons.len());
for (ext, holes) in polygons {
polys.push(build_polygon(ext, holes)?);
}
let multi = geo_types::MultiPolygon(polys);
let opts = MicOptions::default();
let r = maximum_inscribed_circle_multipolygon(&multi, &opts).map_err(core_err)?;
Ok(PyMicResult {
center_x: r.center.x(),
center_y: r.center.y(),
radius: r.radius,
radius_sq: r.radius_sq,
used_engine: used_engine_name(r.used_engine).to_string(),
candidate_count: r.candidate_count,
component_index: r.component_index,
})
})
}
pub fn register(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
let _ = py;
m.add_class::<PyMicResult>()?;
m.add_function(wrap_pyfunction!(solve_mic_py, m)?)?;
m.add_function(wrap_pyfunction!(solve_mic_multipolygon_py, m)?)?;
Ok(())
}