Skip to content

Commit bcb09c0

Browse files
craig[bot]qburke
andcommitted
Merge #139450
139450: builtins: implement ST_3DLength r=yuzefovich a=qburke This patch allows measuring the length of 3D geometries when there was no builtin function to do this before. Fixes: #60866 Release note (sql change): The `ST_3DLength` function is now available for use. Co-authored-by: Quin Burke <[email protected]>
2 parents df3b0fe + ad1c721 commit bcb09c0

File tree

6 files changed

+150
-0
lines changed

6 files changed

+150
-0
lines changed

docs/generated/sql/functions.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1711,6 +1711,11 @@ the locality flag on node startup. Returns an error if no region is set.</p>
17111711
</span></td><td>Immutable</td></tr>
17121712
<tr><td><a name="postgis_wagyu_version"></a><code>postgis_wagyu_version() &rarr; <a href="string.html">string</a></code></td><td><span class="funcdesc"><p>Compatibility placeholder function with PostGIS. Returns a fixed string based on PostGIS 3.0.1, with minor edits.</p>
17131713
</span></td><td>Immutable</td></tr>
1714+
<tr><td><a name="st_3dlength"></a><code>st_3dlength(geometry: geometry) &rarr; <a href="float.html">float</a></code></td><td><span class="funcdesc"><p>Returns the 3-dimensional or 2-dimensional length of the geometry.</p>
1715+
<p>Note ST_3DLength is only valid for LineString or MultiLineString.
1716+
For 2-D lines it will return the 2-D length (same as ST_Length and ST_Length2D)</p>
1717+
<p>This function utilizes the GEOS module.</p>
1718+
</span></td><td>Immutable</td></tr>
17141719
<tr><td><a name="st_addmeasure"></a><code>st_addmeasure(geometry: geometry, start: <a href="float.html">float</a>, end: <a href="float.html">float</a>) &rarr; geometry</code></td><td><span class="funcdesc"><p>Returns a copy of a LineString or MultiLineString with measure coordinates linearly interpolated between the specified start and end values. Any existing M coordinates will be overwritten.</p>
17151720
</span></td><td>Immutable</td></tr>
17161721
<tr><td><a name="st_addpoint"></a><code>st_addpoint(line_string: geometry, point: geometry) &rarr; geometry</code></td><td><span class="funcdesc"><p>Adds a Point to the end of a LineString.</p>

pkg/geo/geomfn/unary_operators.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
package geomfn
77

