github.com/dolthub/go-mysql-server@v0.18.0/sql/expression/function/spatial/polygon.go (about) 1 // Copyright 2020-2021 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 "errors" 19 "fmt" 20 "math" 21 "strings" 22 23 "github.com/dolthub/go-mysql-server/sql/expression" 24 "github.com/dolthub/go-mysql-server/sql/types" 25 26 "github.com/dolthub/go-mysql-server/sql" 27 ) 28 29 // Polygon is a function that returns a Polygon. 30 type Polygon struct { 31 expression.NaryExpression 32 } 33 34 var _ sql.FunctionExpression = (*Polygon)(nil) 35 var _ sql.CollationCoercible = (*Polygon)(nil) 36 37 // NewPolygon creates a new polygon expression. 38 func NewPolygon(args ...sql.Expression) (sql.Expression, error) { 39 if len(args) < 1 { 40 return nil, sql.ErrInvalidArgumentNumber.New("Polygon", "1 or more", len(args)) 41 } 42 return &Polygon{expression.NaryExpression{ChildExpressions: args}}, nil 43 } 44 45 // FunctionName implements sql.FunctionExpression 46 func (p *Polygon) FunctionName() string { 47 return "polygon" 48 } 49 50 // Description implements sql.FunctionExpression 51 func (p *Polygon) Description() string { 52 return "returns a new polygon." 53 } 54 55 // Type implements the sql.Expression interface. 56 func (p *Polygon) Type() sql.Type { 57 return types.PolygonType{} 58 } 59 60 // CollationCoercibility implements the interface sql.CollationCoercible. 61 func (*Polygon) CollationCoercibility(ctx *sql.Context) (collation sql.CollationID, coercibility byte) { 62 return sql.Collation_binary, 5 63 } 64 65 func (p *Polygon) String() string { 66 var args = make([]string, len(p.ChildExpressions)) 67 for i, arg := range p.ChildExpressions { 68 args[i] = arg.String() 69 } 70 return fmt.Sprintf("%s(%s)", p.FunctionName(), strings.Join(args, ",")) 71 } 72 73 // WithChildren implements the Expression interface. 74 func (p *Polygon) WithChildren(children ...sql.Expression) (sql.Expression, error) { 75 return NewPolygon(children...) 76 } 77 78 // TODO: https://www.geeksforgeeks.org/orientation-3-ordered-points/ 79 func pointOrientation(p1, p2, p3 types.Point) int { 80 // compare slopes of line(p1, p2) and line(p2, p3) 81 val := (p2.Y-p1.Y)*(p3.X-p2.X) - (p3.Y-p2.Y)*(p2.X-p1.X) 82 // check orientation 83 if val == 0 { 84 return 0 // collinear or both on axis and perpendicular 85 } else if val > 0 { 86 return 1 // clockwise 87 } else { 88 return 2 // counter-clockwise 89 } 90 } 91 92 // Check if point c is in line segment ab 93 func onSegment(a, b, c types.Point) bool { 94 return c.X > math.Min(a.X, b.X) && c.X < math.Max(a.X, b.X) && c.Y > math.Min(a.Y, b.Y) && c.Y < math.Max(a.Y, b.Y) 95 } 96 97 // TODO: https://www.geeksforgeeks.org/check-if-two-given-line-segments-intersect/ 98 func lineSegmentsIntersect(a, b, c, d types.Point) bool { 99 abc := pointOrientation(a, b, c) 100 abd := pointOrientation(a, b, d) 101 cda := pointOrientation(c, d, a) 102 cdb := pointOrientation(c, d, b) 103 104 // different orientations mean they intersect 105 if (abc != abd) && (cda != cdb) { 106 return true 107 } 108 109 // if orientation is collinear, check if point is inside segment 110 if abc == 0 && onSegment(a, b, c) { 111 return true 112 } 113 if abd == 0 && onSegment(a, b, d) { 114 return true 115 } 116 if cda == 0 && onSegment(c, d, a) { 117 return true 118 } 119 if cdb == 0 && onSegment(c, d, b) { 120 return true 121 } 122 123 // no intersection 124 return false 125 } 126 127 // TODO: should go in line? 128 func isLinearRing(line types.LineString) bool { 129 // Get number of points 130 numPoints := len(line.Points) 131 // Check length of LineString (must be 0 or 4+) points 132 if numPoints != 0 && numPoints < 4 { 133 return false 134 } 135 // Check if it is closed (first and last point are the same) 136 if line.Points[0] != line.Points[numPoints-1] { 137 return false 138 } 139 return true 140 } 141 142 // Eval implements the sql.Expression interface. 143 func (p *Polygon) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { 144 // Allocate array of lines 145 var lines = make([]types.LineString, len(p.ChildExpressions)) 146 147 // Go through each argument 148 for i, arg := range p.ChildExpressions { 149 // Evaluate argument 150 val, err := arg.Eval(ctx, row) 151 if err != nil { 152 return nil, err 153 } 154 // Must be of type linestring, throw error otherwise 155 switch v := val.(type) { 156 case types.LineString: 157 // Check that line is a linear ring 158 if isLinearRing(v) { 159 lines[i] = v 160 } else { 161 return nil, errors.New("Invalid GIS data provided to function polygon.") 162 } 163 case types.GeometryValue: 164 return nil, sql.ErrInvalidArgumentDetails.New(p.FunctionName(), v) 165 default: 166 return nil, sql.ErrIllegalGISValue.New(v) 167 } 168 } 169 170 return types.Polygon{Lines: lines}, nil 171 }