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 }