diff --git a/docs/generated/sql/functions.md b/docs/generated/sql/functions.md index d8cbba82027b..98b16210a3c4 100644 --- a/docs/generated/sql/functions.md +++ b/docs/generated/sql/functions.md @@ -1276,6 +1276,14 @@ has no relationship with the commit order of concurrent transactions.

st_asbinary(geometry: geometry, xdr_or_ndr: string) → bytes

Returns the WKB representation of a given Geometry. This variant has a second argument denoting the encoding - xdr for big endian and ndr for little endian.

+st_asencodedpolyline(geometry: geometry) → string

Returns the geometry as an Encoded Polyline. +This format is used by Google Maps with precision=5 and by Open Source Routing Machine with precision=5 and 6. +Preserves 5 decimal places.

+
+st_asencodedpolyline(geometry: geometry, precision: int4) → string

Returns the geometry as an Encoded Polyline. +This format is used by Google Maps with precision=5 and by Open Source Routing Machine with precision=5 and 6. +Precision specifies how many decimal places will be preserved in Encoded Polyline. Value should be the same on encoding and decoding, or coordinates will be incorrect.

+
st_asewkb(geography: geography) → bytes

Returns the EWKB representation of a given Geography.

st_asewkb(geometry: geometry) → bytes

Returns the EWKB representation of a given Geometry.

