From f9a494f27991a07ff3a9ae6850933205c4f21f69 Mon Sep 17 00:00:00 2001 From: manhhiep92 Date: Sat, 4 Jul 2020 16:45:04 +0800 Subject: [PATCH] geo/geomfn: implement ST_MakePolygon({geometry,_geometry}) Closes #49305 Release note (sql change): Implement ST_MakePolygon functionality for Geometry types. --- docs/generated/sql/functions.md | 4 + pkg/geo/geomfn/polygon.go | 72 +++++ pkg/geo/geomfn/polygon_test.go | 260 ++++++++++++++++++ .../logictest/testdata/logic_test/geospatial | 118 ++++++++ pkg/sql/sem/builtins/geo_builtins.go | 45 +++ 5 files changed, 499 insertions(+) create mode 100644 pkg/geo/geomfn/polygon.go create mode 100644 pkg/geo/geomfn/polygon_test.go diff --git a/docs/generated/sql/functions.md b/docs/generated/sql/functions.md index 254ce54b19fa..c20e976d4b7b 100644 --- a/docs/generated/sql/functions.md +++ b/docs/generated/sql/functions.md @@ -1068,6 +1068,10 @@ given Geometry.

st_makepoint(x: float, y: float) → geometry

Returns a new Point with the given X and Y coordinates.

+st_makepolygon(geometry: geometry) → geometry

Returns a new Polygon with the given outer LineString.

+
+st_makepolygon(outer: geometry, interior: anyelement[]) → geometry

Returns a new Polygon with the given outer LineString and interior (hole) LineString(s).

+
st_maxdistance(geometry_a: geometry, geometry_b: geometry) → float

Returns the maximum distance across every pair of points comprising the given geometries. Note if the geometries are the same, it will return the maximum distance between the geometry’s vertexes.

st_mlinefromtext(str: string, srid: int) → geometry

Returns the Geometry from a WKT or EWKT representation with an SRID. If the shape underneath is not MultiLineString, NULL is returned. If the SRID is present in both the EWKT and the argument, the argument value is used.

