Files
2023-12-19 04:57:40 -08:00

249 lines
7.6 KiB
Go

// Copyright 2023 Dolthub, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// 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 geo
import (
"fmt"
"strconv"
"strings"
"github.com/cockroachdb/errors"
"github.com/pierrre/geohash"
"github.com/twpayne/go-geom"
"github.com/twpayne/go-geom/encoding/ewkb"
"github.com/twpayne/go-geom/encoding/ewkbhex"
"github.com/twpayne/go-geom/encoding/geojson"
"github.com/dolthub/doltgresql/postgres/parser/geo/geopb"
"github.com/dolthub/doltgresql/postgres/parser/geo/geos"
)
// parseEWKBRaw creates a geopb.SpatialObject from an EWKB
// without doing any SRID based checks.
// You most likely want parseEWKB instead.
func parseEWKBRaw(soType geopb.SpatialObjectType, in geopb.EWKB) (geopb.SpatialObject, error) {
t, err := ewkb.Unmarshal(in)
if err != nil {
return geopb.SpatialObject{}, err
}
return spatialObjectFromGeomT(t, soType)
}
// parseAmbiguousText parses a text as a number of different options
// that is available in the geospatial world using the first character as
// a heuristic.
// This matches the PostGIS direct cast from a string to GEOGRAPHY/GEOMETRY.
func parseAmbiguousText(
soType geopb.SpatialObjectType, str string, defaultSRID geopb.SRID,
) (geopb.SpatialObject, error) {
if len(str) == 0 {
return geopb.SpatialObject{}, fmt.Errorf("geo: parsing empty string to geo type")
}
switch str[0] {
case '0':
return parseEWKBHex(soType, str, defaultSRID)
case 0x00, 0x01:
return parseEWKB(soType, []byte(str), defaultSRID, DefaultSRIDIsHint)
case '{':
return parseGeoJSON(soType, []byte(str), defaultSRID)
}
return parseEWKT(soType, geopb.EWKT(str), defaultSRID, DefaultSRIDIsHint)
}
// parseEWKBHex takes a given str assumed to be in EWKB hex and transforms it
// into a SpatialObject.
func parseEWKBHex(
soType geopb.SpatialObjectType, str string, defaultSRID geopb.SRID,
) (geopb.SpatialObject, error) {
t, err := ewkbhex.Decode(str)
if err != nil {
return geopb.SpatialObject{}, err
}
if (defaultSRID != 0 && t.SRID() == 0) || int32(t.SRID()) < 0 {
AdjustGeomTSRID(t, defaultSRID)
}
return spatialObjectFromGeomT(t, soType)
}
// parseEWKB takes given bytes assumed to be EWKB and transforms it into a SpatialObject.
// The defaultSRID will overwrite any SRID set in the EWKB if overwrite is true.
func parseEWKB(
soType geopb.SpatialObjectType,
b []byte,
defaultSRID geopb.SRID,
overwrite defaultSRIDOverwriteSetting,
) (geopb.SpatialObject, error) {
t, err := ewkb.Unmarshal(b)
if err != nil {
return geopb.SpatialObject{}, err
}
if overwrite == DefaultSRIDShouldOverwrite || (defaultSRID != 0 && t.SRID() == 0) || int32(t.SRID()) < 0 {
AdjustGeomTSRID(t, defaultSRID)
}
return spatialObjectFromGeomT(t, soType)
}
// parseGeoJSON takes given bytes assumed to be GeoJSON and transforms it into a SpatialObject.
func parseGeoJSON(
soType geopb.SpatialObjectType, b []byte, defaultSRID geopb.SRID,
) (geopb.SpatialObject, error) {
var t geom.T
if err := geojson.Unmarshal(b, &t); err != nil {
return geopb.SpatialObject{}, err
}
if defaultSRID != 0 && t.SRID() == 0 {
AdjustGeomTSRID(t, defaultSRID)
}
return spatialObjectFromGeomT(t, soType)
}
const sridPrefix = "SRID="
const sridPrefixLen = len(sridPrefix)
type defaultSRIDOverwriteSetting bool
const (
// DefaultSRIDShouldOverwrite implies the parsing function should overwrite
// the SRID with the defaultSRID.
DefaultSRIDShouldOverwrite defaultSRIDOverwriteSetting = true
// DefaultSRIDIsHint implies that the default SRID is only a hint
// and if the SRID is provided by the given EWKT/EWKB, it should be
// used instead.
DefaultSRIDIsHint defaultSRIDOverwriteSetting = false
)
// parseEWKT decodes a WKT string and transforms it into a SpatialObject.
// The defaultSRID will overwrite any SRID set in the EWKT if overwrite is true.
func parseEWKT(
soType geopb.SpatialObjectType,
str geopb.EWKT,
defaultSRID geopb.SRID,
overwrite defaultSRIDOverwriteSetting,
) (geopb.SpatialObject, error) {
srid := defaultSRID
if hasPrefixIgnoreCase(string(str), sridPrefix) {
end := strings.Index(string(str[sridPrefixLen:]), ";")
if end != -1 {
if overwrite != DefaultSRIDShouldOverwrite {
sridInt64, err := strconv.ParseInt(string(str[sridPrefixLen:sridPrefixLen+end]), 10, 32)
if err != nil {
return geopb.SpatialObject{}, err
}
// Only use the parsed SRID if the parsed SRID is > 0 and it was not
// to be overwritten by the DefaultSRID parameter.
if sridInt64 > 0 {
srid = geopb.SRID(sridInt64)
}
}
str = str[sridPrefixLen+end+1:]
} else {
return geopb.SpatialObject{}, fmt.Errorf(
"geo: failed to find ; character with SRID declaration during EWKT decode: %q",
str,
)
}
}
ewkb, err := geos.WKTToEWKB(geopb.WKT(str), srid)
if err != nil {
return geopb.SpatialObject{}, err
}
return parseEWKBRaw(soType, ewkb)
}
// hasPrefixIgnoreCase returns whether a given str begins with a prefix, ignoring case.
// It assumes that the string and prefix contains only ASCII bytes.
func hasPrefixIgnoreCase(str string, prefix string) bool {
if len(str) < len(prefix) {
return false
}
for i := 0; i < len(prefix); i++ {
if ToLowerSingleByte(str[i]) != ToLowerSingleByte(prefix[i]) {
return false
}
}
return true
}
// ParseGeometryPointFromGeoHash converts a GeoHash to a Geometry Point
// using a Lng/Lat Point representation of the GeoHash.
func ParseGeometryPointFromGeoHash(g string, precision int) (Geometry, error) {
box, err := parseGeoHash(g, precision)
if err != nil {
return Geometry{}, err
}
point := box.Center()
geom, gErr := MakeGeometryFromPointCoords(point.Lon, point.Lat)
if gErr != nil {
return Geometry{}, gErr
}
return geom, nil
}
// ParseCartesianBoundingBoxFromGeoHash converts a GeoHash to a CartesianBoundingBox.
func ParseCartesianBoundingBoxFromGeoHash(g string, precision int) (CartesianBoundingBox, error) {
box, err := parseGeoHash(g, precision)
if err != nil {
return CartesianBoundingBox{}, err
}
return CartesianBoundingBox{
BoundingBox: geopb.BoundingBox{
LoX: box.Lon.Min,
HiX: box.Lon.Max,
LoY: box.Lat.Min,
HiY: box.Lat.Max,
},
}, nil
}
func parseGeoHash(g string, precision int) (geohash.Box, error) {
if len(g) == 0 {
return geohash.Box{}, errors.Newf("length of GeoHash must be greater than 0")
}
// If precision is more than the length of the geohash
// or if precision is less than 0 then set
// precision equal to length of geohash.
if precision > len(g) || precision < 0 {
precision = len(g)
}
box, err := geohash.Decode(g[:precision])
if err != nil {
return geohash.Box{}, err
}
return box, nil
}
// ToLowerSingleByte returns the lowercase of a given single ASCII byte.
// A non ASCII byte is returned unchanged.
func ToLowerSingleByte(b byte) byte {
if b >= 'A' && b <= 'Z' {
return 'a' + (b - 'A')
}
return b
}