88
import (
9+
"math"
10+
911
"github.com/cockroachdb/cockroach/pkg/geo"
1012
"github.com/cockroachdb/cockroach/pkg/geo/geos"
1113
"github.com/cockroachdb/errors"
@@ -242,3 +244,86 @@ func CountVertices(t geom.T) int {
242244
return len(t.FlatCoords()) / t.Stride()
243245
}
244246
}
247+
248+
// length3DLineString returns the length of a
249+
// given 3D LINESTRING. Returns an error if
250+
// lineString does not have a z-coordinate.
251+
func length3DLineString(lineString *geom.LineString) (float64, error) {
252+
lineCoords := lineString.Coords()
253+
prevPoint := lineCoords[0]
254+
lineLength := float64(0)
255+
zIndex := lineString.Layout().ZIndex()
256+
if zIndex < 0 || zIndex >= lineString.Stride() {
257+
return 0, errors.AssertionFailedf("Z-Index for LINESTRING is out-of-bounds")
258+
}
259+
for i := 0; i < lineString.NumCoords(); i++ {
260+
curPoint := lineCoords[i]
261+
deltaX := curPoint.X() - prevPoint.X()
262+
deltaY := curPoint.Y() - prevPoint.Y()
263+
deltaZ := curPoint[zIndex] - prevPoint[zIndex]
264+
distBetweenPoints := math.Sqrt(deltaX*deltaX + deltaY*deltaY + deltaZ*deltaZ)
265+
lineLength += distBetweenPoints
266+
prevPoint = curPoint
267+
}
268+
269+
return lineLength, nil
270+
}
271+
272+
// length3DMultiLineString returns the length of a
273+
// given 3D MULTILINESTRING. Returns an error if
274+
// multiLineString is not 3D.
275+
func length3DMultiLineString(multiLineString *geom.MultiLineString) (float64, error) {
276+
multiLineLength := 0.0
277+
for i := 0; i < multiLineString.NumLineStrings(); i++ {
278+
lineLength, err := length3DLineString(multiLineString.LineString(i))
279+
if err != nil {
280+
return 0, err
281+
}
282+
multiLineLength += lineLength
283+
}
284+
285+
return multiLineLength, nil
286+
}
287+
288+
// Length3D returns the length of a given Geometry.
289+
// Compatible with 3D geometries.
290+
// Note only (MULTI)LINESTRING objects have a length.
291+
func Length3D(g geo.Geometry) (float64, error) {
292+
geomRepr, err := g.AsGeomT()
293+
if err != nil {
294+
return 0, err
295+
}
296+
297+
switch geomRepr.Layout() {
298+
case geom.XYZ, geom.XYZM:
299+
return length3DFromGeomT(geomRepr)
300+
}
301+
// Call default length
302+
return lengthFromGeomT(geomRepr)
303+
}
304+
305+
// length3DFromGeomT returns the length from a geom.T, recursing down
306+
// GeometryCollections if required.
307+
// Compatible with 3D geometries.
308+
func length3DFromGeomT(geomRepr geom.T) (float64, error) {
309+
switch geomRepr := geomRepr.(type) {
310+
case *geom.Point, *geom.MultiPoint, *geom.Polygon, *geom.MultiPolygon:
311+
return 0, nil
312+
case *geom.LineString:
313+
return length3DLineString(geomRepr)
314+
case *geom.MultiLineString:
315+
return length3DMultiLineString(geomRepr)
316+
case *geom.GeometryCollection:
317+
total := float64(0)
318+
for _, subG := range geomRepr.Geoms() {
319+
subLength, err := length3DFromGeomT(subG)
320+
if err != nil {
321+
return 0, err
322+
}
323+
total += subLength
324+
}
325+
return total, nil
326+
default:
327+
return 0, errors.AssertionFailedf("unknown geometry type: %T", geomRepr)
328+
}
329+
}

pkg/geo/geomfn/unary_operators_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,3 +252,34 @@ func TestMinimumClearanceLine(t *testing.T) {
252252
})
253253
}
254254
}
255+
256+
func TestLength3D(t *testing.T) {
257+
testCases := []struct {
258+
wkt string
259+
expected float64
260+
}{
261+
{"POINT(1.0 1.0)", 0},
262+
{"POINT(1.0 1.0 1.0)", 0},
263+
{"POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 0.0))", 0},
264+
{"POLYGON((0.0 0.0 0.0, 1.0 0.0 0.0, 1.0 1.0 0.0, 1.0 1.0 1.0, 0.0 0.0 0.0))", 0},
265+
{"LINESTRING(1.0 1.0, 2.0 2.0, 3.0 3.0)", 2.8284271247461903},
266+
{"MULTILINESTRING((1.0 1.0, 2.0 2.0, 3.0 3.0), (6.0 6.0, 7.0 6.0))", 3.8284271247461903},
267+
{"GEOMETRYCOLLECTION (POINT (40 10),LINESTRING (10 10, 20 20, 10 40),POLYGON ((40 40, 20 45, 45 30, 40 40)))", 36.50281539872885},
268+
{"GEOMETRYCOLLECTION (GEOMETRYCOLLECTION(POINT (40 10),LINESTRING (10 10, 20 20, 10 40),POLYGON ((40 40, 20 45, 45 30, 40 40))))", 36.50281539872885},
269+
{"LINESTRING(743238 2967416 1,743238 2967450 1,743265 2967450 3, 743265.625 2967416 3,743238 2967416 3)", 122.70471674145682},
270+
{"LINESTRING(0 0 0, 1 1 1, 0 0 0)", 3.4641016151377544},
271+
{"MULTILINESTRING((0 0 0, 1 1 1, 0 0 0), (743238 2967416 1,743238 2967450 1,743265 2967450 3, 743265.625 2967416 3,743238 2967416 3))", 126.16881835659457},
272+
{"GEOMETRYCOLLECTION (POINT (0 0 0), LINESTRING(0 0 0, 1 1 1, 0 0 0), POLYGON((0.0 0.0 0.0, 1.0 0.0 0.0, 1.0 1.0 0.0, 1.0 1.0 1.0, 0.0 0.0 0.0)))", 3.4641016151377544},
273+
{"GEOMETRYCOLLECTION (GEOMETRYCOLLECTION(POINT (0 0 0), LINESTRING(0 0 0, 1 1 1, 0 0 0), POLYGON((0.0 0.0 0.0, 1.0 0.0 0.0, 1.0 1.0 0.0, 1.0 1.0 1.0, 0.0 0.0 0.0))))", 3.4641016151377544},
274+
}
275+
276+
for _, tc := range testCases {
277+
t.Run(tc.wkt, func(t *testing.T) {
278+
g, err := geo.ParseGeometry(tc.wkt)
279+
require.NoError(t, err)
280+
ret, err := Length3D(g)
281+
require.NoError(t, err)
282+
require.Equal(t, tc.expected, ret)
283+
})
284+
}
285+
}