diff --git a/pkg/geo/geomfn/polygon.go b/pkg/geo/geomfn/polygon.go new file mode 100644 index 000000000000..2115cf1d5d65 --- /dev/null +++ b/pkg/geo/geomfn/polygon.go @@ -0,0 +1,72 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package geomfn + +import ( + "github.com/cockroachdb/cockroach/pkg/geo" + "github.com/cockroachdb/errors" + "github.com/twpayne/go-geom" +) + +// MakePolygon creates a Polygon geometry from linestring and optional inner linestrings. +// Returns errors if geometries are not linestrings. +func MakePolygon(outer *geo.Geometry, interior ...*geo.Geometry) (*geo.Geometry, error) { + layout := geom.XY + outerGeomT, err := outer.AsGeomT() + if err != nil { + return nil, err + } + outerRing, ok := outerGeomT.(*geom.LineString) + if !ok { + return nil, errors.Newf("argument must be LINESTRING geometries") + } + if outerRing.NumCoords() < 4 { + return nil, errors.Newf("shell must have at least 4 points") + } + if !isClosed(layout, outerRing) { + return nil, errors.Newf("shell must be closed") + } + srid := outerRing.SRID() + coords := make([][]geom.Coord, len(interior)+1) + coords[0] = outerRing.Coords() + for i, g := range interior { + interiorRingGeomT, err := g.AsGeomT() + if err != nil { + return nil, err + } + interiorRing, ok := interiorRingGeomT.(*geom.LineString) + if !ok { + return nil, errors.Newf("argument must be LINESTRING geometries") + } + if interiorRing.SRID() != srid { + return nil, errors.Newf("mixed SRIDs are not allowed") + } + if interiorRing.NumCoords() < 4 { + return nil, errors.Newf("holes must have at least 4 points") + } + if !isClosed(layout, interiorRing) { + return nil, errors.Newf("holes must be closed") + } + coords[i+1] = interiorRing.Coords() + } + + polygon, err := geom.NewPolygon(layout).SetSRID(srid).SetCoords(coords) + if err != nil { + return nil, err + } + return geo.NewGeometryFromGeomT(polygon) +} + +// isClosed checks if a LineString is closed to make a valid Polygon. +// Returns whether the last coordinate is the same as the first. +func isClosed(layout geom.Layout, g *geom.LineString) bool { + return g.Coord(0).Equal(layout, g.Coord(g.NumCoords()-1)) +} diff --git a/pkg/geo/geomfn/polygon_test.go b/pkg/geo/geomfn/polygon_test.go new file mode 100644 index 000000000000..a0232d0a4254 --- /dev/null +++ b/pkg/geo/geomfn/polygon_test.go @@ -0,0 +1,260 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package geomfn + +import ( + "testing" + + "github.com/cockroachdb/cockroach/pkg/geo" + "github.com/cockroachdb/cockroach/pkg/geo/geopb" + "github.com/cockroachdb/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMakePolygon(t *testing.T) { + testCases := []struct { + name string + outer string + outerSRID geopb.SRID + interior []string + interiorSRIDs []geopb.SRID + expected string + expectedSRID geopb.SRID + err error + }{ + { + "Single input variant - 2D", + "LINESTRING(75 29,77 29,77 29, 75 29)", + geopb.DefaultGeometrySRID, + []string{}, + []geopb.SRID{}, + "POLYGON((75 29,77 29,77 29,75 29))", + geopb.DefaultGeometrySRID, + nil, + }, + { + "Single input variant - 2D with SRID", + "LINESTRING(75 29,77 29,77 29, 75 29)", + geopb.SRID(4326), + []string{}, + []geopb.SRID{}, + "POLYGON((75 29,77 29,77 29,75 29))", + geopb.SRID(4326), + nil, + }, + { + "Single input variant - 2D square", + "LINESTRING(40 80, 80 80, 80 40, 40 40, 40 80)", + geopb.DefaultGeometrySRID, + []string{}, + []geopb.SRID{}, + "POLYGON((40 80, 80 80, 80 40, 40 40, 40 80))", + geopb.DefaultGeometrySRID, + nil, + }, + { + "With inner holes variant - 2D single interior ring", + "LINESTRING(40 80, 80 80, 80 40, 40 40, 40 80)", + geopb.DefaultGeometrySRID, + []string{ + "LINESTRING(50 70, 70 70, 70 50, 50 50, 50 70)", + }, + []geopb.SRID{geopb.DefaultGeometrySRID}, + "POLYGON((40 80,80 80,80 40,40 40,40 80),(50 70,70 70,70 50,50 50,50 70))", + geopb.DefaultGeometrySRID, + nil, + }, + { + "With inner holes variant - 2D single interior ring with SRIDs", + "LINESTRING(40 80, 80 80, 80 40, 40 40, 40 80)", + geopb.SRID(4326), + []string{ + "LINESTRING(50 70, 70 70, 70 50, 50 50, 50 70)", + }, + []geopb.SRID{geopb.SRID(4326)}, + "POLYGON((40 80,80 80,80 40,40 40,40 80),(50 70,70 70,70 50,50 50,50 70))", + geopb.SRID(4326), + nil, + }, + { + "With inner holes variant - 2D two interior rings", + "LINESTRING(40 80, 80 80, 80 40, 40 40, 40 80)", + geopb.DefaultGeometrySRID, + []string{ + "LINESTRING(50 70, 70 70, 70 50, 50 50, 50 70)", + "LINESTRING(60 60, 75 60, 75 45, 60 45, 60 60)", + }, + []geopb.SRID{geopb.DefaultGeometrySRID, geopb.DefaultGeometrySRID}, + "POLYGON((40 80,80 80,80 40,40 40,40 80),(50 70,70 70,70 50,50 50,50 70),(60 60,75 60,75 45,60 45,60 60))", + geopb.DefaultGeometrySRID, + nil, + }, + { + "With inner holes variant - 2D two interior rings with SRID", + "LINESTRING(40 80, 80 80, 80 40, 40 40, 40 80)", + geopb.SRID(4326), + []string{ + "LINESTRING(50 70, 70 70, 70 50, 50 50, 50 70)", + "LINESTRING(60 60, 75 60, 75 45, 60 45, 60 60)", + }, + []geopb.SRID{geopb.SRID(4326), geopb.SRID(4326)}, + "POLYGON((40 80,80 80,80 40,40 40,40 80),(50 70,70 70,70 50,50 50,50 70),(60 60,75 60,75 45,60 45,60 60))", + geopb.SRID(4326), + nil, + }, + { + "ERROR: Invalid argument - POINT", + "POINT(3 2)", + geopb.DefaultGeometrySRID, + []string{}, + []geopb.SRID{}, + "", + geopb.DefaultGeometrySRID, + errors.Newf("argument must be LINESTRING geometries"), + }, + { + "ERROR: Invalid argument - POINT rings", + "LINESTRING(75 29,77 29,77 29, 75 29)", + geopb.DefaultGeometrySRID, + []string{"POINT(3 2)"}, + []geopb.SRID{0}, + "", + geopb.DefaultGeometrySRID, + errors.Newf("argument must be LINESTRING geometries"), + }, + { + "ERROR: Unmatched SRIDs - Single interior ring", + "LINESTRING(40 80, 80 80, 80 40, 40 40, 40 80)", + geopb.SRID(4326), + []string{ + "LINESTRING(50 70, 70 70, 70 50, 50 50, 50 70)", + }, + []geopb.SRID{geopb.SRID(26918)}, + "", + geopb.DefaultGeometrySRID, + errors.Newf("mixed SRIDs are not allowed"), + }, + { + "ERROR: Unmatched SRIDs - Default SRID on interior ring", + "LINESTRING(40 80, 80 80, 80 40, 40 40, 40 80)", + geopb.SRID(4326), + []string{ + "LINESTRING(50 70, 70 70, 70 50, 50 50, 50 70)", + }, + []geopb.SRID{geopb.DefaultGeometrySRID}, + "", + geopb.DefaultGeometrySRID, + errors.Newf("mixed SRIDs are not allowed"), + }, + { + "ERROR: Unmatched SRIDs - Two interior rings", + "LINESTRING(40 80, 80 80, 80 40, 40 40, 40 80)", + geopb.SRID(4326), + []string{ + "LINESTRING(50 70, 70 70, 70 50, 50 50, 50 70)", + "LINESTRING(60 60, 75 60, 75 45, 60 45, 60 60)", + }, + []geopb.SRID{geopb.SRID(4326), geopb.SRID(26918)}, + "", + geopb.DefaultGeometrySRID, + errors.Newf("mixed SRIDs are not allowed"), + }, + { + "ERROR: Unclosed shell", + "LINESTRING(40 80, 80 80, 80 40, 40 40, 40 70)", + geopb.DefaultGeographySRID, + []string{}, + []geopb.SRID{}, + "", + geopb.DefaultGeometrySRID, + errors.Newf("shell must be closed"), + }, + { + "ERROR: Unclosed interior ring", + "LINESTRING(40 80, 80 80, 80 40, 40 40, 40 80)", + geopb.SRID(4326), + []string{ + "LINESTRING(50 70, 70 70, 70 50, 50 50, 50 60)", + }, + []geopb.SRID{geopb.SRID(4326), geopb.SRID(26918)}, + "", + geopb.DefaultGeometrySRID, + errors.Newf("holes must be closed"), + }, + { + "ERROR: Shell has 3 points", + "LINESTRING(40 80, 80 80, 40 80)", + geopb.DefaultGeographySRID, + []string{}, + []geopb.SRID{}, + "", + geopb.DefaultGeometrySRID, + errors.Newf("shell must have at least 4 points"), + }, + { + "ERROR: Shell has 2 points", + "LINESTRING(40 80, 40 80)", + geopb.DefaultGeographySRID, + []string{}, + []geopb.SRID{}, + "", + geopb.DefaultGeometrySRID, + errors.Newf("shell must have at least 4 points"), + }, + { + "ERROR: Interior ring has 3 points", + "LINESTRING(40 80, 80 80, 80 40, 40 40, 40 80)", + geopb.SRID(4326), + []string{ + "LINESTRING(50 70, 70 70, 50 70)", + }, + []geopb.SRID{geopb.SRID(4326), geopb.SRID(26918)}, + "", + geopb.DefaultGeometrySRID, + errors.Newf("holes must have at least 4 points"), + }, + { + "ERROR: Interior ring has 2 points", + "LINESTRING(40 80, 80 80, 80 40, 40 40, 40 80)", + geopb.SRID(4326), + []string{ + "LINESTRING(50 70, 50 70)", + }, + []geopb.SRID{geopb.SRID(4326), geopb.SRID(26918)}, + "", + geopb.DefaultGeometrySRID, + errors.Newf("holes must have at least 4 points"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + outer, err := geo.MustParseGeometry(tc.outer).CloneWithSRID(tc.outerSRID) + require.NoError(t, err) + interior := make([]*geo.Geometry, 0, len(tc.interior)) + for i, g := range tc.interior { + interiorRing, err := geo.MustParseGeometry(g).CloneWithSRID(tc.interiorSRIDs[i]) + require.NoError(t, err) + interior = append(interior, interiorRing) + } + polygon, err := MakePolygon(outer, interior...) + if tc.err != nil { + require.Errorf(t, err, tc.err.Error()) + } else { + require.NoError(t, err) + expected, err := geo.MustParseGeometry(tc.expected).CloneWithSRID(tc.expectedSRID) + require.NoError(t, err) + assert.Equal(t, expected, polygon) + } + }) + } +} diff --git a/pkg/sql/logictest/testdata/logic_test/geospatial b/pkg/sql/logictest/testdata/logic_test/geospatial index 0386e750e215..d0a0c7654c13 100644 --- a/pkg/sql/logictest/testdata/logic_test/geospatial +++ b/pkg/sql/logictest/testdata/logic_test/geospatial @@ -2576,3 +2576,121 @@ query R SELECT ST_Azimuth(ST_Point(0, 0)::geography, ST_Point(0, 0)::geography) ---- NULL + +statement error argument must be LINESTRING geometries +SELECT ST_MakePolygon('MULTIPOINT(0 0, 1 1)') + +statement error unknown signature: st_makepolygon\(string\) +SELECT ST_MakePolygon('abc') + +statement error argument must be LINESTRING geometries +SELECT ST_MakePolygon( + ST_GeomFromText('LINESTRING(40 80, 80 80, 80 40, 40 40, 40 80)'), + ARRAY[ + ST_GeomFromText('MULTIPOINT(50 70, 70 70, 70 50, 50 50, 50 70)') + ]); + +statement error argument must be LINESTRING geometries +SELECT ST_MakePolygon( + ST_GeomFromText('LINESTRING(40 80, 80 80, 80 40, 40 40, 40 80)'), + ARRAY[ + 'MULTIPOINT(50 70, 70 70, 70 50, 50 50, 50 70)' + ]); + +statement error argument must be LINESTRING geometries +SELECT ST_MakePolygon( + ST_GeomFromText('LINESTRING(40 80, 80 80, 80 40, 40 40, 40 80)'), + ARRAY['abc']); + +# SRID set on shell but not interior rings +statement error mixed SRIDs are not allowed +SELECT ST_MakePolygon( + ST_GeomFromText('LINESTRING(40 80, 80 80, 80 40, 40 40, 40 80)', 4326), + ARRAY[ + ST_GeomFromText('LINESTRING(50 70, 70 70, 70 50, 50 50, 50 70)') + ]); + +# SRIDs on shell and interior ring are unmatched +statement error mixed SRIDs are not allowed +SELECT ST_MakePolygon( + ST_GeomFromText('LINESTRING(40 80, 80 80, 80 40, 40 40, 40 80)', 4326), + ARRAY[ + ST_GeomFromText('LINESTRING(50 70, 70 70, 70 50, 50 50, 50 70)', 3857) + ]); + +# SRIDs on interior rings are unmatched +statement error mixed SRIDs are not allowed +SELECT ST_MakePolygon( + ST_GeomFromText('LINESTRING(40 80, 80 80, 80 40, 40 40, 40 80)', 4326), + ARRAY[ + ST_GeomFromText('LINESTRING(50 70, 70 70, 70 50, 50 50, 50 70)', 4326), + ST_GeomFromText('LINESTRING(60 60, 75 60, 75 45, 60 45, 60 60)', 3857) + ]); + +statement error shell must be closed +SELECT ST_MakePolygon(ST_GeomFromText('LINESTRING(40 80, 80 80, 80 40, 40 40, 40 70)')); + +statement error shell must have at least 4 points +SELECT ST_MakePolygon(ST_GeomFromText('LINESTRING(40 80, 80 80, 40 80)')); + +statement error shell must have at least 4 points +SELECT ST_MakePolygon(ST_GeomFromText('LINESTRING(40 80, 40 80)')); + +statement error holes must be closed +SELECT ST_MakePolygon( + ST_GeomFromText('LINESTRING(40 80, 80 80, 80 40, 40 40, 40 80)'), + ARRAY[ + ST_GeomFromText('LINESTRING(50 70, 70 70, 70 50, 50 50, 50 60)') + ]); + +statement error holes must have at least 4 points +SELECT ST_MakePolygon( + ST_GeomFromText('LINESTRING(40 80, 80 80, 80 40, 40 40, 40 80)'), + ARRAY[ + ST_GeomFromText('LINESTRING(50 70, 70 70, 50 70)') + ]); + +statement error holes must have at least 4 points +SELECT ST_MakePolygon( + ST_GeomFromText('LINESTRING(40 80, 80 80, 80 40, 40 40, 40 80)'), + ARRAY[ + ST_GeomFromText('LINESTRING(50 70, 50 70)') + ]); + +query T +SELECT ST_AsEWKT(ST_MakePolygon( ST_GeomFromText('LINESTRING(75 29,77 29,77 29, 75 29)'))); +---- +POLYGON ((75 29, 77 29, 77 29, 75 29)) + +query T +SELECT ST_AsEWKT(ST_MakePolygon( ST_GeomFromText('LINESTRING(75 29,77 29,77 29, 75 29)', 4326))); +---- +SRID=4326;POLYGON ((75 29, 77 29, 77 29, 75 29)) + +query T +SELECT ST_AsEWKT(ST_MakePolygon( + ST_GeomFromText('LINESTRING(40 80, 80 80, 80 40, 40 40, 40 80)'), + ARRAY[ + ST_GeomFromText('LINESTRING(50 70, 70 70, 70 50, 50 50, 50 70)') + ])); +---- +POLYGON ((40 80, 80 80, 80 40, 40 40, 40 80), (50 70, 70 70, 70 50, 50 50, 50 70)) + +query T +SELECT ST_AsEWKT(ST_MakePolygon( + ST_GeomFromText('LINESTRING(40 80, 80 80, 80 40, 40 40, 40 80)'), + ARRAY[ + ST_GeomFromText('LINESTRING(50 70, 70 70, 70 50, 50 50, 50 70)'), + ST_GeomFromText('LINESTRING(60 60, 75 60, 75 45, 60 45, 60 60)') + ])); +---- +POLYGON ((40 80, 80 80, 80 40, 40 40, 40 80), (50 70, 70 70, 70 50, 50 50, 50 70), (60 60, 75 60, 75 45, 60 45, 60 60)) + +query T +SELECT ST_AsEWKT(ST_MakePolygon( + ST_GeomFromText('LINESTRING(40 80, 80 80, 80 40, 40 40, 40 80)', 4326), + ARRAY[ + ST_GeomFromText('LINESTRING(50 70, 70 70, 70 50, 50 50, 50 70)', 4326) + ])); +---- +SRID=4326;POLYGON ((40 80, 80 80, 80 40, 40 40, 40 80), (50 70, 70 70, 70 50, 50 50, 50 70)) diff --git a/pkg/sql/sem/builtins/geo_builtins.go b/pkg/sql/sem/builtins/geo_builtins.go index 2d54450b040c..b25e1618fb0a 100644 --- a/pkg/sql/sem/builtins/geo_builtins.go +++ b/pkg/sql/sem/builtins/geo_builtins.go @@ -530,6 +530,51 @@ var geoBuiltins = map[string]builtinDefinition{ tree.VolatilityImmutable, ), ), + "st_makepolygon": makeBuiltin( + defProps(), + geometryOverload1( + func(ctx *tree.EvalContext, outer *tree.DGeometry) (tree.Datum, error) { + g, err := geomfn.MakePolygon(outer.Geometry) + if err != nil { + return nil, err + } + return tree.NewDGeometry(g), nil + }, + types.Geometry, + infoBuilder{ + info: `Returns a new Polygon with the given outer LineString.`, + }, + tree.VolatilityImmutable, + ), + tree.Overload{ + Types: tree.ArgTypes{ + {"outer", types.Geometry}, + {"interior", types.AnyArray}, + }, + ReturnType: tree.FixedReturnType(types.Geometry), + Fn: func(ctx *tree.EvalContext, args tree.Datums) (tree.Datum, error) { + outer := args[0].(*tree.DGeometry) + interiorArr := tree.MustBeDArray(args[1]) + interior := make([]*geo.Geometry, len(interiorArr.Array)) + for i, v := range interiorArr.Array { + g, ok := v.(*tree.DGeometry) + if !ok { + return nil, errors.Newf("argument must be LINESTRING geometries") + } + interior[i] = g.Geometry + } + g, err := geomfn.MakePolygon(outer.Geometry, interior...) + if err != nil { + return nil, err + } + return tree.NewDGeometry(g), nil + }, + Info: infoBuilder{ + info: `Returns a new Polygon with the given outer LineString and interior (hole) LineString(s).`, + }.String(), + Volatility: tree.VolatilityImmutable, + }, + ), "st_geomcollfromtext": geometryFromTextCheckShapeBuiltin(geopb.ShapeType_GeometryCollection), "st_geomcollfromwkb": geometryFromWKBCheckShapeBuiltin(geopb.ShapeType_GeometryCollection),