Skip to content

Commit

Permalink
geo/geomfn: implement ST_Buffer
Browse files Browse the repository at this point in the history
Implemented ST_Buffer. Had to write our own parsing logic to make it
compatible with C.

Also ran clang-format and fixed up a few bad infos.

Release note (sql change): Implement ST_Buffer for geometry and string
variants.
  • Loading branch information
otan committed Jun 2, 2020
1 parent 9d1efbd commit bf1245e
Show file tree
Hide file tree
Showing 9 changed files with 728 additions and 5 deletions.
64 changes: 63 additions & 1 deletion docs/generated/sql/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,69 @@ has no relationship with the commit order of concurrent transactions.</p>
</span></td></tr>
<tr><td><a name="st_astext"></a><code>st_astext(geometry: geometry) &rarr; <a href="string.html">string</a></code></td><td><span class="funcdesc"><p>Returns the WKT representation of a given Geometry.</p>
</span></td></tr>
<tr><td><a name="st_buffer"></a><code>st_buffer(geometry: geometry, distance: <a href="float.html">float</a>) &rarr; geometry</code></td><td><span class="funcdesc"><p>Returns a Geometry that represents all points whose distance is less than or equal to the given distance
from the given Geometry.</p>
<p>This function utilizes the GEOS module.</p>
</span></td></tr>
<tr><td><a name="st_buffer"></a><code>st_buffer(geometry: geometry, distance: <a href="float.html">float</a>, buffer_style_params: <a href="string.html">string</a>) &rarr; geometry</code></td><td><span class="funcdesc"><p>Returns a Geometry that represents all points whose distance is less than or equal to the given distance from the
given Geometry.</p>
<p>This variant takes in a space separate parameter string, which will augment the buffer styles. Valid parameters are:</p>
<ul>
<li>quad_segs=&lt;int&gt;, default 8</li>
<li>endcap=&lt;round|flat|butt|square&gt;, default round</li>
<li>join=&lt;round|mitre|miter|bevel&gt;, default round</li>
<li>side=&lt;both|left|right&gt;, default both</li>
<li>mitre_limit=&lt;float&gt;, default 5.0</li>
</ul>
<p>This function utilizes the GEOS module.</p>
</span></td></tr>
<tr><td><a name="st_buffer"></a><code>st_buffer(geometry: geometry, distance: <a href="float.html">float</a>, quad_segs: <a href="int.html">int</a>) &rarr; geometry</code></td><td><span class="funcdesc"><p>Returns a Geometry that represents all points whose distance is less than or equal to the given distance from the
given Geometry.</p>
<p>This variant approximates the circle into quad_seg segments per line (the default is 8).</p>
<p>This function utilizes the GEOS module.</p>
</span></td></tr>
<tr><td><a name="st_buffer"></a><code>st_buffer(geometry_str: <a href="string.html">string</a>, distance: <a href="decimal.html">decimal</a>) &rarr; geometry</code></td><td><span class="funcdesc"><p>Returns a Geometry that represents all points whose distance is less than or equal to the given distance
from the given Geometry.</p>
<p>This function utilizes the GEOS module.</p>
</span></td></tr>
<tr><td><a name="st_buffer"></a><code>st_buffer(geometry_str: <a href="string.html">string</a>, distance: <a href="decimal.html">decimal</a>, buffer_style_params: <a href="string.html">string</a>) &rarr; geometry</code></td><td><span class="funcdesc"><p>Returns a Geometry that represents all points whose distance is less than or equal to the given distance from the
given Geometry.</p>
<p>This variant takes in a space separate parameter string, which will augment the buffer styles. Valid parameters are:</p>
<ul>
<li>quad_segs=&lt;int&gt;, default 8</li>
<li>endcap=&lt;round|flat|butt|square&gt;, default round</li>
<li>join=&lt;round|mitre|miter|bevel&gt;, default round</li>
<li>side=&lt;both|left|right&gt;, default both</li>
<li>mitre_limit=&lt;float&gt;, default 5.0</li>
</ul>
<p>This function utilizes the GEOS module.</p>
</span></td></tr>
<tr><td><a name="st_buffer"></a><code>st_buffer(geometry_str: <a href="string.html">string</a>, distance: <a href="decimal.html">decimal</a>, quad_segs: <a href="int.html">int</a>) &rarr; geometry</code></td><td><span class="funcdesc"><p>Returns a Geometry that represents all points whose distance is less than or equal to the given distance from the
given Geometry.</p>
<p>This variant approximates the circle into quad_seg segments per line (the default is 8).</p>
<p>This function utilizes the GEOS module.</p>
</span></td></tr>
<tr><td><a name="st_buffer"></a><code>st_buffer(geometry_str: <a href="string.html">string</a>, distance: <a href="float.html">float</a>) &rarr; geometry</code></td><td><span class="funcdesc"><p>Returns a Geometry that represents all points whose distance is less than or equal to the given distance
from the given Geometry.</p>
<p>This function utilizes the GEOS module.</p>
</span></td></tr>
<tr><td><a name="st_buffer"></a><code>st_buffer(geometry_str: <a href="string.html">string</a>, distance: <a href="float.html">float</a>, buffer_style_params: <a href="string.html">string</a>) &rarr; geometry</code></td><td><span class="funcdesc"><p>Returns a Geometry that represents all points whose distance is less than or equal to the given distance from the
given Geometry.</p>
<p>This variant takes in a space separate parameter string, which will augment the buffer styles. Valid parameters are:</p>
<ul>
<li>quad_segs=&lt;int&gt;, default 8</li>
<li>endcap=&lt;round|flat|butt|square&gt;, default round</li>
<li>join=&lt;round|mitre|miter|bevel&gt;, default round</li>
<li>side=&lt;both|left|right&gt;, default both</li>
<li>mitre_limit=&lt;float&gt;, default 5.0</li>
</ul>
<p>This function utilizes the GEOS module.</p>
</span></td></tr>
<tr><td><a name="st_buffer"></a><code>st_buffer(geometry_str: <a href="string.html">string</a>, distance: <a href="float.html">float</a>, quad_segs: <a href="int.html">int</a>) &rarr; geometry</code></td><td><span class="funcdesc"><p>Returns a Geometry that represents all points whose distance is less than or equal to the given distance from the
given Geometry.</p>
<p>This variant approximates the circle into quad_seg segments per line (the default is 8).</p>
<p>This function utilizes the GEOS module.</p>
</span></td></tr>
<tr><td><a name="st_centroid"></a><code>st_centroid(geometry: geometry) &rarr; geometry</code></td><td><span class="funcdesc"><p>Returns the centroid of the given geometry.</p>
<p>This function utilizes the GEOS module.</p>
</span></td></tr>
Expand Down Expand Up @@ -1039,7 +1102,6 @@ has no relationship with the commit order of concurrent transactions.</p>
<tr><td><a name="st_segmentize"></a><code>st_segmentize(geography: geography, max_segment_length_meters: <a href="float.html">float</a>) &rarr; geography</code></td><td><span class="funcdesc"><p>Returns a modified Geography having no segment longer than the given max_segment_length meters.</p>
<p>The calculations are done on a sphere.</p>
<p>This function utilizes the S2 library for spherical calculations.</p>
<p>This function will automatically use any available index.</p>
</span></td></tr>
<tr><td><a name="st_setsrid"></a><code>st_setsrid(geography: geography, srid: <a href="int.html">int</a>) &rarr; geography</code></td><td><span class="funcdesc"><p>Sets a Geography to a new SRID without transforming the coordinates.</p>
</span></td></tr>
Expand Down
121 changes: 121 additions & 0 deletions pkg/geo/geomfn/buffer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// 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 (
"strconv"
"strings"

