diff --git a/src/s2geography/geoarrow.cc b/src/s2geography/geoarrow.cc index cf77573..220a3ba 100644 --- a/src/s2geography/geoarrow.cc +++ b/src/s2geography/geoarrow.cc @@ -672,6 +672,380 @@ void Reader::ReadGeography(const ArrowArray* array, int64_t offset, impl_->ReadGeography(array, offset, length, out); } +// Write Geography objects to a GeoArrow array. +// +// This class walks through the geographies and calls the visitor methods +// provided by the geoarrow-c GeoArrowArrayWriter, which then builds up the +// GeoArrow array. +class WriterImpl { + public: + WriterImpl() { + error_.message[0] = '\0'; + writer_.private_data = nullptr; + } + + ~WriterImpl() { + if (writer_.private_data != nullptr) { + GeoArrowArrayWriterReset(&writer_); + } + } + + void Init(const ArrowSchema* schema, const ExportOptions& options) { + options_ = options; + + int code = GeoArrowArrayWriterInitFromSchema(&writer_, schema); + ThrowNotOk(code); + + GeoArrowSchemaView schema_view; + code = GeoArrowSchemaViewInit(&schema_view, schema, &error_); + ThrowNotOk(code); + type_ = schema_view.type; + + InitCommon(); + } + + void Init(GeoArrowType type, const ExportOptions& options) { + options_ = options; + type_ = type; + + int code = GeoArrowArrayWriterInitFromType(&writer_, type); + ThrowNotOk(code); + + InitCommon(); + } + + void InitCommon() { + int code; + + if (type_ == GEOARROW_TYPE_WKT || type_ == GEOARROW_TYPE_LARGE_WKT) { + code = GeoArrowArrayWriterSetPrecision(&writer_, options_.precision()); + ThrowNotOk(code); + code = GeoArrowArrayWriterSetFlatMultipoint(&writer_, false); + ThrowNotOk(code); + } + + visitor_.error = &error_; + code = GeoArrowArrayWriterInitVisitor(&writer_, &visitor_); + ThrowNotOk(code); + + // Currently we always visit single coordinate pairs one by one, so set + // up the appropiate view for that once, which is then reused + coords_view_.n_coords = 1; + coords_view_.n_values = 2; + coords_view_.coords_stride = 2; + coords_view_.values[0] = &coords_[0]; + coords_view_.values[1] = &coords_[1]; + } + + void WriteGeography(const Geography& geog) { VisitFeature(geog); } + + void Finish(struct ArrowArray* out) { + int code = GeoArrowArrayWriterFinish(&writer_, out, &error_); + ThrowNotOk(code); + } + + private: + ExportOptions options_; + GeoArrowType type_; + GeoArrowArrayWriter writer_; + GeoArrowVisitor visitor_; + GeoArrowCoordView coords_view_; + double coords_[2]; + GeoArrowError error_; + + int VisitPoints(const PointGeography& point) { + + if (point.Points().size() == 0) { + // empty Point + GEOARROW_RETURN_NOT_OK(visitor_.geom_start( + &visitor_, GEOARROW_GEOMETRY_TYPE_POINT, GEOARROW_DIMENSIONS_XY)); + GEOARROW_RETURN_NOT_OK(visitor_.geom_end(&visitor_)); + + } else if (point.Points().size() == 1) { + // Point + GEOARROW_RETURN_NOT_OK(visitor_.geom_start( + &visitor_, GEOARROW_GEOMETRY_TYPE_POINT, GEOARROW_DIMENSIONS_XY)); + S2LatLng ll(point.Points()[0]); + coords_[0] = ll.lng().degrees(); + coords_[1] = ll.lat().degrees(); + GEOARROW_RETURN_NOT_OK(visitor_.coords(&visitor_, &coords_view_)); + GEOARROW_RETURN_NOT_OK(visitor_.geom_end(&visitor_)); + + } else { + // MultiPoint + GEOARROW_RETURN_NOT_OK( + visitor_.geom_start(&visitor_, GEOARROW_GEOMETRY_TYPE_MULTIPOINT, + GEOARROW_DIMENSIONS_XY)); + + for (const S2Point& pt : point.Points()) { + GEOARROW_RETURN_NOT_OK(visitor_.geom_start( + &visitor_, GEOARROW_GEOMETRY_TYPE_POINT, GEOARROW_DIMENSIONS_XY)); + S2LatLng ll(pt); + coords_[0] = ll.lng().degrees(); + coords_[1] = ll.lat().degrees(); + GEOARROW_RETURN_NOT_OK(visitor_.coords(&visitor_, &coords_view_)); + GEOARROW_RETURN_NOT_OK(visitor_.geom_end(&visitor_)); + } + + GEOARROW_RETURN_NOT_OK(visitor_.geom_end(&visitor_)); + } + return GEOARROW_OK; + } + + int VisitPolylines(const PolylineGeography& geog) { + + if (geog.Polylines().size() == 0) { + // empty LineString + GEOARROW_RETURN_NOT_OK( + visitor_.geom_start(&visitor_, GEOARROW_GEOMETRY_TYPE_LINESTRING, + GEOARROW_DIMENSIONS_XY)); + GEOARROW_RETURN_NOT_OK(visitor_.geom_end(&visitor_)); + + } else if (geog.Polylines().size() == 1) { + // LineString + GEOARROW_RETURN_NOT_OK( + visitor_.geom_start(&visitor_, GEOARROW_GEOMETRY_TYPE_LINESTRING, + GEOARROW_DIMENSIONS_XY)); + + const auto& poly = geog.Polylines()[0]; + + for (int i = 0; i < poly->num_vertices(); i++) { + S2LatLng ll(poly->vertex(i)); + coords_[0] = ll.lng().degrees(); + coords_[1] = ll.lat().degrees(); + GEOARROW_RETURN_NOT_OK(visitor_.coords(&visitor_, &coords_view_)); + } + + GEOARROW_RETURN_NOT_OK(visitor_.geom_end(&visitor_)); + + } else { + // MultiLineString + GEOARROW_RETURN_NOT_OK( + visitor_.geom_start(&visitor_, GEOARROW_GEOMETRY_TYPE_MULTILINESTRING, + GEOARROW_DIMENSIONS_XY)); + + for (const auto& poly : geog.Polylines()) { + GEOARROW_RETURN_NOT_OK( + visitor_.geom_start(&visitor_, GEOARROW_GEOMETRY_TYPE_LINESTRING, + GEOARROW_DIMENSIONS_XY)); + + for (int i = 0; i < poly->num_vertices(); i++) { + S2LatLng ll(poly->vertex(i)); + coords_[0] = ll.lng().degrees(); + coords_[1] = ll.lat().degrees(); + GEOARROW_RETURN_NOT_OK(visitor_.coords(&visitor_, &coords_view_)); + } + + GEOARROW_RETURN_NOT_OK(visitor_.geom_end(&visitor_)); + } + + GEOARROW_RETURN_NOT_OK(visitor_.geom_end(&visitor_)); + } + + return GEOARROW_OK; + } + + int VisitLoopShell(const S2Loop* loop) { + + if (loop->num_vertices() == 0) { + throw Exception("Unexpected S2Loop with 0 verties"); + } + + GEOARROW_RETURN_NOT_OK(visitor_.ring_start(&visitor_)); + + for (int i = 0; i <= loop->num_vertices(); i++) { + S2LatLng ll(loop->vertex(i)); + coords_[0] = ll.lng().degrees(); + coords_[1] = ll.lat().degrees(); + GEOARROW_RETURN_NOT_OK(visitor_.coords(&visitor_, &coords_view_)); + } + + GEOARROW_RETURN_NOT_OK(visitor_.ring_end(&visitor_)); + + return GEOARROW_OK; + } + + int VisitLoopHole(const S2Loop* loop) { + + if (loop->num_vertices() == 0) { + throw Exception("Unexpected S2Loop with 0 verties"); + } + + GEOARROW_RETURN_NOT_OK(visitor_.ring_start(&visitor_)); + + // For the hole, we use the vertices in reverse order to ensure the holes + // have the opposite orientation of the shell + for (int i = loop->num_vertices() - 1; i >= 0; i--) { + S2LatLng ll(loop->vertex(i)); + coords_[0] = ll.lng().degrees(); + coords_[1] = ll.lat().degrees(); + GEOARROW_RETURN_NOT_OK(visitor_.coords(&visitor_, &coords_view_)); + } + + S2LatLng ll(loop->vertex(loop->num_vertices() - 1)); + coords_[0] = ll.lng().degrees(); + coords_[1] = ll.lat().degrees(); + GEOARROW_RETURN_NOT_OK(visitor_.coords(&visitor_, &coords_view_)); + + GEOARROW_RETURN_NOT_OK(visitor_.ring_end(&visitor_)); + return GEOARROW_OK; + } + + int VisitPolygonShell(const S2Polygon& poly, int loop_start) { + const S2Loop* loop0 = poly.loop(loop_start); + GEOARROW_RETURN_NOT_OK(VisitLoopShell(loop0)); + for (int j = loop_start + 1; j <= poly.GetLastDescendant(loop_start); j++) { + const S2Loop* loop = poly.loop(j); + if (loop->depth() == (loop0->depth() + 1)) { + GEOARROW_RETURN_NOT_OK(VisitLoopHole(loop)); + } + } + + return GEOARROW_OK; + } + + int VisitPolygons(const PolygonGeography& geog) { + const S2Polygon& poly = *geog.Polygon(); + + // find the outer shells (loop depth = 0, 2, 4, etc.) + std::vector outer_shell_loop_ids; + + outer_shell_loop_ids.reserve(poly.num_loops()); + for (int i = 0; i < poly.num_loops(); i++) { + if ((poly.loop(i)->depth() % 2) == 0) { + outer_shell_loop_ids.push_back(i); + } + } + + if (outer_shell_loop_ids.size() == 0) { + // empty Polygon + GEOARROW_RETURN_NOT_OK(visitor_.geom_start( + &visitor_, GEOARROW_GEOMETRY_TYPE_POLYGON, GEOARROW_DIMENSIONS_XY)); + GEOARROW_RETURN_NOT_OK(visitor_.geom_end(&visitor_)); + + } else if (outer_shell_loop_ids.size() == 1) { + // Polygon + GEOARROW_RETURN_NOT_OK(visitor_.geom_start( + &visitor_, GEOARROW_GEOMETRY_TYPE_POLYGON, GEOARROW_DIMENSIONS_XY)); + GEOARROW_RETURN_NOT_OK(VisitPolygonShell(poly, outer_shell_loop_ids[0])); + GEOARROW_RETURN_NOT_OK(visitor_.geom_end(&visitor_)); + + } else { + // MultiPolygon + GEOARROW_RETURN_NOT_OK( + visitor_.geom_start(&visitor_, GEOARROW_GEOMETRY_TYPE_MULTIPOLYGON, + GEOARROW_DIMENSIONS_XY)); + for (size_t i = 0; i < outer_shell_loop_ids.size(); i++) { + GEOARROW_RETURN_NOT_OK(visitor_.geom_start( + &visitor_, GEOARROW_GEOMETRY_TYPE_POLYGON, GEOARROW_DIMENSIONS_XY)); + GEOARROW_RETURN_NOT_OK( + VisitPolygonShell(poly, outer_shell_loop_ids[i])); + GEOARROW_RETURN_NOT_OK(visitor_.geom_end(&visitor_)); + } + GEOARROW_RETURN_NOT_OK(visitor_.geom_end(&visitor_)); + } + + return GEOARROW_OK; + } + + int VisitCollection(const GeographyCollection& geog) { + GEOARROW_RETURN_NOT_OK(visitor_.geom_start( + &visitor_, GEOARROW_GEOMETRY_TYPE_GEOMETRYCOLLECTION, + GEOARROW_DIMENSIONS_XY)); + + for (const auto& child_geog : geog.Features()) { + auto child_point = dynamic_cast(child_geog.get()); + if (child_point != nullptr) { + GEOARROW_RETURN_NOT_OK(VisitPoints(*child_point)); + } else { + auto child_polyline = + dynamic_cast(child_geog.get()); + if (child_polyline != nullptr) { + GEOARROW_RETURN_NOT_OK(VisitPolylines(*child_polyline)); + } else { + auto child_polygon = + dynamic_cast(child_geog.get()); + if (child_polygon != nullptr) { + GEOARROW_RETURN_NOT_OK(VisitPolygons(*child_polygon)); + } else { + auto child_collection = + dynamic_cast(child_geog.get()); + if (child_collection != nullptr) { + GEOARROW_RETURN_NOT_OK(VisitCollection(*child_collection)); + } else { + throw Exception("Unsupported Geography subclass"); + } + } + } + } + } + GEOARROW_RETURN_NOT_OK(visitor_.geom_end(&visitor_)); + + return GEOARROW_OK; + } + + int VisitFeature(const Geography& geog) { + GEOARROW_RETURN_NOT_OK(visitor_.feat_start(&visitor_)); + + auto child_point = dynamic_cast(&geog); + if (child_point != nullptr) { + GEOARROW_RETURN_NOT_OK(VisitPoints(*child_point)); + } else { + auto child_polyline = dynamic_cast(&geog); + if (child_polyline != nullptr) { + GEOARROW_RETURN_NOT_OK(VisitPolylines(*child_polyline)); + } else { + auto child_polygon = dynamic_cast(&geog); + if (child_polygon != nullptr) { + GEOARROW_RETURN_NOT_OK(VisitPolygons(*child_polygon)); + } else { + auto child_collection = + dynamic_cast(&geog); + if (child_collection != nullptr) { + GEOARROW_RETURN_NOT_OK(VisitCollection(*child_collection)); + } else { + throw Exception("Unsupported Geography subclass"); + } + } + } + } + return GEOARROW_OK; + } + + void ThrowNotOk(int code) { + if (code != GEOARROW_OK) { + throw Exception(error_.message); + } + } +}; + +Writer::Writer() : impl_(new WriterImpl()) {} + +Writer::~Writer() { impl_.reset(); } + +void Writer::Init(const ArrowSchema* schema, const ExportOptions& options) { + impl_->Init(schema, options); +} + +void Writer::Init(OutputType output_type, const ExportOptions& options) { + switch (output_type) { + case OutputType::kWKT: + impl_->Init(GEOARROW_TYPE_WKT, options); + break; + case OutputType::kWKB: + impl_->Init(GEOARROW_TYPE_WKB, options); + break; + default: + throw Exception("Output type not supported"); + } +} + +void Writer::WriteGeography(const Geography& geog) { + impl_->WriteGeography(geog); +} + +void Writer::Finish(struct ArrowArray* out) { impl_->Finish(out); } + } // namespace geoarrow } // namespace s2geography diff --git a/src/s2geography/geoarrow.h b/src/s2geography/geoarrow.h index be708d7..52ba639 100644 --- a/src/s2geography/geoarrow.h +++ b/src/s2geography/geoarrow.h @@ -19,17 +19,12 @@ const char* version(); S2::Projection* lnglat(); /// \brief Options used to build Geography objects from GeoArrow arrays -class ImportOptions { + +class TessellationOptions { public: - ImportOptions() - : oriented_(false), - check_(true), - projection_(lnglat()), + TessellationOptions() + : projection_(lnglat()), tessellate_tolerance_(S1Angle::Infinity()) {} - bool oriented() const { return oriented_; } - void set_oriented(bool oriented) { oriented_ = oriented; } - bool check() const { return check_; } - void set_check(bool check) { check_ = check; } S2::Projection* projection() const { return projection_; } void set_projection(S2::Projection* projection) { projection_ = projection; } S1Angle tessellate_tolerance() const { return tessellate_tolerance_; } @@ -37,11 +32,25 @@ class ImportOptions { tessellate_tolerance_ = tessellate_tolerance; } + protected: + S2::Projection* projection_; + S1Angle tessellate_tolerance_; +}; + +class ImportOptions : public TessellationOptions { + public: + ImportOptions() + : TessellationOptions(), + oriented_(false), + check_(true) {} + bool oriented() const { return oriented_; } + void set_oriented(bool oriented) { oriented_ = oriented; } + bool check() const { return check_; } + void set_check(bool check) { check_ = check; } + private: bool oriented_; bool check_; - S2::Projection* projection_; - S1Angle tessellate_tolerance_; }; class ReaderImpl; @@ -69,6 +78,48 @@ class Reader { std::unique_ptr impl_; }; +class ExportOptions : public TessellationOptions { + public: + ExportOptions() + : TessellationOptions(), + precision_(16) {} + // The number of digits after the decimal to output in WKT (default 16) + int precision() const { return precision_; } + void set_precision(int precision) { precision_ = precision; } + + private: + int precision_; +}; + +class WriterImpl; + +/// \brief Array writer for any GeoArrow extension array +/// +/// This class is used to convert Geography objects into an ArrowArray +/// with geoarrow data (serialized or native). +class Writer { + public: + enum class OutputType { kWKT, kWKB }; + Writer(); + ~Writer(); + + void Init(const ArrowSchema* schema) { Init(schema, ExportOptions()); } + + void Init(const ArrowSchema* schema, const ExportOptions& options); + + void Init(OutputType output_type, const ExportOptions& options); + + void WriteGeography(const Geography& geog); + + // TODO + // void WriteNull() + + void Finish(struct ArrowArray* out); + + private: + std::unique_ptr impl_; +}; + } // namespace geoarrow } // namespace s2geography diff --git a/src/s2geography/wkt-writer.cc b/src/s2geography/wkt-writer.cc index 14fd8b7..1cb75c0 100644 --- a/src/s2geography/wkt-writer.cc +++ b/src/s2geography/wkt-writer.cc @@ -2,475 +2,36 @@ #include "s2geography/wkt-writer.h" #include "s2geography/accessors.h" - -// using ryu for double -> char* is ~5x faster! -#ifndef geoarrow_d2s_fixed_n -static inline int geoarrow_compat_d2s_fixed_n(double f, uint32_t precision, - char* result) { - return snprintf(result, 128, "%.*g", precision, f); -} - -#define geoarrow_d2s_fixed_n geoarrow_compat_d2s_fixed_n -#endif +#include "s2geography/geoarrow.h" namespace s2geography { -class WKTStreamWriter : public Handler { - public: - WKTStreamWriter(std::ostream& stream, int significant_digits = 16) - : significant_digits_(significant_digits), - is_first_ring_(true), - is_first_coord_(true), - dimensions_(util::Dimensions::XY), - stream_(stream) { - stack_.reserve(32); - } - - void new_dimensions(util::Dimensions dimensions) { dimensions_ = dimensions; } - - Result feat_start() { - stack_.clear(); - is_first_ring_ = true; - is_first_coord_ = true; - return Result::CONTINUE; - } - - Result null_feat() { throw Exception("null_feat() is not implemented"); } - - Result geom_start(util::GeometryType geometry_type, int64_t size) { - if (stack_.size() > 0 && stack_.back().part > 0) { - write_string(", "); - } - - if (stack_.size() > 0) { - stack_.back().part++; - } - - if (stack_.size() == 0 || - stack_.back().type == util::GeometryType::GEOMETRYCOLLECTION) { - switch (geometry_type) { - case util::GeometryType::POINT: - write_string("POINT"); - break; - case util::GeometryType::LINESTRING: - write_string("LINESTRING"); - break; - case util::GeometryType::POLYGON: - write_string("POLYGON"); - break; - case util::GeometryType::MULTIPOINT: - write_string("MULTIPOINT"); - break; - case util::GeometryType::MULTILINESTRING: - write_string("MULTILINESTRING"); - break; - case util::GeometryType::MULTIPOLYGON: - write_string("MULTIPOLYGON"); - break; - case util::GeometryType::GEOMETRYCOLLECTION: - write_string("GEOMETRYCOLLECTION"); - - break; - default: - throw Exception("Unknown geometry type in WKTWriter"); - } - - write_char(' '); - - switch (dimensions_) { - case util::Dimensions::XYZM: - write_string("ZM "); - break; - case util::Dimensions::XYZ: - write_string("Z "); - break; - case util::Dimensions::XYM: - write_string("M "); - break; - default: - break; - } - } - - if (size == 0) { - write_string("EMPTY"); - } else { - write_char('('); - } - - stack_.push_back(State{geometry_type, size, 0}); - is_first_ring_ = true; - is_first_coord_ = true; - return Result::CONTINUE; - } - - Result ring_start(int64_t /*size*/) { - if (!is_first_ring_) { - write_string(", "); - } else { - is_first_ring_ = false; - } - - write_char('('); - - is_first_coord_ = true; - return Result::CONTINUE; - } - - Result coords(const double* coord, int64_t n, int32_t coord_size) { - for (int64_t i = 0; i < n; i++) { - if (!is_first_coord_) { - write_string(", "); - } - - write_coord(coord[i * coord_size]); - for (int32_t j = 1; j < coord_size; j++) { - write_char(' '); - write_coord(coord[i * coord_size + j]); - } - - is_first_coord_ = false; - } - - return Result::CONTINUE; - } - - Result ring_end() { - write_char(')'); - return Result::CONTINUE; - } - - Result geom_end() { - if (stack_.size() > 0 && stack_.back().size != 0) { - write_char(')'); - } - - if (stack_.size() > 0) { - stack_.pop_back(); - } - - return Result::CONTINUE; - } - - Result feat_end() { return Result::CONTINUE; } - - private: - class State { - public: - util::GeometryType type; - int64_t size; - int64_t part; - }; - - int significant_digits_; - std::vector stack_; - bool is_first_ring_; - bool is_first_coord_; - util::Dimensions dimensions_; - - char write_buffer_[1024]; - std::ostream& stream_; - - void write_coord(double value) { - int n_needed = - geoarrow_d2s_fixed_n(value, significant_digits_, write_buffer_); - stream_ << std::string(write_buffer_, n_needed); - } - - void write_string(const char* value) { stream_ << value; } - - void write_char(char value) { stream_ << std::string(&value, 1); } -}; - WKTWriter::WKTWriter() : WKTWriter(16) {} -WKTWriter::WKTWriter(int significant_digits) - : geometry_type_(util::GeometryType::GEOMETRY_TYPE_UNKNOWN) { - this->exporter_ = - absl::make_unique(stream_, significant_digits); - exporter_->new_dimensions(util::Dimensions::XY); - exporter_->new_geometry_type(util::GeometryType::GEOMETRY_TYPE_UNKNOWN); -} +WKTWriter::WKTWriter(int precision) { + geoarrow::ExportOptions options; + options.set_precision(precision); -std::string WKTWriter::write_feature(const Geography& geog) { - stream_.str(""); - handle_feature(geog, exporter_.get()); - return stream_.str(); + writer_ = absl::make_unique(); + writer_->Init(geoarrow::Writer::OutputType::kWKT, options); } -Handler::Result WKTWriter::handle_points(const PointGeography& geog, - Handler* handler) { - Handler::Result result; - double coords[2]; +WKTWriter::WKTWriter(const geoarrow::ExportOptions& options) { + writer_ = absl::make_unique(); + writer_->Init(geoarrow::Writer::OutputType::kWKT, options); - if (geog.Points().size() == 0) { - handler->new_geometry_type(util::GeometryType::POINT); - HANDLE_OR_RETURN(handler->geom_start(util::GeometryType::POINT, 0)); - HANDLE_OR_RETURN(handler->geom_end()); - } else if (geog.Points().size() == 1) { - handler->new_geometry_type(util::GeometryType::POINT); - handler->geom_start(util::GeometryType::POINT, 1); - S2LatLng ll(geog.Points()[0]); - coords[0] = ll.lng().degrees(); - coords[1] = ll.lat().degrees(); - HANDLE_OR_RETURN(handler->coords(coords, 1, 2)); - HANDLE_OR_RETURN(handler->geom_end()); - } else { - handler->new_geometry_type(util::GeometryType::MULTIPOINT); - HANDLE_OR_RETURN(handler->geom_start(util::GeometryType::MULTIPOINT, - geog.Points().size())); - - for (const S2Point& pt : geog.Points()) { - handler->geom_start(util::GeometryType::POINT, 1); - S2LatLng ll(pt); - coords[0] = ll.lng().degrees(); - coords[1] = ll.lat().degrees(); - HANDLE_OR_RETURN(handler->coords(coords, 1, 2)); - HANDLE_OR_RETURN(handler->geom_end()); - } - - handler->geom_end(); - } - - return Handler::Result::CONTINUE; } -Handler::Result WKTWriter::handle_polylines(const PolylineGeography& geog, - Handler* handler) { - Handler::Result result; - double coords[2]; - - if (geog.Polylines().size() == 0) { - handler->new_geometry_type(util::GeometryType::LINESTRING); - HANDLE_OR_RETURN(handler->geom_start(util::GeometryType::LINESTRING, 0)); - HANDLE_OR_RETURN(handler->geom_end()); - } else if (geog.Polylines().size() == 1) { - handler->new_geometry_type(util::GeometryType::LINESTRING); - - const auto& poly = geog.Polylines()[0]; - HANDLE_OR_RETURN(handler->geom_start(util::GeometryType::LINESTRING, - poly->num_vertices())); - - for (int i = 0; i < poly->num_vertices(); i++) { - S2LatLng ll(poly->vertex(i)); - coords[0] = ll.lng().degrees(); - coords[1] = ll.lat().degrees(); - HANDLE_OR_RETURN(handler->coords(coords, 1, 2)); - } - - handler->geom_end(); - } else { - handler->new_geometry_type(util::GeometryType::MULTILINESTRING); - HANDLE_OR_RETURN(handler->geom_start(util::GeometryType::MULTILINESTRING, - geog.Polylines().size())); - - for (const auto& poly : geog.Polylines()) { - HANDLE_OR_RETURN(handler->geom_start(util::GeometryType::LINESTRING, - poly->num_vertices())); - - for (int i = 0; i < poly->num_vertices(); i++) { - S2LatLng ll(poly->vertex(i)); - coords[0] = ll.lng().degrees(); - coords[1] = ll.lat().degrees(); - HANDLE_OR_RETURN(handler->coords(coords, 1, 2)); - } - - HANDLE_OR_RETURN(handler->geom_end()); - } - - HANDLE_OR_RETURN(handler->geom_end()); - } - - return Handler::Result::CONTINUE; -} - -Handler::Result handle_loop_shell(const S2Loop* loop, Handler* handler) { - Handler::Result result; - double coords[2]; - - if (loop->num_vertices() == 0) { - throw Exception("Unexpected S2Loop with 0 verties"); - } - - HANDLE_OR_RETURN(handler->ring_start(loop->num_vertices() + 1)); - - for (int i = 0; i <= loop->num_vertices(); i++) { - S2LatLng ll(loop->vertex(i)); - coords[0] = ll.lng().degrees(); - coords[1] = ll.lat().degrees(); - HANDLE_OR_RETURN(handler->coords(coords, 1, 2)); - } - - HANDLE_OR_RETURN(handler->ring_end()); - return Handler::Result::CONTINUE; -} - -Handler::Result handle_loop_hole(const S2Loop* loop, Handler* handler) { - Handler::Result result; - double coords[2]; - - if (loop->num_vertices() == 0) { - throw Exception("Unexpected S2Loop with 0 verties"); - } - - HANDLE_OR_RETURN(handler->ring_start(loop->num_vertices() + 1)); - - for (int i = loop->num_vertices() - 1; i >= 0; i--) { - S2LatLng ll(loop->vertex(i)); - coords[0] = ll.lng().degrees(); - coords[1] = ll.lat().degrees(); - HANDLE_OR_RETURN(handler->coords(coords, 1, 2)); - } - - S2LatLng ll(loop->vertex(loop->num_vertices() - 1)); - coords[0] = ll.lng().degrees(); - coords[1] = ll.lat().degrees(); - HANDLE_OR_RETURN(handler->coords(coords, 1, 2)); - - HANDLE_OR_RETURN(handler->ring_end()); - return Handler::Result::CONTINUE; -} - -Handler::Result handle_polygon_shell(const S2Polygon& poly, int loop_start, - Handler* handler) { - Handler::Result result; - - const S2Loop* loop0 = poly.loop(loop_start); - HANDLE_OR_RETURN(handle_loop_shell(loop0, handler)); - for (int j = loop_start + 1; j <= poly.GetLastDescendant(loop_start); j++) { - const S2Loop* loop = poly.loop(j); - if (loop->depth() == (loop0->depth() + 1)) { - HANDLE_OR_RETURN(handle_loop_hole(loop, handler)); - } - } - - return Handler::Result::CONTINUE; -} - -Handler::Result WKTWriter::handle_polygon(const PolygonGeography& geog, - Handler* handler) { - const S2Polygon& poly = *geog.Polygon(); - - // find the outer shells (loop depth = 0, 2, 4, etc.) - std::vector outer_shell_loop_ids; - std::vector outer_shell_loop_sizes; - - outer_shell_loop_ids.reserve(poly.num_loops()); - for (int i = 0; i < poly.num_loops(); i++) { - if ((poly.loop(i)->depth() % 2) == 0) { - outer_shell_loop_ids.push_back(i); - } - } - - // count the number of rings in each - outer_shell_loop_sizes.reserve(outer_shell_loop_ids.size()); - for (const auto loop_start : outer_shell_loop_ids) { - const S2Loop* loop0 = poly.loop(loop_start); - int num_loops = 1; - - for (int j = loop_start + 1; j <= poly.GetLastDescendant(loop_start); j++) { - const S2Loop* loop = poly.loop(j); - num_loops += loop->depth() == (loop0->depth() + 1); - } - - outer_shell_loop_sizes.push_back(num_loops); - } - - Handler::Result result; - if (outer_shell_loop_ids.size() == 0) { - handler->new_geometry_type(util::GeometryType::POLYGON); - HANDLE_OR_RETURN(handler->geom_start(util::GeometryType::POLYGON, 0)); - HANDLE_OR_RETURN(handler->geom_end()); - } else if (outer_shell_loop_ids.size() == 1) { - handler->new_geometry_type(util::GeometryType::POLYGON); - HANDLE_OR_RETURN(handler->geom_start(util::GeometryType::POLYGON, - outer_shell_loop_sizes[0])); - HANDLE_OR_RETURN( - handle_polygon_shell(poly, outer_shell_loop_ids[0], handler)); - HANDLE_OR_RETURN(handler->geom_end()); - } else { - handler->new_geometry_type(util::GeometryType::MULTIPOLYGON); - HANDLE_OR_RETURN(handler->geom_start(util::GeometryType::MULTIPOLYGON, - outer_shell_loop_ids.size())); - for (size_t i = 0; i < outer_shell_loop_sizes.size(); i++) { - HANDLE_OR_RETURN(handler->geom_start(util::GeometryType::POLYGON, - outer_shell_loop_sizes[i])); - HANDLE_OR_RETURN( - handle_polygon_shell(poly, outer_shell_loop_ids[i], handler)); - HANDLE_OR_RETURN(handler->geom_end()); - } - HANDLE_OR_RETURN(handler->geom_end()); - } - - return Handler::Result::CONTINUE; -} - -Handler::Result WKTWriter::handle_collection(const GeographyCollection& geog, - Handler* handler) { - Handler::Result result; - handler->new_geometry_type(util::GeometryType::GEOMETRYCOLLECTION); - HANDLE_OR_RETURN(handler->geom_start(util::GeometryType::GEOMETRYCOLLECTION, - geog.Features().size())); - for (const auto& child_geog : geog.Features()) { - auto child_point = dynamic_cast(child_geog.get()); - if (child_point != nullptr) { - HANDLE_OR_RETURN(handle_points(*child_point, handler)); - } else { - auto child_polyline = - dynamic_cast(child_geog.get()); - if (child_polyline != nullptr) { - HANDLE_OR_RETURN(handle_polylines(*child_polyline, handler)); - } else { - auto child_polygon = - dynamic_cast(child_geog.get()); - if (child_polygon != nullptr) { - HANDLE_OR_RETURN(handle_polygon(*child_polygon, handler)); - } else { - auto child_collection = - dynamic_cast(child_geog.get()); - if (child_collection != nullptr) { - HANDLE_OR_RETURN(handle_collection(*child_collection, handler)); - } else { - throw Exception("Unsupported Geography subclass"); - } - } - } - } - } - HANDLE_OR_RETURN(handler->geom_end()); - return Handler::Result::CONTINUE; -} - -Handler::Result WKTWriter::handle_feature(const Geography& geog, - Handler* handler) { - Handler::Result result; - - HANDLE_OR_RETURN(handler->feat_start()); +std::string WKTWriter::write_feature(const Geography& geog) { + ArrowArray array; + writer_->WriteGeography(geog); + writer_->Finish(&array); - auto child_point = dynamic_cast(&geog); - if (child_point != nullptr) { - HANDLE_OR_RETURN(handle_points(*child_point, handler)); - } else { - auto child_polyline = dynamic_cast(&geog); - if (child_polyline != nullptr) { - HANDLE_OR_RETURN(handle_polylines(*child_polyline, handler)); - } else { - auto child_polygon = dynamic_cast(&geog); - if (child_polygon != nullptr) { - HANDLE_OR_RETURN(handle_polygon(*child_polygon, handler)); - } else { - auto child_collection = dynamic_cast(&geog); - if (child_collection != nullptr) { - HANDLE_OR_RETURN(handle_collection(*child_collection, handler)); - } else { - throw Exception("Unsupported Geography subclass"); - } - } - } - } + const auto offsets = static_cast(array.buffers[1]); + const auto data = static_cast(array.buffers[2]); + std::string result(data, offsets[1]); - HANDLE_OR_RETURN(handler->feat_end()); - return Handler::Result::CONTINUE; + return result; } } // namespace s2geography diff --git a/src/s2geography/wkt-writer.h b/src/s2geography/wkt-writer.h index 3d6039e..e80432a 100644 --- a/src/s2geography/wkt-writer.h +++ b/src/s2geography/wkt-writer.h @@ -4,7 +4,7 @@ #include #include -#include "s2geography/geoarrow-imports.h" +#include "s2geography/geoarrow.h" #include "s2geography/geography.h" namespace s2geography { @@ -12,23 +12,13 @@ namespace s2geography { class WKTWriter { public: WKTWriter(); - WKTWriter(int significant_digits); + WKTWriter(int precision); + WKTWriter(const geoarrow::ExportOptions& options); std::string write_feature(const Geography& geog); private: - std::unique_ptr exporter_; - util::GeometryType geometry_type_; - std::stringstream stream_; - - Handler::Result handle_points(const PointGeography& geog, Handler* handler); - Handler::Result handle_polylines(const PolylineGeography& geog, - Handler* handler); - Handler::Result handle_polygon(const PolygonGeography& geog, - Handler* handler); - Handler::Result handle_collection(const GeographyCollection& geog, - Handler* handler); - Handler::Result handle_feature(const Geography& geog, Handler* handler); + std::unique_ptr writer_; }; } // namespace s2geography diff --git a/src/s2geography/wkt-writer_test.cc b/src/s2geography/wkt-writer_test.cc index 839a81f..ed5022f 100644 --- a/src/s2geography/wkt-writer_test.cc +++ b/src/s2geography/wkt-writer_test.cc @@ -12,17 +12,16 @@ TEST(WKTWriter, SignificantDigits) { auto geog = reader.read_feature("POINT (0 3.333333333333334)"); WKTWriter writer_default; - EXPECT_EQ(writer_default.write_feature(*geog), "POINT (0 3.333333333333334)"); + EXPECT_EQ(writer_default.write_feature(*geog), "POINT (0 3.3333333333333344)"); WKTWriter writer_6digits(6); - EXPECT_EQ(writer_6digits.write_feature(*geog), "POINT (0 3.33333)"); - + EXPECT_EQ(writer_6digits.write_feature(*geog), "POINT (0 3.333333)"); } static std::string wktRoundTrip(const std::string &wktIn) { WKTReader reader; - WKTWriter writer; + WKTWriter writer(2); auto geog = reader.read_feature(wktIn); return writer.write_feature(*geog); } @@ -44,6 +43,42 @@ TEST(WKTWriter, EmptyGeometry) { } } +TEST(WKTWriter, LineString) { + std::string wkt("LINESTRING (30 10, 10 30, 40 40)"); + EXPECT_EQ(wktRoundTrip(wkt), wkt); +} + +TEST(WKTWriter, Polygon) { + std::string wkt("POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))"); + EXPECT_EQ(wktRoundTrip(wkt), wkt); + + std::string wkt2("POLYGON ((35 10, 45 45, 15 40, 10 20, 35 10), (20 30, 35 35, 30 20, 20 30))"); + EXPECT_EQ(wktRoundTrip(wkt2), wkt2); +} + +TEST(WKTWriter, MultiPoint) { + std::string wkt("MULTIPOINT ((10 40), (40 30), (20 20), (30 10))"); + EXPECT_EQ(wktRoundTrip(wkt), wkt); +} + +TEST(WKTWriter, MultiLineString) { + std::string wkt("MULTILINESTRING ((10 10, 20 20, 10 40), (40 40, 30 30, 40 20, 30 10))"); + EXPECT_EQ(wktRoundTrip(wkt), wkt); +} + +TEST(WKTWriter, MultiPolygon) { + std::string wkt("MULTIPOLYGON (((30 20, 45 40, 10 40, 30 20)), ((15 5, 40 10, 10 20, 5 10, 15 5)))"); + EXPECT_EQ(wktRoundTrip(wkt), wkt); + + std::string wkt2("MULTIPOLYGON (((40 40, 20 45, 45 30, 40 40)), ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35), (30 20, 20 15, 20 25, 30 20)))"); + EXPECT_EQ(wktRoundTrip(wkt2), wkt2); +} + +TEST(WKTWriter, Collection) { + std::string wkt("GEOMETRYCOLLECTION (POINT (40 10), LINESTRING (10 10, 20 20, 10 40), POLYGON ((40 40, 20 45, 45 30, 40 40)))"); + EXPECT_EQ(wktRoundTrip(wkt), wkt); +} + TEST(WKTWriter, InvalidLineString) { std::string wkt("LINESTRING (0 0)"); EXPECT_EQ(wktRoundTrip(wkt), wkt);