diff --git a/docs/generated/sql/functions.md b/docs/generated/sql/functions.md
index 450a6127dc53..5a89dde4cffc 100644
--- a/docs/generated/sql/functions.md
+++ b/docs/generated/sql/functions.md
@@ -1235,6 +1235,10 @@ Bottom Left.
st_force2d(geometry: geometry) → geometry | Returns a Geometry which only contains X and Y coordinates.
|
+st_forcepolygonccw(geometry: geometry) → geometry | Returns a Geometry where all Polygon objects have exterior rings in the counter-clockwise orientation and interior rings in the clockwise orientation. Non-Polygon objects are unchanged.
+ |
+st_forcepolygoncw(geometry: geometry) → geometry | Returns a Geometry where all Polygon objects have exterior rings in the clockwise orientation and interior rings in the counter-clockwise orientation. Non-Polygon objects are unchanged.
+ |
st_geogfromewkb(val: bytes) → geography | Returns the Geography from an EWKB representation.
|
st_geogfromewkt(val: string) → geography | Returns the Geography from an EWKT representation.
@@ -1338,6 +1342,10 @@ calculated, the result is transformed back into a Geography with SRID 4326.
|
st_isempty(geometry: geometry) → bool | Returns whether the geometry is empty.
|
+st_ispolygonccw(geometry: geometry) → bool | Returns whether the Polygon objects inside the Geometry have exterior rings in the counter-clockwise orientation and interior rings in the clockwise orientation. Non-Polygon objects are considered counter-clockwise.
+ |
+st_ispolygoncw(geometry: geometry) → bool | Returns whether the Polygon objects inside the Geometry have exterior rings in the clockwise orientation and interior rings in the counter-clockwise orientation. Non-Polygon objects are considered clockwise.
+ |
st_isring(geometry: geometry) → bool | Returns whether the geometry is a single linestring that is closed and simple, as defined by ST_IsClosed and ST_IsSimple.
This function utilizes the GEOS module.
|
diff --git a/pkg/geo/geomfn/force.go b/pkg/geo/geomfn/force_layout.go
similarity index 100%
rename from pkg/geo/geomfn/force.go
rename to pkg/geo/geomfn/force_layout.go
diff --git a/pkg/geo/geomfn/force_test.go b/pkg/geo/geomfn/force_layout_test.go
similarity index 100%
rename from pkg/geo/geomfn/force_test.go
rename to pkg/geo/geomfn/force_layout_test.go
diff --git a/pkg/geo/geomfn/orientation.go b/pkg/geo/geomfn/orientation.go
new file mode 100644
index 000000000000..a06d651d2ef9
--- /dev/null
+++ b/pkg/geo/geomfn/orientation.go
@@ -0,0 +1,154 @@
+// 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"
+)
+
+// Orientation defines an orientation of a shape.
+type Orientation int
+
+const (
+ // OrientationCW denotes a clockwise orientation.
+ OrientationCW Orientation = iota
+ // OrientationCCW denotes a counter-clockwise orientation
+ OrientationCCW
+)
+
+// HasPolygonOrientation checks whether a given Geometry have polygons
+// that matches the given Orientation.
+// Non-Polygon objects
+func HasPolygonOrientation(g geo.Geometry, o Orientation) (bool, error) {
+ t, err := g.AsGeomT()
+ if err != nil {
+ return false, err
+ }
+ return hasPolygonOrientation(t, o)
+}
+
+func hasPolygonOrientation(g geom.T, o Orientation) (bool, error) {
+ switch g := g.(type) {
+ case *geom.Polygon:
+ for i := 0; i < g.NumLinearRings(); i++ {
+ isCCW := geo.IsLinearRingCCW(g.LinearRing(i))
+ // Interior rings should be the reverse orientation of the exterior ring.
+ if i > 0 {
+ isCCW = !isCCW
+ }
+ switch o {
+ case OrientationCW:
+ if isCCW {
+ return false, nil
+ }
+ case OrientationCCW:
+ if !isCCW {
+ return false, nil
+ }
+ default:
+ return false, errors.Newf("unexpected orientation: %v", o)
+ }
+ }
+ return true, nil
+ case *geom.MultiPolygon:
+ for i := 0; i < g.NumPolygons(); i++ {
+ if ret, err := hasPolygonOrientation(g.Polygon(i), o); !ret || err != nil {
+ return ret, err
+ }
+ }
+ return true, nil
+ case *geom.GeometryCollection:
+ for i := 0; i < g.NumGeoms(); i++ {
+ if ret, err := hasPolygonOrientation(g.Geom(i), o); !ret || err != nil {
+ return ret, err
+ }
+ }
+ return true, nil
+ case *geom.Point, *geom.MultiPoint, *geom.LineString, *geom.MultiLineString:
+ return true, nil
+ default:
+ return false, errors.Newf("unhandled geometry type: %T", g)
+ }
+}
+
+// ForcePolygonOrientation forces orientations within polygons
+// to be oriented the prescribed way.
+func ForcePolygonOrientation(g geo.Geometry, o Orientation) (geo.Geometry, error) {
+ t, err := g.AsGeomT()
+ if err != nil {
+ return geo.Geometry{}, err
+ }
+
+ if err := forcePolygonOrientation(t, o); err != nil {
+ return geo.Geometry{}, err
+ }
+ return geo.MakeGeometryFromGeomT(t)
+}
+
+func forcePolygonOrientation(g geom.T, o Orientation) error {
+ switch g := g.(type) {
+ case *geom.Polygon:
+ for i := 0; i < g.NumLinearRings(); i++ {
+ isCCW := geo.IsLinearRingCCW(g.LinearRing(i))
+ // Interior rings should be the reverse orientation of the exterior ring.
+ if i > 0 {
+ isCCW = !isCCW
+ }
+ reverse := false
+ switch o {
+ case OrientationCW:
+ if isCCW {
+ reverse = true
+ }
+ case OrientationCCW:
+ if !isCCW {
+ reverse = true
+ }
+ default:
+ return errors.Newf("unexpected orientation: %v", o)
+ }
+
+ if reverse {
+ // Reverse coordinates from both ends.
+ // Do this by swapping up to the middle of the array of elements, which guarantees
+ // each end get swapped. This works for an odd number of elements as well as
+ // the middle element ends swapping with itself, which is ok.
+ coords := g.LinearRing(i).FlatCoords()
+ for cIdx := 0; cIdx < len(coords)/2; cIdx += g.Stride() {
+ for sIdx := 0; sIdx < g.Stride(); sIdx++ {
+ coords[cIdx+sIdx], coords[len(coords)-cIdx-g.Stride()+sIdx] = coords[len(coords)-cIdx-g.Stride()+sIdx], coords[cIdx+sIdx]
+ }
+ }
+ }
+ }
+ return nil
+ case *geom.MultiPolygon:
+ for i := 0; i < g.NumPolygons(); i++ {
+ if err := forcePolygonOrientation(g.Polygon(i), o); err != nil {
+ return err
+ }
+ }
+ return nil
+ case *geom.GeometryCollection:
+ for i := 0; i < g.NumGeoms(); i++ {
+ if err := forcePolygonOrientation(g.Geom(i), o); err != nil {
+ return err
+ }
+ }
+ return nil
+ case *geom.Point, *geom.MultiPoint, *geom.LineString, *geom.MultiLineString:
+ return nil
+ default:
+ return errors.Newf("unhandled geometry type: %T", g)
+ }
+}
diff --git a/pkg/geo/geomfn/orientation_test.go b/pkg/geo/geomfn/orientation_test.go
new file mode 100644
index 000000000000..126fee805603
--- /dev/null
+++ b/pkg/geo/geomfn/orientation_test.go
@@ -0,0 +1,226 @@
+// 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/stretchr/testify/require"
+)
+
+var orientationTestCases = []struct {
+ desc string
+ wkt string
+ isCCW bool
+ isCW bool
+ forcedCCWWKT string
+ forcedCWWKT string
+}{
+ {
+ desc: "POINT",
+ wkt: "POINT(10 20)",
+ isCCW: true,
+ isCW: true,
+ forcedCCWWKT: "POINT(10 20)",
+ forcedCWWKT: "POINT(10 20)",
+ },
+ {
+ desc: "MULTIPOINT",
+ wkt: "MULTIPOINT((10 20), (20 30))",
+ isCCW: true,
+ isCW: true,
+ forcedCCWWKT: "MULTIPOINT((10 20), (20 30))",
+ forcedCWWKT: "MULTIPOINT((10 20), (20 30))",
+ },
+ {
+ desc: "LINESTRING",
+ wkt: "LINESTRING(10 20, 20 30)",
+ isCCW: true,
+ isCW: true,
+ forcedCCWWKT: "LINESTRING(10 20, 20 30)",
+ forcedCWWKT: "LINESTRING(10 20, 20 30)",
+ },
+ {
+ desc: "MULTILINESTRING",
+ wkt: "MULTILINESTRING((10 20, 20 30), (20 30, 30 40))",
+ isCCW: true,
+ isCW: true,
+ forcedCCWWKT: "MULTILINESTRING((10 20, 20 30), (20 30, 30 40))",
+ forcedCWWKT: "MULTILINESTRING((10 20, 20 30), (20 30, 30 40))",
+ },
+ {
+ desc: "POLYGON from wikipedia",
+ wkt: "POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))",
+ isCCW: true,
+ isCW: false,
+ forcedCCWWKT: "POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))",
+ forcedCWWKT: "POLYGON ((30 10, 10 20, 20 40, 40 40, 30 10))",
+ },
+ {
+ desc: "POLYGON with interior rings correctly CW",
+ wkt: "POLYGON((0 0, 0 1, 1 1, 1 0, 0 0), (0.1 0.1, 0.9 0.1, 0.9 0.9, 0.1 0.9, 0.1 0.1))",
+ isCCW: false,
+ isCW: true,
+ forcedCCWWKT: "POLYGON((0 0, 1 0, 1 1, 0 1, 0 0), (0.1 0.1, 0.1 0.9, 0.9 0.9, 0.9 0.1, 0.1 0.1))",
+ forcedCWWKT: "POLYGON((0 0, 0 1, 1 1, 1 0, 0 0), (0.1 0.1, 0.9 0.1, 0.9 0.9, 0.1 0.9, 0.1 0.1))",
+ },
+ {
+ desc: "POLYGON with interior rings correctly CCW",
+ wkt: "POLYGON((0 0, 1 0, 1 1, 0 1, 0 0), (0.1 0.1, 0.1 0.9, 0.9 0.9, 0.9 0.1, 0.1 0.1))",
+ isCCW: true,
+ isCW: false,
+ forcedCCWWKT: "POLYGON((0 0, 1 0, 1 1, 0 1, 0 0), (0.1 0.1, 0.1 0.9, 0.9 0.9, 0.9 0.1, 0.1 0.1))",
+ forcedCWWKT: "POLYGON((0 0, 0 1, 1 1, 1 0, 0 0), (0.1 0.1, 0.9 0.1, 0.9 0.9, 0.1 0.9, 0.1 0.1))",
+ },
+ {
+ desc: "POLYGON with all exterior and interior rings CW",
+ wkt: "POLYGON((0 0, 0 1, 1 1, 1 0, 0 0), (0.1 0.1, 0.1 0.9, 0.9 0.9, 0.9 0.1, 0.1 0.1))",
+ isCCW: false,
+ isCW: false,
+ forcedCCWWKT: "POLYGON((0 0, 1 0, 1 1, 0 1, 0 0), (0.1 0.1, 0.1 0.9, 0.9 0.9, 0.9 0.1, 0.1 0.1))",
+ forcedCWWKT: "POLYGON((0 0, 0 1, 1 1, 1 0, 0 0), (0.1 0.1, 0.9 0.1, 0.9 0.9, 0.1 0.9, 0.1 0.1))",
+ },
+ {
+ desc: "MultiPolygon all CW",
+ wkt: `MULTIPOLYGON(
+ ((0 0, 0 1, 1 1, 1 0, 0 0), (0.1 0.1, 0.9 0.1, 0.9 0.9, 0.1 0.9, 0.1 0.1)),
+ ((10 10, 10 11, 11 11, 11 10, 10 10), (10.1 10.1, 10.9 10.1, 10.9 10.9, 10.1 10.9, 10.1 10.1))
+ )`,
+ isCCW: false,
+ isCW: true,
+ forcedCCWWKT: `MULTIPOLYGON(
+ ((0 0, 1 0, 1 1, 0 1, 0 0), (0.1 0.1, 0.1 0.9, 0.9 0.9, 0.9 0.1, 0.1 0.1)),
+ ((10 10, 11 10, 11 11, 10 11, 10 10), (10.1 10.1, 10.1 10.9, 10.9 10.9, 10.9 10.1, 10.1 10.1))
+ )`,
+ forcedCWWKT: `MULTIPOLYGON(
+ ((0 0, 0 1, 1 1, 1 0, 0 0), (0.1 0.1, 0.9 0.1, 0.9 0.9, 0.1 0.9, 0.1 0.1)),
+ ((10 10, 10 11, 11 11, 11 10, 10 10), (10.1 10.1, 10.9 10.1, 10.9 10.9, 10.1 10.9, 10.1 10.1))
+ )`,
+ },
+ {
+ desc: "MultiPolygon mixed everything",
+ wkt: `MULTIPOLYGON(
+ ((0 0, 0 1, 1 1, 1 0, 0 0), (0.1 0.1, 0.1 0.9, 0.9 0.9, 0.9 0.1, 0.1 0.1)),
+ ((10 10, 11 10, 11 11, 10 11, 10 10), (10.1 10.1, 10.9 10.1, 10.9 10.9, 10.1 10.9, 10.1 10.1))
+ )`,
+ isCCW: false,
+ isCW: false,
+ forcedCCWWKT: `MULTIPOLYGON(
+ ((0 0, 1 0, 1 1, 0 1, 0 0), (0.1 0.1, 0.1 0.9, 0.9 0.9, 0.9 0.1, 0.1 0.1)),
+ ((10 10, 11 10, 11 11, 10 11, 10 10), (10.1 10.1, 10.1 10.9, 10.9 10.9, 10.9 10.1, 10.1 10.1))
+ )`,
+ forcedCWWKT: `MULTIPOLYGON(
+ ((0 0, 0 1, 1 1, 1 0, 0 0), (0.1 0.1, 0.9 0.1, 0.9 0.9, 0.1 0.9, 0.1 0.1)),
+ ((10 10, 10 11, 11 11, 11 10, 10 10), (10.1 10.1, 10.9 10.1, 10.9 10.9, 10.1 10.9, 10.1 10.1))
+ )`,
+ },
+ {
+ desc: "GEOMETRYCOLLECTION all CW",
+ wkt: `GEOMETRYCOLLECTION(
+ LINESTRING(0 0, 1 0),
+ MULTIPOLYGON(
+ ((0 0, 0 1, 1 1, 1 0, 0 0), (0.1 0.1, 0.9 0.1, 0.9 0.9, 0.1 0.9, 0.1 0.1)),
+ ((10 10, 10 11, 11 11, 11 10, 10 10), (10.1 10.1, 10.9 10.1, 10.9 10.9, 10.1 10.9, 10.1 10.1))
+ ),
+ POLYGON((0 0, 0 1, 1 1, 1 0, 0 0), (0.1 0.1, 0.9 0.1, 0.9 0.9, 0.1 0.9, 0.1 0.1))
+ )`,
+ isCCW: false,
+ isCW: true,
+ forcedCCWWKT: `GEOMETRYCOLLECTION(
+ LINESTRING(0 0, 1 0),
+ MULTIPOLYGON(
+ ((0 0, 1 0, 1 1, 0 1, 0 0), (0.1 0.1, 0.1 0.9, 0.9 0.9, 0.9 0.1, 0.1 0.1)),
+ ((10 10, 11 10, 11 11, 10 11, 10 10), (10.1 10.1, 10.1 10.9, 10.9 10.9, 10.9 10.1, 10.1 10.1))
+ ),
+ POLYGON((0 0, 1 0, 1 1, 0 1, 0 0), (0.1 0.1, 0.1 0.9, 0.9 0.9, 0.9 0.1, 0.1 0.1))
+ )`,
+ forcedCWWKT: `GEOMETRYCOLLECTION(
+ LINESTRING(0 0, 1 0),
+ MULTIPOLYGON(
+ ((0 0, 0 1, 1 1, 1 0, 0 0), (0.1 0.1, 0.9 0.1, 0.9 0.9, 0.1 0.9, 0.1 0.1)),
+ ((10 10, 10 11, 11 11, 11 10, 10 10), (10.1 10.1, 10.9 10.1, 10.9 10.9, 10.1 10.9, 10.1 10.1))
+ ),
+ POLYGON((0 0, 0 1, 1 1, 1 0, 0 0), (0.1 0.1, 0.9 0.1, 0.9 0.9, 0.1 0.9, 0.1 0.1))
+ )`,
+ },
+ {
+ desc: "GEOMETRYCOLLECTION mixed everything",
+ wkt: `GEOMETRYCOLLECTION(
+ LINESTRING(0 0, 1 0),
+ MULTIPOLYGON(
+ ((0 0, 0 1, 1 1, 1 0, 0 0), (0.1 0.1, 0.1 0.9, 0.9 0.9, 0.9 0.1, 0.1 0.1)),
+ ((10 10, 11 10, 11 11, 10 11, 10 10), (10.1 10.1, 10.9 10.1, 10.9 10.9, 10.1 10.9, 10.1 10.1))
+ ),
+ POLYGON((0 0, 0 1, 1 1, 1 0, 0 0), (0.1 0.1, 0.9 0.1, 0.9 0.9, 0.1 0.9, 0.1 0.1))
+ )`,
+ isCCW: false,
+ isCW: false,
+ forcedCCWWKT: `GEOMETRYCOLLECTION(
+ LINESTRING(0 0, 1 0),
+ MULTIPOLYGON(
+ ((0 0, 1 0, 1 1, 0 1, 0 0), (0.1 0.1, 0.1 0.9, 0.9 0.9, 0.9 0.1, 0.1 0.1)),
+ ((10 10, 11 10, 11 11, 10 11, 10 10), (10.1 10.1, 10.1 10.9, 10.9 10.9, 10.9 10.1, 10.1 10.1))
+ ),
+ POLYGON((0 0, 1 0, 1 1, 0 1, 0 0), (0.1 0.1, 0.1 0.9, 0.9 0.9, 0.9 0.1, 0.1 0.1))
+ )`,
+ forcedCWWKT: `GEOMETRYCOLLECTION(
+ LINESTRING(0 0, 1 0),
+ MULTIPOLYGON(
+ ((0 0, 0 1, 1 1, 1 0, 0 0), (0.1 0.1, 0.9 0.1, 0.9 0.9, 0.1 0.9, 0.1 0.1)),
+ ((10 10, 10 11, 11 11, 11 10, 10 10), (10.1 10.1, 10.9 10.1, 10.9 10.9, 10.1 10.9, 10.1 10.1))
+ ),
+ POLYGON((0 0, 0 1, 1 1, 1 0, 0 0), (0.1 0.1, 0.9 0.1, 0.9 0.9, 0.1 0.9, 0.1 0.1))
+ )`,
+ },
+}
+
+func TestHasPolygonOrientation(t *testing.T) {
+ for _, tc := range orientationTestCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ g, err := geo.ParseGeometry(tc.wkt)
+ require.NoError(t, err)
+
+ t.Run("ccw", func(t *testing.T) {
+ ret, err := HasPolygonOrientation(g, OrientationCCW)
+ require.NoError(t, err)
+ require.Equal(t, tc.isCCW, ret)
+ })
+
+ t.Run("cw", func(t *testing.T) {
+ ret, err := HasPolygonOrientation(g, OrientationCW)
+ require.NoError(t, err)
+ require.Equal(t, tc.isCW, ret)
+ })
+ })
+ }
+}
+
+func TestForcePolygonOrientation(t *testing.T) {
+ for _, tc := range orientationTestCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ g, err := geo.ParseGeometry(tc.wkt)
+ require.NoError(t, err)
+
+ t.Run("ccw", func(t *testing.T) {
+ ret, err := ForcePolygonOrientation(g, OrientationCCW)
+ require.NoError(t, err)
+ require.Equal(t, geo.MustParseGeometry(tc.forcedCCWWKT), ret)
+ })
+
+ t.Run("cw", func(t *testing.T) {
+ ret, err := ForcePolygonOrientation(g, OrientationCW)
+ require.NoError(t, err)
+ require.Equal(t, geo.MustParseGeometry(tc.forcedCWWKT), ret)
+ })
+ })
+ }
+}
diff --git a/pkg/sql/logictest/testdata/logic_test/geospatial b/pkg/sql/logictest/testdata/logic_test/geospatial
index be49bb8d26eb..abb1c55dd21e 100644
--- a/pkg/sql/logictest/testdata/logic_test/geospatial
+++ b/pkg/sql/logictest/testdata/logic_test/geospatial
@@ -1213,6 +1213,30 @@ Square (left) GEOMETRYCOLLECTION EMPTY
Square (right) POLYGON ((0 0, 0 0.5, 0.5 0.5, 0.5 0, 0 0))
Square overlapping left and right square POLYGON ((0 0, 0 0.5, 0.5 0.5, 0.5 0, 0 0))
+# CW/CCW predicates.
+query TBBTT
+SELECT
+ dsc,
+ ST_IsPolygonCW(geom),
+ ST_IsPolygonCCW(geom),
+ ST_AsText(ST_ForcePolygonCW(geom)),
+ ST_AsText(ST_ForcePolygonCCW(geom))
+FROM geom_operators_test
+ORDER BY dsc
+----
+Empty GeometryCollection true true GEOMETRYCOLLECTION EMPTY GEOMETRYCOLLECTION EMPTY
+Empty LineString true true LINESTRING EMPTY LINESTRING EMPTY
+Empty Point true true POINT EMPTY POINT EMPTY
+Faraway point true true POINT (5 5) POINT (5 5)
+Line going through left and right square true true LINESTRING (-0.5 0.5, 0.5 0.5) LINESTRING (-0.5 0.5, 0.5 0.5)
+NULL NULL NULL NULL NULL
+Nested Geometry Collection true true GEOMETRYCOLLECTION (GEOMETRYCOLLECTION (POINT (0 0))) GEOMETRYCOLLECTION (GEOMETRYCOLLECTION (POINT (0 0)))
+Point middle of Left Square true true POINT (-0.5 0.5) POINT (-0.5 0.5)
+Point middle of Right Square true true POINT (0.5 0.5) POINT (0.5 0.5)
+Square (left) false true POLYGON ((-1 0, -1 1, 0 1, 0 0, -1 0)) POLYGON ((-1 0, 0 0, 0 1, -1 1, -1 0))
+Square (right) false true POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0)) POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))
+Square overlapping left and right square false true POLYGON ((-0.1 0, -0.1 1, 1 1, 1 0, -0.1 0)) POLYGON ((-0.1 0, 1 0, 1 1, -0.1 1, -0.1 0))
+
# Functions which take in strings as input as well.
query TT
SELECT
diff --git a/pkg/sql/sem/builtins/geo_builtins.go b/pkg/sql/sem/builtins/geo_builtins.go
index 05295a19075e..194f5f43c8a5 100644
--- a/pkg/sql/sem/builtins/geo_builtins.go
+++ b/pkg/sql/sem/builtins/geo_builtins.go
@@ -2008,6 +2008,62 @@ Flags shown square brackets after the geometry type have the following meaning:
tree.VolatilityImmutable,
),
),
+ "st_forcepolygoncw": makeBuiltin(
+ defProps(),
+ geometryOverload1(
+ func(ctx *tree.EvalContext, g *tree.DGeometry) (tree.Datum, error) {
+ ret, err := geomfn.ForcePolygonOrientation(g.Geometry, geomfn.OrientationCW)
+ if err != nil {
+ return nil, err
+ }
+ return tree.NewDGeometry(ret), nil
+ },
+ types.Geometry,
+ infoBuilder{
+ info: "Returns a Geometry where all Polygon objects have exterior rings in the clockwise orientation and interior rings in the counter-clockwise orientation. Non-Polygon objects are unchanged.",
+ },
+ tree.VolatilityImmutable,
+ ),
+ ),
+ "st_forcepolygonccw": makeBuiltin(
+ defProps(),
+ geometryOverload1(
+ func(ctx *tree.EvalContext, g *tree.DGeometry) (tree.Datum, error) {
+ ret, err := geomfn.ForcePolygonOrientation(g.Geometry, geomfn.OrientationCCW)
+ if err != nil {
+ return nil, err
+ }
+ return tree.NewDGeometry(ret), nil
+ },
+ types.Geometry,
+ infoBuilder{
+ info: "Returns a Geometry where all Polygon objects have exterior rings in the counter-clockwise orientation and interior rings in the clockwise orientation. Non-Polygon objects are unchanged.",
+ },
+ tree.VolatilityImmutable,
+ ),
+ ),
+ "st_ispolygoncw": makeBuiltin(
+ defProps(),
+ geometryOverload1UnaryPredicate(
+ func(g geo.Geometry) (bool, error) {
+ return geomfn.HasPolygonOrientation(g, geomfn.OrientationCW)
+ },
+ infoBuilder{
+ info: "Returns whether the Polygon objects inside the Geometry have exterior rings in the clockwise orientation and interior rings in the counter-clockwise orientation. Non-Polygon objects are considered clockwise.",
+ },
+ ),
+ ),
+ "st_ispolygonccw": makeBuiltin(
+ defProps(),
+ geometryOverload1UnaryPredicate(
+ func(g geo.Geometry) (bool, error) {
+ return geomfn.HasPolygonOrientation(g, geomfn.OrientationCCW)
+ },
+ infoBuilder{
+ info: "Returns whether the Polygon objects inside the Geometry have exterior rings in the counter-clockwise orientation and interior rings in the clockwise orientation. Non-Polygon objects are considered counter-clockwise.",
+ },
+ ),
+ ),
"st_numgeometries": makeBuiltin(
defProps(),
geometryOverload1(