"github.com/cockroachdb/cockroach/pkg/geo"
"github.com/cockroachdb/cockroach/pkg/geo/geos"
"github.com/cockroachdb/errors"
)

// BufferParams is a wrapper around the geos.BufferParams.
type BufferParams struct {
p geos.BufferParams
}

// MakeDefaultBufferParams returns the default BufferParams/
func MakeDefaultBufferParams() BufferParams {
return BufferParams{
p: geos.BufferParams{
EndCapStyle: geos.BufferParamsEndCapStyleRound,
JoinStyle: geos.BufferParamsJoinStyleRound,
SingleSided: false,
QuadrantSegments: 8,
MitreLimit: 5.0,
},
}
}

// WithQuadrantSegments returns a copy of the BufferParams with the quadrantSegments set.
func (b BufferParams) WithQuadrantSegments(quadrantSegments int) BufferParams {
ret := b
ret.p.QuadrantSegments = quadrantSegments
return ret
}

// ParseBufferParams parses the given buffer params from a SQL string into
// the BufferParams form.
// The string must be of the same format as specified by https://postgis.net/docs/ST_Buffer.html.
// Returns the BufferParams, as well as the modified distance.
func ParseBufferParams(s string, distance float64) (BufferParams, float64, error) {
p := MakeDefaultBufferParams()
fields := strings.Fields(s)
for _, field := range fields {
fParams := strings.Split(field, "=")
if len(fParams) != 2 {
return BufferParams{}, 0, errors.Newf("unknown buffer parameter: %s", fParams)
}
f, val := fParams[0], fParams[1]
switch strings.ToLower(f) {
case "quad_segs":
valInt, err := strconv.ParseInt(val, 10, 64)
if err != nil {
return BufferParams{}, 0, errors.Wrapf(err, "invalid int for %s: %s", f, val)
}
p.p.QuadrantSegments = int(valInt)
case "endcap":
switch strings.ToLower(val) {
case "round":
p.p.EndCapStyle = geos.BufferParamsEndCapStyleRound
case "flat", "butt":
p.p.EndCapStyle = geos.BufferParamsEndCapStyleFlat
case "square":
p.p.EndCapStyle = geos.BufferParamsEndCapStyleSquare
default:
return BufferParams{}, 0, errors.Newf("unknown endcap: %s (accepted: round, flat, square)", val)
}
case "join":
switch strings.ToLower(val) {
case "round":
p.p.JoinStyle = geos.BufferParamsJoinStyleRound
case "mitre", "miter":
p.p.JoinStyle = geos.BufferParamsJoinStyleMitre
case "bevel":
p.p.JoinStyle = geos.BufferParamsJoinStyleBevel
default:
return BufferParams{}, 0, errors.Newf("unknown join: %s (accepted: round, mitre, bevel)", val)
}
case "mitre_limit", "miter_limit":
valFloat, err := strconv.ParseFloat(val, 64)
if err != nil {
return BufferParams{}, 0, errors.Wrapf(err, "invalid float for %s: %s", f, val)
}
p.p.MitreLimit = valFloat
case "side":
switch strings.ToLower(val) {
case "both":
p.p.SingleSided = false
case "left":
p.p.SingleSided = true
case "right":
p.p.SingleSided = true
distance *= -1
default:
return BufferParams{}, 0, errors.Newf("unknown side: %s (accepted: both, left, right)", val)
}
default:
return BufferParams{}, 0, errors.Newf("unknown field: %s (accepted fields: quad_segs, endcap, join, mitre_limit, side)", f)
}
}
return p, distance, nil
}

