Skip to content

Commit

Permalink
builtins: implement ST_IsPolygonCW/CCW and ST_ForcePolygonCW/CCW
Browse files Browse the repository at this point in the history
Release note (sql change): implement the ST_IsPolygonCW,
ST_IsPolygonCCW, ST_ForcePolygonCW and ST_ForcePolygonCCW builtins.

Release justification: low risk, high benefit changes
  • Loading branch information
otan committed Aug 27, 2020
1 parent 926280a commit 88453d6
Show file tree
Hide file tree
Showing 7 changed files with 468 additions and 0 deletions.
8 changes: 8 additions & 0 deletions docs/generated/sql/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -1235,6 +1235,10 @@ Bottom Left.</p>
</span></td></tr>
<tr><td><a name="st_force2d"></a><code>st_force2d(geometry: geometry) &rarr; geometry</code></td><td><span class="funcdesc"><p>Returns a Geometry which only contains X and Y coordinates.</p>
</span></td></tr>
<tr><td><a name="st_forcepolygonccw"></a><code>st_forcepolygonccw(geometry: geometry) &rarr; geometry</code></td><td><span class="funcdesc"><p>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.</p>
</span></td></tr>
<tr><td><a name="st_forcepolygoncw"></a><code>st_forcepolygoncw(geometry: geometry) &rarr; geometry</code></td><td><span class="funcdesc"><p>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.</p>
</span></td></tr>
<tr><td><a name="st_geogfromewkb"></a><code>st_geogfromewkb(val: <a href="bytes.html">bytes</a>) &rarr; geography</code></td><td><span class="funcdesc"><p>Returns the Geography from an EWKB representation.</p>
</span></td></tr>
<tr><td><a name="st_geogfromewkt"></a><code>st_geogfromewkt(val: <a href="string.html">string</a>) &rarr; geography</code></td><td><span class="funcdesc"><p>Returns the Geography from an EWKT representation.</p>
Expand Down Expand Up @@ -1338,6 +1342,10 @@ calculated, the result is transformed back into a Geography with SRID 4326.</p>
</span></td></tr>
<tr><td><a name="st_isempty"></a><code>st_isempty(geometry: geometry) &rarr; <a href="bool.html">bool</a></code></td><td><span class="funcdesc"><p>Returns whether the geometry is empty.</p>
</span></td></tr>
<tr><td><a name="st_ispolygonccw"></a><code>st_ispolygonccw(geometry: geometry) &rarr; <a href="bool.html">bool</a></code></td><td><span class="funcdesc"><p>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.</p>
</span></td></tr>
<tr><td><a name="st_ispolygoncw"></a><code>st_ispolygoncw(geometry: geometry) &rarr; <a href="bool.html">bool</a></code></td><td><span class="funcdesc"><p>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.</p>
</span></td></tr>
<tr><td><a name="st_isring"></a><code>st_isring(geometry: geometry) &rarr; <a href="bool.html">bool</a></code></td><td><span class="funcdesc"><p>Returns whether the geometry is a single linestring that is closed and simple, as defined by ST_IsClosed and ST_IsSimple.</p>
<p>This function utilizes the GEOS module.</p>
</span></td></tr>
Expand Down
File renamed without changes.
File renamed without changes.
154 changes: 154 additions & 0 deletions pkg/geo/geomfn/orientation.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading

0 comments on commit 88453d6

Please sign in to comment.