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  }