github.com/liucxer/courier@v1.7.1/h3/polygon_algos.go (about) 1 package h3 2 3 import "math" 4 5 /** Macro: Normalize longitude, dealing with transmeridian arcs */ 6 func NORMALIZE_LON(lon float64, isTransmeridian bool) float64 { 7 if isTransmeridian && lon < 0 { 8 return lon + M_2PI 9 } 10 return lon 11 } 12 13 type GeoIterator interface { 14 // zero 15 IsZero() bool 16 // true for continue 17 NewIterate() func(vertexA *GeoCoord, vertexB *GeoCoord) bool 18 } 19 20 func pointInside(iterator GeoIterator, bbox *BBox, coord *GeoCoord) bool { 21 // fail fast if we're outside the bounding box 22 if !bboxContains(bbox, coord) { 23 return false 24 } 25 26 isTransmeridian := bboxIsTransmeridian(bbox) 27 contains := false 28 29 lat := coord.Lat 30 lng := NORMALIZE_LON(coord.Lon, isTransmeridian) 31 32 var a GeoCoord 33 var b GeoCoord 34 35 iterate := iterator.NewIterate() 36 37 for { 38 if !iterate(&a, &b) { 39 break 40 } 41 42 // Ray casting algo requires the second point to always be higher 43 // than the first, so swap if needed 44 if a.Lat > b.Lat { 45 tmp := a 46 a = b 47 b = tmp 48 } 49 50 // If we're totally above or below the latitude ranges, the test 51 // ray cannot intersect the line segment, so let's move on 52 if lat < a.Lat || lat > b.Lat { 53 continue 54 } 55 56 aLng := NORMALIZE_LON(a.Lon, isTransmeridian) 57 bLng := NORMALIZE_LON(b.Lon, isTransmeridian) 58 59 // Rays are cast in the longitudinal direction, in case a point 60 // exactly matches, to decide tiebreakers, bias westerly 61 if aLng == lng || bLng == lng { 62 lng -= EPSILON 63 } 64 65 // For the latitude of the point, compute the longitude of the 66 // point that lies on the line segment defined by a and b 67 // This is done by computing the percent above a the Lat is, 68 // and traversing the same percent in the longitudinal direction 69 // of a to b 70 ratio := (lat - a.Lat) / (b.Lat - a.Lat) 71 testLng := NORMALIZE_LON(aLng+(bLng-aLng)*ratio, isTransmeridian) 72 73 // Intersection of the ray 74 if testLng > lng { 75 contains = !contains 76 } 77 } 78 79 return contains 80 } 81 82 /** 83 * Create a bounding box from a simple polygon loop. 84 * Known limitations: 85 * - Does not support polygons with two adjacent points > 180 degrees of 86 * longitude apart. These will be interpreted as crossing the antimeridian. 87 * - Does not currently support polygons containing a pole. 88 * @param loop Loop of coordinates 89 * @param bbox Output bbox 90 */ 91 func bboxFrom(iterator GeoIterator, bbox *BBox) { 92 // Early exit if there are no vertices 93 if iterator.IsZero() { 94 *bbox = BBox{} 95 return 96 } 97 98 bbox.south = math.MaxFloat64 99 bbox.west = math.MaxFloat64 100 bbox.north = -math.MaxFloat64 101 bbox.east = -math.MaxFloat64 102 minPosLon := math.MaxFloat64 103 maxNegLon := -math.MaxFloat64 104 105 isTransmeridian := false 106 107 var lon, lat float64 108 var coord, next GeoCoord 109 110 iterate := iterator.NewIterate() 111 112 for { 113 if !iterate(&coord, &next) { 114 break 115 } 116 117 lat = coord.Lat 118 lon = coord.Lon 119 120 if lat < bbox.south { 121 bbox.south = lat 122 } 123 if lon < bbox.west { 124 bbox.west = lon 125 } 126 if lat > bbox.north { 127 bbox.north = lat 128 } 129 if lon > bbox.east { 130 bbox.east = lon 131 } 132 133 // Save the min positive and max negative longitude for 134 // use in the transmeridian case 135 if lon > 0 && lon < minPosLon { 136 minPosLon = lon 137 } 138 if lon < 0 && lon > maxNegLon { 139 maxNegLon = lon 140 } 141 // check for arcs > 180 degrees longitude, flagging as transmeridian 142 if math.Abs(lon-next.Lon) > M_PI { 143 isTransmeridian = true 144 } 145 } 146 147 // Swap east and west if transmeridian 148 if isTransmeridian { 149 bbox.east = maxNegLon 150 bbox.west = minPosLon 151 } 152 } 153 154 /** 155 * Whether the winding order of a given loop is clockwise, with normalization 156 * for loops crossing the antimeridian. 157 * @param loop The loop to check 158 * @param isTransmeridian Whether the loop crosses the antimeridian 159 * @return Whether the loop is clockwise 160 */ 161 func isClockwiseNormalized(iterator GeoIterator, isTransmeridian bool) bool { 162 sum := float64(0) 163 var a, b GeoCoord 164 165 iterate := iterator.NewIterate() 166 167 for { 168 if !iterate(&a, &b) { 169 break 170 } 171 // If we identify a transmeridian arc (> 180 degrees longitude), 172 // start over with the transmeridian flag set 173 if !isTransmeridian && math.Abs(a.Lon-b.Lon) > M_PI { 174 return isClockwiseNormalized(iterator, true) 175 } 176 177 sum += ((NORMALIZE_LON(b.Lon, isTransmeridian) - NORMALIZE_LON(a.Lon, isTransmeridian)) * (b.Lat + a.Lat)) 178 } 179 180 return sum > 0 181 } 182 183 /** 184 * Whether the winding order of a given loop is clockwise. In GeoJSON, 185 * clockwise loops are always inner loops (holes). 186 * @param loop The loop to check 187 * @return Whether the loop is clockwise 188 */ 189 func isClockwise(iterator GeoIterator) bool { 190 return isClockwiseNormalized(iterator, false) 191 }