From 4f2289f5bafdbf8781cdbf333171fd842affa65d Mon Sep 17 00:00:00 2001 From: Max Gabrielsson Date: Wed, 5 Apr 2023 19:21:48 +0200 Subject: [PATCH] update readme, add license, add st_makeline, st_flipcoordinates, fix nulls not being properly written in st_write --- LICENSE | 7 + README.md | 32 ++- .../include/spatial/core/functions/scalar.hpp | 8 + .../core/functions/scalar/CMakeLists.txt | 2 + .../core/functions/scalar/st_collect.cpp | 2 - .../functions/scalar/st_flipcoordinates.cpp | 267 ++++++++++++++++++ .../core/functions/scalar/st_makeline.cpp | 106 +++++++ .../src/spatial/gdal/functions/st_write.cpp | 1 + .../test/sql/geometry/st_flipcoordinates.test | 52 ++++ spatial/test/sql/geometry/st_makeline.test | 56 ++++ 10 files changed, 517 insertions(+), 16 deletions(-) create mode 100644 LICENSE create mode 100644 spatial/src/spatial/core/functions/scalar/st_flipcoordinates.cpp create mode 100644 spatial/src/spatial/core/functions/scalar/st_makeline.cpp create mode 100644 spatial/test/sql/geometry/st_flipcoordinates.test create mode 100644 spatial/test/sql/geometry/st_makeline.test diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..b7d0b3a8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright 2018-2023 DuckDB Labs BV + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 767611f2..84175271 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # DuckDB Spatial Extension +🚧 WORK IN PROGRESS 🚧 + **Table of contents** - [DuckDB Spatial Extension](#duckdb-spatial-extension) - [What is this?](#what-is-this) @@ -150,14 +152,14 @@ ON ST_Within(st_transform(dropoff_point, 'EPSG:4326', 'ESRI:102718'), end_zone.g We can export the joined table to a GeoJSONSeq file using the GDAL copy function, passing in a GDAL layer creation option. -Since GeoJSON only supports a single geometry per feature, we need to use the `ST_Collect` function to combine the pickup and dropoff points into a single multi point geometry. +Since GeoJSON only supports a single geometry per feature, we can use the `ST_MakeLine` function to combine the pickup and dropoff points into a single line geometry. The default coordinate reference system for GeoJSON is WGS84, but the coordinates are expected to be in longitude/latitude, so we need to flip the geometry using the `ST_FlipCoordinates` function. ```sql COPY ( SELECT - ST_AsWKB(ST_Collect([pickup_point, dropoff_point])) as wkb_geometry, - start_zone, - end_zone, + ST_AsWKB(ST_FlipCoordinates(ST_MakeLine(pickup_point, dropoff_point))) as wkb_geometry, + start_zone, + end_zone, time::VARCHAR as trip_time FROM joined) TO 'joined.geojsonseq' @@ -169,16 +171,16 @@ WITH (FORMAT GDAL, DRIVER 'GeoJSONSeq', LAYER_CREATION_OPTIONS 'WRITE_BBOX=YES') ```json -{ "type": "Feature", "properties": { "start_zone": "JFK Airport", "end_zone": "Park Slope", "trip_time": "00:52:00" }, "geometry": { "type": "MultiPoint", "coordinates": [ [ 40.643515, -73.789923 ], [ 40.680395, -73.97608 ] ] } } -{ "type": "Feature", "properties": { "start_zone": "JFK Airport", "end_zone": "Park Slope", "trip_time": "00:35:00" }, "geometry": { "type": "MultiPoint", "coordinates": [ [ 40.645422, -73.776445 ], [ 40.670782, -73.98427 ] ] } } -{ "type": "Feature", "properties": { "start_zone": "JFK Airport", "end_zone": "Park Slope", "trip_time": "00:45:42" }, "geometry": { "type": "MultiPoint", "coordinates": [ [ 40.645065, -73.776878 ], [ 40.662571, -73.992153 ] ] } } -{ "type": "Feature", "properties": { "start_zone": "JFK Airport", "end_zone": "Park Slope", "trip_time": "00:36:00" }, "geometry": { "type": "MultiPoint", "coordinates": [ [ 40.641508, -73.788028 ], [ 40.670927, -73.97584 ] ] } } -{ "type": "Feature", "properties": { "start_zone": "JFK Airport", "end_zone": "Park Slope", "trip_time": "00:47:58" }, "geometry": { "type": "MultiPoint", "coordinates": [ [ 40.644749, -73.781855 ], [ 40.663663, -73.980129 ] ] } } -{ "type": "Feature", "properties": { "start_zone": "JFK Airport", "end_zone": "Park Slope", "trip_time": "00:32:10" }, "geometry": { "type": "MultiPoint", "coordinates": [ [ 40.641559, -73.787494 ], [ 40.673479, -73.974694 ] ] } } -{ "type": "Feature", "properties": { "start_zone": "JFK Airport", "end_zone": "Park Slope", "trip_time": "00:36:59" }, "geometry": { "type": "MultiPoint", "coordinates": [ [ 40.643342, -73.790138 ], [ 40.662379, -73.982721 ] ] } } -{ "type": "Feature", "properties": { "start_zone": "JFK Airport", "end_zone": "Park Slope", "trip_time": "00:32:00" }, "geometry": { "type": "MultiPoint", "coordinates": [ [ 40.641248, -73.786952 ], [ 40.676237, -73.97421 ] ] } } -{ "type": "Feature", "properties": { "start_zone": "JFK Airport", "end_zone": "Park Slope", "trip_time": "00:33:21" }, "geometry": { "type": "MultiPoint", "coordinates": [ [ 40.648514, -73.783892 ], [ 40.669721, -73.979283 ] ] } } -{ "type": "Feature", "properties": { "start_zone": "JFK Airport", "end_zone": "Park Slope", "trip_time": "00:35:45" }, "geometry": { "type": "MultiPoint", "coordinates": [ [ 40.645272, -73.776643 ], [ 40.66723, -73.978873 ] ] } } +{ "type": "Feature", "properties": { "start_zone": "JFK Airport", "end_zone": "Park Slope", "trip_time": "00:52:00" }, "geometry": { "type": "LineString", "coordinates": [ [ -73.789923, 40.643515 ], [ -73.97608, 40.680395 ] ] } } +{ "type": "Feature", "properties": { "start_zone": "JFK Airport", "end_zone": "Park Slope", "trip_time": "00:35:00" }, "geometry": { "type": "LineString", "coordinates": [ [ -73.776445, 40.645422 ], [ -73.98427, 40.670782 ] ] } } +{ "type": "Feature", "properties": { "start_zone": "JFK Airport", "end_zone": "Park Slope", "trip_time": "00:45:42" }, "geometry": { "type": "LineString", "coordinates": [ [ -73.776878, 40.645065 ], [ -73.992153, 40.662571 ] ] } } +{ "type": "Feature", "properties": { "start_zone": "JFK Airport", "end_zone": "Park Slope", "trip_time": "00:36:00" }, "geometry": { "type": "LineString", "coordinates": [ [ -73.788028, 40.641508 ], [ -73.97584, 40.670927 ] ] } } +{ "type": "Feature", "properties": { "start_zone": "JFK Airport", "end_zone": "Park Slope", "trip_time": "00:47:58" }, "geometry": { "type": "LineString", "coordinates": [ [ -73.781855, 40.644749 ], [ -73.980129, 40.663663 ] ] } } +{ "type": "Feature", "properties": { "start_zone": "JFK Airport", "end_zone": "Park Slope", "trip_time": "00:32:10" }, "geometry": { "type": "LineString", "coordinates": [ [ -73.787494, 40.641559 ], [ -73.974694, 40.673479 ] ] } } +{ "type": "Feature", "properties": { "start_zone": "JFK Airport", "end_zone": "Park Slope", "trip_time": "00:36:59" }, "geometry": { "type": "LineString", "coordinates": [ [ -73.790138, 40.643342 ], [ -73.982721, 40.662379 ] ] } } +{ "type": "Feature", "properties": { "start_zone": "JFK Airport", "end_zone": "Park Slope", "trip_time": "00:32:00" }, "geometry": { "type": "LineString", "coordinates": [ [ -73.786952, 40.641248 ], [ -73.97421, 40.676237 ] ] } } +{ "type": "Feature", "properties": { "start_zone": "JFK Airport", "end_zone": "Park Slope", "trip_time": "00:33:21" }, "geometry": { "type": "LineString", "coordinates": [ [ -73.783892, 40.648514 ], [ -73.979283, 40.669721 ] ] } } +{ "type": "Feature", "properties": { "start_zone": "JFK Airport", "end_zone": "Park Slope", "trip_time": "00:35:45" }, "geometry": { "type": "LineString", "coordinates": [ [ -73.776643, 40.645272 ], [ -73.978873, 40.66723 ] ] } } ``` @@ -281,9 +283,11 @@ Again, please feel free to open an issue if there is a particular function you w | ST_DWithin | 🧭 | 🔄 | 🔄 | 🔄 | 🔄 (as POLYGON) | | ST_Envelope | 🧭 | 🔄 | 🔄 | 🔄 | 🔄 (as POLYGON) | | ST_Equals | 🧭 | 🔄 | 🔄 | 🔄 | 🔄 (as POLYGON) | +| ST_FlipCoordinates | 🦆 | 🦆 | 🦆 | 🦆 | 🦆 | | ST_GeomFromText | 🧭 | 🔄 | 🔄 | 🔄 | 🔄 (as POLYGON) | | ST_GeomFromWKB | 🦆 | 🦆 | 🦆 | 🦆 | 🔄 (as POLYGON) | | ST_GeometryType | 🦆 | 🦆 | 🦆 | 🦆 | 🔄 (as POLYGON) | +| ST_MakeLine | 🦆 | | 🦆 | | | | ST_Intersection | 🧭 | 🔄 | 🔄 | 🔄 | 🔄 (as POLYGON) | | ST_Intersects | 🧭 | 🔄 | 🔄 | 🔄 | 🔄 (as POLYGON) | | ST_IsClosed | 🧭 | 🔄 | 🔄 | 🔄 | 🔄 (as POLYGON) | diff --git a/spatial/include/spatial/core/functions/scalar.hpp b/spatial/include/spatial/core/functions/scalar.hpp index 4006e1af..a99e8841 100644 --- a/spatial/include/spatial/core/functions/scalar.hpp +++ b/spatial/include/spatial/core/functions/scalar.hpp @@ -16,10 +16,12 @@ struct CoreScalarFunctions { RegisterStCollect(context); RegisterStContains(context); RegisterStDistance(context); + RegisterStFlipCoordinates(context); RegisterStGeometryType(context); RegisterStGeomFromWKB(context); RegisterStIsEmpty(context); RegisterStLength(context); + RegisterStMakeLine(context); RegisterStPoint(context); RegisterStX(context); RegisterStY(context); @@ -47,6 +49,9 @@ struct CoreScalarFunctions { // ST_Distance static void RegisterStDistance(ClientContext &context); + // ST_FlipCoordinates + static void RegisterStFlipCoordinates(ClientContext &context); + // ST_GeometryType static void RegisterStGeometryType(ClientContext &context); @@ -59,6 +64,9 @@ struct CoreScalarFunctions { // ST_Length static void RegisterStLength(ClientContext &context); + // ST_MakeLine + static void RegisterStMakeLine(ClientContext &context); + // ST_Point static void RegisterStPoint(ClientContext &context); diff --git a/spatial/src/spatial/core/functions/scalar/CMakeLists.txt b/spatial/src/spatial/core/functions/scalar/CMakeLists.txt index 28ea4b5a..618cdb82 100644 --- a/spatial/src/spatial/core/functions/scalar/CMakeLists.txt +++ b/spatial/src/spatial/core/functions/scalar/CMakeLists.txt @@ -7,7 +7,9 @@ set(EXTENSION_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/st_collect.cpp ${CMAKE_CURRENT_SOURCE_DIR}/st_contains.cpp ${CMAKE_CURRENT_SOURCE_DIR}/st_distance.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/st_flipcoordinates.cpp ${CMAKE_CURRENT_SOURCE_DIR}/st_length.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/st_makeline.cpp ${CMAKE_CURRENT_SOURCE_DIR}/st_geometrytype.cpp ${CMAKE_CURRENT_SOURCE_DIR}/st_geomfromwkb.cpp ${CMAKE_CURRENT_SOURCE_DIR}/st_point.cpp diff --git a/spatial/src/spatial/core/functions/scalar/st_collect.cpp b/spatial/src/spatial/core/functions/scalar/st_collect.cpp index 77f52f9b..614091b4 100644 --- a/spatial/src/spatial/core/functions/scalar/st_collect.cpp +++ b/spatial/src/spatial/core/functions/scalar/st_collect.cpp @@ -12,8 +12,6 @@ namespace spatial { namespace core { -using namespace spatial::core; - static void CollectFunction(DataChunk &args, ExpressionState &state, Vector &result) { auto &lstate = GeometryFunctionLocalState::ResetAndGet(state); auto count = args.size(); diff --git a/spatial/src/spatial/core/functions/scalar/st_flipcoordinates.cpp b/spatial/src/spatial/core/functions/scalar/st_flipcoordinates.cpp new file mode 100644 index 00000000..ea62470e --- /dev/null +++ b/spatial/src/spatial/core/functions/scalar/st_flipcoordinates.cpp @@ -0,0 +1,267 @@ +#include "duckdb/parser/parsed_data/create_scalar_function_info.hpp" +#include "duckdb/common/vector_operations/generic_executor.hpp" +#include "spatial/common.hpp" +#include "spatial/core/functions/scalar.hpp" +#include "spatial/core/functions/common.hpp" +#include "spatial/core/geometry/geometry.hpp" +#include "spatial/core/types.hpp" + + +namespace spatial { + +namespace core { + +// TODO: We should be able to optimize these and avoid the flatten + +//------------------------------------------------------------------------------ +// POINT_2D +//------------------------------------------------------------------------------ +static void PointFlipCoordinatesFunction(DataChunk &args, ExpressionState &state, Vector &result) { + auto input = args.data[0]; + auto count = args.size(); + + // TODO: Avoid flatten + input.Flatten(count); + + auto &coords_in = StructVector::GetEntries(input); + auto x_data_in = FlatVector::GetData(*coords_in[0]); + auto y_data_in = FlatVector::GetData(*coords_in[1]); + + auto &coords_out = StructVector::GetEntries(result); + auto x_data_out = FlatVector::GetData(*coords_out[0]); + auto y_data_out = FlatVector::GetData(*coords_out[1]); + + memcpy(x_data_out, y_data_in, count * sizeof(double)); + memcpy(y_data_out, x_data_in, count * sizeof(double)); + + if(count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + +//------------------------------------------------------------------------------ +// LINESTRING_2D +//------------------------------------------------------------------------------ +static void LineStringFlipCoordinatesFunction(DataChunk &args, ExpressionState &state, Vector &result) { + auto input = args.data[0]; + auto count = args.size(); + + // TODO: Avoid flatten + input.Flatten(count); + + auto coord_vec_in = ListVector::GetEntry(input); + auto &coords_in = StructVector::GetEntries(coord_vec_in); + auto x_data_in = FlatVector::GetData(*coords_in[0]); + auto y_data_in = FlatVector::GetData(*coords_in[1]); + + auto coord_count = ListVector::GetListSize(input); + ListVector::Reserve(result, coord_count); + ListVector::SetListSize(result, coord_count); + + auto line_entries_in = ListVector::GetData(input); + auto line_entries_out = ListVector::GetData(result); + memcpy(line_entries_out, line_entries_in, count * sizeof(list_entry_t)); + + auto coord_vec_out = ListVector::GetEntry(result); + auto &coords_out = StructVector::GetEntries(coord_vec_out); + auto x_data_out = FlatVector::GetData(*coords_out[0]); + auto y_data_out = FlatVector::GetData(*coords_out[1]); + + memcpy(x_data_out, y_data_in, coord_count * sizeof(double)); + memcpy(y_data_out, x_data_in, coord_count * sizeof(double)); + + if(count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + +//------------------------------------------------------------------------------ +// POLYGON_2D +//------------------------------------------------------------------------------ +static void PolygonFlipCoordinatesFunction(DataChunk &args, ExpressionState &state, Vector &result) { + auto input = args.data[0]; + auto count = args.size(); + + // TODO: Avoid flatten + input.Flatten(count); + + auto ring_vec_in = ListVector::GetEntry(input); + auto ring_count = ListVector::GetListSize(input); + + auto coord_vec_in = ListVector::GetEntry(ring_vec_in); + auto &coords_in = StructVector::GetEntries(coord_vec_in); + auto x_data_in = FlatVector::GetData(*coords_in[0]); + auto y_data_in = FlatVector::GetData(*coords_in[1]); + + auto coord_count = ListVector::GetListSize(ring_vec_in); + + ListVector::Reserve(result, ring_count); + ListVector::SetListSize(result, ring_count); + auto ring_vec_out = ListVector::GetEntry(result); + ListVector::Reserve(ring_vec_out, coord_count); + ListVector::SetListSize(ring_vec_out, coord_count); + + auto ring_entries_in = ListVector::GetData(input); + auto ring_entries_out = ListVector::GetData(result); + memcpy(ring_entries_out, ring_entries_in, count * sizeof(list_entry_t)); + + auto coord_entries_in = ListVector::GetData(ring_vec_in); + auto coord_entries_out = ListVector::GetData(ring_vec_out); + memcpy(coord_entries_out, coord_entries_in, ring_count * sizeof(list_entry_t)); + + + auto coord_vec_out = ListVector::GetEntry(ring_vec_out); + auto &coords_out = StructVector::GetEntries(coord_vec_out); + auto x_data_out = FlatVector::GetData(*coords_out[0]); + auto y_data_out = FlatVector::GetData(*coords_out[1]); + + memcpy(x_data_out, y_data_in, coord_count * sizeof(double)); + memcpy(y_data_out, x_data_in, coord_count * sizeof(double)); + + if(count == 1) { + result.SetVectorType(VectorType::CONSTANT_VECTOR); + } +} + +//------------------------------------------------------------------------------ +// BOX_2D +//------------------------------------------------------------------------------ +static void BoxFlipCoordinatesFunction(DataChunk &args, ExpressionState &state, Vector &result) { + + auto input = args.data[0]; + auto count = args.size(); + + // TODO: Avoid flatten + input.Flatten(count); + + auto &children_in = StructVector::GetEntries(input); + auto min_x_in = FlatVector::GetData(*children_in[0]); + auto min_y_in = FlatVector::GetData(*children_in[1]); + auto max_x_in = FlatVector::GetData(*children_in[2]); + auto max_y_in = FlatVector::GetData(*children_in[3]); + + auto &children_out = StructVector::GetEntries(result); + auto min_x_out = FlatVector::GetData(*children_out[0]); + auto min_y_out = FlatVector::GetData(*children_out[1]); + auto max_x_out = FlatVector::GetData(*children_out[2]); + auto max_y_out = FlatVector::GetData(*children_out[3]); + + memcpy(min_x_out, min_y_in, count * sizeof(double)); + memcpy(min_y_out, min_x_in, count * sizeof(double)); + memcpy(max_x_out, max_y_in, count * sizeof(double)); + memcpy(max_y_out, max_x_in, count * sizeof(double)); +} + +//------------------------------------------------------------------------------ +// GEOMETRY +//------------------------------------------------------------------------------ +static void FlipVertexVector(VertexVector &vertices) { + for(idx_t i = 0; i < vertices.count; i++) { + std::swap(vertices[i].x, vertices[i].y); + } +} +static void FlipGeometry(Point &point) { + FlipVertexVector(point.data); +} + +static void FlipGeometry(LineString &line) { + FlipVertexVector(line.points); +} + +static void FlipGeometry(Polygon &poly) { + for(idx_t i = 0; i < poly.Count(); i++) { + auto &ring = poly.rings[i]; + FlipVertexVector(ring); + } +} + +static void FlipGeometry(MultiPoint &multi_point) { + for (idx_t i = 0; i < multi_point.Count(); i++) { + FlipGeometry(multi_point.points[i]); + } +} + +static void FlipGeometry(MultiLineString &multi_line) { + for (idx_t i = 0; i < multi_line.Count(); i++) { + FlipGeometry(multi_line.linestrings[i]); + } +} + +static void FlipGeometry(MultiPolygon &multi_poly) { + for (idx_t i = 0; i < multi_poly.Count(); i++) { + FlipGeometry(multi_poly.polygons[i]); + } +} + +static void FlipGeometry(Geometry &geom); + +static void FlipGeometry(GeometryCollection &geom) { + for (idx_t i = 0; i < geom.Count(); i++) { + FlipGeometry(geom.geometries[i]); + } +} + +static void FlipGeometry(Geometry &geom) { + switch(geom.Type()) { + case GeometryType::POINT: + FlipGeometry(geom.GetPoint()); + break; + case GeometryType::LINESTRING: + FlipGeometry(geom.GetLineString()); + break; + case GeometryType::POLYGON: + FlipGeometry(geom.GetPolygon()); + break; + case GeometryType::MULTIPOINT: + FlipGeometry(geom.GetMultiPoint()); + break; + case GeometryType::MULTILINESTRING: + FlipGeometry(geom.GetMultiLineString()); + break; + case GeometryType::MULTIPOLYGON: + FlipGeometry(geom.GetMultiPolygon()); + break; + case GeometryType::GEOMETRYCOLLECTION: + FlipGeometry(geom.GetGeometryCollection()); + break; + default: + throw NotImplementedException("Unimplemented geometry type!"); + } +} + +static void GeometryFlipCoordinatesFunction(DataChunk &args, ExpressionState &state, Vector &result) { + + auto &lstate = GeometryFunctionLocalState::ResetAndGet(state); + + auto input = args.data[0]; + auto count = args.size(); + + UnaryExecutor::Execute(input, result, count, [&](string_t input) { + auto geom = lstate.factory.Deserialize(input); + auto copy = lstate.factory.CopyGeometry(geom); + FlipGeometry(copy); + return lstate.factory.Serialize(result, copy); + }); +} + +//------------------------------------------------------------------------------ +// Register functions +//------------------------------------------------------------------------------ +void CoreScalarFunctions::RegisterStFlipCoordinates(ClientContext &context) { + auto &catalog = Catalog::GetSystemCatalog(context); + + ScalarFunctionSet flip_function_set("ST_FlipCoordinates"); + flip_function_set.AddFunction(ScalarFunction({GeoTypes::POINT_2D()}, GeoTypes::POINT_2D(), PointFlipCoordinatesFunction)); + flip_function_set.AddFunction(ScalarFunction({GeoTypes::LINESTRING_2D()}, GeoTypes::LINESTRING_2D(), LineStringFlipCoordinatesFunction)); + flip_function_set.AddFunction(ScalarFunction({GeoTypes::POLYGON_2D()}, GeoTypes::POLYGON_2D(), PolygonFlipCoordinatesFunction)); + flip_function_set.AddFunction(ScalarFunction({GeoTypes::BOX_2D()}, GeoTypes::BOX_2D(), BoxFlipCoordinatesFunction)); + flip_function_set.AddFunction(ScalarFunction({GeoTypes::GEOMETRY()}, GeoTypes::GEOMETRY(), GeometryFlipCoordinatesFunction, nullptr, nullptr, nullptr, GeometryFunctionLocalState::Init)); + + CreateScalarFunctionInfo info(std::move(flip_function_set)); + info.on_conflict = OnCreateConflict::ALTER_ON_CONFLICT; + catalog.CreateFunction(context, &info); +} + +} // namespace core + +} // namespace spatial \ No newline at end of file diff --git a/spatial/src/spatial/core/functions/scalar/st_makeline.cpp b/spatial/src/spatial/core/functions/scalar/st_makeline.cpp new file mode 100644 index 00000000..fb8174a7 --- /dev/null +++ b/spatial/src/spatial/core/functions/scalar/st_makeline.cpp @@ -0,0 +1,106 @@ +#include "spatial/common.hpp" +#include "spatial/core/types.hpp" +#include "spatial/core/functions/scalar.hpp" +#include "spatial/core/functions/common.hpp" +#include "spatial/core/geometry/geometry.hpp" + +#include "duckdb/parser/parsed_data/create_scalar_function_info.hpp" +#include "duckdb/common/vector_operations/unary_executor.hpp" +#include "duckdb/common/vector_operations/binary_executor.hpp" + +namespace spatial { + +namespace core { + +static void MakeLineListFunction(DataChunk &args, ExpressionState &state, Vector &result) { + auto &lstate = GeometryFunctionLocalState::ResetAndGet(state); + auto count = args.size(); + auto &child_vec = ListVector::GetEntry(args.data[0]); + UnifiedVectorFormat format; + child_vec.ToUnifiedFormat(count, format); + + + UnaryExecutor::Execute(args.data[0], result, count, [&](list_entry_t &geometry_list) { + auto offset = geometry_list.offset; + auto length = geometry_list.length; + + auto line_geom = lstate.factory.CreateLineString(length); + + for(idx_t i = offset; i < offset + length; i++) { + + auto mapped_idx = format.sel->get_index(i); + if(!format.validity.RowIsValid(mapped_idx)) { + continue; + } + auto geometry_blob = ((string_t*)format.data)[mapped_idx]; + auto geometry = lstate.factory.Deserialize(geometry_blob); + + if(geometry.Type() != GeometryType::POINT) { + throw InvalidInputException("ST_MakeLine only accepts POINT geometries"); + } + auto &point = geometry.GetPoint(); + if(point.IsEmpty()) { + continue; + } + line_geom.points.Add(point.GetVertex()); + } + + if(line_geom.Count() == 1) { + throw InvalidInputException("ST_MakeLine requires zero or two or more POINT geometries"); + } + + return lstate.factory.Serialize(result, Geometry(line_geom)); + }); +} + + +static void MakeLineBinaryFunction(DataChunk &args, ExpressionState &state, Vector &result) { + auto &lstate = GeometryFunctionLocalState::ResetAndGet(state); + auto count = args.size(); + + + BinaryExecutor::Execute(args.data[0], args.data[1], result, count, + [&](string_t &geom_blob_left, string_t &geom_blob_right) { + + auto geometry_left = lstate.factory.Deserialize(geom_blob_left); + auto geometry_right = lstate.factory.Deserialize(geom_blob_right); + + if(geometry_left.Type() != GeometryType::POINT || geometry_right.Type() != GeometryType::POINT) { + throw InvalidInputException("ST_MakeLine only accepts POINT geometries"); + } + + auto &point_left = geometry_left.GetPoint(); + auto &point_right = geometry_right.GetPoint(); + + // TODO: we should add proper abstractions to append/concat VertexVectors + auto line_geom = lstate.factory.CreateLineString(2); + if(!point_left.IsEmpty()) { + line_geom.points.Add(point_left.GetVertex()); + } + if(!point_right.IsEmpty()) { + line_geom.points.Add(point_right.GetVertex()); + } + + if(line_geom.Count() == 1) { + throw InvalidInputException("ST_MakeLine requires zero or two or more POINT geometries"); + } + + return lstate.factory.Serialize(result, Geometry(line_geom)); + }); +} + +void CoreScalarFunctions::RegisterStMakeLine(ClientContext &context) { + auto &catalog = Catalog::GetSystemCatalog(context); + + ScalarFunctionSet set("ST_MakeLine"); + + set.AddFunction(ScalarFunction({LogicalType::LIST(GeoTypes::GEOMETRY())}, GeoTypes::GEOMETRY(), MakeLineListFunction, nullptr, nullptr, nullptr, GeometryFunctionLocalState::Init)); + set.AddFunction(ScalarFunction({GeoTypes::GEOMETRY(), GeoTypes::GEOMETRY()}, GeoTypes::GEOMETRY(), MakeLineBinaryFunction, nullptr, nullptr, nullptr, GeometryFunctionLocalState::Init)); + CreateScalarFunctionInfo info(std::move(set)); + info.on_conflict = OnCreateConflict::ALTER_ON_CONFLICT; + catalog.CreateFunction(context, &info); +} + +} // namespace spatials + +} // namespace spatial diff --git a/spatial/src/spatial/gdal/functions/st_write.cpp b/spatial/src/spatial/gdal/functions/st_write.cpp index 5c36b5e0..048c1a2e 100644 --- a/spatial/src/spatial/gdal/functions/st_write.cpp +++ b/spatial/src/spatial/gdal/functions/st_write.cpp @@ -301,6 +301,7 @@ static void SetOgrFieldFromValue(OGRFeature *feature, int field_idx, const Logic // TODO: Set field by index always instead of by name for performance. if (value.IsNull()) { feature->SetFieldNull(field_idx); + return; } switch (type.id()) { case LogicalTypeId::BOOLEAN: diff --git a/spatial/test/sql/geometry/st_flipcoordinates.test b/spatial/test/sql/geometry/st_flipcoordinates.test new file mode 100644 index 00000000..5430ecf2 --- /dev/null +++ b/spatial/test/sql/geometry/st_flipcoordinates.test @@ -0,0 +1,52 @@ +require spatial + +query I +SELECT ST_FlipCoordinates(ST_Point2D(1,2)) +---- +POINT (2 1) + +query I +SELECT ST_FlipCoordinates(ST_GeomFromText('LINESTRING(1 2, 3 4)')) +---- +LINESTRING (2 1, 4 3) + +query I +SELECT ST_FlipCoordinates(ST_GeomFromText('POLYGON((1 2, 3 4, 5 6, 1 2))')) +---- +POLYGON ((2 1, 4 3, 6 5, 2 1)) + +query I +SELECT ST_FlipCoordinates(ST_GeomFromText('MULTIPOINT(1 2, 3 4)')) +---- +MULTIPOINT (2 1, 4 3) + +query I +SELECT ST_FlipCoordinates(ST_GeomFromText('MULTILINESTRING((1 2, 3 4), (5 6, 7 8))')) +---- +MULTILINESTRING ((2 1, 4 3), (6 5, 8 7)) + +query I +SELECT ST_FlipCoordinates(ST_GeomFromText('MULTIPOLYGON(((1 2, 3 4, 5 6, 1 2)), ((7 8, 9 10, 11 12, 7 8)))')) +---- +MULTIPOLYGON (((2 1, 4 3, 6 5, 2 1)), ((8 7, 10 9, 12 11, 8 7))) + +query I +SELECT ST_FlipCoordinates(ST_GeomFromText('GEOMETRYCOLLECTION(POINT(1 2), LINESTRING(3 4, 5 6))')) +---- +GEOMETRYCOLLECTION (POINT (2 1), LINESTRING (4 3, 6 5)) + +query I +SELECT ST_FlipCoordinates(ST_GeomFromText('GEOMETRYCOLLECTION EMPTY')) +---- +GEOMETRYCOLLECTION EMPTY + +query I +SELECT ST_FlipCoordinates(ST_GeomFromText('POINT EMPTY')) +---- +POINT EMPTY + +query I +SELECT ST_FlipCoordinates(NULL) +---- +NULL + diff --git a/spatial/test/sql/geometry/st_makeline.test b/spatial/test/sql/geometry/st_makeline.test new file mode 100644 index 00000000..9b42ebea --- /dev/null +++ b/spatial/test/sql/geometry/st_makeline.test @@ -0,0 +1,56 @@ +require spatial + +# Test MakeLine binary function +query I +SELECT ST_AsText(ST_MakeLine(ST_Point(1,1), ST_Point(2,2))) +---- +LINESTRING (1 1, 2 2) + +# Test MakeLine list function +query I +SELECT ST_AsText(ST_MakeLine([ST_Point(1,1), ST_Point(2,2)])) +---- +LINESTRING (1 1, 2 2) + +query I +SELECT ST_AsText(ST_MakeLine([ST_Point(1,1), ST_Point(2,2), ST_Point(3,3)])) +---- +LINESTRING (1 1, 2 2, 3 3) + +# Test MakeLine list function with empty list +query I +SELECT ST_AsText(ST_MakeLine([])) +---- +LINESTRING EMPTY + +# Test MakeLine list function with null +query I +SELECT ST_AsText(ST_MakeLine([ST_Point(1,1), NULL, ST_Point(3,3)])) +---- +LINESTRING (1 1, 3 3) + +query I +SELECT ST_AsText(ST_MakeLine([NULL])) +---- +LINESTRING EMPTY + +# Handle invalid cases (not enough points) +statement error +SELECT ST_AsText(ST_MakeLine([ST_Point(1,1), NULL])) +---- +Invalid Input Error: ST_MakeLine requires zero or two or more POINT geometries + +statement error +SELECT ST_AsText(ST_MakeLine([ST_Point(1,1), NULL, ST_GeomFromText('POINT EMPTY')])) +---- +Invalid Input Error: ST_MakeLine requires zero or two or more POINT geometries + +statement error +SELECT ST_AsText(ST_MakeLine(ST_Point(1,2), ST_GeomFromText('POINT EMPTY'))) +---- +Invalid Input Error: ST_MakeLine requires zero or two or more POINT geometries + +query I +SELECT ST_AsText(ST_MakeLine(NULL, ST_Point(1,2))) +---- +NULL