// Buffer buffers a given Geometry by the supplied parameters.
func Buffer(g *geo.Geometry, params BufferParams, distance float64) (*geo.Geometry, error) {
bufferedGeom, err := geos.Buffer(g.EWKB(), params.p, distance)
if err != nil {
return nil, err
}
return geo.ParseGeometryFromEWKB(bufferedGeom)
}
86 changes: 86 additions & 0 deletions pkg/geo/geomfn/buffer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// 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/geos"
"github.com/stretchr/testify/require"
)

func TestParseBufferParams(t *testing.T) {
testCases := []struct {
s string
d float64

expected BufferParams
expectedD float64
}{
{
s: "",
d: 100,
expected: BufferParams{p: geos.BufferParams{
EndCapStyle: geos.BufferParamsEndCapStyleRound,
JoinStyle: geos.BufferParamsJoinStyleRound,
SingleSided: false,
QuadrantSegments: 8,
MitreLimit: 5.0,
}},
expectedD: 100,
},
{
s: "endcap=flat join=mitre quad_segs=4",
d: 100,
expected: BufferParams{p: geos.BufferParams{
EndCapStyle: geos.BufferParamsEndCapStyleFlat,
JoinStyle: geos.BufferParamsJoinStyleMitre,
SingleSided: false,
QuadrantSegments: 4,
MitreLimit: 5.0,
}},
expectedD: 100,
},
{
s: "side=left",
d: 100,
expected: BufferParams{p: geos.BufferParams{
EndCapStyle: geos.BufferParamsEndCapStyleRound,
JoinStyle: geos.BufferParamsJoinStyleRound,
SingleSided: true,
QuadrantSegments: 8,
MitreLimit: 5.0,
}},
expectedD: 100,
},
{
s: "side=right",
d: 100,
expected: BufferParams{p: geos.BufferParams{
EndCapStyle: geos.BufferParamsEndCapStyleRound,
JoinStyle: geos.BufferParamsJoinStyleRound,
SingleSided: true,
QuadrantSegments: 8,
MitreLimit: 5.0,
}},
expectedD: -100,
},
}

for _, tc := range testCases {
t.Run(tc.s, func(t *testing.T) {
s, d, err := ParseBufferParams(tc.s, tc.d)
require.NoError(t, err)
require.Equal(t, tc.expected, s)
require.Equal(t, tc.expectedD, d)
})
}
}
64 changes: 64 additions & 0 deletions pkg/geo/geos/geos.cc
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ typedef void (*CR_GEOS_MessageHandler)(const char*, void*);
typedef void* CR_GEOS_WKTReader;
typedef void* CR_GEOS_WKBReader;
typedef void* CR_GEOS_WKBWriter;
typedef void* CR_GEOS_BufferParams;

