Commit 67252bc4 authored by craig[bot]'s avatar craig[bot]

Merge #49827 #49833 #49869 #49870 #49871

49827: geo/geomfn: Implements ST_Segmentize for geometry r=otan a=abhishek20123g

Fixes https://github.com/cockroachdb/cockroach/issues/49029

This PR implements ST_Segmentize({geometry, float8}) builtin
function, which allows modify given geometry such that no
segment longer than the given max_segment_length.

Also this PR refactors and add extra test cases for
ST_Segmentize for geography.

Release note (sql change): This PR implements ST_Segmentize({geometry,
float8}) builtin function.

49833: geo/geomfn: implement Intersection, PointOnSurface, Union r=sumeerbhola a=otan

The last of the topology operators up to Chapter 20.

Resolves #48951
Resolves #49832 
Resolves #49064

Release note (sql change): Implements the ST_Intersection,
ST_PointOnSurface and ST_Union builtin functions.

49869: vendor: bump golang/protobuf to 1.4.2 r=knz a=tbg

v1.4.1 aggressively deprecated something (by inserting panics) that was
reachable via gogoproto's marshaler. Luckily, v1.4.2 has this "fixed";
it caused enough trouble for others as well.

Closes #49842.

Release note: None

49870: schemachange: unskip TestDropWhileBackfill r=spaskob a=spaskob

Disabling the GC job was preventing this test from completing.
Tested with `test stress`: 1000 successful runs.

Fixes #44944.

Release note: none.

49871: kvserver: fixup test failure message r=andreimatei a=andreimatei

Expected and real err were reversed.

