rene 0.2.0

Computational geometry.
Documentation
use pyo3::exceptions::PyValueError;
use pyo3::sync::PyOnceLock;
use pyo3::type_object::PyTypeInfo;
use pyo3::types::{PyModule, PyTuple, PyTypeMethods};
use pyo3::{
    intern, pyclass, pymethods, pymodule, Bound, IntoPyObject, Py, PyAny,
    PyResult, Python,
};

use crate::constants::{
    MIN_CONTOUR_VERTICES_COUNT, MIN_MULTIPOLYGON_POLYGONS_COUNT,
    MIN_MULTISEGMENT_SEGMENTS_COUNT,
};
use crate::locatable::Location;
use crate::oriented::Orientation;
use crate::relatable::Relation;

use super::traits::TryToPyAny;

#[pymodule]
fn _crene(_py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> {
    use pyo3::types::PyModuleMethods;
    module.add_class::<PyLocation>()?;
    module.add_class::<PyOrientation>()?;
    module.add_class::<PyRelation>()?;
    module.add("MIN_CONTOUR_VERTICES_COUNT", MIN_CONTOUR_VERTICES_COUNT)?;
    module.add(
        "MIN_MULTIPOLYGON_POLYGONS_COUNT",
        MIN_MULTIPOLYGON_POLYGONS_COUNT,
    )?;
    module.add(
        "MIN_MULTISEGMENT_SEGMENTS_COUNT",
        MIN_MULTISEGMENT_SEGMENTS_COUNT,
    )?;
    Ok(())
}

impl TryToPyAny for Location {
    fn try_to_py_any(self, py: Python<'_>) -> PyResult<Bound<'_, PyAny>> {
        use pyo3::types::PyAnyMethods;
        static LOCATION_CLS: PyOnceLock<Py<PyAny>> = PyOnceLock::new();
        LOCATION_CLS
            .get_or_try_init(py, || {
                py.import("rene.enums")?
                    .getattr(intern!(py, "Location"))
                    .map(|value| value.into_pyobject(py).unwrap().unbind())
            })?
            .getattr(
                py,
                match self {
                    Location::Boundary => intern!(py, "BOUNDARY"),
                    Location::Exterior => intern!(py, "EXTERIOR"),
                    Location::Interior => intern!(py, "INTERIOR"),
                },
            )
            .map(|value| value.into_bound(py))
    }
}

impl TryToPyAny for Orientation {
    fn try_to_py_any(self, py: Python<'_>) -> PyResult<Bound<'_, PyAny>> {
        use pyo3::types::PyAnyMethods;
        static ORIENTATION_CLS: PyOnceLock<Py<PyAny>> = PyOnceLock::new();
        ORIENTATION_CLS
            .get_or_try_init(py, || {
                py.import("rene.enums")?
                    .getattr(intern!(py, "Orientation"))
                    .map(|value| value.into_pyobject(py).unwrap().unbind())
            })?
            .getattr(
                py,
                match self {
                    Orientation::Clockwise => {
                        intern!(py, "CLOCKWISE")
                    }
                    Orientation::Collinear => {
                        intern!(py, "COLLINEAR")
                    }
                    Orientation::Counterclockwise => {
                        intern!(py, "COUNTERCLOCKWISE")
                    }
                },
            )
            .map(|value| value.into_bound(py))
    }
}

impl TryToPyAny for Relation {
    fn try_to_py_any(self, py: Python<'_>) -> PyResult<Bound<'_, PyAny>> {
        use pyo3::types::PyAnyMethods;
        static RELATION_CLS: pyo3::sync::PyOnceLock<Py<PyAny>> =
            pyo3::sync::PyOnceLock::new();
        RELATION_CLS
            .get_or_try_init(py, || {
                py.import("rene.enums")?
                    .getattr(intern!(py, "Relation"))
                    .map(|value| value.into_pyobject(py).unwrap().unbind())
            })?
            .getattr(
                py,
                match self {
                    Relation::Component => intern!(py, "COMPONENT"),
                    Relation::Composite => intern!(py, "COMPOSITE"),
                    Relation::Cover => intern!(py, "COVER"),
                    Relation::Cross => intern!(py, "CROSS"),
                    Relation::Disjoint => intern!(py, "DISJOINT"),
                    Relation::Enclosed => intern!(py, "ENCLOSED"),
                    Relation::Encloses => intern!(py, "ENCLOSES"),
                    Relation::Equal => intern!(py, "EQUAL"),
                    Relation::Overlap => intern!(py, "OVERLAP"),
                    Relation::Touch => intern!(py, "TOUCH"),
                    Relation::Within => intern!(py, "WITHIN"),
                },
            )
            .map(|value| value.into_bound(py))
    }
}

#[pyclass(name = "Location", module = "rene.enums")]
struct PyLocation(Location);

#[pyclass(name = "Orientation", module = "rene.enums")]
struct PyOrientation(Orientation);

#[pyclass(name = "Relation", module = "rene.enums")]
struct PyRelation(Relation);

#[pymethods]
impl PyLocation {
    #[classattr]
    const BOUNDARY: PyLocation = PyLocation(Location::Boundary);

    #[classattr]
    const EXTERIOR: PyLocation = PyLocation(Location::Exterior);

    #[classattr]
    const INTERIOR: PyLocation = PyLocation(Location::Interior);

    fn __repr__(&self, py: Python<'_>) -> PyResult<String> {
        Ok(format!(
            "{}.{}",
            Self::type_object(py).name()?,
            match self.0 {
                Location::Boundary => "BOUNDARY",
                Location::Exterior => "EXTERIOR",
                Location::Interior => "INTERIOR",
            }
        ))
    }
}

#[pymethods]
impl PyOrientation {
    #[classattr]
    const CLOCKWISE: PyOrientation = PyOrientation(Orientation::Clockwise);

    #[classattr]
    const COLLINEAR: PyOrientation = PyOrientation(Orientation::Collinear);

    #[classattr]
    const COUNTERCLOCKWISE: PyOrientation =
        PyOrientation(Orientation::Counterclockwise);

    fn __repr__(&self, py: Python<'_>) -> PyResult<String> {
        Ok(format!(
            "{}.{}",
            Self::type_object(py).name()?,
            match self.0 {
                Orientation::Clockwise => "CLOCKWISE",
                Orientation::Collinear => "COLLINEAR",
                Orientation::Counterclockwise => "COUNTERCLOCKWISE",
            }
        ))
    }
}

#[allow(non_snake_case)]
#[pymethods]
impl PyRelation {
    #[classattr]
    fn COMPONENT(py: Python<'_>) -> Py<PyRelation> {
        to_py_relation_values(py)[0].clone_ref(py)
    }

    #[classattr]
    fn COMPOSITE(py: Python<'_>) -> Py<PyRelation> {
        to_py_relation_values(py)[1].clone_ref(py)
    }

    #[classattr]
    fn COVER(py: Python<'_>) -> Py<PyRelation> {
        to_py_relation_values(py)[2].clone_ref(py)
    }

    #[classattr]
    fn CROSS(py: Python<'_>) -> Py<PyRelation> {
        to_py_relation_values(py)[3].clone_ref(py)
    }

    #[classattr]
    fn DISJOINT(py: Python<'_>) -> Py<PyRelation> {
        to_py_relation_values(py)[4].clone_ref(py)
    }

    #[classattr]
    fn ENCLOSED(py: Python<'_>) -> Py<PyRelation> {
        to_py_relation_values(py)[5].clone_ref(py)
    }

    #[classattr]
    fn ENCLOSES(py: Python<'_>) -> Py<PyRelation> {
        to_py_relation_values(py)[6].clone_ref(py)
    }

    #[classattr]
    fn EQUAL(py: Python<'_>) -> Py<PyRelation> {
        to_py_relation_values(py)[7].clone_ref(py)
    }

    #[classattr]
    fn OVERLAP(py: Python<'_>) -> Py<PyRelation> {
        to_py_relation_values(py)[8].clone_ref(py)
    }

    #[classattr]
    fn TOUCH(py: Python<'_>) -> Py<PyRelation> {
        to_py_relation_values(py)[9].clone_ref(py)
    }

    #[classattr]
    fn WITHIN(py: Python<'_>) -> Py<PyRelation> {
        to_py_relation_values(py)[10].clone_ref(py)
    }

    #[new]
    #[pyo3(signature = (value, /))]
    fn new(value: &Bound<'_, PyAny>, py: Python<'_>) -> PyResult<Py<Self>> {
        use pyo3::types::PyAnyMethods;
        let values = to_py_relation_values(py);
        match value.extract::<usize>() {
            Ok(value) if 1 <= value && value <= values.len() => {
                Ok(values[value - 1].clone_ref(py))
            }
            _ => Err(PyValueError::new_err(format!(
                "{} is not a valid {}",
                value.repr()?,
                Self::type_object(py).name()?
            ))),
        }
    }

    #[getter]
    fn complement(&self, py: Python<'_>) -> Py<PyRelation> {
        match self.0 {
            Relation::Component => Self::COMPOSITE(py),
            Relation::Composite => Self::COMPONENT(py),
            Relation::Cover => Self::WITHIN(py),
            Relation::Cross => Self::CROSS(py),
            Relation::Disjoint => Self::DISJOINT(py),
            Relation::Enclosed => Self::ENCLOSES(py),
            Relation::Encloses => Self::ENCLOSED(py),
            Relation::Equal => Self::EQUAL(py),
            Relation::Overlap => Self::OVERLAP(py),
            Relation::Touch => Self::TOUCH(py),
            Relation::Within => Self::COVER(py),
        }
    }

    #[getter]
    fn value(&self) -> u8 {
        match self.0 {
            Relation::Component => 1,
            Relation::Composite => 2,
            Relation::Cover => 3,
            Relation::Cross => 4,
            Relation::Disjoint => 5,
            Relation::Enclosed => 6,
            Relation::Encloses => 7,
            Relation::Equal => 8,
            Relation::Overlap => 9,
            Relation::Touch => 10,
            Relation::Within => 11,
        }
    }

    fn __getnewargs__<'py>(
        &self,
        py: Python<'py>,
    ) -> PyResult<Bound<'py, PyTuple>> {
        PyTuple::new(py, [self.value()])
    }

    fn __repr__(&self, py: Python<'_>) -> PyResult<String> {
        Ok(format!(
            "{}.{}",
            Self::type_object(py).name()?,
            match self.0 {
                Relation::Component => "COMPONENT",
                Relation::Composite => "COMPOSITE",
                Relation::Cover => "COVER",
                Relation::Cross => "CROSS",
                Relation::Disjoint => "DISJOINT",
                Relation::Enclosed => "ENCLOSED",
                Relation::Encloses => "ENCLOSES",
                Relation::Equal => "EQUAL",
                Relation::Overlap => "OVERLAP",
                Relation::Touch => "TOUCH",
                Relation::Within => "WITHIN",
            }
        ))
    }
}

fn to_py_relation_values(py: Python<'_>) -> &[Py<PyRelation>; 11] {
    static VALUES: PyOnceLock<[Py<PyRelation>; 11]> = PyOnceLock::new();
    VALUES.get_or_init(py, || {
        [
            Bound::new(py, PyRelation(Relation::Component))
                .unwrap()
                .into(),
            Bound::new(py, PyRelation(Relation::Composite))
                .unwrap()
                .into(),
            Bound::new(py, PyRelation(Relation::Cover)).unwrap().into(),
            Bound::new(py, PyRelation(Relation::Cross)).unwrap().into(),
            Bound::new(py, PyRelation(Relation::Disjoint))
                .unwrap()
                .into(),
            Bound::new(py, PyRelation(Relation::Enclosed))
                .unwrap()
                .into(),
            Bound::new(py, PyRelation(Relation::Encloses))
                .unwrap()
                .into(),
            Bound::new(py, PyRelation(Relation::Equal)).unwrap().into(),
            Bound::new(py, PyRelation(Relation::Overlap))
                .unwrap()
                .into(),
            Bound::new(py, PyRelation(Relation::Touch)).unwrap().into(),
            Bound::new(py, PyRelation(Relation::Within)).unwrap().into(),
        ]
    })
}