github.com/masterhung0112/hk_server/v5@v5.0.0-20220302090640-ec71aef15e1c/model/geo_polygon.go (about)

     1  package model
     2  
     3  import (
     4  	"math"
     5  )
     6  
     7  // A GeoPolygon is carved out of a 2D plane by a set of (possibly disjoint) contours.
     8  // It can thus contain holes, and can be self-intersecting.
     9  type GeoPolygon struct {
    10  	points []*GeoPoint
    11  }
    12  
    13  // NewPolygon: Creates and returns a new pointer to a GeoPolygon
    14  // composed of the passed in points.  Points are
    15  // considered to be in order such that the last GeoPoint
    16  // forms an edge with the first GeoPoint.
    17  func NewPolygon(points []*GeoPoint) *GeoPolygon {
    18  	return &GeoPolygon{points: points}
    19  }
    20  
    21  // Points returns the points of the current GeoPolygon.
    22  func (p *GeoPolygon) Points() []*GeoPoint {
    23  	return p.points
    24  }
    25  
    26  // Add: Appends the passed in contour to the current GeoPolygon.
    27  func (p *GeoPolygon) Add(GeoPoint *GeoPoint) {
    28  	p.points = append(p.points, GeoPoint)
    29  }
    30  
    31  // IsClosed returns whether or not the GeoPolygon is closed.
    32  // TODO:  This can obviously be improved, but for now,
    33  //        this should be sufficient for detecting if points
    34  //        are contained using the raycast algorithm.
    35  func (p *GeoPolygon) IsClosed() bool {
    36  	if len(p.points) < 3 {
    37  		return false
    38  	}
    39  
    40  	return true
    41  }
    42  
    43  // Contains returns whether or not the current GeoPolygon contains the passed in GeoPoint.
    44  func (p *GeoPolygon) Contains(GeoPoint *GeoPoint) bool {
    45  	if !p.IsClosed() {
    46  		return false
    47  	}
    48  
    49  	start := len(p.points) - 1
    50  	end := 0
    51  
    52  	contains := p.intersectsWithRaycast(GeoPoint, p.points[start], p.points[end])
    53  
    54  	for i := 1; i < len(p.points); i++ {
    55  		if p.intersectsWithRaycast(GeoPoint, p.points[i-1], p.points[i]) {
    56  			contains = !contains
    57  		}
    58  	}
    59  
    60  	return contains
    61  }
    62  
    63  // Using the raycast algorithm, this returns whether or not the passed in GeoPoint
    64  // Intersects with the edge drawn by the passed in start and end points.
    65  // Original implementation: http://rosettacode.org/wiki/Ray-casting_algorithm#Go
    66  func (p *GeoPolygon) intersectsWithRaycast(GeoPoint *GeoPoint, start *GeoPoint, end *GeoPoint) bool {
    67  	// Always ensure that the the first GeoPoint
    68  	// has a y coordinate that is less than the second GeoPoint
    69  	if start.Lng > end.Lng {
    70  
    71  		// Switch the points if otherwise.
    72  		start, end = end, start
    73  
    74  	}
    75  
    76  	// Move the GeoPoint's y coordinate
    77  	// outside of the bounds of the testing region
    78  	// so we can start drawing a ray
    79  	for GeoPoint.Lng == start.Lng || GeoPoint.Lng == end.Lng {
    80  		newLng := math.Nextafter(GeoPoint.Lng, math.Inf(1))
    81  		GeoPoint = NewPoint(GeoPoint.Lat, newLng)
    82  	}
    83  
    84  	// If we are outside of the GeoPolygon, indicate so.
    85  	if GeoPoint.Lng < start.Lng || GeoPoint.Lng > end.Lng {
    86  		return false
    87  	}
    88  
    89  	if start.Lat > end.Lat {
    90  		if GeoPoint.Lat > start.Lat {
    91  			return false
    92  		}
    93  		if GeoPoint.Lat < end.Lat {
    94  			return true
    95  		}
    96  
    97  	} else {
    98  		if GeoPoint.Lat > end.Lat {
    99  			return false
   100  		}
   101  		if GeoPoint.Lat < start.Lat {
   102  			return true
   103  		}
   104  	}
   105  
   106  	raySlope := (GeoPoint.Lng - start.Lng) / (GeoPoint.Lat - start.Lat)
   107  	diagSlope := (end.Lng - start.Lng) / (end.Lat - start.Lat)
   108  
   109  	return raySlope >= diagSlope
   110  }