pkg/sql/logictest/testdata/logic_test/geospatial_zm

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,11 @@ SELECT st_length('LINESTRING M(0 0 -25, 1 1 -50, 2 2 0)')
383383
----
384384
2.8284271247461903
385385

386+
query R
387+
SELECT ST_3DLength('LINESTRING(743238 2967416 1,743238 2967450 1,743265 2967450 3, 743265.625 2967416 3,743238 2967416 3)')
388+
----
389+
122.70471674145682
390+
386391
query TTT nosort
387392
SELECT
388393
encode(ST_AsTWKB(t, 5), 'hex'),

pkg/sql/sem/builtins/fixed_oids.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2652,6 +2652,7 @@ var builtinOidsArray = []string{
26522652
2689: `jsonb_path_exists(target: jsonb, path: jsonpath) -> bool`,
26532653
2690: `jsonb_path_exists(target: jsonb, path: jsonpath, vars: jsonb) -> bool`,
26542654
2691: `jsonb_path_exists(target: jsonb, path: jsonpath, vars: jsonb, silent: bool) -> bool`,
2655+
2692: `st_3dlength(geometry: geometry) -> float`,
26552656
}
26562657

26572658
var builtinOidsBySignature map[string]oid.Oid

pkg/sql/sem/builtins/geo_builtins.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,25 @@ Note ST_Length is only valid for LineString - use ST_Perimeter for Polygon.`,
258258
volatility.Immutable,
259259
)
260260

261+
var length3DOverloadGeometry1 = geometryOverload1(
262+
func(_ context.Context, _ *eval.Context, g *tree.DGeometry) (tree.Datum, error) {
263+
ret, err := geomfn.Length3D(g.Geometry)
264+
if err != nil {
265+
return nil, err
266+
}
267+
return tree.NewDFloat(tree.DFloat(ret)), nil
268+
},
269+
types.Float,
270+
infoBuilder{
271+
info: `Returns the 3-dimensional or 2-dimensional length of the geometry.
272+
273+
Note ST_3DLength is only valid for LineString or MultiLineString.
274+
For 2-D lines it will return the 2-D length (same as ST_Length and ST_Length2D)`,
275+
libraryUsage: usesGEOS,
276+
},
277+
volatility.Immutable,
278+
)
279+
261280
var perimeterOverloadGeometry1 = geometryOverload1(
262281
func(_ context.Context, _ *eval.Context, g *tree.DGeometry) (tree.Datum, error) {
263282
ret, err := geomfn.Perimeter(g.Geometry)
@@ -3126,6 +3145,10 @@ The requested number of points must be not larger than 65336.`,
31263145
defProps(),
31273146
lengthOverloadGeometry1,
31283147
),
3148+
"st_3dlength": makeBuiltin(
3149+
defProps(),
3150+
length3DOverloadGeometry1,
3151+
),
31293152
"st_perimeter": makeBuiltin(
31303153
defProps(),
31313154
append(

0 commit comments

Comments
 (0)