|
| 1 | +#include <pybind11/pybind11.h> |
| 2 | +#include <pybind11/operators.h> |
| 3 | + |
| 4 | +#include <sstream> |
| 5 | + |
| 6 | +#include "s2/s1interval.h" |
| 7 | + |
| 8 | +namespace py = pybind11; |
| 9 | + |
| 10 | +namespace { |
| 11 | + |
| 12 | +void MaybeThrowInvalidPoint(double p) { |
| 13 | + if (!S1Interval::IsValidPoint(p)) { |
| 14 | + throw py::value_error("Invalid S1 point: " + std::to_string(p)); |
| 15 | + } |
| 16 | +} |
| 17 | + |
| 18 | +} // namespace |
| 19 | + |
| 20 | +void bind_s1interval(py::module& m) { |
| 21 | + py::class_<S1Interval>(m, "S1Interval") |
| 22 | + // Constructors |
| 23 | + .def(py::init<>(), "Default constructor creates an empty interval") |
| 24 | + .def(py::init([](double lo, double hi) { |
| 25 | + MaybeThrowInvalidPoint(lo); |
| 26 | + MaybeThrowInvalidPoint(hi); |
| 27 | + return S1Interval(lo, hi); |
| 28 | + }), |
| 29 | + py::arg("lo"), py::arg("hi"), |
| 30 | + "Constructor that accepts the endpoints of the interval.\n\n" |
| 31 | + "Both endpoints must be in the range -Pi to Pi inclusive.\n" |
| 32 | + "Raises ValueError if either bound is outside that range.") |
| 33 | + |
| 34 | + // Static factory methods |
| 35 | + .def_static("empty", &S1Interval::Empty, "Returns the empty interval") |
| 36 | + .def_static("full", &S1Interval::Full, "Returns the full interval") |
| 37 | + .def_static("from_point", &S1Interval::FromPoint, py::arg("p"), |
| 38 | + "Constructs an interval containing a single point") |
| 39 | + .def_static("from_point_pair", |
| 40 | + [](double p1, double p2) { |
| 41 | + MaybeThrowInvalidPoint(p1); |
| 42 | + MaybeThrowInvalidPoint(p2); |
| 43 | + return S1Interval::FromPointPair(p1, p2); |
| 44 | + }, |
| 45 | + py::arg("p1"), py::arg("p2"), |
| 46 | + "Constructs the minimal interval containing two points") |
| 47 | + |
| 48 | + // Properties |
| 49 | + .def_property_readonly("lo", &S1Interval::lo, "Lower bound") |
| 50 | + .def_property_readonly("hi", &S1Interval::hi, "Upper bound") |
| 51 | + .def("bounds", [](const S1Interval& self) { |
| 52 | + return py::make_tuple(self.lo(), self.hi()); |
| 53 | + }, "Return bounds as a tuple (lo, hi)") |
| 54 | + |
| 55 | + // Predicates |
| 56 | + .def("is_full", &S1Interval::is_full, |
| 57 | + "Return true if the interval contains all points on the unit circle") |
| 58 | + .def("is_empty", &S1Interval::is_empty, |
| 59 | + "Return true if the interval is empty, i.e. it contains no points") |
| 60 | + .def("is_inverted", &S1Interval::is_inverted, |
| 61 | + "Return true if lo() > hi(). (This is true for empty intervals.)") |
| 62 | + |
| 63 | + // Geometric operations |
| 64 | + .def("center", &S1Interval::GetCenter, |
| 65 | + "Return the midpoint of the interval.\n\n" |
| 66 | + "For full and empty intervals, the result is arbitrary.") |
| 67 | + .def("length", &S1Interval::GetLength, |
| 68 | + "Return the length of the interval.\n\n" |
| 69 | + "The length of an empty interval is negative.") |
| 70 | + .def("complement_center", &S1Interval::GetComplementCenter, |
| 71 | + "Return the midpoint of the complement of the interval.\n\n" |
| 72 | + "For full and empty intervals, the result is arbitrary. For a\n" |
| 73 | + "singleton interval, the result is its antipodal point on S1.") |
| 74 | + .def("contains", [](const S1Interval& self, double p) { |
| 75 | + MaybeThrowInvalidPoint(p); |
| 76 | + return self.Contains(p); |
| 77 | + }, py::arg("p"), |
| 78 | + "Return true if the interval (which is closed) contains the point 'p'") |
| 79 | + .def("interior_contains", [](const S1Interval& self, double p) { |
| 80 | + MaybeThrowInvalidPoint(p); |
| 81 | + return self.InteriorContains(p); |
| 82 | + }, py::arg("p"), |
| 83 | + "Return true if the interior of the interval contains the point 'p'") |
| 84 | + .def("contains", py::overload_cast<const S1Interval&>( |
| 85 | + &S1Interval::Contains, py::const_), |
| 86 | + py::arg("other"), |
| 87 | + "Return true if the interval contains the given interval 'y'") |
| 88 | + .def("interior_contains", py::overload_cast<const S1Interval&>( |
| 89 | + &S1Interval::InteriorContains, py::const_), |
| 90 | + py::arg("other"), |
| 91 | + "Return true if the interior of this interval contains the entire interval 'y'") |
| 92 | + .def("intersects", &S1Interval::Intersects, py::arg("other"), |
| 93 | + "Return true if the two intervals contain any points in common") |
| 94 | + .def("interior_intersects", &S1Interval::InteriorIntersects, |
| 95 | + py::arg("other"), |
| 96 | + "Return true if the interior of this interval contains any point of 'y'") |
| 97 | + .def("add_point", [](S1Interval& self, double p) { |
| 98 | + MaybeThrowInvalidPoint(p); |
| 99 | + self.AddPoint(p); |
| 100 | + }, py::arg("p"), |
| 101 | + "Expand the interval to contain the given point 'p'.\n\n" |
| 102 | + "The point should be an angle in the range [-Pi, Pi].") |
| 103 | + .def("project", [](const S1Interval& self, double p) { |
| 104 | + if (self.is_empty()) throw py::value_error("Invalid S1Interval"); |
| 105 | + MaybeThrowInvalidPoint(p); |
| 106 | + return self.Project(p); |
| 107 | + }, py::arg("p"), |
| 108 | + "Return the closest point in the interval to 'p'.\n\n" |
| 109 | + "The interval must be non-empty.") |
| 110 | + .def("expanded", &S1Interval::Expanded, py::arg("margin"), |
| 111 | + "Return interval expanded on each side by 'margin' (radians).\n\n" |
| 112 | + "If 'margin' is negative, shrink the interval instead. The resulting\n" |
| 113 | + "interval may be empty or full. Any expansion of a full interval remains\n" |
| 114 | + "full, and any expansion of an empty interval remains empty.") |
| 115 | + .def("union", &S1Interval::Union, py::arg("other"), |
| 116 | + "Return the smallest interval containing this interval and 'y'") |
| 117 | + .def("intersection", &S1Interval::Intersection, py::arg("other"), |
| 118 | + "Return the smallest interval containing the intersection with 'y'.\n\n" |
| 119 | + "Note that the region of intersection may consist of two disjoint intervals.") |
| 120 | + .def("complement", &S1Interval::Complement, |
| 121 | + "Return the complement of the interior of the interval") |
| 122 | + .def("directed_hausdorff_distance", |
| 123 | + &S1Interval::GetDirectedHausdorffDistance, |
| 124 | + py::arg("other"), |
| 125 | + "Return the directed Hausdorff distance to 'y'") |
| 126 | + // Note: default value must match C++ signature in s1interval.h |
| 127 | + .def("approx_equals", &S1Interval::ApproxEquals, |
| 128 | + py::arg("other"), py::arg("max_error") = 1e-15, |
| 129 | + "Return true if approximately equal to 'y'.\n\n" |
| 130 | + "Two intervals are approximately equal if each endpoint can be moved\n" |
| 131 | + "by at most 'max_error' (radians) to match the other interval.") |
| 132 | + |
| 133 | + // Operators |
| 134 | + .def(py::self == py::self, |
| 135 | + "Return true if two intervals contain the same set of points") |
| 136 | + .def(py::self != py::self, |
| 137 | + "Return true if two intervals do not contain the same set of points") |
| 138 | + |
| 139 | + // String representation |
| 140 | + .def("__repr__", [](const S1Interval& i) { |
| 141 | + std::ostringstream oss; |
| 142 | + oss << "S1Interval(" << i << ")"; |
| 143 | + return oss.str(); |
| 144 | + }) |
| 145 | + .def("__str__", [](const S1Interval& i) { |
| 146 | + std::ostringstream oss; |
| 147 | + oss << i; |
| 148 | + return oss.str(); |
| 149 | + }); |
| 150 | +} |
0 commit comments