github.com/dolthub/go-mysql-server@v0.18.0/sql/expression/function/spatial/st_intersects.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  
    20  	"github.com/dolthub/go-mysql-server/sql/types"
    21  
    22  	"github.com/dolthub/go-mysql-server/sql"
    23  	"github.com/dolthub/go-mysql-server/sql/expression"
    24  )
    25  
    26  // Intersects is a function that returns true if the two geometries intersect
    27  type Intersects struct {
    28  	expression.BinaryExpressionStub
    29  }
    30  
    31  var _ sql.FunctionExpression = (*Intersects)(nil)
    32  var _ sql.CollationCoercible = (*Intersects)(nil)
    33  
    34  // NewIntersects creates a new Intersects expression.
    35  func NewIntersects(g1, g2 sql.Expression) sql.Expression {
    36  	return &Intersects{
    37  		expression.BinaryExpressionStub{
    38  			LeftChild:  g1,
    39  			RightChild: g2,
    40  		},
    41  	}
    42  }
    43  
    44  // FunctionName implements sql.FunctionExpression
    45  func (i *Intersects) FunctionName() string {
    46  	return "st_intersects"
    47  }
    48  
    49  // Description implements sql.FunctionExpression
    50  func (i *Intersects) Description() string {
    51  	return "returns 1 or 0 to indicate whether g1 spatially intersects g2."
    52  }
    53  
    54  // Type implements the sql.Expression interface.
    55  func (i *Intersects) Type() sql.Type {
    56  	return types.Boolean
    57  }
    58  
    59  // CollationCoercibility implements the interface sql.CollationCoercible.
    60  func (*Intersects) CollationCoercibility(ctx *sql.Context) (collation sql.CollationID, coercibility byte) {
    61  	return sql.Collation_binary, 5
    62  }
    63  
    64  func (i *Intersects) String() string {
    65  	return fmt.Sprintf("%s(%s,%s)", i.FunctionName(), i.LeftChild, i.RightChild)
    66  }
    67  
    68  func (i *Intersects) DebugString() string {
    69  	return fmt.Sprintf("%s(%s,%s)", i.FunctionName(), sql.DebugString(i.LeftChild), sql.DebugString(i.RightChild))
    70  }
    71  
    72  // WithChildren implements the Expression interface.
    73  func (i *Intersects) WithChildren(children ...sql.Expression) (sql.Expression, error) {
    74  	if len(children) != 2 {
    75  		return nil, sql.ErrInvalidChildrenNumber.New(i, len(children), 2)
    76  	}
    77  	return NewIntersects(children[0], children[1]), nil
    78  }
    79  
    80  // isPointIntersectLine checks if Point p intersects the LineString l
    81  // Note: this will return true if p is a terminal point of l
    82  // Alternatively, we could calculate if dist(ap) + dist(ab) == dist(ap)
    83  func isPointIntersectLine(p types.Point, l types.LineString) bool {
    84  	for i := 1; i < len(l.Points); i++ {
    85  		a, b := l.Points[i-1], l.Points[i]
    86  		if isInBBox(a, b, p) && orientation(a, b, p) == 0 {
    87  			return true
    88  		}
    89  	}
    90  	return false
    91  }
    92  
    93  // isPointIntersectPolyBoundary checks if Point p intersects the Polygon boundary
    94  func isPointIntersectPolyBoundary(point types.Point, poly types.Polygon) bool {
    95  	for _, line := range poly.Lines {
    96  		if isPointIntersectLine(point, line) {
    97  			return true
    98  		}
    99  	}
   100  	return false
   101  }
   102  
   103  // isPointIntersectPolyInterior checks if a Point p intersects the Polygon Interior
   104  // Point outside the first LineString is not in Polygon Interior
   105  // Point inside the other LineStrings is not in Polygon Interior
   106  // Note: this returns true for Polygon boundaries?
   107  func isPointIntersectPolyInterior(point types.Point, poly types.Polygon) bool {
   108  	if !isPointWithinClosedLineString(point, poly.Lines[0]) {
   109  		return false
   110  	}
   111  	for i := 1; i < len(poly.Lines); i++ {
   112  		if isPointWithinClosedLineString(point, poly.Lines[i]) {
   113  			return false
   114  		}
   115  	}
   116  	return true
   117  }
   118  
   119  func isPointIntersects(p types.Point, g types.GeometryValue) bool {
   120  	switch g := g.(type) {
   121  	case types.Point:
   122  		return isPointEqual(p, g)
   123  	case types.LineString:
   124  		return isPointIntersectLine(p, g)
   125  	case types.Polygon:
   126  		return isPointIntersectPolyBoundary(p, g) || isPointIntersectPolyInterior(p, g)
   127  	case types.MultiPoint:
   128  		for _, pp := range g.Points {
   129  			if isPointIntersects(p, pp) {
   130  				return true
   131  			}
   132  		}
   133  	case types.MultiLineString:
   134  		for _, l := range g.Lines {
   135  			if isPointIntersects(p, l) {
   136  				return true
   137  			}
   138  		}
   139  	case types.MultiPolygon:
   140  		for _, pp := range g.Polygons {
   141  			if isPointIntersects(p, pp) {
   142  				return true
   143  			}
   144  		}
   145  	case types.GeomColl:
   146  		for _, gg := range g.Geoms {
   147  			if isPointIntersects(p, gg) {
   148  				return true
   149  			}
   150  		}
   151  	}
   152  	return false
   153  }
   154  
   155  // linesIntersect checks if line segment ab intersects line cd
   156  // Edge case for collinear points is to check if they are within the bounding box
   157  // Reference: https://www.geeksforgeeks.org/check-if-two-given-line-segments-intersect/
   158  func linesIntersect(a, b, c, d types.Point) bool {
   159  	abc := orientation(a, b, c)
   160  	abd := orientation(a, b, d)
   161  	cda := orientation(c, d, a)
   162  	cdb := orientation(c, d, b)
   163  
   164  	// different orientations mean they intersect
   165  	if (abc != abd) && (cda != cdb) {
   166  		return true
   167  	}
   168  
   169  	// if orientation is collinear, check if point is inside segment
   170  	if abc == 0 && isInBBox(a, b, c) {
   171  		return true
   172  	}
   173  	if abd == 0 && isInBBox(a, b, d) {
   174  		return true
   175  	}
   176  	if cda == 0 && isInBBox(c, d, a) {
   177  		return true
   178  	}
   179  	if cdb == 0 && isInBBox(c, d, b) {
   180  		return true
   181  	}
   182  
   183  	return false
   184  }
   185  
   186  func isLineIntersectLine(l1, l2 types.LineString) bool {
   187  	for i := 1; i < len(l1.Points); i++ {
   188  		for j := 1; j < len(l2.Points); j++ {
   189  			if linesIntersect(l1.Points[i-1], l1.Points[i], l2.Points[j-1], l2.Points[j]) {
   190  				return true
   191  			}
   192  		}
   193  	}
   194  	return false
   195  }
   196  
   197  func isLineIntersects(l types.LineString, g types.GeometryValue) bool {
   198  	switch g := g.(type) {
   199  	case types.Point:
   200  		return isPointIntersects(g, l)
   201  	case types.LineString:
   202  		return isLineIntersectLine(l, g)
   203  	case types.Polygon:
   204  		for _, p := range l.Points {
   205  			if isPointIntersects(p, g) {
   206  				return true
   207  			}
   208  		}
   209  		for _, line := range g.Lines {
   210  			if isLineIntersectLine(l, line) {
   211  				return true
   212  			}
   213  		}
   214  	case types.MultiPoint:
   215  		for _, p := range g.Points {
   216  			if isLineIntersects(l, p) {
   217  				return true
   218  			}
   219  		}
   220  	case types.MultiLineString:
   221  		for _, line := range g.Lines {
   222  			if isLineIntersects(l, line) {
   223  				return true
   224  			}
   225  		}
   226  	case types.MultiPolygon:
   227  		for _, p := range g.Polygons {
   228  			if isLineIntersects(l, p) {
   229  				return true
   230  			}
   231  		}
   232  	case types.GeomColl:
   233  		for _, geom := range g.Geoms {
   234  			if isLineIntersects(l, geom) {
   235  				return true
   236  			}
   237  		}
   238  	}
   239  	return false
   240  }
   241  
   242  func isPolyIntersects(p types.Polygon, g types.GeometryValue) bool {
   243  	switch g := g.(type) {
   244  	case types.Point:
   245  		return isPointIntersects(g, p)
   246  	case types.LineString:
   247  		return isLineIntersects(g, p)
   248  	case types.Polygon:
   249  		for _, l := range g.Lines {
   250  			for _, point := range l.Points {
   251  				if isPointIntersects(point, p) {
   252  					return true
   253  				}
   254  			}
   255  		}
   256  		for _, l := range p.Lines {
   257  			for _, point := range l.Points {
   258  				if isPointIntersects(point, g) {
   259  					return true
   260  				}
   261  			}
   262  		}
   263  	case types.MultiPoint:
   264  		for _, point := range g.Points {
   265  			if isPolyIntersects(p, point) {
   266  				return true
   267  			}
   268  		}
   269  	case types.MultiLineString:
   270  		for _, l := range g.Lines {
   271  			if isPolyIntersects(p, l) {
   272  				return true
   273  			}
   274  		}
   275  	case types.MultiPolygon:
   276  		for _, poly := range g.Polygons {
   277  			if isPolyIntersects(p, poly) {
   278  				return true
   279  			}
   280  		}
   281  	case types.GeomColl:
   282  		for _, geom := range g.Geoms {
   283  			if isPolyIntersects(p, geom) {
   284  				return true
   285  			}
   286  		}
   287  	}
   288  	return false
   289  }
   290  
   291  func isIntersects(g1, g2 types.GeometryValue) bool {
   292  	switch g1 := g1.(type) {
   293  	case types.Point:
   294  		return isPointIntersects(g1, g2)
   295  	case types.LineString:
   296  		return isLineIntersects(g1, g2)
   297  	case types.Polygon:
   298  		return isPolyIntersects(g1, g2)
   299  	case types.MultiPoint:
   300  		for _, p := range g1.Points {
   301  			if isIntersects(p, g2) {
   302  				return true
   303  			}
   304  		}
   305  	case types.MultiLineString:
   306  		for _, l := range g1.Lines {
   307  			if isIntersects(l, g2) {
   308  				return true
   309  			}
   310  		}
   311  	case types.MultiPolygon:
   312  		for _, p := range g1.Polygons {
   313  			if isIntersects(p, g2) {
   314  				return true
   315  			}
   316  		}
   317  	case types.GeomColl:
   318  		for _, g := range g1.Geoms {
   319  			if isIntersects(g, g2) {
   320  				return true
   321  			}
   322  		}
   323  	}
   324  	return false
   325  }
   326  
   327  // validateGeomComp validates that variables geom1 and geom2 are comparable geometries.
   328  // 1. Nil values, return nil
   329  // 2. Not a types.GeometryValue, return error
   330  // 3. SRIDs don't match, return error
   331  // 4. Empty GeometryCollection, return nil
   332  func validateGeomComp(geom1, geom2 interface{}, funcName string) (types.GeometryValue, types.GeometryValue, error) {
   333  	if geom1 == nil || geom2 == nil {
   334  		return nil, nil, nil
   335  	}
   336  	g1, ok := geom1.(types.GeometryValue)
   337  	if !ok {
   338  		return nil, nil, sql.ErrInvalidGISData.New(funcName)
   339  	}
   340  	g2, ok := geom2.(types.GeometryValue)
   341  	if !ok {
   342  		return nil, nil, sql.ErrInvalidGISData.New(funcName)
   343  	}
   344  	if g1.GetSRID() != g2.GetSRID() {
   345  		return nil, nil, sql.ErrDiffSRIDs.New(funcName, g1.GetSRID(), g2.GetSRID())
   346  	}
   347  	if gc, ok := g1.(types.GeomColl); ok && countConcreteGeoms(gc) == 0 {
   348  		return nil, nil, nil
   349  	}
   350  	if gc, ok := g2.(types.GeomColl); ok && countConcreteGeoms(gc) == 0 {
   351  		return nil, nil, nil
   352  	}
   353  	return g1, g2, nil
   354  }
   355  
   356  // Eval implements the sql.Expression interface.
   357  func (i *Intersects) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) {
   358  	geom1, err := i.LeftChild.Eval(ctx, row)
   359  	if err != nil {
   360  		return nil, err
   361  	}
   362  	geom2, err := i.RightChild.Eval(ctx, row)
   363  	if err != nil {
   364  		return nil, err
   365  	}
   366  	g1, g2, err := validateGeomComp(geom1, geom2, i.FunctionName())
   367  	if err != nil {
   368  		return nil, err
   369  	}
   370  	if g1 == nil || g2 == nil {
   371  		return nil, nil
   372  	}
   373  	return isIntersects(g1, g2), nil
   374  }