Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: add projection support in geoarrow output #69

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ set(CPP_SOURCES
src/spherely.cpp)

if(${s2geography_VERSION} VERSION_GREATER_EQUAL "0.2.0")
set(CPP_SOURCES ${CPP_SOURCES} src/geoarrow.cpp)
set(CPP_SOURCES ${CPP_SOURCES} src/geoarrow.cpp src/projections.cpp)
endif()

add_library(spherely MODULE ${CPP_SOURCES})
Expand Down
10 changes: 8 additions & 2 deletions src/geoarrow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "constants.hpp"
#include "creation.hpp"
#include "geography.hpp"
#include "projections.hpp"
#include "pybind11.hpp"

namespace py = pybind11;
Expand Down Expand Up @@ -197,6 +198,7 @@ class ArrowArrayHolder {

ArrowArrayHolder to_geoarrow(py::array_t<PyObjectGeography> input,
py::object output_schema,
Projection projection,
bool planar,
float tessellate_tolerance,
int precision) {
Expand All @@ -207,11 +209,10 @@ ArrowArrayHolder to_geoarrow(py::array_t<PyObjectGeography> input,

s2geog::geoarrow::ExportOptions options;
options.set_precision(precision);
options.set_projection(projection.s2_projection());
if (planar) {
auto tol = S1Angle::Radians(tessellate_tolerance / EARTH_RADIUS_METERS);
options.set_tessellate_tolerance(tol);
// TODO make this configurable
// options.set_projection(s2geog::geoarrow::mercator());
}

if (!output_schema.is(py::none())) {
Expand Down Expand Up @@ -315,6 +316,7 @@ void init_geoarrow(py::module& m) {
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,
Expand All @@ -338,6 +340,10 @@ void init_geoarrow(py::module& m) {
can be a ``pyarrow.DataType`` or ``pyarrow.Field``, and you can
use the ``geoarrow.pyarrow`` package to construct such geoarrow
extension types.
projection : spherely.Projection, default Projection.lnglat()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
projection : spherely.Projection, default Projection.lnglat()
projection : :py:class:`Projection`, default :py:meth:`Projection.lnglat`

Although maybe both are rendered as hyperlinks by sphinx? Either way it needs Projection be added to the API reference doc.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I added it to the API docs to see if the linking would go automatically, which I suppose will be the case>? (at least for Geography it works)

But our docs get build with the released s2geography, of course, so this is not yet included

The projection to use when converting the geographies to the output
encoding. By default, uses longitude/latitude coordinates ("plate
carree" projection).
planar : bool, default False
If set to True, the edges of linestrings and polygons in the output
are assumed to be linear on the plane. In that case, additional
Expand Down
18 changes: 18 additions & 0 deletions src/projections.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#include "projections.hpp"

#include <s2/s2projections.h>
#include <s2geography.h>

#include "geography.hpp"
#include "pybind11.hpp"

namespace py = pybind11;
namespace s2geog = s2geography;
using namespace spherely;

void init_projections(py::module& m) {
py::class_<Projection>(m, "Projection")
.def("lnglat", &Projection::lnglat)
.def("pseudo_mercator", &Projection::pseudo_mercator)
.def("orthographic", &Projection::orthographic);
}
38 changes: 38 additions & 0 deletions src/projections.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#ifndef SPHERELY_PROJECTIONS_H_
#define SPHERELY_PROJECTIONS_H_

#include <s2/s2latlng.h>
#include <s2/s2projections.h>
#include <s2geography.h>

#include "pybind11.hpp"

namespace py = pybind11;
namespace s2geog = s2geography;
using namespace spherely;

class Projection {
public:
Projection(std::shared_ptr<S2::Projection> projection)
: m_s2_projection(std::move(projection)) {}

std::shared_ptr<S2::Projection> s2_projection() {
return m_s2_projection;
}

static Projection lnglat() {
return Projection(std::move(s2geog::lnglat()));
}
static Projection pseudo_mercator() {
return Projection(std::move(s2geog::pseudo_mercator()));
}
static Projection orthographic(double longitude, double latitude) {
return Projection(
std::move(s2geog::orthographic(S2LatLng::FromDegrees(latitude, longitude))));
}

private:
std::shared_ptr<S2::Projection> m_s2_projection;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why a shared pointer is needed (apart from that this is what is used in s2geography), but that's not a big deal anyway. Feel free to ignore this comment.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We indeed need a shared pointer to pass to s2geography (and it is also what is returned by the s2geography constructors), so then I though that might be easier to just store the shared_ptr on the object, instead of unpacking it / packing it again in a shared_ptr ?

};

#endif // SPHERELY_PROJECTIONS_H_
2 changes: 2 additions & 0 deletions src/spherely.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ void init_io(py::module&);
#if defined(S2GEOGRAPHY_VERSION_MAJOR) && \
(S2GEOGRAPHY_VERSION_MAJOR >= 1 || S2GEOGRAPHY_VERSION_MINOR >= 2)
void init_geoarrow(py::module&);
void init_projections(py::module&);
#endif

PYBIND11_MODULE(spherely, m) {
Expand All @@ -31,6 +32,7 @@ PYBIND11_MODULE(spherely, m) {
init_io(m);
#if defined(S2GEOGRAPHY_VERSION_MAJOR) && \
(S2GEOGRAPHY_VERSION_MAJOR >= 1 || S2GEOGRAPHY_VERSION_MINOR >= 2)
init_projections(m);
init_geoarrow(m);
#endif

Expand Down
54 changes: 54 additions & 0 deletions tests/test_geoarrow.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,57 @@ def test_to_geoarrow_invalid_output_schema():

with pytest.raises(ValueError, match="Did you pass a valid schema"):
spherely.to_geoarrow(arr, output_schema=pa.schema([("test", pa.int64())]))


def test_to_geoarrow_projected():
arr = spherely.points([1, 2, 3], [1, 2, 3])
point_schema = ga.point().with_coord_type(ga.CoordType.INTERLEAVED)
result = pa.array(
spherely.to_geoarrow(
arr, output_schema=point_schema, projection=spherely.Projection.lnglat()
)
)

coords = np.asarray(result.storage.values)
expected = np.array([1, 1, 2, 2, 3, 3], dtype="float64")
np.testing.assert_allclose(coords, expected)

# Output to pseudo mercator - generation of expected result
# import pyproj
# trans = pyproj.Transformer.from_crs("EPSG:4326", "EPSG:3857", always_xy=True)
# trans.transform([1, 2, 3], [1, 2, 3])
result = pa.array(
spherely.to_geoarrow(
arr,
output_schema=point_schema,
projection=spherely.Projection.pseudo_mercator(),
)
)
coords = np.asarray(result.storage.values)
expected = np.array(
[
111319.49079327357,
111325.1428663851,
222638.98158654713,
222684.20850554405,
333958.4723798207,
334111.1714019596,
],
dtype="float64",
)
np.testing.assert_allclose(coords, expected)

# Output to orthographic
result = pa.array(
spherely.to_geoarrow(
arr,
output_schema=point_schema,
projection=spherely.Projection.orthographic(0.0, 0.0),
)
)
coords = np.asarray(result.storage.values)
expected = np.array(
[0.01744975, 0.01745241, 0.03487824, 0.0348995, 0.05226423, 0.05233596],
dtype="float64",
)
np.testing.assert_allclose(coords, expected, rtol=1e-06)
Loading