Files
astro/internal/geodata/polygon_union.go
T

903 lines
28 KiB
Go
Raw Normal View History

package geodata
import (
"fmt"
"math"
"sort"
)
const polygonUnionEpsilon = 1e-9
const polygonUnionProbeOffset = 1e-7
const polygonUnionSnapGrid = 1e-7
const polygonUnionNodeGrid = 1e-7
const polygonUnionContainmentToleranceKM = 1.0
const polygonUnionLocationBinCount = 64
type polygonUnionPoint struct {
x float64
y float64
}
type polygonUnionRing struct {
points []polygonUnionPoint
edges []polygonUnionSourceEdge
locationEdges []polygonUnionSourceEdge
locationBins [][]polygonUnionSourceEdge
params [][]float64
minX float64
maxX float64
minY float64
maxY float64
}
type polygonUnionSourceEdge struct {
start polygonUnionPoint
end polygonUnionPoint
minX float64
maxX float64
minY float64
maxY float64
}
type polygonUnionSourceEdgeRef struct {
ringIndex int
edgeIndex int
edge polygonUnionSourceEdge
}
type polygonUnionEdge struct {
start polygonUnionPoint
end polygonUnionPoint
}
type polygonUnionNode struct {
x int64
y int64
}
type polygonUnionEdgeKey struct {
start polygonUnionNode
end polygonUnionNode
}
// UnionPolygons 合并相互重叠的地理多边形环,不相交的输入保持分离。
// UnionPolygons merges overlapping geographic polygon rings. Ordinary small
// regions use the fast equirectangular branch; polar or antimeridian regions
// are rotated to a local spherical chart first, so the planar edge splitter
// never sees a coordinate singularity. Disjoint inputs remain disjoint.
func UnionPolygons(polygons [][]GeoPoint) ([][]GeoPoint, error) {
if polygonUnionNeedsSphericalChart(polygons) {
// Preserve the numerically stable legacy result when it already retains
// every input boundary and pole cap. The rotated chart is a recovery
// path for the singular cases; applying it to an ordinary antimeridian
// band can unnecessarily change the displayed equirectangular chord.
// 绕极环在平面图幅里无法闭合,平面并集必然失败,不必先算一遍。
if !polygonUnionWindsAroundPole(polygons) {
if planar, err := unionPolygonsPlanar(polygons); err == nil &&
polygonUnionContainsInputs(planar, polygons) {
return planar, nil
}
}
result, err := unionPolygonsSphericalChart(polygons)
if err == nil {
return result, nil
}
if snapped := snapPolygonUnionInputs(polygons); snapped != nil {
if result, retryErr := unionPolygonsSphericalChart(snapped); retryErr == nil {
return result, nil
}
}
return nil, err
}
result, err := unionPolygonsPlanar(polygons)
if err == nil {
return result, nil
}
if snapped := snapPolygonUnionInputs(polygons); snapped != nil {
if result, retryErr := unionPolygonsPlanar(snapped); retryErr == nil {
return result, nil
}
}
return nil, err
}
// snapPolygonUnionInputs retries a failed boolean join after quantizing input
// vertices to a centimetre-scale angular grid. Intersections generated from
// adjacent temporal footprints can differ by a few nanodegrees; the primary
// union keeps full precision, while this bounded retry only closes that
// numerical seam when the exact join cannot form a ring.
func snapPolygonUnionInputs(polygons [][]GeoPoint) [][]GeoPoint {
if len(polygons) == 0 {
return nil
}
result := make([][]GeoPoint, len(polygons))
changed := false
for polygonIndex, polygon := range polygons {
if len(polygon) < 3 {
return nil
}
result[polygonIndex] = make([]GeoPoint, len(polygon))
for pointIndex, point := range polygon {
longitude := math.Round(point.Longitude/polygonUnionSnapGrid) * polygonUnionSnapGrid
latitude := math.Round(point.Latitude/polygonUnionSnapGrid) * polygonUnionSnapGrid
result[polygonIndex][pointIndex] = GeoPoint{Longitude: longitude, Latitude: latitude}
changed = changed || longitude != point.Longitude || latitude != point.Latitude
}
}
if !changed {
return nil
}
return result
}
func polygonUnionContainsInputs(result, inputs [][]GeoPoint) bool {
if !SphericalPolygonsContainPathsWithinKM(
result, inputs, true, polygonUnionContainmentToleranceKM,
) {
return false
}
for _, input := range inputs {
open := openGeoRing(input)
if !polygonUnionRingWindsAroundPole(open) {
continue
}
for _, pole := range []GeoPoint{{Longitude: 0, Latitude: 90}, {Longitude: 0, Latitude: -90}} {
if sphericalPolygonContainsOrTouches(open, pole) &&
!SphericalPolygonsContainPaths(result, [][]GeoPoint{{pole}}, false) {
return false
}
}
}
return true
}
// polygonUnionWindsAroundPole 报告是否有环绕极点一圈。
func polygonUnionWindsAroundPole(polygons [][]GeoPoint) bool {
for _, polygon := range polygons {
if polygonUnionRingWindsAroundPole(openGeoRing(polygon)) {
return true
}
}
return false
}
// polygonUnionRingWindsAroundPole 用与 polygonUnionRings 相同的展开闭合判定识别绕极环。
func polygonUnionRingWindsAroundPole(ring []GeoPoint) bool {
if len(ring) < 3 {
return false
}
last := ring[0].Longitude
for _, point := range ring[1:] {
last += math.Remainder(point.Longitude-last, 360)
}
closure := last + math.Remainder(ring[0].Longitude-last, 360)
return math.Abs(closure-ring[0].Longitude) > 180
}
func unionPolygonsPlanar(polygons [][]GeoPoint) ([][]GeoPoint, error) {
rings, err := polygonUnionRings(polygons)
if err != nil {
return nil, err
}
polygonUnionAddIntersections(rings)
edges := polygonUnionOuterEdges(rings)
if len(edges) == 0 {
return nil, fmt.Errorf("polygon union has no outer edges")
}
result, err := polygonUnionJoinEdges(edges)
if err != nil {
return nil, err
}
if len(result) == 0 {
return nil, fmt.Errorf("polygon union has no usable rings")
}
return result, nil
}
type polygonUnionChart struct {
xAxis geoVector3
yAxis geoVector3
zAxis geoVector3
}
func polygonUnionNeedsSphericalChart(polygons [][]GeoPoint) bool {
for _, polygon := range polygons {
if len(polygon) < 3 {
continue
}
open := openGeoRing(polygon)
for index, point := range open {
if math.Abs(point.Latitude) >= 70 {
return true
}
next := open[(index+1)%len(open)]
if math.Abs(next.Longitude-point.Longitude) > 180 {
return true
}
}
}
return false
}
func unionPolygonsSphericalChart(polygons [][]GeoPoint) ([][]GeoPoint, error) {
chart, ok := newPolygonUnionChart(polygons)
if !ok {
return unionPolygonsPlanar(polygons)
}
projected := make([][]GeoPoint, len(polygons))
for polygonIndex, polygon := range polygons {
projected[polygonIndex] = make([]GeoPoint, len(polygon))
for pointIndex, point := range polygon {
projected[polygonIndex][pointIndex] = chart.project(point)
}
}
merged, err := unionPolygonsPlanar(projected)
if err != nil {
return nil, err
}
result := make([][]GeoPoint, len(merged))
for polygonIndex, polygon := range merged {
result[polygonIndex] = make([]GeoPoint, len(polygon))
for pointIndex, point := range polygon {
result[polygonIndex][pointIndex] = chart.unproject(point)
}
}
return result, nil
}
func newPolygonUnionChart(polygons [][]GeoPoint) (polygonUnionChart, bool) {
center := geoVector3{}
var first geoVector3
haveFirst := false
for _, polygon := range polygons {
for _, point := range openGeoRing(polygon) {
vector := geoPointVector(point)
center = geoVectorAdd(center, vector)
if !haveFirst {
first = vector
haveFirst = true
}
}
}
center, ok := geoVectorNormalize(center)
if !ok {
center, ok = geoVectorNormalize(first)
if !ok {
return polygonUnionChart{}, false
}
}
globalNorth := geoVector3{z: 1}
zAxis, ok := geoVectorNormalize(geoVectorAdd(globalNorth, geoVectorScale(center, -geoVectorDot(globalNorth, center))))
if !ok {
zAxis, ok = geoVectorNormalize(geoVectorAdd(geoVector3{x: 1}, geoVectorScale(center, -center.x)))
if !ok {
return polygonUnionChart{}, false
}
}
yAxis, ok := geoVectorNormalize(geoVectorCross(zAxis, center))
if !ok {
return polygonUnionChart{}, false
}
return polygonUnionChart{xAxis: center, yAxis: yAxis, zAxis: zAxis}, true
}
func (chart polygonUnionChart) project(point GeoPoint) GeoPoint {
vector := geoPointVector(point)
return GeoPoint{
Longitude: math.Atan2(geoVectorDot(vector, chart.yAxis), geoVectorDot(vector, chart.xAxis)) * 180 / math.Pi,
Latitude: math.Asin(math.Max(-1, math.Min(1, geoVectorDot(vector, chart.zAxis)))) * 180 / math.Pi,
}
}
func (chart polygonUnionChart) unproject(point GeoPoint) GeoPoint {
latitude := point.Latitude * math.Pi / 180
longitude := point.Longitude * math.Pi / 180
cosLatitude := math.Cos(latitude)
vector := geoVectorAdd(
geoVectorScale(chart.xAxis, cosLatitude*math.Cos(longitude)),
geoVectorAdd(
geoVectorScale(chart.yAxis, cosLatitude*math.Sin(longitude)),
geoVectorScale(chart.zAxis, math.Sin(latitude)),
),
)
return geoVectorPoint(vector)
}
func polygonUnionRings(polygons [][]GeoPoint) ([]polygonUnionRing, error) {
rings := make([]polygonUnionRing, 0, len(polygons))
reference := 0.0
haveReference := false
for polygonIndex, source := range polygons {
source = openGeoRing(source)
if len(source) < 3 {
return nil, fmt.Errorf("polygon %d requires at least three points", polygonIndex)
}
points := make([]polygonUnionPoint, len(source))
points[0] = polygonUnionPoint{x: source[0].Longitude, y: source[0].Latitude}
for index := 1; index < len(source); index++ {
points[index] = polygonUnionPoint{
x: points[index-1].x + math.Remainder(source[index].Longitude-points[index-1].x, 360),
y: source[index].Latitude,
}
}
// A pole-winding ring closes one full longitude turn away. Joining it
// directly in this chart creates an artificial chord through its interior;
// let UnionPolygons retry in the rotated spherical chart instead.
closure := points[len(points)-1].x + math.Remainder(points[0].x-points[len(points)-1].x, 360)
if math.Abs(closure-points[0].x) > 180 {
return nil, fmt.Errorf("polygon %d winds around a chart pole", polygonIndex)
}
mean := polygonUnionMeanLongitude(points)
if !haveReference {
reference = mean
haveReference = true
} else {
shift := math.Round((reference-mean)/360) * 360
for index := range points {
points[index].x += shift
}
}
ring := polygonUnionRingForPoints(points)
ring.params = make([][]float64, len(points))
for index := range ring.params {
ring.params[index] = []float64{0, 1}
}
rings = append(rings, ring)
}
if len(rings) == 0 {
return nil, fmt.Errorf("polygon union requires at least one polygon")
}
return rings, nil
}
func polygonUnionRingForPoints(points []polygonUnionPoint) polygonUnionRing {
if len(points) == 0 {
return polygonUnionRing{points: points}
}
edges := make([]polygonUnionSourceEdge, len(points))
for index, start := range points {
end := points[(index+1)%len(points)]
edges[index] = polygonUnionSourceEdge{
start: start, end: end,
minX: math.Min(start.x, end.x), maxX: math.Max(start.x, end.x),
minY: math.Min(start.y, end.y), maxY: math.Max(start.y, end.y),
}
}
minX, maxX := points[0].x, points[0].x
minY, maxY := points[0].y, points[0].y
for _, point := range points[1:] {
minX, maxX = math.Min(minX, point.x), math.Max(maxX, point.x)
minY, maxY = math.Min(minY, point.y), math.Max(maxY, point.y)
}
locationEdges := append([]polygonUnionSourceEdge(nil), edges...)
sort.Slice(locationEdges, func(first, second int) bool {
if locationEdges[first].minY != locationEdges[second].minY {
return locationEdges[first].minY < locationEdges[second].minY
}
return locationEdges[first].maxY < locationEdges[second].maxY
})
return polygonUnionRing{
points: points, edges: edges, locationEdges: locationEdges,
locationBins: polygonUnionBuildLocationBins(edges, minY, maxY),
minX: minX, maxX: maxX, minY: minY, maxY: maxY,
}
}
func polygonUnionAddIntersections(rings []polygonUnionRing) {
edges := make([]polygonUnionSourceEdgeRef, 0)
for ringIndex, ring := range rings {
for edgeIndex, edge := range ring.edges {
edges = append(edges, polygonUnionSourceEdgeRef{
ringIndex: ringIndex,
edgeIndex: edgeIndex,
edge: edge,
})
}
}
sort.Slice(edges, func(first, second int) bool {
a, b := edges[first], edges[second]
if a.edge.minX != b.edge.minX {
return a.edge.minX < b.edge.minX
}
if a.edge.maxX != b.edge.maxX {
return a.edge.maxX < b.edge.maxX
}
if a.ringIndex != b.ringIndex {
return a.ringIndex < b.ringIndex
}
return a.edgeIndex < b.edgeIndex
})
for firstIndex, first := range edges {
for secondIndex := firstIndex + 1; secondIndex < len(edges); secondIndex++ {
second := edges[secondIndex]
if second.edge.minX > first.edge.maxX+polygonUnionEpsilon {
break
}
if first.ringIndex == second.ringIndex && polygonUnionEdgesAdjacent(
first.edgeIndex, second.edgeIndex, len(rings[first.ringIndex].points),
) {
continue
}
if !polygonUnionEdgeBoundsOverlap(first.edge, second.edge) {
continue
}
firstParams, secondParams := polygonUnionSegmentIntersections(
first.edge.start, first.edge.end, second.edge.start, second.edge.end,
)
firstRing := &rings[first.ringIndex]
secondRing := &rings[second.ringIndex]
firstRing.params[first.edgeIndex] = append(firstRing.params[first.edgeIndex], firstParams...)
secondRing.params[second.edgeIndex] = append(secondRing.params[second.edgeIndex], secondParams...)
}
}
}
func polygonUnionEdgeBoundsOverlap(first, second polygonUnionSourceEdge) bool {
return first.minX <= second.maxX+polygonUnionEpsilon && second.minX <= first.maxX+polygonUnionEpsilon &&
first.minY <= second.maxY+polygonUnionEpsilon && second.minY <= first.maxY+polygonUnionEpsilon
}
func polygonUnionEdgesAdjacent(first, second, count int) bool {
return (first+1)%count == second || (second+1)%count == first
}
func polygonUnionSegmentIntersections(
firstStart, firstEnd, secondStart, secondEnd polygonUnionPoint,
) ([]float64, []float64) {
firstDelta := polygonUnionSubtract(firstEnd, firstStart)
secondDelta := polygonUnionSubtract(secondEnd, secondStart)
offset := polygonUnionSubtract(secondStart, firstStart)
denominator := polygonUnionCross(firstDelta, secondDelta)
if math.Abs(denominator) > polygonUnionEpsilon {
firstParam := polygonUnionCross(offset, secondDelta) / denominator
secondParam := polygonUnionCross(offset, firstDelta) / denominator
if firstParam < -polygonUnionEpsilon || firstParam > 1+polygonUnionEpsilon ||
secondParam < -polygonUnionEpsilon || secondParam > 1+polygonUnionEpsilon {
return nil, nil
}
return []float64{polygonUnionClampParam(firstParam)}, []float64{polygonUnionClampParam(secondParam)}
}
if math.Abs(polygonUnionCross(offset, firstDelta)) > polygonUnionEpsilon {
return nil, nil
}
firstParams := make([]float64, 0, 2)
secondParams := make([]float64, 0, 2)
for _, point := range []polygonUnionPoint{secondStart, secondEnd} {
if value, ok := polygonUnionPointSegmentParam(point, firstStart, firstEnd); ok {
firstParams = append(firstParams, value)
}
}
for _, point := range []polygonUnionPoint{firstStart, firstEnd} {
if value, ok := polygonUnionPointSegmentParam(point, secondStart, secondEnd); ok {
secondParams = append(secondParams, value)
}
}
return firstParams, secondParams
}
func polygonUnionPointSegmentParam(point, start, end polygonUnionPoint) (float64, bool) {
delta := polygonUnionSubtract(end, start)
lengthSquared := delta.x*delta.x + delta.y*delta.y
if lengthSquared <= polygonUnionEpsilon*polygonUnionEpsilon {
return 0, false
}
value := ((point.x-start.x)*delta.x + (point.y-start.y)*delta.y) / lengthSquared
if value < -polygonUnionEpsilon || value > 1+polygonUnionEpsilon {
return 0, false
}
projected := polygonUnionInterpolate(start, end, value)
if math.Hypot(projected.x-point.x, projected.y-point.y) > polygonUnionEpsilon {
return 0, false
}
return polygonUnionClampParam(value), true
}
func polygonUnionOuterEdges(rings []polygonUnionRing) []polygonUnionEdge {
edges := make(map[polygonUnionEdgeKey]polygonUnionEdge)
for ringIndex, ring := range rings {
for edgeIndex, start := range ring.points {
end := ring.points[(edgeIndex+1)%len(ring.points)]
params := polygonUnionUniqueParams(ring.params[edgeIndex])
for index := 1; index < len(params); index++ {
firstParam, secondParam := params[index-1], params[index]
if secondParam-firstParam <= polygonUnionEpsilon {
continue
}
pieceStart := polygonUnionInterpolate(start, end, firstParam)
pieceEnd := polygonUnionInterpolate(start, end, secondParam)
delta := polygonUnionSubtract(pieceEnd, pieceStart)
length := math.Hypot(delta.x, delta.y)
if length <= polygonUnionEpsilon {
continue
}
middle := polygonUnionInterpolate(pieceStart, pieceEnd, 0.5)
normal := polygonUnionPoint{
x: -delta.y / length * polygonUnionProbeOffset,
y: delta.x / length * polygonUnionProbeOffset,
}
leftInside := polygonUnionInsideAnyRing(polygonUnionAdd(middle, normal), rings, ringIndex)
rightInside := polygonUnionInsideAnyRing(polygonUnionSubtract(middle, normal), rings, ringIndex)
if leftInside == rightInside {
continue
}
if rightInside {
pieceStart, pieceEnd = pieceEnd, pieceStart
}
edge := polygonUnionEdge{start: pieceStart, end: pieceEnd}
key := polygonUnionKey(edge)
edges[key] = edge
}
}
}
result := make([]polygonUnionEdge, 0, len(edges))
for _, edge := range edges {
result = append(result, edge)
}
sort.Slice(result, func(first, second int) bool {
a, b := result[first], result[second]
if a.start.x != b.start.x {
return a.start.x < b.start.x
}
if a.start.y != b.start.y {
return a.start.y < b.start.y
}
if a.end.x != b.end.x {
return a.end.x < b.end.x
}
return a.end.y < b.end.y
})
return result
}
func polygonUnionInsideAnyRing(point polygonUnionPoint, rings []polygonUnionRing, preferred int) bool {
if preferred >= 0 && preferred < len(rings) && polygonUnionRingContainsPoint(point, rings[preferred]) {
return true
}
for index, ring := range rings {
if index != preferred && polygonUnionRingContainsPoint(point, ring) {
return true
}
}
return false
}
func polygonUnionRingContainsPoint(point polygonUnionPoint, ring polygonUnionRing) bool {
margin := polygonUnionProbeOffset + polygonUnionEpsilon
if point.x < ring.minX-margin || point.x > ring.maxX+margin ||
point.y < ring.minY-margin || point.y > ring.maxY+margin {
return false
}
return polygonUnionPointLocationInRing(point, ring) >= 0
}
func polygonUnionJoinEdges(edges []polygonUnionEdge) ([][]GeoPoint, error) {
outgoing := make(map[polygonUnionNode][]int, len(edges))
for index, edge := range edges {
outgoing[polygonUnionNodeForPoint(edge.start)] = append(
outgoing[polygonUnionNodeForPoint(edge.start)], index,
)
}
used := make([]bool, len(edges))
type resultRing struct {
points []GeoPoint
area float64
}
results := make([]resultRing, 0, 2)
// chartRings 保留图幅坐标,用于并集后检测孔洞。
chartRings := make([][]polygonUnionPoint, 0, 2)
for firstEdgeIndex := range edges {
if used[firstEdgeIndex] {
continue
}
firstNode := polygonUnionNodeForPoint(edges[firstEdgeIndex].start)
currentEdgeIndex := firstEdgeIndex
points := make([]polygonUnionPoint, 0, len(edges))
for step := 0; step <= len(edges); step++ {
if used[currentEdgeIndex] {
return nil, fmt.Errorf("polygon union outer edges form a repeated branch")
}
current := edges[currentEdgeIndex]
used[currentEdgeIndex] = true
points = append(points, current.start)
nextNode := polygonUnionNodeForPoint(current.end)
if nextNode == firstNode {
break
}
candidates := outgoing[nextNode]
nextEdgeIndex := -1
for _, candidate := range candidates {
if used[candidate] {
continue
}
if nextEdgeIndex < 0 || polygonUnionClockwiseTurn(
current, edges[candidate],
) < polygonUnionClockwiseTurn(current, edges[nextEdgeIndex]) {
nextEdgeIndex = candidate
}
}
if nextEdgeIndex < 0 {
return nil, fmt.Errorf("polygon union outer edges are open at %.9f, %.9f", current.end.x, current.end.y)
}
currentEdgeIndex = nextEdgeIndex
}
points = polygonUnionDeduplicatePoints(points)
area := polygonUnionSignedArea(points)
if len(points) < 3 || math.Abs(area) <= polygonUnionEpsilon {
continue
}
if area < 0 {
polygonUnionReversePoints(points)
area = -area
}
geographic := make([]GeoPoint, len(points))
for index, point := range points {
geographic[index] = GeoPoint{
Longitude: polygonUnionNormalizeLongitude(point.x),
Latitude: point.y,
}
}
results = append(results, resultRing{points: geographic, area: area})
chartRings = append(chartRings, points)
}
// 环列表无法表达孔洞(内外边界都会当成实体面),检测到严格包含就报错。
if inner, outer, ratio, found := polygonUnionNestedRing(chartRings); found && ratio >= polygonUnionHoleAreaFraction {
return nil, fmt.Errorf(
"polygon union produced a hole (ring %d, area %.6g lies strictly inside ring %d, area %.6g); a ring list cannot represent it",
inner, math.Abs(polygonUnionSignedArea(chartRings[inner])),
outer, math.Abs(polygonUnionSignedArea(chartRings[outer])),
)
}
sort.Slice(results, func(first, second int) bool { return results[first].area > results[second].area })
polygons := make([][]GeoPoint, len(results))
for index, result := range results {
polygons[index] = result.points
}
return polygons, nil
}
// polygonUnionHoleAreaFraction 是判定真孔洞的相对面积下限:低于它按数值细条处理,达到或超过则报错。
const polygonUnionHoleAreaFraction = 0.01
// polygonUnionNestedRing 找出被另一个环严格包含的环(孔洞);贴边不算包含。
func polygonUnionNestedRing(rings [][]polygonUnionPoint) (inner, outer int, ratio float64, found bool) {
bestRatio := 0.0
for candidate := range rings {
candidateArea := math.Abs(polygonUnionSignedArea(rings[candidate]))
for container := range rings {
if candidate == container {
continue
}
strictlyInside := false
contained := true
for _, vertex := range rings[candidate] {
switch polygonUnionPointLocation(vertex, rings[container]) {
case 1:
strictlyInside = true
case 0:
// 贴边:不改变判定
default:
contained = false
}
if !contained {
break
}
}
if !contained || !strictlyInside {
continue
}
containerArea := math.Abs(polygonUnionSignedArea(rings[container]))
if containerArea <= 0 {
continue
}
if candidateRatio := candidateArea / containerArea; candidateRatio > bestRatio {
bestRatio, inner, outer, found = candidateRatio, candidate, container, true
}
}
}
return inner, outer, bestRatio, found
}
func polygonUnionClockwiseTurn(incoming, outgoing polygonUnionEdge) float64 {
incomingAngle := math.Atan2(incoming.end.y-incoming.start.y, incoming.end.x-incoming.start.x)
reverseAngle := incomingAngle + math.Pi
outgoingAngle := math.Atan2(outgoing.end.y-outgoing.start.y, outgoing.end.x-outgoing.start.x)
turn := math.Mod(reverseAngle-outgoingAngle, 2*math.Pi)
if turn < 0 {
turn += 2 * math.Pi
}
return turn
}
// polygonUnionPointLocation returns 1 inside, 0 on the boundary, and -1 outside.
func polygonUnionPointLocation(point polygonUnionPoint, ring []polygonUnionPoint) int {
inside := false
for index, start := range ring {
end := ring[(index+1)%len(ring)]
if _, ok := polygonUnionPointSegmentParam(point, start, end); ok {
return 0
}
if (start.y > point.y) != (end.y > point.y) {
intersectionX := start.x + (end.x-start.x)*(point.y-start.y)/(end.y-start.y)
if point.x < intersectionX {
inside = !inside
}
}
}
if inside {
return 1
}
return -1
}
func polygonUnionPointLocationOnEdges(
point polygonUnionPoint,
edges []polygonUnionSourceEdge,
) int {
inside := false
for _, edge := range edges {
if edge.minY > point.y+polygonUnionEpsilon {
break
}
if edge.maxY < point.y-polygonUnionEpsilon {
continue
}
if point.x >= edge.minX-polygonUnionEpsilon && point.x <= edge.maxX+polygonUnionEpsilon &&
point.y >= edge.minY-polygonUnionEpsilon && point.y <= edge.maxY+polygonUnionEpsilon {
if _, ok := polygonUnionPointSegmentParam(point, edge.start, edge.end); ok {
return 0
}
}
start := edge.start
end := edge.end
if (start.y > point.y) != (end.y > point.y) {
intersectionX := start.x + (end.x-start.x)*(point.y-start.y)/(end.y-start.y)
if point.x < intersectionX {
inside = !inside
}
}
}
if inside {
return 1
}
return -1
}
func polygonUnionBuildLocationBins(
edges []polygonUnionSourceEdge,
minimumY, maximumY float64,
) [][]polygonUnionSourceEdge {
bins := make([][]polygonUnionSourceEdge, polygonUnionLocationBinCount)
for _, edge := range edges {
first := polygonUnionLocationBin(edge.minY-polygonUnionEpsilon, minimumY, maximumY)
last := polygonUnionLocationBin(edge.maxY+polygonUnionEpsilon, minimumY, maximumY)
for index := first; index <= last; index++ {
bins[index] = append(bins[index], edge)
}
}
for index := range bins {
sort.Slice(bins[index], func(first, second int) bool {
if bins[index][first].minY != bins[index][second].minY {
return bins[index][first].minY < bins[index][second].minY
}
return bins[index][first].maxY < bins[index][second].maxY
})
}
return bins
}
func polygonUnionPointLocationInRing(point polygonUnionPoint, ring polygonUnionRing) int {
if len(ring.locationBins) != polygonUnionLocationBinCount {
return polygonUnionPointLocationOnEdges(point, ring.locationEdges)
}
index := polygonUnionLocationBin(point.y, ring.minY, ring.maxY)
return polygonUnionPointLocationOnEdges(point, ring.locationBins[index])
}
func polygonUnionLocationBin(value, minimum, maximum float64) int {
if maximum <= minimum {
return 0
}
index := int((value - minimum) / (maximum - minimum) * polygonUnionLocationBinCount)
if index < 0 {
return 0
}
if index >= polygonUnionLocationBinCount {
return polygonUnionLocationBinCount - 1
}
return index
}
func polygonUnionUniqueParams(values []float64) []float64 {
sort.Float64s(values)
result := values[:0]
for _, value := range values {
value = polygonUnionClampParam(value)
if len(result) == 0 || value-result[len(result)-1] > polygonUnionEpsilon {
result = append(result, value)
}
}
return result
}
func polygonUnionDeduplicatePoints(points []polygonUnionPoint) []polygonUnionPoint {
result := points[:0]
for _, point := range points {
if len(result) == 0 || polygonUnionNodeForPoint(result[len(result)-1]) != polygonUnionNodeForPoint(point) {
result = append(result, point)
}
}
if len(result) > 1 && polygonUnionNodeForPoint(result[0]) == polygonUnionNodeForPoint(result[len(result)-1]) {
result = result[:len(result)-1]
}
return result
}
func polygonUnionSignedArea(points []polygonUnionPoint) float64 {
area := 0.0
for index, current := range points {
next := points[(index+1)%len(points)]
area += current.x*next.y - next.x*current.y
}
return area / 2
}
func polygonUnionMeanLongitude(points []polygonUnionPoint) float64 {
value := 0.0
for _, point := range points {
value += point.x
}
return value / float64(len(points))
}
func polygonUnionReversePoints(points []polygonUnionPoint) {
for left, right := 0, len(points)-1; left < right; left, right = left+1, right-1 {
points[left], points[right] = points[right], points[left]
}
}
func polygonUnionInterpolate(start, end polygonUnionPoint, fraction float64) polygonUnionPoint {
return polygonUnionPoint{
x: start.x + (end.x-start.x)*fraction,
y: start.y + (end.y-start.y)*fraction,
}
}
func polygonUnionSubtract(first, second polygonUnionPoint) polygonUnionPoint {
return polygonUnionPoint{x: first.x - second.x, y: first.y - second.y}
}
func polygonUnionAdd(first, second polygonUnionPoint) polygonUnionPoint {
return polygonUnionPoint{x: first.x + second.x, y: first.y + second.y}
}
func polygonUnionCross(first, second polygonUnionPoint) float64 {
return first.x*second.y - first.y*second.x
}
func polygonUnionClampParam(value float64) float64 {
return math.Max(0, math.Min(1, value))
}
func polygonUnionNodeForPoint(point polygonUnionPoint) polygonUnionNode {
return polygonUnionNode{
x: int64(math.Round(point.x / polygonUnionNodeGrid)),
y: int64(math.Round(point.y / polygonUnionNodeGrid)),
}
}
func polygonUnionKey(edge polygonUnionEdge) polygonUnionEdgeKey {
return polygonUnionEdgeKey{
start: polygonUnionNodeForPoint(edge.start),
end: polygonUnionNodeForPoint(edge.end),
}
}
func polygonUnionNormalizeLongitude(value float64) float64 {
value = math.Mod(value+180, 360)
if value < 0 {
value += 360
}
return value - 180
}