// Function declarations from `capi/geos_c.h.in` in GEOS.
typedef CR_GEOS_Handle (*CR_GEOS_init_r)();
Expand All @@ -63,6 +64,21 @@ typedef CR_GEOS_Geometry (*CR_GEOS_WKBReader_read_r)(CR_GEOS_Handle, CR_GEOS_WKB
size_t);
typedef void (*CR_GEOS_WKBReader_destroy_r)(CR_GEOS_Handle, CR_GEOS_WKBReader);

typedef CR_GEOS_BufferParams (*CR_GEOS_BufferParams_create_r)(CR_GEOS_Handle);
typedef void (*CR_GEOS_GEOSBufferParams_destroy_r)(CR_GEOS_Handle, CR_GEOS_BufferParams);
typedef int (*CR_GEOS_BufferParams_setEndCapStyle_r)(CR_GEOS_Handle, CR_GEOS_BufferParams,
int endCapStyle);
typedef int (*CR_GEOS_BufferParams_setJoinStyle_r)(CR_GEOS_Handle, CR_GEOS_BufferParams,
int joinStyle);
typedef int (*CR_GEOS_BufferParams_setMitreLimit_r)(CR_GEOS_Handle, CR_GEOS_BufferParams,
double mitreLimit);
typedef int (*CR_GEOS_BufferParams_setQuadrantSegments_r)(CR_GEOS_Handle, CR_GEOS_BufferParams,
int quadrantSegments);
typedef int (*CR_GEOS_BufferParams_setSingleSided_r)(CR_GEOS_Handle, CR_GEOS_BufferParams,
int singleSided);
typedef CR_GEOS_Geometry (*CR_GEOS_BufferWithParams_r)(CR_GEOS_Handle, CR_GEOS_Geometry,
CR_GEOS_BufferParams, double width);

