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  }