diff --git a/docs/api.rst b/docs/api.rst index 0395f45..1d8935a 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -10,6 +10,9 @@ API reference Geography properties -------------------- +Functions that provide access to properties of :py:class:`~spherely.Geography` +objects without side-effects (except for ``prepare`` and ``destroy_prepared``). + .. autosummary:: :toctree: _api_generated/ @@ -28,6 +31,9 @@ Geography properties Geography creation ------------------ +Functions that build new :py:class:`~spherely.Geography` objects from +coordinates or existing geographies. + .. autosummary:: :toctree: _api_generated/ @@ -44,6 +50,9 @@ Geography creation Input/Output ------------ +Functions that convert :py:class:`~spherely.Geography` objects to/from an +external format such as `WKT `_. + .. autosummary:: :toctree: _api_generated/ @@ -53,13 +62,14 @@ Input/Output to_wkb from_geoarrow to_geoarrow - Projection .. _api_measurement: Measurement ----------- +Functions that compute measurements of one or more geographies. + .. autosummary:: :toctree: _api_generated/ @@ -73,6 +83,9 @@ Measurement Predicates ---------- +Functions that return ``True`` or ``False`` for some spatial relationship +between two geographies. + .. autosummary:: :toctree: _api_generated/ @@ -90,6 +103,9 @@ Predicates Overlays (boolean operations) ----------------------------- +Functions that generate a new geography based on the combination of two +geographies. + .. autosummary:: :toctree: _api_generated/ @@ -103,6 +119,8 @@ Overlays (boolean operations) Constructive operations ----------------------- +Functions that generate a new geography based on input. + .. autosummary:: :toctree: _api_generated/ diff --git a/docs/api_hidden.rst b/docs/api_hidden.rst index 91f6127..12058bc 100644 --- a/docs/api_hidden.rst +++ b/docs/api_hidden.rst @@ -8,3 +8,7 @@ Geography Geography.dimensions Geography.nshape + Projection + Projection.lnglat + Projection.pseudo_mercator + Projection.orthographic diff --git a/docs/conf.py b/docs/conf.py index f75c6c6..10fd3db 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,18 +18,26 @@ "python": ("https://docs.python.org/3", None), "numpy": ("https://numpy.org/doc/stable", None), "shapely": ("https://shapely.readthedocs.io/en/latest/", None), + "pyarrow": ("https://arrow.apache.org/docs/", None), } -autodoc_typehints = "none" - napoleon_google_docstring = False napoleon_numpy_docstring = True napoleon_use_param = False napoleon_use_rtype = False napoleon_preprocess_types = True napoleon_type_aliases = { + # general terms + "sequence": ":term:`sequence`", + "iterable": ":term:`iterable`", + # numpy terms "array_like": ":term:`array_like`", "array-like": ":term:`array-like `", + # objects without namespace: spherely + "EARTH_RADIUS_METERS": "spherely.EARTH_RADIUS_METERS", + # objects without namespace: numpy + "ndarray": "~numpy.ndarray", + "array": ":term:`array`", } # source_suffix = ['.rst', '.md'] @@ -61,8 +69,6 @@ use_repository_button=True, use_issues_button=True, home_page_in_toc=False, - extra_navbar="", - navbar_footer_text="", ) html_static_path = ["_static"] diff --git a/src/accessors-geog.cpp b/src/accessors-geog.cpp index 3e489b1..9b894f2 100644 --- a/src/accessors-geog.cpp +++ b/src/accessors-geog.cpp @@ -26,22 +26,6 @@ PyObjectGeography convex_hull(PyObjectGeography a) { return make_py_geography(s2geog::s2_convex_hull(a_ptr)); } -double get_x(PyObjectGeography a) { - auto geog = a.as_geog_ptr(); - if (geog->geog_type() != GeographyType::Point) { - throw py::value_error("Only Point geometries supported"); - } - return s2geog::s2_x(geog->geog()); -} - -double get_y(PyObjectGeography a) { - auto geog = a.as_geog_ptr(); - if (geog->geog_type() != GeographyType::Point) { - throw py::value_error("Only Point geometries supported"); - } - return s2geog::s2_y(geog->geog()); -} - double distance(PyObjectGeography a, PyObjectGeography b, double radius = numeric_constants::EARTH_RADIUS_METERS) { @@ -67,66 +51,62 @@ void init_accessors(py::module& m) { m.def("centroid", py::vectorize(¢roid), - py::arg("a"), - R"pbdoc( + py::arg("geography"), + py::pos_only(), + R"pbdoc(centroid(geography, /) + Computes the centroid of each geography. Parameters ---------- - a : :py:class:`Geography` or array_like - Geography object + geography : :py:class:`Geography` or array_like + Geography object(s). + + Returns + ------- + Geography or array + A single or an array of POINT Geography object(s). )pbdoc"); m.def("boundary", py::vectorize(&boundary), - py::arg("a"), - R"pbdoc( + py::arg("geography"), + py::pos_only(), + R"pbdoc(boundary(geography, /) + Computes the boundary of each geography. Parameters ---------- - a : :py:class:`Geography` or array_like - Geography object + geography : :py:class:`Geography` or array_like + Geography object(s). + + Returns + ------- + Geography or array + A single or an array of either (MULTI)POINT or (MULTI)LINESTRING + Geography object(s). )pbdoc"); m.def("convex_hull", py::vectorize(&convex_hull), - py::arg("a"), - R"pbdoc( - Computes the convex hull of each geography. + py::arg("geography"), + py::pos_only(), + R"pbdoc(convex_hull(geography, /) - Parameters - ---------- - a : :py:class:`Geography` or array_like - Geography object - - )pbdoc"); - - m.def("get_x", - py::vectorize(&get_x), - py::arg("a"), - R"pbdoc( - Returns the longitude value of the Point (in degrees). + Computes the convex hull of each geography. Parameters ---------- - a: :py:class:`Geography` or array_like + geography : :py:class:`Geography` or array_like Geography object(s). - )pbdoc"); - - m.def("get_y", - py::vectorize(&get_y), - py::arg("a"), - R"pbdoc( - Returns the latitude value of the Point (in degrees). - - Parameters - ---------- - a: :py:class:`Geography` or array_like - Geography object(s). + Returns + ------- + Geography or array + A single or an array of POLYGON Geography object(s). )pbdoc"); @@ -135,64 +115,92 @@ void init_accessors(py::module& m) { py::arg("a"), py::arg("b"), py::arg("radius") = numeric_constants::EARTH_RADIUS_METERS, - R"pbdoc( + R"pbdoc(distance(a, b, radius=spherely.EARTH_RADIUS_METERS) + Calculate the distance between two geographies. Parameters ---------- a : :py:class:`Geography` or array_like - Geography object + Geography object(s). b : :py:class:`Geography` or array_like - Geography object + Geography object(s). radius : float, optional - Radius of Earth in meters, default 6,371,010 + Radius of Earth in meters, default 6,371,010. + + Returns + ------- + float or array + Distance value(s), in meters. )pbdoc"); m.def("area", py::vectorize(&area), - py::arg("a"), + py::arg("geography"), + py::pos_only(), py::arg("radius") = numeric_constants::EARTH_RADIUS_METERS, - R"pbdoc( + R"pbdoc(area(geography, /, radius=spherely.EARTH_RADIUS_METERS) + Calculate the area of the geography. Parameters ---------- - a : :py:class:`Geography` or array_like - Geography object + geography : :py:class:`Geography` or array_like + Geography object(s). radius : float, optional - Radius of Earth in meters, default 6,371,010 + Radius of Earth in meters, default 6,371,010. + + Returns + ------- + float or array + Area value(s), in square meters. )pbdoc"); m.def("length", py::vectorize(&length), - py::arg("a"), + py::arg("geography"), + py::pos_only(), py::arg("radius") = numeric_constants::EARTH_RADIUS_METERS, - R"pbdoc( + R"pbdoc(length(geography, /, radius=spherely.EARTH_RADIUS_METERS) + Calculates the length of a line geography, returning zero for other types. Parameters ---------- - a : :py:class:`Geography` or array_like - Geography object + geography : :py:class:`Geography` or array_like + Geography object(s). radius : float, optional - Radius of Earth in meters, default 6,371,010 + Radius of Earth in meters, default 6,371,010. + + Returns + ------- + float or array + Length value(s), in meters. )pbdoc"); m.def("perimeter", py::vectorize(&perimeter), - py::arg("a"), + py::arg("geography"), + py::pos_only(), py::arg("radius") = numeric_constants::EARTH_RADIUS_METERS, - R"pbdoc( + R"pbdoc(perimeter(geography, /, radius=spherely.EARTH_RADIUS_METERS) + Calculates the perimeter of a polygon geography, returning zero for other types. Parameters ---------- - a : :py:class:`Geography` or array_like - Geography object + geography : :py:class:`Geography` or array_like + Geography object(s). radius : float, optional - Radius of Earth in meters, default 6,371,010 + Radius of Earth in meters, default 6,371,010. + + Returns + ------- + float or array + Perimeter value(s), in meters. + )pbdoc"); } diff --git a/src/boolean-operations.cpp b/src/boolean-operations.cpp index aad4cb6..f04ef4d 100644 --- a/src/boolean-operations.cpp +++ b/src/boolean-operations.cpp @@ -37,13 +37,19 @@ void init_boolean_operations(py::module& m) { py::vectorize(BooleanOp(S2BooleanOperation::OpType::UNION)), py::arg("a"), py::arg("b"), - R"pbdoc( + R"pbdoc(union(a, b) + Computes the union of both geographies. Parameters ---------- a, b : :py:class:`Geography` or array_like - Geography object + Geography object(s). + + Returns + ------- + Geography or array + New Geography object(s) representing the union of the input geographies. )pbdoc"); @@ -51,13 +57,19 @@ void init_boolean_operations(py::module& m) { py::vectorize(BooleanOp(S2BooleanOperation::OpType::INTERSECTION)), py::arg("a"), py::arg("b"), - R"pbdoc( + R"pbdoc(intersection(a, b) + Computes the intersection of both geographies. Parameters ---------- a, b : :py:class:`Geography` or array_like - Geography object + Geography object(s). + + Returns + ------- + Geography or array + New Geography object(s) representing the intersection of the input geographies. )pbdoc"); @@ -65,13 +77,19 @@ void init_boolean_operations(py::module& m) { py::vectorize(BooleanOp(S2BooleanOperation::OpType::DIFFERENCE)), py::arg("a"), py::arg("b"), - R"pbdoc( + R"pbdoc(difference(a, b) + Computes the difference of both geographies. Parameters ---------- a, b : :py:class:`Geography` or array_like - Geography object + Geography object(s). + + Returns + ------- + Geography or array + New Geography object(s) representing the difference of the input geographies. )pbdoc"); @@ -79,13 +97,20 @@ void init_boolean_operations(py::module& m) { py::vectorize(BooleanOp(S2BooleanOperation::OpType::SYMMETRIC_DIFFERENCE)), py::arg("a"), py::arg("b"), - R"pbdoc( + R"pbdoc(symmetric_difference(a, b) + Computes the symmetric difference of both geographies. Parameters ---------- a, b : :py:class:`Geography` or array_like - Geography object + Geography object(s). + + Returns + ------- + Geography or array + New Geography object(s) representing the symmetric difference of + the input geographies. )pbdoc"); } diff --git a/src/creation.cpp b/src/creation.cpp index 3b64ac3..92ffe70 100644 --- a/src/creation.cpp +++ b/src/creation.cpp @@ -291,9 +291,6 @@ std::unique_ptr create_collection(const std::vector &fea // void init_creation(py::module &m) { - py::options options; - options.disable_function_signatures(); - // ----- scalar Geography creation functions m.def( @@ -313,7 +310,8 @@ void init_creation(py::module &m) { }, py::arg("longitude") = py::none(), py::arg("latitude") = py::none(), - R"pbdoc(create_point(longitude: float | None = None, latitude: float | None = None) -> Geography + R"pbdoc(create_point(longitude=None, latitude=None) + Create a POINT geography. Parameters @@ -323,12 +321,18 @@ void init_creation(py::module &m) { latitude : float, optional latitude coordinate, in degrees. + Returns + ------- + point : Geography + A new POINT geography object. + )pbdoc"); m.def("create_multipoint", &create_multipoint>, py::arg("points"), - R"pbdoc(create_multipoint(points: Sequence) -> Geography + R"pbdoc(create_multipoint(points) + Create a MULTIPOINT geography. Parameters @@ -337,6 +341,12 @@ void init_creation(py::module &m) { A sequence of (longitude, latitude) coordinates (in degrees) or POINT :class:`~spherely.Geography` objects. + Returns + ------- + multipoint : Geography + A new MULTIPOINT (or POINT if a single point is passed) + geography object. + )pbdoc") .def("create_multipoint", &create_multipoint, py::arg("points")); @@ -344,7 +354,8 @@ void init_creation(py::module &m) { "create_linestring", [](py::none) { return make_geography(std::make_unique()); }, py::arg("vertices") = py::none(), - R"pbdoc(create_linestring(vertices: Sequence | None = None) -> Geography + R"pbdoc(create_linestring(vertices=None) + Create a LINESTRING geography. Parameters @@ -353,6 +364,11 @@ void init_creation(py::module &m) { A sequence of (longitude, latitude) coordinates (in degrees) or POINT :class:`~spherely.Geography` objects. + Returns + ------- + linestring : Geography + A new LINESTRING geography object. + )pbdoc") .def( "create_linestring", &create_linestring>, py::arg("vertices")) @@ -361,7 +377,8 @@ void init_creation(py::module &m) { m.def("create_multilinestring", &create_multilinestring>, py::arg("lines"), - R"pbdoc(create_multilinestring(lines: Sequence) -> Geography + R"pbdoc(create_multilinestring(lines) + Create a MULTILINESTRING geography. Parameters @@ -371,6 +388,12 @@ void init_creation(py::module &m) { a sequence of sequences of POINT :class:`~spherely.Geography` objects or a sequence of LINESTRING :class:`~spherely.Geography` objects. + Returns + ------- + multilinestring : Geography + A new MULTILINESTRING (or LINESTRING if a single line is passed) + geography object. + )pbdoc") .def("create_multilinestring", &create_multilinestring, py::arg("lines")) .def( @@ -386,7 +409,8 @@ void init_creation(py::module &m) { py::arg("shell") = py::none(), py::arg("holes") = py::none(), py::arg("oriented") = false, - R"pbdoc(create_polygon(shell: Sequence | None = None, holes: Sequence | None = None) -> Geography + R"pbdoc(create_polygon(shell=None, holes=None, oriented=False) + Create a POLYGON geography. Parameters @@ -404,6 +428,11 @@ void init_creation(py::module &m) { By default (False), it will return the polygon with the smaller area. + Returns + ------- + polygon : Geography + A new POLYGON geography object. + )pbdoc") .def("create_polygon", &create_polygon>, @@ -419,7 +448,8 @@ void init_creation(py::module &m) { m.def("create_multipolygon", &create_multipolygon, py::arg("polygons"), - R"pbdoc(create_multipolygon(polygons: Sequence) -> Geography + R"pbdoc(create_multipolygon(polygons) + Create a MULTIPOLYGON geography. Parameters @@ -427,12 +457,19 @@ void init_creation(py::module &m) { polygons : sequence A sequence of POLYGON :class:`~spherely.Geography` objects. + Returns + ------- + multipolygon : Geography + A new MULTIPOLYGON (or POLYGON if a single polygon is passed) + geography object. + )pbdoc"); m.def("create_collection", &create_collection, py::arg("geographies"), - R"pbdoc(create_collection(geographies: Sequence) -> Geography + R"pbdoc(create_collection(geographies) + Create a GEOMETRYCOLLECTION geography from arbitrary geographies. Parameters @@ -440,6 +477,11 @@ void init_creation(py::module &m) { geographies : sequence A sequence of :class:`~spherely.Geography` objects. + Returns + ------- + collection : Geography + A new GEOMETRYCOLLECTION geography object. + )pbdoc"); // ----- vectorized Geography creation functions @@ -448,7 +490,8 @@ void init_creation(py::module &m) { py::vectorize(&point), py::arg("longitude"), py::arg("latitude"), - R"pbdoc( + R"pbdoc(points(longitude, latitude) + Create an array of points. Parameters @@ -463,7 +506,8 @@ void init_creation(py::module &m) { m.def("points", &points, py::arg("coords"), - R"pbdoc( + R"pbdoc(points(coords) + Create an array of points. Parameters diff --git a/src/generate_spherely_vfunc_types.py b/src/generate_spherely_vfunc_types.py index 87a8537..a8063c3 100644 --- a/src/generate_spherely_vfunc_types.py +++ b/src/generate_spherely_vfunc_types.py @@ -40,12 +40,12 @@ def update_stub_file(path, **type_specs): def _vfunctype_factory(class_name, n_in, **optargs): """Create new VFunc types. - Based on the number of input arrays and optional arguments and their types.""" - arg_names = ( - ["geography"] - if n_in == 1 and not optargs - else list(string.ascii_lowercase[:n_in]) - ) + Based on the number of input arrays and optional arguments and their types. + """ + arg_names = list(string.ascii_lowercase[:n_in]) + if n_in == 1: + arg_names[0] = "geography" + class_code = [ f"class {class_name}(", " Generic[_NameType, _ScalarReturnType, _ArrayReturnDType]", @@ -65,6 +65,8 @@ def _vfunctype_factory(class_name, n_in, **optargs): f"{arg_name}: {arg_type}" for arg_name, arg_type in zip(arg_names, arg_types) ) + if n_in == 1: + arg_str += ", /" return_type = ( "_ScalarReturnType" if all(t == geog_types[0] for t in arg_types) diff --git a/src/geoarrow.cpp b/src/geoarrow.cpp index aac0562..0ad8f61 100644 --- a/src/geoarrow.cpp +++ b/src/geoarrow.cpp @@ -258,17 +258,19 @@ void init_geoarrow(py::module& m) { py::class_(m, "ArrowArrayHolder") .def("__arrow_c_array__", &ArrowArrayHolder::return_capsules); - m.def("from_geoarrow", - &from_geoarrow, - py::arg("input"), - py::pos_only(), - py::kw_only(), - py::arg("oriented") = false, - py::arg("planar") = false, - py::arg("tessellate_tolerance") = 100.0, - py::arg("projection") = Projection::lnglat(), - py::arg("geometry_encoding") = py::none(), - R"pbdoc( + m.def( + "from_geoarrow", + &from_geoarrow, + py::arg("geographies"), + py::pos_only(), + py::kw_only(), + py::arg("oriented") = false, + py::arg("planar") = false, + py::arg("tessellate_tolerance") = 100.0, + py::arg("projection") = Projection::lnglat(), + py::arg("geometry_encoding") = py::none(), + R"pbdoc(from_geoarrow(geographies, /, *, oriented=False, planar=False, tessellate_tolerance=100.0, projection=spherely.Projection.lnglat(), geometry_encoding=None) + Create an array of geographies from an Arrow array object with a GeoArrow extension type. @@ -282,7 +284,7 @@ void init_geoarrow(py::module& m) { Parameters ---------- - input : pyarrow.Array, Arrow array + geographies : pyarrow.Array, Arrow array Any array object implementing the Arrow PyCapsule Protocol (i.e. has a ``__arrow_c_array__`` method). The type of the array should be one of the geoarrow geometry types. @@ -315,19 +317,27 @@ void init_geoarrow(py::module& m) { Arrow array without geoarrow type but with a plain string or binary type, if specifying this keyword with "WKT" or "WKB", respectively. + + Returns + ------- + Geography or array + An array of geography objects. + )pbdoc"); - m.def("to_geoarrow", - &to_geoarrow, - py::arg("input"), - py::pos_only(), - py::kw_only(), - py::arg("output_schema") = py::none(), - py::arg("projection") = Projection::lnglat(), - py::arg("planar") = false, - py::arg("tessellate_tolerance") = 100.0, - py::arg("precision") = 6, - R"pbdoc( + m.def( + "to_geoarrow", + &to_geoarrow, + py::arg("geographies"), + py::pos_only(), + py::kw_only(), + py::arg("output_schema") = py::none(), + py::arg("projection") = Projection::lnglat(), + py::arg("planar") = false, + py::arg("tessellate_tolerance") = 100.0, + py::arg("precision") = 6, + R"pbdoc(to_geoarrow(geographies, /, *, output_schema=None, projection=spherely.Projection.lnglat(), planar=False, tessellate_tolerance=100.0, precision=6) + Convert an array of geographies to an Arrow array object with a GeoArrow extension type. @@ -335,8 +345,8 @@ void init_geoarrow(py::module& m) { Parameters ---------- - input : array_like - An array of geography objects. + geographies : array_like + An array of :py:class:`~spherely.Geography` objects. output_schema : Arrow schema, pyarrow.DataType, pyarrow.Field, default None The geoarrow extension type to use for the output. This can indicate one of the native geoarrow types (e.g. "point", "linestring", "polygon", @@ -368,7 +378,7 @@ void init_geoarrow(py::module& m) { Returns ------- ArrowArrayHolder - A generic Arrow array object with geograhies encoded to GeoArrow. + A generic Arrow array object with geographies encoded to GeoArrow. Examples -------- diff --git a/src/geography.cpp b/src/geography.cpp index bd527fc..bbe57b2 100644 --- a/src/geography.cpp +++ b/src/geography.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -228,7 +229,27 @@ std::int8_t get_type_id(PyObjectGeography obj) { } int get_dimensions(PyObjectGeography obj) { - return obj.as_geog_ptr()->dimension(); + // note: in case of a collection with features of different dimensions: + // - Geography::dimension() returns -1 + // - s2geography::s2_dimension(geog) returns the max value found in collection + // => we want the latter here. + return s2geog::s2_dimension(obj.as_geog_ptr()->geog()); +} + +double get_x(PyObjectGeography obj) { + auto geog = obj.as_geog_ptr(); + if (geog->geog_type() != GeographyType::Point) { + throw py::value_error("Only Point geometries supported"); + } + return s2geog::s2_x(geog->geog()); +} + +double get_y(PyObjectGeography obj) { + auto geog = obj.as_geog_ptr(); + if (geog->geog_type() != GeographyType::Point) { + throw py::value_error("Only Point geometries supported"); + } + return s2geog::s2_y(geog->geog()); } /* @@ -261,18 +282,23 @@ PyObjectGeography destroy_prepared(PyObjectGeography obj) { void init_geography(py::module &m) { // Geography types - auto pygeography_types = py::enum_(m, "GeographyType", py::arithmetic(), R"pbdoc( - The enumeration of Geography types - )pbdoc"); - - pygeography_types.value("NONE", GeographyType::None); - pygeography_types.value("POINT", GeographyType::Point); - pygeography_types.value("LINESTRING", GeographyType::LineString); - pygeography_types.value("POLYGON", GeographyType::Polygon); - pygeography_types.value("MULTIPOLYGON", GeographyType::MultiPolygon); - pygeography_types.value("MULTIPOINT", GeographyType::MultiPoint); - pygeography_types.value("MULTILINESTRING", GeographyType::MultiLineString); - pygeography_types.value("GEOMETRYCOLLECTION", GeographyType::GeometryCollection); + auto pygeography_types = py::enum_( + m, "GeographyType", py::arithmetic(), "The enumeration of Geography types."); + + pygeography_types.value("NONE", GeographyType::None, "Undefined geography type (-1)."); + pygeography_types.value("POINT", GeographyType::Point, "Single point geography type (0)."); + pygeography_types.value( + "LINESTRING", GeographyType::LineString, "Single line geography type (1)."); + pygeography_types.value( + "POLYGON", GeographyType::Polygon, "Single polygon geography type (2)."); + pygeography_types.value( + "MULTIPOINT", GeographyType::MultiPoint, "Multiple point geography type (3)."); + pygeography_types.value( + "MULTILINESTRING", GeographyType::MultiLineString, "Multiple line geography type (4)."); + pygeography_types.value( + "MULTIPOLYGON", GeographyType::MultiPolygon, "Multiple polygon geography type (5)."); + pygeography_types.value( + "GEOMETRYCOLLECTION", GeographyType::GeometryCollection, "Collection geography type (6)."); // Geography classes @@ -323,31 +349,97 @@ void init_geography(py::module &m) { m.def("get_type_id", py::vectorize(&get_type_id), py::arg("geography"), - R"pbdoc( + py::pos_only(), + R"pbdoc(get_type_id(geography, /) + Returns the type ID of a geography. - None (missing) is -1 - POINT is 0 - LINESTRING is 1 + - POLYGON is 2 + - MULTIPOINT is 3 + - MULTILINESTRING is 4 + - MULTIPOLYGON is 5 + - GEOMETRYCOLLECTION is 6 Parameters ---------- geography : :py:class:`Geography` or array_like Geography object(s). + Returns + ------- + type_id : int or array + The type id(s) of the input geography object(s). + See also the ``value`` property of the + :py:class:`GeographyType` enumeration. + + See Also + -------- + GeographyType + )pbdoc"); - m.def("get_dimensions", py::vectorize(&get_dimensions), py::arg("geography"), R"pbdoc( - Returns the inherent dimensionality of a geography. + m.def("get_dimensions", + py::vectorize(&get_dimensions), + py::arg("geography"), + py::pos_only(), + R"pbdoc(get_dimensions(geography, /) - The inherent dimension is 0 for points, 1 for linestrings and 2 for - polygons. For geometrycollections it is the max of the containing elements. - Empty collections and None values return -1. + Returns the inherent dimensionality of a geography. Parameters ---------- geography : :py:class:`Geography` or array_like - Geography object(s) + Geography object(s). + + Returns + ------- + dimensions : int or array + The inherent dimension is 0 for points, 1 for linestrings and 2 for + polygons. For geometrycollections it is either the max of the containing + elements or -1 for empty collections. + + )pbdoc"); + + m.def("get_x", + py::vectorize(&get_x), + py::arg("geography"), + py::pos_only(), + R"pbdoc(get_x(geography, /) + + Returns the longitude value of the Point (in degrees). + + Parameters + ---------- + geography: :py:class:`Geography` or array_like + POINT Geography object(s). + + Returns + ------- + float or array + Longitude coordinate value(s). + + )pbdoc"); + + m.def("get_y", + py::vectorize(&get_y), + py::arg("geography"), + py::pos_only(), + R"pbdoc(get_y(geography, /) + + Returns the latitude value of the Point (in degrees). + + Parameters + ---------- + geography: :py:class:`Geography` or array_like + POINT Geography object(s). + + Returns + ------- + float or array + Latitude coordinate value(s). )pbdoc"); @@ -356,7 +448,9 @@ void init_geography(py::module &m) { m.def("is_geography", py::vectorize(&is_geography), py::arg("obj"), - R"pbdoc( + py::pos_only(), + R"pbdoc(is_geography(obj, /) + Returns True if the object is a :py:class:`Geography`, False otherwise. Parameters @@ -371,7 +465,9 @@ void init_geography(py::module &m) { m.def("is_prepared", py::vectorize(&is_prepared), py::arg("geography"), - R"pbdoc( + py::pos_only(), + R"pbdoc(is_prepared(geography, /) + Returns True if the geography object is "prepared", False otherwise. A prepared geography is a normal geography with added information such as @@ -384,7 +480,7 @@ void init_geography(py::module &m) { Parameters ---------- geography : :py:class:`Geography` or array_like - Geography object(s) + Geography object(s). See Also -------- @@ -396,7 +492,9 @@ void init_geography(py::module &m) { m.def("prepare", py::vectorize(&prepare), py::arg("geography"), - R"pbdoc( + py::pos_only(), + R"pbdoc(prepare(geography, /) + Prepare a geography, improving performance of other operations. A prepared geography is a normal geography with added information such as @@ -407,10 +505,17 @@ void init_geography(py::module &m) { efficient to call this function on an array that partially contains prepared geographies. + This function updates the input geographies in-place! + Parameters ---------- geography : :py:class:`Geography` or array_like - Geography object(s) + Geography object(s). + + Returns + ------- + prepared : Geography or array + The same input Geography object(s) with an attached index. See Also -------- @@ -422,7 +527,9 @@ void init_geography(py::module &m) { m.def("destroy_prepared", py::vectorize(&destroy_prepared), py::arg("geography"), - R"pbdoc( + py::pos_only(), + R"pbdoc(destroy_prepared(geography, /) + Destroy the prepared part of a geography, freeing up memory. Note that the prepared geography will always be cleaned up if the @@ -430,10 +537,17 @@ void init_geography(py::module &m) { very specific circumstances, such as freeing up memory without losing the geographies, or benchmarking. + This function updates the input geographies in-place! + Parameters ---------- geography : :py:class:`Geography` or array_like - Geography object(s) + Geography object(s). + + Returns + ------- + unprepared : Geography or array + The same input Geography object(s) with no attached index. See Also -------- diff --git a/src/io.cpp b/src/io.cpp index 2f88a48..f0b54e9 100644 --- a/src/io.cpp +++ b/src/io.cpp @@ -23,8 +23,8 @@ class FromWKT { m_reader = std::make_shared(options); } - PyObjectGeography operator()(py::str a) const { - return make_py_geography(m_reader->read_feature(a)); + PyObjectGeography operator()(py::str string) const { + return make_py_geography(m_reader->read_feature(string)); } private: @@ -37,8 +37,8 @@ class ToWKT { m_writer = std::make_shared(precision); } - py::str operator()(PyObjectGeography a) const { - auto res = m_writer->write_feature(a.as_geog_ptr()->geog()); + py::str operator()(PyObjectGeography obj) const { + auto res = m_writer->write_feature(obj.as_geog_ptr()->geog()); return py::str(res); } @@ -59,8 +59,8 @@ class FromWKB { m_reader = std::make_shared(options); } - PyObjectGeography operator()(py::bytes a) const { - return make_py_geography(m_reader->ReadFeature(a)); + PyObjectGeography operator()(py::bytes bytes) const { + return make_py_geography(m_reader->ReadFeature(bytes)); } private: @@ -73,8 +73,8 @@ class ToWKB { m_writer = std::make_shared(); } - py::bytes operator()(PyObjectGeography a) const { - return m_writer->WriteFeature(a.as_geog_ptr()->geog()); + py::bytes operator()(PyObjectGeography obj) const { + return m_writer->WriteFeature(obj.as_geog_ptr()->geog()); } private: @@ -84,20 +84,24 @@ class ToWKB { void init_io(py::module& m) { m.def( "from_wkt", - [](py::array_t a, bool oriented, bool planar, float tessellate_tolerance) { - return py::vectorize(FromWKT(oriented, planar, tessellate_tolerance))(std::move(a)); + [](py::array_t string, bool oriented, bool planar, float tessellate_tolerance) { + return py::vectorize(FromWKT(oriented, planar, tessellate_tolerance))( + std::move(string)); }, - py::arg("a"), + py::arg("geography"), + py::pos_only(), + py::kw_only(), py::arg("oriented") = false, py::arg("planar") = false, py::arg("tessellate_tolerance") = 100.0, - R"pbdoc( + R"pbdoc(from_wkt(geography, /, *, oriented=False, planar=False, tessellate_tolerance=100.0) + Creates geographies from the Well-Known Text (WKT) representation. Parameters ---------- - a : str or array_like - WKT strings. + geography : str or array_like + The WKT string(s) to convert. oriented : bool, default False Set to True if polygon ring directions are known to be correct (i.e., exterior rings are defined counter clockwise and interior @@ -116,43 +120,58 @@ void init_io(py::module& m) { satisfy the planar edge constraint. This is only used if `planar` is set to True. + Returns + ------- + Geography or array + A single or an array of geography objects. + )pbdoc"); m.def( "to_wkt", - [](py::array_t a, int precision) { - return py::vectorize(ToWKT(precision))(std::move(a)); + [](py::array_t obj, int precision) { + return py::vectorize(ToWKT(precision))(std::move(obj)); }, - py::arg("a"), + py::arg("geography"), + py::pos_only(), py::arg("precision") = 6, - R"pbdoc( + R"pbdoc(to_wkt(geography, /, precision=6) + Returns the WKT representation of each geography. Parameters ---------- - a : :py:class:`Geography` or array_like - Geography object(s) + geography : :py:class:`Geography` or array_like + Geography object(s). precision : int, default 6 The number of decimal places to include in the output. + Returns + ------- + str or array + A string or an array of strings. + )pbdoc"); m.def( "from_wkb", - [](py::array_t a, bool oriented, bool planar, float tessellate_tolerance) { - return py::vectorize(FromWKB(oriented, planar, tessellate_tolerance))(std::move(a)); + [](py::array_t bytes, bool oriented, bool planar, float tessellate_tolerance) { + return py::vectorize(FromWKB(oriented, planar, tessellate_tolerance))(std::move(bytes)); }, - py::arg("a"), + py::arg("geography"), + py::pos_only(), + py::kw_only(), py::arg("oriented") = false, py::arg("planar") = false, py::arg("tessellate_tolerance") = 100.0, - R"pbdoc( + R"pbdoc(from_wkb(geography, /, *, oriented=False, planar=False, tessellate_tolerance=100.0) + Creates geographies from the Well-Known Bytes (WKB) representation. Parameters ---------- - a : bytes or array_like - WKB objects. + geography : bytes or array_like + The WKB byte object(s) to convert. oriented : bool, default False Set to True if polygon ring directions are known to be correct (i.e., exterior rings are defined counter clockwise and interior @@ -171,18 +190,30 @@ void init_io(py::module& m) { satisfy the planar edge constraint. This is only used if `planar` is set to True. + Returns + ------- + Geography or array + A single or an array of geography objects. + )pbdoc"); m.def("to_wkb", py::vectorize(ToWKB()), - py::arg("a"), - R"pbdoc( + py::arg("geography"), + py::pos_only(), + R"pbdoc(to_wkb(geography, /) + Returns the WKB representation of each geography. Parameters ---------- - a : :py:class:`Geography` or array_like - Geography object(s) + geography : :py:class:`Geography` or array_like + Geography object(s). + + Returns + ------- + bytes or array + A bytes object or an array of bytes. )pbdoc"); } diff --git a/src/predicates.cpp b/src/predicates.cpp index 38b46c0..521dfc5 100644 --- a/src/predicates.cpp +++ b/src/predicates.cpp @@ -69,7 +69,8 @@ void init_predicates(py::module& m) { py::vectorize(Predicate(s2geog::s2_intersects)), py::arg("a"), py::arg("b"), - R"pbdoc( + R"pbdoc(intersects(a, b) + Returns True if A and B share any portion of space. Intersects implies that overlaps, touches and within are True. @@ -77,7 +78,11 @@ void init_predicates(py::module& m) { Parameters ---------- a, b : :py:class:`Geography` or array_like - Geography object(s) + Geography object(s). + + Returns + ------- + bool or array )pbdoc"); @@ -85,7 +90,8 @@ void init_predicates(py::module& m) { py::vectorize(Predicate(s2geog::s2_equals)), py::arg("a"), py::arg("b"), - R"pbdoc( + R"pbdoc(equals(a, b) + Returns True if A and B are spatially equal. If A is within B and B is within A, A and B are considered equal. The @@ -94,7 +100,11 @@ void init_predicates(py::module& m) { Parameters ---------- a, b : :py:class:`Geography` or array_like - Geography object(s) + Geography object(s). + + Returns + ------- + bool or array )pbdoc"); @@ -102,13 +112,18 @@ void init_predicates(py::module& m) { py::vectorize(Predicate(s2geog::s2_contains)), py::arg("a"), py::arg("b"), - R"pbdoc( + R"pbdoc(contains(a, b) + Returns True if B is completely inside A. Parameters ---------- a, b : :py:class:`Geography` or array_like - Geography object(s) + Geography object(s). + + Returns + ------- + bool or array )pbdoc"); @@ -120,13 +135,18 @@ void init_predicates(py::module& m) { })), py::arg("a"), py::arg("b"), - R"pbdoc( + R"pbdoc(within(a, b) + Returns True if A is completely inside B. Parameters ---------- a, b : :py:class:`Geography` or array_like - Geography object(s) + Geography object(s). + + Returns + ------- + bool or array )pbdoc"); @@ -138,14 +158,19 @@ void init_predicates(py::module& m) { })), py::arg("a"), py::arg("b"), - R"pbdoc( + R"pbdoc(disjoint(a, b) + Returns True if A boundaries and interior does not intersect at all with those of B. Parameters ---------- a, b : :py:class:`Geography` or array_like - Geography object(s) + Geography object(s). + + Returns + ------- + bool or array )pbdoc"); @@ -153,7 +178,8 @@ void init_predicates(py::module& m) { py::vectorize(TouchesPredicate()), py::arg("a"), py::arg("b"), - R"pbdoc( + R"pbdoc(touches(a, b) + Returns True if A and B intersect, but their interiors do not intersect. A and B must have at least one point in common, where the common point @@ -162,7 +188,11 @@ void init_predicates(py::module& m) { Parameters ---------- a, b : :py:class:`Geography` or array_like - Geography object(s) + Geography object(s). + + Returns + ------- + bool or array )pbdoc"); @@ -180,13 +210,18 @@ void init_predicates(py::module& m) { closed_options)), py::arg("a"), py::arg("b"), - R"pbdoc( + R"pbdoc(covers(a, b) + Returns True if every point in B lies inside the interior or boundary of A. Parameters ---------- a, b : :py:class:`Geography` or array_like - Geography object(s) + Geography object(s). + + Returns + ------- + bool or array Notes ----- @@ -206,16 +241,22 @@ void init_predicates(py::module& m) { closed_options)), py::arg("a"), py::arg("b"), - R"pbdoc( + R"pbdoc(covered_by(a, b) + Returns True if every point in A lies inside the interior or boundary of B. Parameters ---------- a, b : :py:class:`Geography` or array_like - Geography object(s) + Geography object(s). + + Returns + ------- + bool or array See Also -------- covers + )pbdoc"); } diff --git a/src/projections.cpp b/src/projections.cpp index 9c2078b..c5fefad 100644 --- a/src/projections.cpp +++ b/src/projections.cpp @@ -7,12 +7,52 @@ #include "pybind11.hpp" namespace py = pybind11; -namespace s2geog = s2geography; using namespace spherely; void init_projections(py::module& m) { - py::class_(m, "Projection") - .def("lnglat", &Projection::lnglat) - .def("pseudo_mercator", &Projection::pseudo_mercator) - .def("orthographic", &Projection::orthographic); + py::class_ projection(m, "Projection", R"pbdoc( + Lightweight wrapper for selecting common reference systems used to + project Geography points or vertices. + + Cannot be instantiated directly. + + )pbdoc"); + + projection + .def_static("lnglat", &Projection::lnglat, R"pbdoc(lnglat() + + Selects the "plate carree" projection. + + This projection maps coordinates on the sphere to (longitude, latitude) pairs. + The x coordinates (longitude) span [-180, 180] and the y coordinates (latitude) + span [-90, 90]. + + )pbdoc") + .def_static("pseudo_mercator", &Projection::pseudo_mercator, R"pbdoc(pseudo_mercator() + + Selects the spherical Mercator projection. + + When used together with WGS84 coordinates, known as the "Web + Mercator" or "WGS84/Pseudo-Mercator" projection. + + )pbdoc") + .def_static("orthographic", + &Projection::orthographic, + py::arg("longitude"), + py::arg("latitude"), + R"pbdoc(orthographic(longitude, latitude) + + Selects an orthographic projection with the given centre point. + + The resulting coordinates depict a single hemisphere of the globe as + it appears from outer space, centred on the given point. + + Parameters + ---------- + longitude : float + Longitude coordinate of the center point, in degrees. + latitude : float + Latitude coordinate of the center point, in degrees. + + )pbdoc"); } diff --git a/src/projections.hpp b/src/projections.hpp index 7989053..f91f9a0 100644 --- a/src/projections.hpp +++ b/src/projections.hpp @@ -7,7 +7,6 @@ #include "pybind11.hpp" -namespace py = pybind11; namespace s2geog = s2geography; using namespace spherely; diff --git a/src/spherely.cpp b/src/spherely.cpp index fe28991..1f4dce7 100644 --- a/src/spherely.cpp +++ b/src/spherely.cpp @@ -15,6 +15,9 @@ void init_geoarrow(py::module&); void init_projections(py::module&); PYBIND11_MODULE(spherely, m) { + py::options options; + options.disable_function_signatures(); + m.doc() = R"pbdoc( Spherely --------- diff --git a/src/spherely.pyi b/src/spherely.pyi index 7168340..11186c2 100644 --- a/src/spherely.pyi +++ b/src/spherely.pyi @@ -89,10 +89,10 @@ class _VFunc_Nin1_Nout1(Generic[_NameType, _ScalarReturnType, _ArrayReturnDType] @property def __name__(self) -> _NameType: ... @overload - def __call__(self, geography: Geography) -> _ScalarReturnType: ... + def __call__(self, geography: Geography, /) -> _ScalarReturnType: ... @overload def __call__( - self, geography: Iterable[Geography] + self, geography: Iterable[Geography], / ) -> npt.NDArray[_ArrayReturnDType]: ... class _VFunc_Nin2_Nout1(Generic[_NameType, _ScalarReturnType, _ArrayReturnDType]): @@ -142,11 +142,11 @@ class _VFunc_Nin1optradius_Nout1( def __name__(self) -> _NameType: ... @overload def __call__( - self, a: Geography, radius: float = 6371010.0 + self, geography: Geography, /, radius: float = 6371010.0 ) -> _ScalarReturnType: ... @overload def __call__( - self, a: Iterable[Geography], radius: float = 6371010.0 + self, geography: Iterable[Geography], /, radius: float = 6371010.0 ) -> npt.NDArray[_ArrayReturnDType]: ... class _VFunc_Nin1optprecision_Nout1( @@ -155,10 +155,12 @@ class _VFunc_Nin1optprecision_Nout1( @property def __name__(self) -> _NameType: ... @overload - def __call__(self, a: Geography, precision: int = 6) -> _ScalarReturnType: ... + def __call__( + self, geography: Geography, /, precision: int = 6 + ) -> _ScalarReturnType: ... @overload def __call__( - self, a: Iterable[Geography], precision: int = 6 + self, geography: Iterable[Geography], /, precision: int = 6 ) -> npt.NDArray[_ArrayReturnDType]: ... # /// End types @@ -266,28 +268,36 @@ to_wkb: _VFunc_Nin1_Nout1[Literal["to_wkb"], bytes, object] @overload def from_wkt( - a: str, + geography: str, + /, + *, oriented: bool = False, planar: bool = False, tessellate_tolerance: float = 100.0, ) -> Geography: ... @overload def from_wkt( - a: list[str] | npt.NDArray[np.str_], + geography: list[str] | npt.NDArray[np.str_], + /, + *, oriented: bool = False, planar: bool = False, tessellate_tolerance: float = 100.0, ) -> T_NDArray_Geography: ... @overload def from_wkb( - a: bytes, + geography: bytes, + /, + *, oriented: bool = False, planar: bool = False, tessellate_tolerance: float = 100.0, ) -> Geography: ... @overload def from_wkb( - a: Iterable[bytes], + geography: Iterable[bytes], + /, + *, oriented: bool = False, planar: bool = False, tessellate_tolerance: float = 100.0, @@ -304,7 +314,7 @@ class ArrowArrayExportable(Protocol): class ArrowArrayHolder(ArrowArrayExportable): ... def to_geoarrow( - input: Geography | T_NDArray_Geography, + geographies: Geography | T_NDArray_Geography, /, *, output_schema: ArrowSchemaExportable | str | None = None, @@ -314,7 +324,7 @@ def to_geoarrow( precision: int = 6, ) -> ArrowArrayExportable: ... def from_geoarrow( - input: ArrowArrayExportable, + geographies: ArrowArrayExportable, /, *, oriented: bool = False, diff --git a/tests/test_accessors.py b/tests/test_accessors.py index bdf4128..9c74451 100644 --- a/tests/test_accessors.py +++ b/tests/test_accessors.py @@ -87,37 +87,6 @@ def test_convex_hull(geog, expected) -> None: assert spherely.equals(actual, expected) -def test_get_x_y() -> None: - # scalar - a = spherely.create_point(1.5, 2.6) - assert spherely.get_x(a) == pytest.approx(1.5, abs=1e-14) - assert spherely.get_y(a) == pytest.approx(2.6, abs=1e-14) - - # array - arr = np.array( - [ - spherely.create_point(0, 1), - spherely.create_point(1, 2), - spherely.create_point(2, 3), - ] - ) - - actual = spherely.get_x(arr) - expected = np.array([0, 1, 2], dtype="float64") - np.testing.assert_allclose(actual, expected) - - actual = spherely.get_y(arr) - expected = np.array([1, 2, 3], dtype="float64") - np.testing.assert_allclose(actual, expected) - - # only points are supported - with pytest.raises(ValueError): - spherely.get_x(spherely.create_linestring([(0, 1), (1, 2)])) - - with pytest.raises(ValueError): - spherely.get_y(spherely.create_linestring([(0, 1), (1, 2)])) - - @pytest.mark.parametrize( "geog_a, geog_b, expected", [ diff --git a/tests/test_geography.py b/tests/test_geography.py index 6713141..fd676d2 100644 --- a/tests/test_geography.py +++ b/tests/test_geography.py @@ -6,6 +6,17 @@ import spherely +def test_geography_type() -> None: + assert spherely.GeographyType.NONE.value == -1 + assert spherely.GeographyType.POINT.value == 0 + assert spherely.GeographyType.LINESTRING.value == 1 + assert spherely.GeographyType.POLYGON.value == 2 + assert spherely.GeographyType.MULTIPOINT.value == 3 + assert spherely.GeographyType.MULTILINESTRING.value == 4 + assert spherely.GeographyType.MULTIPOLYGON.value == 5 + assert spherely.GeographyType.GEOMETRYCOLLECTION.value == 6 + + def test_is_geography() -> None: arr = np.array([1, 2.33, spherely.create_point(30, 6)]) @@ -85,6 +96,57 @@ def test_get_dimensions() -> None: assert spherely.get_dimensions(spherely.create_point(5, 40)) == 0 +def test_get_x_y() -> None: + # scalar + a = spherely.create_point(1.5, 2.6) + assert spherely.get_x(a) == pytest.approx(1.5, abs=1e-14) + assert spherely.get_y(a) == pytest.approx(2.6, abs=1e-14) + + # array + arr = np.array( + [ + spherely.create_point(0, 1), + spherely.create_point(1, 2), + spherely.create_point(2, 3), + ] + ) + + actual = spherely.get_x(arr) + expected = np.array([0, 1, 2], dtype="float64") + np.testing.assert_allclose(actual, expected) + + actual = spherely.get_y(arr) + expected = np.array([1, 2, 3], dtype="float64") + np.testing.assert_allclose(actual, expected) + + # only points are supported + with pytest.raises(ValueError): + spherely.get_x(spherely.create_linestring([(0, 1), (1, 2)])) + + with pytest.raises(ValueError): + spherely.get_y(spherely.create_linestring([(0, 1), (1, 2)])) + + +@pytest.mark.parametrize( + "empty_geog, expected", + [ + (spherely.create_point(), 0), + (spherely.create_linestring(), 1), + (spherely.create_polygon(), 2), + (spherely.create_collection([]), -1), + ], +) +def test_get_dimensions_empty(empty_geog, expected) -> None: + assert spherely.get_dimensions(empty_geog) == expected + + +def test_get_dimensions_collection() -> None: + geog = spherely.create_collection( + [spherely.create_point(0, 0), spherely.create_polygon([(0, 0), (1, 1), (2, 0)])] + ) + assert spherely.get_dimensions(geog) == 2 + + def test_prepare() -> None: # test array geog = np.array(