diff --git a/pkg/geo/parse.go b/pkg/geo/parse.go index d4a67a93d755..7493ad1aadba 100644 --- a/pkg/geo/parse.go +++ b/pkg/geo/parse.go @@ -238,6 +238,19 @@ func parseGeoHash(g string, precision int) (geohash.Box, error) { return box, nil } +// GeometryToEncodedPolyline turns the provided geometry and precision into a Polyline ASCII +func GeometryToEncodedPolyline(g Geometry, p int) (string, error) { + gt, err := g.AsGeomT() + if err != nil { + return "", fmt.Errorf("error parsing input geometry: %v", err) + } + if gt.SRID() != 4326 { + return "", errors.New("only SRID 4326 is supported") + } + + return encodePolylinePoints(gt.FlatCoords(), p), nil +} + // ParseEncodedPolyline takes the encoded polyline ASCII and precision, decodes the points and returns them as a geometry func ParseEncodedPolyline(encodedPolyline string, precision int) (Geometry, error) { flatCoords := decodePolylinePoints(encodedPolyline, precision) diff --git a/pkg/geo/parse_test.go b/pkg/geo/parse_test.go index 9e3a5edbe600..fb840a947ab8 100644 --- a/pkg/geo/parse_test.go +++ b/pkg/geo/parse_test.go @@ -689,6 +689,68 @@ func TestParseHash(t *testing.T) { } } +func TestGeometryToEncodedPolyline(t *testing.T) { + for _, tc := range []struct { + desc string + geomString string + p int + expectedOutput string + }{ + { + "two points, precision 1", + "SRID=4326;LINESTRING(6.0 7.0, 4.1 2)", + 1, + "kCwBbBd@", + }, + { + "two points, negative value included, precision 8", + "SRID=4326;LINESTRING(9.00000001 -1.00009999, 0.00000007 0.00000091)", + 8, + "|_cw}Daosrst@secw}Drnsrst@", + }, + { + "four points, negative value included, precision 4", + "SRID=4326;LINESTRING(-8.65 77.23, 180.0 180.0, 41.35 95.6, 856.2344 843.9999)", + 4, + "wkcn@f}gDgfv}@gqcrB~lor@f_ssA}dxgMwujpN", + }, + { + "two points, decimal places to be rounded, precision 5", + "SRID=4326;LINESTRING(19.38949 52.09179, 24.74476 59.36716)", + 5, + "ud}|Hi_juBa~kk@m}t_@", + }, + { + "two points, decimal places rounded to integers, precision 0", + "SRID=4326;LINESTRING(1.555 2.77, 3.555 4.055)", + 0, + "ECAC", + }, + { + "three points, negative value included, precision 5", + "SRID=4326;LINESTRING(-120.2 38.5,-120.95 40.7,-126.453 43.252)", + 5, + "_p~iF~ps|U_ulLnnqC_mqNvxq`@", + }, + } { + t.Run(tc.desc, func(t *testing.T) { + g, err := ParseGeometry(tc.geomString) + require.NoError(t, err) + encodedPolyline, err := GeometryToEncodedPolyline(g, tc.p) + require.NoError(t, err) + require.Equal(t, tc.expectedOutput, encodedPolyline) + }) + } + + t.Run("SRID different than 4326 or missing", func(t *testing.T) { + g, err := ParseGeometry("LINESTRING(6.0 7.0, 4.1 2)") + require.NoError(t, err) + _, err = GeometryToEncodedPolyline(g, 5) + require.Error(t, err) + require.Contains(t, err.Error(), "SRID 4326 is supported") + }) +} + func TestParseEncodedPolyline(t *testing.T) { testCases := []struct { desc string diff --git a/pkg/geo/polyline.go b/pkg/geo/polyline.go index 6e0e4cb56a51..c5bd24377716 100644 --- a/pkg/geo/polyline.go +++ b/pkg/geo/polyline.go @@ -10,7 +10,10 @@ package geo -import "math" +import ( + "math" + "strings" +) // decodePolylinePoints decodes encoded Polyline according to the polyline algorithm: https://developers.google.com/maps/documentation/utilities/polylinealgorithm func decodePolylinePoints(encoded string, precision int) []float64 { @@ -54,3 +57,38 @@ func decodePointValue(idx int, bytes []byte) (int, float64) { } return idx, pointValue } + +// encodePolylinePoints encodes provided points using the algorithm: https://developers.google.com/maps/documentation/utilities/polylinealgorithm +// Assumes there are no malformed points - length of the input slice should be even. +func encodePolylinePoints(points []float64, precision int) string { + lastLat := 0 + lastLng := 0 + var res strings.Builder + for i := 1; i < len(points); i += 2 { + lat := int(math.Round(points[i-1] * math.Pow10(precision))) + lng := int(math.Round(points[i] * math.Pow10(precision))) + res = encodePointValue(lng-lastLng, res) + res = encodePointValue(lat-lastLat, res) + lastLat = lat + lastLng = lng + } + + return res.String() +} + +func encodePointValue(diff int, b strings.Builder) strings.Builder { + var shifted int + shifted = diff << 1 + if diff < 0 { + shifted = ^shifted + } + rem := shifted + for rem >= 0x20 { + b.WriteRune(rune(0x20 | (rem & 0x1f) + 63)) + + rem = rem >> 5 + } + + b.WriteRune(rune(rem + 63)) + return b +} diff --git a/pkg/sql/logictest/testdata/logic_test/geospatial b/pkg/sql/logictest/testdata/logic_test/geospatial index fc154a42cd61..f65011da0a27 100644 --- a/pkg/sql/logictest/testdata/logic_test/geospatial +++ b/pkg/sql/logictest/testdata/logic_test/geospatial @@ -5414,7 +5414,6 @@ SRID=4326;LINESTRING (15 15, 30 30) SRID=4326;MULTIPOLYGON (((10.6196552761 SRID=4004;LINESTRING (15 15, 30 30) SRID=4004;MULTIPOLYGON (((10.619655276155134 10.441798171725758, 22.619864948040426 9.819300638757898, 22.619864948040426 21.037511025421814, 10.619655276155134 22.270575488008195, 10.619655276155134 10.441798171725758)), ((19.568710311035989 21.434982311490455, 22.619864948040426 21.037511025421814, 22.619864948040423 23.917746630193268, 19.568710311035993 24.357076692620154, 19.568710311035989 21.434982311490455)), ((22.619864948040426 21.037511025421814, 34.5085229876684 18.949950004453903, 34.5085229876684 29.532803705822648, 22.619864948040426 32.39984017391933, 22.619864948040426 21.037511025421814))) SRID=4004;MULTIPOLYGON (((0 0, 45 0, 45 35.264389682754654, 0 45, 0 0))) SRID=4004;LINESTRING (-180 -90, 180 90) SRID=4004;MULTIPOLYGON (((45 35.264389682754654, 135 35.264389682754654, -135 35.264389682754654, -45 35.264389682754654, 45 35.264389682754654)), ((135 35.264389682754654, 135 -35.264389682754654, -135 -35.264389682754654, -135 35.264389682754654, 135 35.264389682754654)), ((-135 -35.264389682754654, 135 -35.264389682754654, 45 -35.264389682754654, -45 -35.264389682754654, -135 -35.264389682754654))) SRID=4004;MULTIPOLYGON (((45 35.264389682754654, 135 35.264389682754654, -135 35.264389682754654, -45 35.264389682754654, 45 35.264389682754654)), ((135 35.264389682754654, 135 -35.264389682754654, -135 -35.264389682754654, -135 35.264389682754654, 135 35.264389682754654)), ((-135 -35.264389682754654, 135 -35.264389682754654, 45 -35.264389682754654, -45 -35.264389682754654, -135 -35.264389682754654))) - subtest st_memsize query I @@ -5480,3 +5479,14 @@ SRID=4326;LINESTRING (9.00000001 -1.00009999, 0 0.000011) statement error parsing geography error SELECT ST_AsEWKT(ST_LineFromEncodedPolyline('NO'), 5) + +query TT +SELECT + ST_AsEncodedPolyline(GeomFromEWKT('SRID=4326;LINESTRING(-120.2 38.5,-120.95 40.7,-126.453 43.252)')), + ST_AsEncodedPolyline(GeomFromEWKT('SRID=4326;LINESTRING(9.00000001 -1.00009999, 0.00000007 0.00000091)'), 8); +---- +_p~iF~ps|U_ulLnnqC_mqNvxq`@ +|_cw}Daosrst@secw}Drnsrst@ + +statement error only SRID 4326 is supported +SELECT ST_AsEncodedPolyline(GeomFromEWKT('SRID=4004;LINESTRING(9.00000001 -1.00009999, 0.00000007 0.00000091)'), 8); diff --git a/pkg/sql/sem/builtins/geo_builtins.go b/pkg/sql/sem/builtins/geo_builtins.go index 7940071802fe..a4b0e9176318 100644 --- a/pkg/sql/sem/builtins/geo_builtins.go +++ b/pkg/sql/sem/builtins/geo_builtins.go @@ -4835,6 +4835,50 @@ The swap_ordinate_string parameter is a 2-character string naming the ordinates }, ), + "st_asencodedpolyline": makeBuiltin(defProps(), + tree.Overload{ + Types: tree.ArgTypes{ + {"geometry", types.Geometry}, + }, + ReturnType: tree.FixedReturnType(types.String), + Fn: func(_ *tree.EvalContext, args tree.Datums) (tree.Datum, error) { + g := tree.MustBeDGeometry(args[0]).Geometry + s, err := geo.GeometryToEncodedPolyline(g, 5) + if err != nil { + return nil, err + } + return tree.NewDString(s), nil + }, + Info: infoBuilder{ + info: `Returns the geometry as an Encoded Polyline. +This format is used by Google Maps with precision=5 and by Open Source Routing Machine with precision=5 and 6. +Preserves 5 decimal places.`, + }.String(), + Volatility: tree.VolatilityImmutable, + }, + tree.Overload{ + Types: tree.ArgTypes{ + {"geometry", types.Geometry}, + {"precision", types.Int4}, + }, + ReturnType: tree.FixedReturnType(types.String), + Fn: func(_ *tree.EvalContext, args tree.Datums) (tree.Datum, error) { + g := tree.MustBeDGeometry(args[0]).Geometry + p := int(tree.MustBeDInt(args[1])) + s, err := geo.GeometryToEncodedPolyline(g, p) + if err != nil { + return nil, err + } + return tree.NewDString(s), nil + }, + Info: infoBuilder{ + info: `Returns the geometry as an Encoded Polyline. +This format is used by Google Maps with precision=5 and by Open Source Routing Machine with precision=5 and 6. +Precision specifies how many decimal places will be preserved in Encoded Polyline. Value should be the same on encoding and decoding, or coordinates will be incorrect.`, + }.String(), + Volatility: tree.VolatilityImmutable, + }, + ), "st_linefromencodedpolyline": makeBuiltin(defProps(), tree.Overload{ Types: tree.ArgTypes{ @@ -5405,7 +5449,6 @@ See http://developers.google.com/maps/documentation/utilities/polylinealgorithm` // Unimplemented. // - "st_asencodedpolyline": makeBuiltin(tree.FunctionProperties{UnsupportedWithIssue: 48872}), "st_asgml": makeBuiltin(tree.FunctionProperties{UnsupportedWithIssue: 48877}), "st_aslatlontext": makeBuiltin(tree.FunctionProperties{UnsupportedWithIssue: 48882}), "st_assvg": makeBuiltin(tree.FunctionProperties{UnsupportedWithIssue: 48883}),