From 02bacf121c7a3d7252319a4db06603e440d8e5ad Mon Sep 17 00:00:00 2001 From: Tobie Morgan Hitchcock Date: Thu, 13 May 2021 01:28:13 +0100 Subject: [PATCH] Add geometry contains + inside database functions --- sql/funcs.go | 1 + util/fncs/fnc.go | 2 ++ util/fncs/geo.go | 30 +++++++++++++++++-- util/geof/contains.go | 31 ++++++++++++++++++++ util/geof/inside.go | 16 ++++++++-- util/geof/raycast.go | 68 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 143 insertions(+), 5 deletions(-) create mode 100644 util/geof/contains.go create mode 100644 util/geof/raycast.go diff --git a/sql/funcs.go b/sql/funcs.go index caa0b41c..b9297b0f 100644 --- a/sql/funcs.go +++ b/sql/funcs.go @@ -97,6 +97,7 @@ var funcs = map[string]map[int]interface{}{ "geo.point": {1: nil, 2: nil}, "geo.circle": {2: nil}, "geo.polygon": {-1: nil}, + "geo.contains": {2: nil}, "geo.distance": {2: nil}, "geo.inside": {2: nil}, "geo.intersects": {2: nil}, diff --git a/util/fncs/fnc.go b/util/fncs/fnc.go index 4985af34..9bdbdf8c 100644 --- a/util/fncs/fnc.go +++ b/util/fncs/fnc.go @@ -70,6 +70,8 @@ func Run(ctx context.Context, name string, args ...interface{}) (interface{}, er return geoCircle(ctx, args...) case "geo.polygon": return geoPolygon(ctx, args...) + case "geo.contains": + return geoContains(ctx, args...) case "geo.distance": return geoDistance(ctx, args...) case "geo.inside": diff --git a/util/fncs/geo.go b/util/fncs/geo.go index 170ac761..5b0132b7 100644 --- a/util/fncs/geo.go +++ b/util/fncs/geo.go @@ -15,6 +15,7 @@ package fncs import ( + "context" "github.com/abcum/surreal/sql" @@ -57,8 +58,22 @@ func geoCircle(ctx context.Context, args ...interface{}) (interface{}, error) { func geoPolygon(ctx context.Context, args ...interface{}) (interface{}, error) { switch len(args) { - case 0, 1, 2: + case 0, 2: // Not enough arguments, so just ignore + case 1: + var pnts []*sql.Point + if a, ok := ensureSlice(args[0]); ok { + for _, a := range a { + if p, _ := ensurePoint(a); p != nil { + pnts = append(pnts, p) + } else if p := ensureFloats(a); len(p) == 2 { + pnts = append(pnts, sql.NewPoint(p[0], p[1])) + } else { + return nil, nil + } + } + return sql.NewPolygon(pnts...), nil + } default: var pnts []*sql.Point for _, a := range args { @@ -75,6 +90,15 @@ func geoPolygon(ctx context.Context, args ...interface{}) (interface{}, error) { return nil, nil } +func geoContains(ctx context.Context, args ...interface{}) (interface{}, error) { + if a, ok := ensurePolygon(args[0]); ok { + if b, ok := ensurePoint(args[1]); ok { + return geof.Contains(a, b), nil + } + } + return false, nil +} + func geoDistance(ctx context.Context, args ...interface{}) (interface{}, error) { if pnt, ok := ensurePoint(args[0]); ok { if frm, ok := ensurePoint(args[1]); ok { @@ -85,8 +109,8 @@ func geoDistance(ctx context.Context, args ...interface{}) (interface{}, error) } func geoInside(ctx context.Context, args ...interface{}) (interface{}, error) { - if a, ok := ensureGeometry(args[0]); ok { - if b, ok := ensureGeometry(args[1]); ok { + if a, ok := ensurePoint(args[0]); ok { + if b, ok := ensurePolygon(args[1]); ok { return geof.Inside(a, b), nil } } diff --git a/util/geof/contains.go b/util/geof/contains.go new file mode 100644 index 00000000..0762485c --- /dev/null +++ b/util/geof/contains.go @@ -0,0 +1,31 @@ +// Copyright © 2016 Abcum Ltd +// +// 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. + +package geof + +import ( + "github.com/abcum/surreal/sql" +) + +func Contains(a *sql.Polygon, b *sql.Point) bool { + beg := len(a.PS) - 1 + end := 0 + contains := raycast(b, a.PS[beg], a.PS[end]) + for i := 1; i < len(a.PS); i++ { + if raycast(b, a.PS[i-1], a.PS[i]) { + contains = !contains + } + } + return contains +} diff --git a/util/geof/inside.go b/util/geof/inside.go index 48134f53..22d99578 100644 --- a/util/geof/inside.go +++ b/util/geof/inside.go @@ -14,6 +14,18 @@ package geof -func Inside(a, b interface{}) bool { - return false +import ( + "github.com/abcum/surreal/sql" +) + +func Inside(a *sql.Point, b *sql.Polygon) bool { + beg := len(b.PS) - 1 + end := 0 + contains := raycast(a, b.PS[beg], b.PS[end]) + for i := 1; i < len(b.PS); i++ { + if raycast(a, b.PS[i-1], b.PS[i]) { + contains = !contains + } + } + return contains } diff --git a/util/geof/raycast.go b/util/geof/raycast.go new file mode 100644 index 00000000..8f9a856a --- /dev/null +++ b/util/geof/raycast.go @@ -0,0 +1,68 @@ +// Copyright © 2016 Abcum Ltd +// +// 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. + +package geof + +import ( + "math" + + "github.com/abcum/surreal/sql" +) + +func raycast(point, beg, end *sql.Point) bool { + + // Always ensure that the the first point + // has a Y coordinate that is less than + // the second point. Switch if not. + + if beg.LO > end.LO { + beg, end = end, beg + } + + // Move the point's Y coordinate outside of + // the bounds of the testing region so that + // we can start drawing a ray + for point.LO == beg.LO || point.LO == end.LO { + lng := math.Nextafter(point.LO, math.Inf(1)) + point = sql.NewPoint(point.LA, lng) + } + + // If we are outside of the polygon, indicate so. + if point.LO < beg.LO || point.LO > end.LO { + return false + } + + if beg.LA > end.LA { + if point.LA > beg.LA { + return false + } + if point.LA < end.LA { + return true + } + + } else { + if point.LA > end.LA { + return false + } + if point.LA < beg.LA { + return true + } + } + + raySlope := (point.LO - beg.LO) / (point.LA - beg.LA) + diagSlope := (end.LO - beg.LO) / (end.LA - beg.LA) + + return raySlope >= diagSlope + +}