diff --git a/src/spatial/modules/main/spatial_functions_scalar.cpp b/src/spatial/modules/main/spatial_functions_scalar.cpp index 6ca172a4..36c62c27 100644 --- a/src/spatial/modules/main/spatial_functions_scalar.cpp +++ b/src/spatial/modules/main/spatial_functions_scalar.cpp @@ -3003,6 +3003,83 @@ struct ST_Dump { } }; + +//====================================================================================================================== +// ST_Expand +//====================================================================================================================== + +struct ST_Expand { + + //------------------------------------------------------------------------------------------------------------------ + // GEOMETRY + //------------------------------------------------------------------------------------------------------------------ + static void Execute(DataChunk &args, ExpressionState &state, Vector &result) { + auto &lstate = LocalState::ResetAndGet(state); + + BinaryExecutor::Execute(args.data[0], args.data[1], result, args.size(), [&](const string_t &blob, double distance) { + sgl::geometry geom; + lstate.Deserialize(blob, geom); + auto bbox = sgl::extent_xy::smallest(); + + if (sgl::ops::get_total_extent_xy(geom, bbox) == 0) { + const sgl::geometry empty(sgl::geometry_type::GEOMETRY_COLLECTION, false, false); + return lstate.Serialize(result, empty); + } else { + sgl::geometry expanded(sgl::geometry_type::POLYGON, false, false); + const auto min_x = bbox.min.x - distance; + const auto min_y = bbox.min.y - distance; + const auto max_x = bbox.max.x + distance; + const auto max_y = bbox.max.y + distance; + const double buffer[10] = {min_x, min_y, min_x, max_y, max_x, max_y, max_x, min_y, min_x, min_y}; + + sgl::geometry ring(sgl::geometry_type::LINESTRING, false, false); + ring.set_vertex_array(buffer, 5); + expanded.append_part(&ring); + return lstate.Serialize(result, expanded); + } + }); + } + + //------------------------------------------------------------------------------------------------------------------ + // Documentation + //------------------------------------------------------------------------------------------------------------------ + static constexpr auto DESCRIPTION = R"( + Expand the input geometry by the specified distance, returning a polygon. + + `geom` is the input geometry. + + `distance` is the target distance for the expansion, using the same units as the input geometry. + + This is a planar operation and will not take into account the curvature of the earth. + )"; + static constexpr auto EXAMPLE = R"( + SELECT ST_AsText(ST_Expand(ST_GeomFromText('POINT(20 30)'), 0.1)); + )"; + + //------------------------------------------------------------------------------------------------------------------ + // Register + //------------------------------------------------------------------------------------------------------------------ + static void Register(ExtensionLoader &loader) { + FunctionBuilder::RegisterScalar(loader, "ST_Expand", [](ScalarFunctionBuilder &func) { + func.AddVariant([](ScalarFunctionVariantBuilder &variant) { + variant.AddParameter("geom", GeoTypes::GEOMETRY()); + variant.AddParameter("distance", LogicalType::DOUBLE); + variant.SetReturnType(GeoTypes::GEOMETRY()); + + variant.SetInit(LocalState::Init); + variant.SetFunction(Execute); + }); + + func.SetDescription(DESCRIPTION); + func.SetExample(EXAMPLE); + + func.SetTag("ext", "spatial"); + func.SetTag("category", "property"); + }); + } +}; + + //====================================================================================================================== // ST_Extent //====================================================================================================================== @@ -9279,6 +9356,7 @@ void RegisterSpatialScalarFunctions(ExtensionLoader &loader) { ST_DistanceWithin::Register(loader); ST_Dump::Register(loader); ST_EndPoint::Register(loader); + ST_Expand::Register(loader); ST_Extent::Register(loader); ST_Extent_Approx::Register(loader); // Op_IntersectApprox::Register(loader); diff --git a/test/sql/geometry/st_expand.test b/test/sql/geometry/st_expand.test new file mode 100644 index 00000000..9f2c1f7d --- /dev/null +++ b/test/sql/geometry/st_expand.test @@ -0,0 +1,46 @@ +require spatial + +query I +SELECT ST_AsText(ST_Expand(ST_MakePoint(153.0, -38.0), 0.001)); +---- +POLYGON ((152.999 -38.001, 152.999 -37.999, 153.001 -37.999, 153.001 -38.001, 152.999 -38.001)) + +query I +SELECT ST_AsText(ST_Expand(ST_MakePoint(153.0, -38.0), 0.0)); +---- +POLYGON ((153 -38, 153 -38, 153 -38, 153 -38, 153 -38)) + +query I +SELECT ST_AsText(ST_Expand(ST_GeomFromText('POINT(20 30)'), 0.001)); +---- +POLYGON ((19.999 29.999, 19.999 30.001, 20.001 30.001, 20.001 29.999, 19.999 29.999)) + +query I +SELECT ST_AsText(ST_Expand(ST_GeomFromText('GEOMETRYCOLLECTION(POINT(20 30))'), 0.001)); +---- +POLYGON ((19.999 29.999, 19.999 30.001, 20.001 30.001, 20.001 29.999, 19.999 29.999)) + +query I +SELECT ST_AsText(ST_Expand(ST_GeomFromText('POLYGON((20 30, 21 30, 21 31, 20 31, 20 30))'), 0.1)); +---- +POLYGON ((19.9 29.9, 19.9 31.1, 21.1 31.1, 21.1 29.9, 19.9 29.9)) + +query I +SELECT ST_AsText(ST_Expand(ST_GeomFromText('POLYGON((20 30, 21 30, 22 32, 21 31, 20 31, 20 30))'), 0.1)); +---- +POLYGON ((19.9 29.9, 19.9 32.1, 22.1 32.1, 22.1 29.9, 19.9 29.9)) + +query I +SELECT ST_AsText(ST_Expand(ST_MakeEnvelope(20, 30, 21, 31), 0.1)); +---- +POLYGON ((19.9 29.9, 19.9 31.1, 21.1 31.1, 21.1 29.9, 19.9 29.9)) + +query I +SELECT ST_AsText(ST_Expand(ST_MakeEnvelope(153.2, -38.8, 153.5, -38.7), 0.0)); +---- +POLYGON ((153.2 -38.8, 153.2 -38.7, 153.5 -38.7, 153.5 -38.8, 153.2 -38.8)) + +query I +SELECT ST_AsText(ST_Expand(ST_GeomFromText('GEOMETRYCOLLECTION EMPTY'), 0.001)); +---- +GEOMETRYCOLLECTION EMPTY \ No newline at end of file