github.com/dolthub/go-mysql-server@v0.18.0/sql/expression/function/spatial/st_within.go (about) 1 // Copyright 2023 Dolthub, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package spatial 16 17 import ( 18 "fmt" 19 "math" 20 21 "github.com/dolthub/go-mysql-server/sql" 22 "github.com/dolthub/go-mysql-server/sql/expression" 23 "github.com/dolthub/go-mysql-server/sql/types" 24 ) 25 26 // Within is a function that true if left is spatially within right 27 type Within struct { 28 expression.BinaryExpressionStub 29 } 30 31 var _ sql.FunctionExpression = (*Within)(nil) 32 var _ sql.CollationCoercible = (*Within)(nil) 33 34 // NewWithin creates a new Within expression. 35 func NewWithin(g1, g2 sql.Expression) sql.Expression { 36 return &Within{ 37 expression.BinaryExpressionStub{ 38 LeftChild: g1, 39 RightChild: g2, 40 }, 41 } 42 } 43 44 // FunctionName implements sql.FunctionExpression 45 func (w *Within) FunctionName() string { 46 return "st_within" 47 } 48 49 // Description implements sql.FunctionExpression 50 func (w *Within) Description() string { 51 return "returns 1 or 0 to indicate whether g1 is spatially within g2. This tests the opposite relationship as st_contains()." 52 } 53 54 // Type implements the sql.Expression interface. 55 func (w *Within) Type() sql.Type { 56 return types.Boolean 57 } 58 59 // CollationCoercibility implements the interface sql.CollationCoercible. 60 func (*Within) CollationCoercibility(ctx *sql.Context) (collation sql.CollationID, coercibility byte) { 61 return sql.Collation_binary, 5 62 } 63 64 func (w *Within) String() string { 65 return fmt.Sprintf("%s(%s,%s)", w.FunctionName(), w.LeftChild, w.RightChild) 66 } 67 68 // WithChildren implements the Expression interface. 69 func (w *Within) WithChildren(children ...sql.Expression) (sql.Expression, error) { 70 if len(children) != 2 { 71 return nil, sql.ErrInvalidChildrenNumber.New(w, len(children), 2) 72 } 73 return NewWithin(children[0], children[1]), nil 74 } 75 76 // orientation returns the orientation of points: a, b, c in that order 77 // 0 = points are collinear 78 // 1 = points are clockwise 79 // 2 = points are counter-clockwise 80 // Reference: https://www.geeksforgeeks.org/orientation-3-ordered-points/ 81 func orientation(a, b, c types.Point) int { 82 // compare slopes of line(a, b) and line(b, c) 83 val := (b.Y-a.Y)*(c.X-b.X) - (b.X-a.X)*(c.Y-b.Y) 84 if val > 0 { 85 return 1 86 } else if val < 0 { 87 return 2 88 } else { 89 return 0 90 } 91 } 92 93 // isInBBox checks if Point c is within the bounding box created by Points a and b 94 func isInBBox(a, b, c types.Point) bool { 95 return c.X >= math.Min(a.X, b.X) && 96 c.X <= math.Max(a.X, b.X) && 97 c.Y >= math.Min(a.Y, b.Y) && 98 c.Y <= math.Max(a.Y, b.Y) 99 } 100 101 // Closed LineStrings have no Terminal Points, so will always return false for Closed LineStrings 102 func isTerminalPoint(p types.Point, l types.LineString) bool { 103 return !isClosed(l) && (isPointEqual(p, startPoint(l)) || isPointEqual(p, endPoint(l))) 104 } 105 106 // isPointWithinClosedLineString checks if a point lies inside a Closed LineString 107 // Assume p is not Within l, and l is a Closed LineString. 108 // Cast a horizontal ray from p to the right, and count the number of line segment intersections 109 // A Point on the interior of a Closed LineString will intersect with an odd number of line segments 110 // A simpler, but possibly more compute intensive option is to sum angles, and check if equal to 2pi or 360 deg 111 // Reference: https://en.wikipedia.org/wiki/Point_in_polygon 112 func isPointWithinClosedLineString(p types.Point, l types.LineString) bool { 113 hasOddInters := false 114 for i := 1; i < len(l.Points); i++ { 115 a := l.Points[i-1] 116 b := l.Points[i] 117 // ignore horizontal line segments 118 if a.Y == b.Y { 119 continue 120 } 121 // p is either above or below line segment, will never intersect 122 // we use >, but not >= for max, because of vertex intersections 123 if p.Y <= math.Min(a.Y, b.Y) || p.Y > math.Max(a.Y, b.Y) { 124 continue 125 } 126 // p is to the right of entire line segment, will never intersect 127 if p.X >= math.Max(a.X, b.X) { 128 continue 129 } 130 q := types.Point{X: math.Max(a.X, b.X), Y: p.Y} 131 if !linesIntersect(a, b, p, q) { 132 continue 133 } 134 hasOddInters = !hasOddInters 135 } 136 return hasOddInters 137 } 138 139 // countConcreteGeoms recursively counts all the Geometry Types that are not GeomColl inside a GeomColl 140 func countConcreteGeoms(gc types.GeomColl) int { 141 count := 0 142 for _, g := range gc.Geoms { 143 if innerGC, ok := g.(types.GeomColl); ok { 144 count += countConcreteGeoms(innerGC) 145 } 146 count++ 147 } 148 return count 149 } 150 151 func isPointWithin(p types.Point, g types.GeometryValue) bool { 152 switch g := g.(type) { 153 case types.Point: 154 return isPointEqual(p, g) 155 case types.LineString: 156 // Terminal Points of LineStrings are not considered a part of their Interior 157 if isTerminalPoint(p, g) { 158 return false 159 } 160 return isPointIntersectLine(p, g) 161 case types.Polygon: 162 // Points on the Polygon Boundary are not considered part of the Polygon 163 if isPointIntersectPolyBoundary(p, g) { 164 return false 165 } 166 return isPointIntersectPolyInterior(p, g) 167 case types.MultiPoint: 168 // Point is considered within MultiPoint if it's equal to at least one Point 169 for _, pp := range g.Points { 170 if isPointWithin(p, pp) { 171 return true 172 } 173 } 174 case types.MultiLineString: 175 // Point is considered within MultiLineString if it is within at least one LineString 176 // Edge Case: If point is a terminal point for an odd number of lines, 177 // then it's not within the entire MultiLineString. 178 // This is the case regardless of how many other LineStrings the point is in 179 isOddTerminalPoint := false 180 for _, l := range g.Lines { 181 if isTerminalPoint(p, l) { 182 isOddTerminalPoint = !isOddTerminalPoint 183 } 184 } 185 if isOddTerminalPoint { 186 return false 187 } 188 189 for _, l := range g.Lines { 190 if isPointWithin(p, l) { 191 return true 192 } 193 } 194 case types.MultiPolygon: 195 // Point is considered within MultiPolygon if it is within at least one Polygon 196 for _, poly := range g.Polygons { 197 if isPointWithin(p, poly) { 198 return true 199 } 200 } 201 case types.GeomColl: 202 // Point is considered within GeomColl if it is within at least one Geometry 203 for _, gg := range g.Geoms { 204 if isPointWithin(p, gg) { 205 return true 206 } 207 } 208 } 209 return false 210 } 211 212 func isWithin(g1, g2 types.GeometryValue) bool { 213 switch g1 := g1.(type) { 214 case types.Point: 215 return isPointWithin(g1, g2) 216 case types.LineString: 217 case types.Polygon: 218 case types.MultiPoint: 219 case types.MultiLineString: 220 case types.MultiPolygon: 221 case types.GeomColl: 222 // TODO (james): implement these 223 } 224 return false 225 } 226 227 // Eval implements the sql.Expression interface. 228 func (w *Within) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { 229 geom1, err := w.LeftChild.Eval(ctx, row) 230 if err != nil { 231 return nil, err 232 } 233 geom2, err := w.RightChild.Eval(ctx, row) 234 if err != nil { 235 return nil, err 236 } 237 g1, g2, err := validateGeomComp(geom1, geom2, w.FunctionName()) 238 if err != nil { 239 return nil, err 240 } 241 if g1 == nil || g2 == nil { 242 return nil, nil 243 } 244 245 // TODO (james): remove this switch block when the other comparisons are implemented 246 switch geom1.(type) { 247 case types.LineString: 248 return nil, sql.ErrUnsupportedGISTypeForSpatialFunc.New("LineString", w.FunctionName()) 249 case types.Polygon: 250 return nil, sql.ErrUnsupportedGISTypeForSpatialFunc.New("Polygon", w.FunctionName()) 251 case types.MultiPoint: 252 return nil, sql.ErrUnsupportedGISTypeForSpatialFunc.New("MultiPoint", w.FunctionName()) 253 case types.MultiLineString: 254 return nil, sql.ErrUnsupportedGISTypeForSpatialFunc.New("MultiLineString", w.FunctionName()) 255 case types.MultiPolygon: 256 return nil, sql.ErrUnsupportedGISTypeForSpatialFunc.New("MultiPolygon", w.FunctionName()) 257 case types.GeomColl: 258 return nil, sql.ErrUnsupportedGISTypeForSpatialFunc.New("GeomColl", w.FunctionName()) 259 } 260 261 return isWithin(g1, g2), nil 262 }