Release note: None
Co-authored-by: default avatarabhishek20123g <[email protected]>
Co-authored-by: default avatarOliver Tan <[email protected]>
Co-authored-by: default avatarTobias Schottdorf <[email protected]>
Co-authored-by: default avatarSpas Bojanov <[email protected]>
Co-authored-by: default avatarAndrei Matei <[email protected]>
......@@ -864,7 +864,7 @@
revision = "23def4e6c14b4da8ac2ed8007337bc5eb5007998"
[[projects]]
digest = "1:59a826b72bb40d1f541f1438cfdfcea63948327478b82c72fc1349b33ddff7ca"
digest = "1:7852a48216dd9d7b8f09af45e5641a04fbe1947126081b546fe92ddd5e02256a"
name = "github.com/golang/protobuf"
packages = [
"descriptor",
......@@ -879,8 +879,8 @@
"ptypes/wrappers",
]
pruneopts = "UT"
revision = "6c66de79d66478d166c7ea05f5d2ccaf016fbd6b"
version = "v1.4.1"
revision = "d04d7b157bb510b1e0c10132224b616ac0e26b17"
version = "v1.4.2"
[[projects]]
branch = "master"
......
......@@ -133,7 +133,7 @@ ignored = [
# (google.golang.org/protobuf) so there's no pressing reason
# to do anything.
name = "github.com/golang/protobuf"
version = "=v1.4.1"
version = "=v1.4.2"
[[constraint]]
name = "github.com/gogo/protobuf"
......
......@@ -931,6 +931,9 @@ given Geometry.</p>
</span></td></tr>
<tr><td><a name="st_interiorringn"></a><code>st_interiorringn(geometry: geometry, n: <a href="int.html">int</a>) &rarr; geometry</code></td><td><span class="funcdesc"><p>Returns the n-th (1-indexed) interior ring of a Polygon as a LineString. Returns NULL if the shape is not a Polygon, or the ring does not exist.</p>
</span></td></tr>
<tr><td><a name="st_intersection"></a><code>st_intersection(geometry_a: geometry, geometry_b: geometry) &rarr; geometry</code></td><td><span class="funcdesc"><p>Returns the point intersections of the given geometries.</p>
<p>This function utilizes the GEOS module.</p>
</span></td></tr>
<tr><td><a name="st_intersects"></a><code>st_intersects(geography_a: geography, geography_b: geography) &rarr; <a href="bool.html">bool</a></code></td><td><span class="funcdesc"><p>Returns true if geography_a shares any portion of space with geography_b.</p>
<p>The calculations performed are have a precision of 1cm.</p>
<p>This function utilizes the S2 library for spherical calculations.</p>
......@@ -1088,6 +1091,9 @@ given Geometry.</p>
</span></td></tr>
<tr><td><a name="st_pointn"></a><code>st_pointn(geometry: geometry, n: <a href="int.html">int</a>) &rarr; geometry</code></td><td><span class="funcdesc"><p>Returns the n-th Point of a LineString (1-indexed). Returns NULL if out of bounds or not a LineString.</p>
</span></td></tr>
<tr><td><a name="st_pointonsurface"></a><code>st_pointonsurface(geometry: geometry) &rarr; geometry</code></td><td><span class="funcdesc"><p>Returns a point that intersects with the given Geometry.</p>
<p>This function utilizes the GEOS module.</p>
</span></td></tr>
<tr><td><a name="st_polyfromtext"></a><code>st_polyfromtext(str: <a href="string.html">string</a>, srid: <a href="int.html">int</a>) &rarr; geometry</code></td><td><span class="funcdesc"><p>Returns the Geometry from a WKT or EWKT representation with an SRID. If the shape underneath is not Polygon, NULL is returned. If the SRID is present in both the EWKT and the argument, the argument value is used.</p>
</span></td></tr>
<tr><td><a name="st_polyfromtext"></a><code>st_polyfromtext(val: <a href="string.html">string</a>) &rarr; geometry</code></td><td><span class="funcdesc"><p>Returns the Geometry from a WKT or EWKT representation. If the shape underneath is not Polygon, NULL is returned.</p>
......@@ -1114,6 +1120,8 @@ given Geometry.</p>
<p>The calculations are done on a sphere.</p>
<p>This function utilizes the S2 library for spherical calculations.</p>
</span></td></tr>
<tr><td><a name="st_segmentize"></a><code>st_segmentize(geometry: geometry, max_segment_length: <a href="float.html">float</a>) &rarr; geometry</code></td><td><span class="funcdesc"><p>Returns a modified Geometry having no segment longer than the given max_segment_length. Length units are in units of spatial reference.</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>
<tr><td><a name="st_setsrid"></a><code>st_setsrid(geometry: geometry, srid: <a href="int.html">int</a>) &rarr; geometry</code></td><td><span class="funcdesc"><p>Sets a Geometry to a new SRID without transforming the coordinates.</p>
......@@ -1160,6 +1168,9 @@ given Geometry.</p>
<tr><td><a name="st_transform"></a><code>st_transform(geometry: geometry, to_proj_text: <a href="string.html">string</a>) &rarr; geometry</code></td><td><span class="funcdesc"><p>Transforms a geometry into the coordinate reference system referenced by the projection text by projecting its coordinates.</p>
<p>This function utilizes the PROJ library for coordinate projections.</p>
</span></td></tr>
<tr><td><a name="st_union"></a><code>st_union(geometry_a: geometry, geometry_b: geometry) &rarr; geometry</code></td><td><span class="funcdesc"><p>Returns the union of the given geometries as a single Geometry object.</p>
<p>This function utilizes the GEOS module.</p>
</span></td></tr>
<tr><td><a name="st_within"></a><code>st_within(geometry_a: geometry, geometry_b: geometry) &rarr; <a href="bool.html">bool</a></code></td><td><span class="funcdesc"><p>Returns true if geometry_a is completely inside geometry_b.</p>
<p>This function utilizes the GEOS module.</p>
<p>This function will automatically use any available index.</p>
......
......@@ -241,6 +241,12 @@ func (g *Geometry) BoundingBoxIntersects(o *Geometry) bool {
return g.SpatialObject.BoundingBox.Intersects(o.SpatialObject.BoundingBox)
}
// Layout returns the geom layout of the given geometry.
func (g *Geometry) Layout() geom.Layout {
// We are currently always 2D.
return geom.XY
}
//
// Geography
//
......
......@@ -15,6 +15,7 @@ import (
"github.com/cockroachdb/cockroach/pkg/geo"
"github.com/cockroachdb/cockroach/pkg/geo/geographiclib"
"github.com/cockroachdb/cockroach/pkg/geo/geosegmentize"
"github.com/cockroachdb/errors"
"github.com/golang/geo/s2"
"github.com/twpayne/go-geom"
......@@ -40,7 +41,7 @@ func Segmentize(geography *geo.Geography, segmentMaxLength float64) (*geo.Geogra
// Convert segmentMaxLength to Angle with respect to earth sphere as
// further calculation is done considering segmentMaxLength as Angle.
segmentMaxAngle := segmentMaxLength / spheroid.SphereRadius
segGeometry, err := segmentizeGeom(geometry, segmentMaxAngle)
segGeometry, err := geosegmentize.SegmentizeGeom(geometry, segmentMaxAngle, segmentizeCoords)
if err != nil {
return nil, err
}
......@@ -48,123 +49,35 @@ func Segmentize(geography *geo.Geography, segmentMaxLength float64) (*geo.Geogra
}
}
// segmentizeGeom returns a modified geom.T having no segment longer than
// the given maximum segment length.
func segmentizeGeom(geometry geom.T, segmentMaxAngle float64) (geom.T, error) {
if geometry.Empty() {
return geometry, nil
}
switch geometry := geometry.(type) {
case *geom.Point, *geom.MultiPoint:
return geometry, nil
case *geom.LineString:
var allFlatCoordinates []float64
for pointIdx := 1; pointIdx < geometry.NumCoords(); pointIdx++ {
allFlatCoordinates = append(
allFlatCoordinates,
segmentizeCoords(geometry.Coord(pointIdx-1), geometry.Coord(pointIdx), segmentMaxAngle)...,
)
}
// Appending end point as it wasn't included in the iteration of coordinates.
allFlatCoordinates = append(allFlatCoordinates, geometry.Coord(geometry.NumCoords()-1)...)
return geom.NewLineStringFlat(geom.XY, allFlatCoordinates).SetSRID(geometry.SRID()), nil
case *geom.MultiLineString:
segMultiLine := geom.NewMultiLineString(geom.XY).SetSRID(geometry.SRID())
for lineIdx := 0; lineIdx < geometry.NumLineStrings(); lineIdx++ {
l, err := segmentizeGeom(geometry.LineString(lineIdx), segmentMaxAngle)
if err != nil {
return nil, err
}
err = segMultiLine.Push(l.(*geom.LineString))
if err != nil {
return nil, err
}
}
return segMultiLine, nil
case *geom.LinearRing:
var allFlatCoordinates []float64
for pointIdx := 1; pointIdx < geometry.NumCoords(); pointIdx++ {
allFlatCoordinates = append(
allFlatCoordinates,
segmentizeCoords(geometry.Coord(pointIdx-1), geometry.Coord(pointIdx), segmentMaxAngle)...,
)
}
// Appending end point as it wasn't included in the iteration of coordinates.
allFlatCoordinates = append(allFlatCoordinates, geometry.Coord(geometry.NumCoords()-1)...)
return geom.NewLinearRingFlat(geom.XY, allFlatCoordinates).SetSRID(geometry.SRID()), nil
case *geom.Polygon:
segPolygon := geom.NewPolygon(geom.XY).SetSRID(geometry.SRID())
for loopIdx := 0; loopIdx < geometry.NumLinearRings(); loopIdx++ {
l, err := segmentizeGeom(geometry.LinearRing(loopIdx), segmentMaxAngle)
if err != nil {
return nil, err
}
err = segPolygon.Push(l.(*geom.LinearRing))
if err != nil {
return nil, err
}
}
return segPolygon, nil
case *geom.MultiPolygon:
segMultiPolygon := geom.NewMultiPolygon(geom.XY).SetSRID(geometry.SRID())
for polygonIdx := 0; polygonIdx < geometry.NumPolygons(); polygonIdx++ {
p, err := segmentizeGeom(geometry.Polygon(polygonIdx), segmentMaxAngle)
if err != nil {
return nil, err
}
err = segMultiPolygon.Push(p.(*geom.Polygon))
if err != nil {
return nil, err
}
}
return segMultiPolygon, nil
case *geom.GeometryCollection:
segGeomCollection := geom.NewGeometryCollection().SetSRID(geometry.SRID())
for geoIdx := 0; geoIdx < geometry.NumGeoms(); geoIdx++ {
g, err := segmentizeGeom(geometry.Geom(geoIdx), segmentMaxAngle)
if err != nil {
return nil, err
}
err = segGeomCollection.Push(g)
if err != nil {
return nil, err
}
}
return segGeomCollection, nil
}
return nil, errors.Newf("unknown type: %T", geometry)
}
// segmentizeCoords inserts multiple points between given two-coordinate and
// return resultant points as flat []float64. Such that distance between any two
// segmentizeCoords inserts multiple points between given two coordinates and
// return resultant point as flat []float64. Such that distance between any two
// points is less than given maximum segment's length, the total number of
// segments is the power of 2, and all the segments are of the same length.
// NOTE: List of points does not consist of end point.
// Note: List of points does not consist of end point.
func segmentizeCoords(a geom.Coord, b geom.Coord, segmentMaxAngle float64) []float64 {
// Converted geom.Coord into s2.Point so we can segmentize the coordinates.
pointA := s2.PointFromLatLng(s2.LatLngFromDegrees(a.Y(), a.X()))
pointB := s2.PointFromLatLng(s2.LatLngFromDegrees(b.Y(), b.X()))
allSegmentizedCoordinates := a.Clone()
chordAngleBetweenPoints := s2.ChordAngleBetweenPoints(pointA, pointB).Angle().Radians()
if segmentMaxAngle <= chordAngleBetweenPoints {
// This calculation is to determine the total number of segment between given
// 2 coordinates, ensuring that the segments are divided into parts divisible by
// a power of 2.
//
// For that fraction by segment must be less than or equal to
// the fraction of max segment length to distance between point, since the
// total number of segment must be power of 2 therefore we can write as
// 1 / (2^n)[numberOfSegmentToCreate] <= segmentMaxLength / distanceBetweenPoints < 1 / (2^(n-1))
// (2^n)[numberOfSegmentToCreate] >= distanceBetweenPoints / segmentMaxLength > 2^(n-1)
// therefore n = ceil(log2(segmentMaxLength/distanceBetweenPoints)). Hence
// numberOfSegmentToCreate = 2^(ceil(log2(segmentMaxLength/distanceBetweenPoints))).
numberOfSegmentToCreate := int(math.Pow(2, math.Ceil(math.Log2(chordAngleBetweenPoints/segmentMaxAngle))))
for pointInserted := 1; pointInserted < numberOfSegmentToCreate; pointInserted++ {
newPoint := s2.Interpolate(float64(pointInserted)/float64(numberOfSegmentToCreate), pointA, pointB)
latLng := s2.LatLngFromPoint(newPoint)
allSegmentizedCoordinates = append(allSegmentizedCoordinates, latLng.Lng.Degrees(), latLng.Lat.Degrees())
}
// This calculation is to determine the total number of segment between given
// 2 coordinates, ensuring that the segments are divided into parts divisible by
// a power of 2.
//
// For that fraction by segment must be less than or equal to
// the fraction of max segment length to distance between point, since the
// total number of segment must be power of 2 therefore we can write as
// 1 / (2^n)[numberOfSegmentToCreate] <= segmentMaxLength / distanceBetweenPoints < 1 / (2^(n-1))
// (2^n)[numberOfSegmentToCreate] >= distanceBetweenPoints / segmentMaxLength > 2^(n-1)
// therefore n = ceil(log2(segmentMaxLength/distanceBetweenPoints)). Hence
// numberOfSegmentToCreate = 2^(ceil(log2(segmentMaxLength/distanceBetweenPoints))).
numberOfSegmentToCreate := int(math.Pow(2, math.Ceil(math.Log2(chordAngleBetweenPoints/segmentMaxAngle))))
allSegmentizedCoordinates := make([]float64, 0, 2*(1+numberOfSegmentToCreate))
allSegmentizedCoordinates = append(allSegmentizedCoordinates, a.Clone()...)
for pointInserted := 1; pointInserted < numberOfSegmentToCreate; pointInserted++ {
newPoint := s2.Interpolate(float64(pointInserted)/float64(numberOfSegmentToCreate), pointA, pointB)
latLng := s2.LatLngFromPoint(newPoint)
allSegmentizedCoordinates = append(allSegmentizedCoordinates, latLng.Lng.Degrees(), latLng.Lat.Degrees())
}
return allSegmentizedCoordinates
}
......@@ -97,7 +97,7 @@ func TestSegmentize(t *testing.T) {
},
}
for _, test := range segmentizeTestCases {
t.Run(fmt.Sprintf("%s, maximum segment length: %v", test.wkt, test.maxSegmentLength), func(t *testing.T) {
t.Run(fmt.Sprintf("%s, maximum segment length: %f", test.wkt, test.maxSegmentLength), func(t *testing.T) {
geog, err := geo.ParseGeography(test.wkt)
require.NoError(t, err)
modifiedGeog, err := Segmentize(geog, test.maxSegmentLength)
......@@ -107,9 +107,9 @@ func TestSegmentize(t *testing.T) {
require.Equal(t, expectedGeog, modifiedGeog)
})
}
// Test for segment maximum length as negative for geometry collection.
t.Run(fmt.Sprintf("%s, maximum segment length: %v", segmentizeTestCases[9].wkt, 0.0), func(t *testing.T) {
geog, err := geo.ParseGeography(segmentizeTestCases[9].wkt)
// Test for segment maximum length as negative.
t.Run("Error when maximum segment length is less than 0", func(t *testing.T) {
geog, err := geo.ParseGeography("MULTILINESTRING ((0 0, 1 1, 5 5), (5 5, 0 0))")
require.NoError(t, err)
_, err = Segmentize(geog, 0)
require.EqualError(t, err, "maximum segment length must be positive")
......@@ -152,6 +152,20 @@ func TestSegmentizeCoords(t *testing.T) {
segmentMaxLength: -1,
resultantCoordinates: []float64{85, 85},
},
{
desc: `Coordinate(0, 0) to Coordinate(0, 0), 0.29`,
a: geom.Coord{0, 0},
b: geom.Coord{0, 0},
segmentMaxLength: 0.29,
resultantCoordinates: []float64{0, 0},
},
{
desc: `Coordinate(85, 85) to Coordinate(0, 0), 1.563200444168918`,
a: geom.Coord{85, 85},
b: geom.Coord{0, 0},
segmentMaxLength: 1.563200444168918,
resultantCoordinates: []float64{85, 85},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
......
// 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 (
"math"
"github.com/cockroachdb/cockroach/pkg/geo"
"github.com/cockroachdb/cockroach/pkg/geo/geosegmentize"
"github.com/cockroachdb/errors"
"github.com/twpayne/go-geom"
)
// Segmentize return modified Geometry having no segment longer
// that given maximum segment length.
// This works by inserting the extra points in such a manner that
// minimum number of new segments with equal length is created,
// between given two-points such that each segment has length less
// than or equal to given maximum segment length.
func Segmentize(g *geo.Geometry, segmentMaxLength float64) (*geo.Geometry, error) {
geometry, err := g.AsGeomT()
if err != nil {
return nil, err
}
switch geometry := geometry.(type) {
case *geom.Point, *geom.MultiPoint:
return g, nil
default:
if segmentMaxLength <= 0 {
return nil, errors.Newf("maximum segment length must be positive")
}
segGeometry, err := geosegmentize.SegmentizeGeom(geometry, segmentMaxLength, segmentizeCoords)
if err != nil {
return nil, err
}
return geo.NewGeometryFromGeom(segGeometry)
}
}
// segmentizeCoords inserts multiple points between given two coordinates and
// return resultant point as flat []float64. Points are inserted in such a
// way that they create minimum number segments of equal length such that each
// segment has a length less than or equal to given maximum segment length.
// Note: List of points does not consist of end point.
func segmentizeCoords(a geom.Coord, b geom.Coord, maxSegmentLength float64) []float64 {
distanceBetweenPoints := math.Sqrt(math.Pow(a.X()-b.X(), 2) + math.Pow(b.Y()-a.Y(), 2))
// numberOfSegmentToCreate represent the total number of segments
// in which given two coordinates will be divided.
numberOfSegmentToCreate := int(math.Ceil(distanceBetweenPoints / maxSegmentLength))
// segmentFraction represent the fraction of length each segment
// has with respect to total length between two coordinates.
allSegmentizedCoordinates := make([]float64, 0, 2*(1+numberOfSegmentToCreate))
allSegmentizedCoordinates = append(allSegmentizedCoordinates, a.Clone()...)
segmentFraction := 1.0 / float64(numberOfSegmentToCreate)
for pointInserted := 1; pointInserted < numberOfSegmentToCreate; pointInserted++ {
allSegmentizedCoordinates = append(
allSegmentizedCoordinates,
b.X()*float64(pointInserted)*segmentFraction+a.X()*(1-float64(pointInserted)*segmentFraction),
b.Y()*float64(pointInserted)*segmentFraction+a.Y()*(1-float64(pointInserted)*segmentFraction),
)
}
return allSegmentizedCoordinates
}
// 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 (
"fmt"
"testing"
"github.com/cockroachdb/cockroach/pkg/geo"
"github.com/stretchr/testify/require"
"github.com/twpayne/go-geom"
)
func TestSegmentize(t *testing.T) {
segmentizeTestCases := []struct {
wkt string
maxSegmentLength float64
expectedWKT string
}{
{
wkt: "POINT (1.0 1.0)",
maxSegmentLength: 1,
expectedWKT: "POINT (1.0 1.0)",
},
{
wkt: "LINESTRING (1.0 1.0, 2.0 2.0, 3.0 3.0)",
maxSegmentLength: 1,
expectedWKT: "LINESTRING (1.0 1.0, 1.5 1.5, 2.0 2.0, 2.5 2.5, 3.0 3.0)",
},
{
wkt: "LINESTRING (1.0 1.0, 2.0 2.0, 3.0 3.0)",
maxSegmentLength: 0.33333,
expectedWKT: "LINESTRING (1.0 1.0, 1.2000000000000002 1.2000000000000002, 1.4 1.4, 1.6 1.6, 1.8 1.8, 2.0 2.0, 2.2 2.2, 2.4000000000000004 2.4000000000000004, 2.5999999999999996 2.5999999999999996, 2.8000000000000003 2.8000000000000003, 3 3)",
},
{
wkt: "LINESTRING EMPTY",
maxSegmentLength: 1,
expectedWKT: "LINESTRING EMPTY",
},
{
wkt: "LINESTRING (1.0 1.0, 2.0 2.0, 3.0 3.0)",
maxSegmentLength: 2,
expectedWKT: "LINESTRING (1.0 1.0, 2.0 2.0, 3.0 3.0)",
},
{
wkt: "LINESTRING (0.0 0.0, 0.0 10.0, 0.0 16.0)",
maxSegmentLength: 3,
expectedWKT: "LINESTRING (0.0 0.0,0.0 2.5,0.0 5.0,0.0 7.5,0.0 10.0,0.0 13.0,0.0 16.0)",
},
{
wkt: "POLYGON ((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 0.0))",
maxSegmentLength: 0.8,
expectedWKT: "POLYGON ((0.0 0.0, 0.5 0.0, 1.0 0.0, 1.0 0.5, 1.0 1.0, 0.5 0.5, 0.0 0.0))",
},
{
wkt: "POLYGON ((0.0 0.0, 1.0 0.0, 3.0 3.0, 0.0 0.0), (0.1 0.1, 0.2 0.1, 0.2 0.2, 0.1 0.1))",
maxSegmentLength: 1,
expectedWKT: "POLYGON ((0.0 0.0, 1.0 0.0, 1.5 0.75, 2.0 1.5, 2.5 2.25, 3.0 3.0, 2.4000000000000004 2.4000000000000004, 1.7999999999999998 1.7999999999999998, 1.1999999999999997 1.1999999999999997, 0.5999999999999999 0.5999999999999999, 0.0 0.0), (0.1 0.1, 0.2 0.1, 0.2 0.2, 0.1 0.1))",
},
{
wkt: "POLYGON ((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 0.0), (0.1 0.1, 0.2 0.1, 0.2 0.2, 0.1 0.1))",
maxSegmentLength: 5,
expectedWKT: "POLYGON ((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 0.0), (0.1 0.1, 0.2 0.1, 0.2 0.2, 0.1 0.1))",
},
{
wkt: "POLYGON EMPTY",
maxSegmentLength: 1,
expectedWKT: "POLYGON EMPTY",
},
{
wkt: "MULTIPOINT ((1.0 1.0), (2.0 2.0))",
maxSegmentLength: 1,
expectedWKT: "MULTIPOINT ((1.0 1.0), (2.0 2.0))",
},
{
wkt: "MULTILINESTRING ((1.0 1.0, 2.0 2.0, 3.0 3.0), (6.0 6.0, 7.0 6.0))",
maxSegmentLength: 1,
expectedWKT: "MULTILINESTRING ((1.0 1.0, 1.5 1.5, 2.0 2.0, 2.5 2.5, 3.0 3.0), (6.0 6.0, 7.0 6.0))",
},
{
wkt: "MULTILINESTRING (EMPTY, (1.0 1.0, 2.0 2.0, 3.0 3.0), (6.0 6.0, 7.0 6.0))",
maxSegmentLength: 1,
expectedWKT: "MULTILINESTRING (EMPTY, (1.0 1.0, 1.5 1.5, 2.0 2.0, 2.5 2.5, 3.0 3.0), (6.0 6.0, 7.0 6.0))",
},
{
wkt: "MULTIPOLYGON (((3.0 3.0, 4.0 3.0, 4.0 4.0, 3.0 3.0)), ((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 0.0), (0.1 0.1, 0.2 0.1, 0.2 0.2, 0.1 0.1)))",
maxSegmentLength: 1,
expectedWKT: "MULTIPOLYGON (((3.0 3.0, 4.0 3.0, 4.0 4.0, 3.5 3.5, 3.0 3.0)), ((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.5 0.5, 0.0 0.0), (0.1 0.1, 0.2 0.1, 0.2 0.2, 0.1 0.1)))",
},
{
wkt: "GEOMETRYCOLLECTION (POINT (40.0 10.0), LINESTRING (10.0 10.0, 20.0 20.0, 10.0 40.0), POLYGON ((40.0 40.0, 20.0 45.0, 45.0 30.0, 40.0 40.0)))",
maxSegmentLength: 10,
expectedWKT: "GEOMETRYCOLLECTION (POINT (40.0 10.0), LINESTRING (10.0 10.0, 15.0 15.0, 20.0 20.0, 16.666666666666668 26.666666666666668, 13.333333333333334 33.33333333333333, 10.0 40.0), POLYGON ((40.0 40.0, 33.333333333333336 41.66666666666667, 26.666666666666668 43.333333333333336, 20.0 45.0, 28.333333333333336 40.0, 36.66666666666667 35.0, 45.0 30.0, 42.5 35.0, 40.0 40.0)))",
},
{
wkt: "MULTIPOINT ((0.0 0.0), (1.0 1.0))",
maxSegmentLength: -1,
expectedWKT: "MULTIPOINT ((0.0 0.0), (1.0 1.0))",
},
}
for _, test := range segmentizeTestCases {
t.Run(fmt.Sprintf("%s, maximum segment length: %f", test.wkt, test.maxSegmentLength), func(t *testing.T) {
geom, err := geo.ParseGeometry(test.wkt)
require.NoError(t, err)
modifiedGeom, err := Segmentize(geom, test.maxSegmentLength)
require.NoError(t, err)
expectedGeom, err := geo.ParseGeometry(test.expectedWKT)
require.NoError(t, err)
require.Equal(t, expectedGeom, modifiedGeom)
})
}
// Test for segment maximum length as negative.
t.Run("Error when maximum segment length is less than 0", func(t *testing.T) {
geom, err := geo.ParseGeometry("MULTILINESTRING ((0 0, 1 1, 5 5), (5 5, 0 0))")
require.NoError(t, err)
_, err = Segmentize(geom, 0)
require.EqualError(t, err, "maximum segment length must be positive")
})
}
func TestSegmentizeCoords(t *testing.T) {
testCases := []struct {
desc string
a geom.Coord
b geom.Coord
segmentMaxLength float64
resultantCoordinates []float64
}{
{
desc: `Coordinate(0, 0) to Coordinate(1, 1), 1`,
a: geom.Coord{0, 0},
b: geom.Coord{1, 1},
segmentMaxLength: 1,
resultantCoordinates: []float64{0, 0, 0.5, 0.5},
},
{
desc: `Coordinate(0, 0) to Coordinate(1, 1), 0.3`,
a: geom.Coord{0, 0},
b: geom.Coord{1, 1},
segmentMaxLength: 0.3,
resultantCoordinates: []float64{0, 0, 0.2, 0.2, 0.4, 0.4, 0.6000000000000001, 0.6000000000000001, 0.8, 0.8},
},
{
desc: `Coordinate(0, 0) to Coordinate(1, 0), 0.49999999999999`,
a: geom.Coord{0, 0},
b: geom.Coord{1, 0},
segmentMaxLength: 0.49999999999999,
resultantCoordinates: []float64{0, 0, 0.3333333333333333, 0, 0.6666666666666666, 0},
},
{
desc: `Coordinate(1, 1) to Coordinate(0, 0), -1`,
a: geom.Coord{1, 1},
b: geom.Coord{0, 0},
segmentMaxLength: -1,
resultantCoordinates: []float64{1, 1},
},
{
desc: `Coordinate(1, 1) to Coordinate(0, 0), 2`,
a: geom.Coord{1, 1},
b: geom.Coord{0, 0},
segmentMaxLength: 2,
resultantCoordinates: []float64{1, 1},
},
{
desc: `Coordinate(0, 0) to Coordinate(0, 0), 1`,
a: geom.Coord{0, 0},
b: geom.Coord{0, 0},
segmentMaxLength: 1,
resultantCoordinates: []float64{0, 0},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
convertedPoints := segmentizeCoords(test.a, test.b, test.segmentMaxLength)
require.Equal(t, test.resultantCoordinates, convertedPoints)
})
}
}
// 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/cockroach/pkg/geo/geos"
"github.com/twpayne/go-geom"
)
// Centroid returns the Centroid of a given Geometry.
func Centroid(g *geo.Geometry) (*geo.Geometry, error) {
if g.Empty() {
return geo.NewGeometryFromGeom(geom.NewPointEmpty(g.Layout()))
}
centroidEWKB, err := geos.Centroid(g.EWKB())
if err != nil {
return nil, err
}
return geo.ParseGeometryFromEWKB(centroidEWKB)
}
// PointOnSurface returns the PointOnSurface of a given Geometry.
func PointOnSurface(g *geo.Geometry) (*geo.Geometry, error) {
if g.Empty() {
return geo.NewGeometryFromGeom(geom.NewPointEmpty(g.Layout()))
}
pointOnSurfaceEWKB, err := geos.PointOnSurface(g.EWKB())
if err != nil {
return nil, err
}
return geo.ParseGeometryFromEWKB(pointOnSurfaceEWKB)
}
// Intersection returns the geometries of intersection between A and B.
func Intersection(a *geo.Geometry, b *geo.Geometry) (*geo.Geometry, error) {
if a.SRID() != b.SRID() {
return nil, geo.NewMismatchingSRIDsError(a, b)
}
retEWKB, err := geos.Intersection(a.EWKB(), b.EWKB())
if err != nil {
return nil, err
}
return geo.ParseGeometryFromEWKB(retEWKB)
}
// Union returns the geometries of intersection between A and B.
func Union(a *geo.Geometry, b *geo.Geometry) (*geo.Geometry, error) {
if a.SRID() != b.SRID() {
return nil, geo.NewMismatchingSRIDsError(a, b)
}
retEWKB, err := geos.Union(a.EWKB(), b.EWKB())
if err != nil {
return nil, err
}
return geo.ParseGeometryFromEWKB(retEWKB)
}
// 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 (
"fmt"
"testing"
"github.com/cockroachdb/cockroach/pkg/geo"
"github.com/stretchr/testify/require"
"github.com/twpayne/go-geom"
)
func TestCentroid(t *testing.T) {
testCases := []struct {
wkt string
expected string
}{
{"POINT(1.0 1.0)", "POINT (1.0 1.0)"},
{"SRID=4326;POINT(1.0 1.0)", "SRID=4326;POINT (1.0 1.0)"},
{"LINESTRING(1.0 1.0, 2.0 2.0, 3.0 3.0)", "POINT (2.0 2.0)"},
{"POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 0.0))", "POINT (0.666666666666667 0.333333333333333)"},
{"POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 0.0), (0.1 0.1, 0.2 0.1, 0.2 0.2, 0.1 0.1))", "POINT (0.671717171717172 0.335353535353535)"},
{"MULTIPOINT((1.0 1.0), (2.0 2.0))", "POINT (1.5 1.5)"},
{"MULTILINESTRING((1.0 1.0, 2.0 2.0, 3.0 3.0), (6.0 6.0, 7.0 6.0))", "POINT (3.17541743733684 3.04481549985497)"},
{"MULTIPOLYGON(((3.0 3.0, 4.0 3.0, 4.0 4.0, 3.0 3.0)), ((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 0.0), (0.1 0.1, 0.2 0.1, 0.2 0.2, 0.1 0.1)))", "POINT (2.17671691792295 1.84187604690117)"},
{"GEOMETRYCOLLECTION (POINT (40 10),LINESTRING (10 10, 20 20, 10 40),POLYGON ((40 40, 20 45, 45 30, 40 40)))", "POINT (35 38.3333333333333)"},
}
for _, tc := range testCases {
t.Run(tc.wkt, func(t *testing.T) {
g, err := geo.ParseGeometry(tc.wkt)
require.NoError(t, err)
ret, err := Centroid(g)
require.NoError(t, err)
retAsGeomT, err := ret.AsGeomT()
require.NoError(t, err)
expected, err := geo.ParseGeometry(tc.expected)
require.NoError(t, err)
expectedAsGeomT, err := expected.AsGeomT()
require.NoError(t, err)
// Ensure points are close in terms of precision.
require.InEpsilon(t, expectedAsGeomT.(*geom.Point).X(), retAsGeomT.(*geom.Point).X(), 2e-10)
require.InEpsilon(t, expectedAsGeomT.(*geom.Point).Y(), retAsGeomT.(*geom.Point).Y(), 2e-10)
require.Equal(t, expected.SRID(), ret.SRID())
})
}
}
func TestPointOnSurface(t *testing.T) {
testCases := []struct {
wkt string
expected string
}{
{"POINT(1.0 1.0)", "POINT (1.0 1.0)"},