typedef int (*CR_GEOS_Area_r)(CR_GEOS_Handle, CR_GEOS_Geometry, double*);
typedef int (*CR_GEOS_Length_r)(CR_GEOS_Handle, CR_GEOS_Geometry, double*);
typedef CR_GEOS_Geometry (*CR_GEOS_Centroid_r)(CR_GEOS_Handle, CR_GEOS_Geometry);
Expand Down Expand Up @@ -106,6 +122,15 @@ struct CR_GEOS {
CR_GEOS_Context_setErrorMessageHandler_r GEOSContext_setErrorMessageHandler_r;
CR_GEOS_Free_r GEOSFree_r;

CR_GEOS_BufferParams_create_r GEOSBufferParams_create_r;
CR_GEOS_GEOSBufferParams_destroy_r GEOSBufferParams_destroy_r;
CR_GEOS_BufferParams_setEndCapStyle_r GEOSBufferParams_setEndCapStyle_r;
CR_GEOS_BufferParams_setJoinStyle_r GEOSBufferParams_setJoinStyle_r;
CR_GEOS_BufferParams_setMitreLimit_r GEOSBufferParams_setMitreLimit_r;
CR_GEOS_BufferParams_setQuadrantSegments_r GEOSBufferParams_setQuadrantSegments_r;
CR_GEOS_BufferParams_setSingleSided_r GEOSBufferParams_setSingleSided_r;
CR_GEOS_BufferWithParams_r GEOSBufferWithParams_r;

CR_GEOS_SetSRID_r GEOSSetSRID_r;
CR_GEOS_GetSRID_r GEOSGetSRID_r;
CR_GEOS_GeomDestroy_r GEOSGeom_destroy_r;
Expand Down Expand Up @@ -167,6 +192,14 @@ struct CR_GEOS {
INIT(GEOSFree_r);
INIT(GEOSContext_setErrorMessageHandler_r);
INIT(GEOSGeom_destroy_r);
INIT(GEOSBufferParams_create_r);
INIT(GEOSBufferParams_destroy_r);
INIT(GEOSBufferParams_setEndCapStyle_r);
INIT(GEOSBufferParams_setJoinStyle_r);
INIT(GEOSBufferParams_setMitreLimit_r);
INIT(GEOSBufferParams_setQuadrantSegments_r);
INIT(GEOSBufferParams_setSingleSided_r);
INIT(GEOSBufferWithParams_r);
INIT(GEOSSetSRID_r);
INIT(GEOSGetSRID_r);
INIT(GEOSArea_r);
Expand Down Expand Up @@ -314,6 +347,37 @@ CR_GEOS_Status CR_GEOS_ClipEWKBByRect(CR_GEOS* lib, CR_GEOS_Slice ewkb, double x
return toGEOSString(error.data(), error.length());
}

CR_GEOS_Status CR_GEOS_Buffer(CR_GEOS* lib, CR_GEOS_Slice ewkb, CR_GEOS_BufferParamsInput params,
double distance, CR_GEOS_String* ret) {
std::string error;
auto handle = initHandleWithErrorBuffer(lib, &error);
*ret = {.data = NULL, .len = 0};

auto geom = CR_GEOS_GeometryFromSlice(lib, handle, ewkb);
if (geom != nullptr) {
auto gParams = lib->GEOSBufferParams_create_r(handle);
if (gParams != nullptr) {
if (lib->GEOSBufferParams_setEndCapStyle_r(handle, gParams, params.endCapStyle) &&
lib->GEOSBufferParams_setJoinStyle_r(handle, gParams, params.joinStyle) &&
lib->GEOSBufferParams_setMitreLimit_r(handle, gParams, params.mitreLimit) &&
lib->GEOSBufferParams_setQuadrantSegments_r(handle, gParams, params.quadrantSegments) &&
lib->GEOSBufferParams_setSingleSided_r(handle, gParams, params.singleSided)) {
auto bufferedGeom = lib->GEOSBufferWithParams_r(handle, geom, gParams, distance);
if (bufferedGeom != nullptr) {
auto srid = lib->GEOSGetSRID_r(handle, geom);
CR_GEOS_writeGeomToEWKB(lib, handle, bufferedGeom, ret, srid);
lib->GEOSGeom_destroy_r(handle, bufferedGeom);
}
}
lib->GEOSBufferParams_destroy_r(handle, gParams);
}
lib->GEOSGeom_destroy_r(handle, geom);
}

lib->GEOS_finish_r(handle);
return toGEOSString(error.data(), error.length());
}

//
// Unary operators
//
Expand Down
Loading

0 comments on commit bf1245e

Please sign in to comment.