package geodata import ( "errors" "fmt" "math" ) // ErrOpenBoundaryEndpointTrackReversal 表示扫掠边界的端点轨迹发生了物理折返。 // ErrOpenBoundaryEndpointTrackReversal reports a physical reversal along a swept boundary endpoint track. var ErrOpenBoundaryEndpointTrackReversal = errors.New("open boundary endpoint track reversal") const ( openBoundaryEndpointStepNoiseKM = 1.0 openBoundaryEndpointProgressBackKM = 5.0 ) // OpenBoundarySweepSample 是用于构造连续扫掠区域的一个边界样本。 // OpenBoundarySweepSample is one sampled boundary used to build a continuous // sweep. Closed samples are ignored because they already describe an // instantaneous footprint and are not part of an open non-central band. type OpenBoundarySweepSample struct { Boundaries [][]GeoPoint Closed bool } type openBoundarySweepSample struct { boundary []GeoPoint start GeoPoint end GeoPoint } // OpenBoundarySweep 将开放边界弧连接为连续扫掠多边形。 // OpenBoundarySweep joins open boundary arcs into continuous swept polygons. // The first and last arcs close each group; the matching endpoint tracks form // the two sides. A large endpoint jump starts a new group instead of creating // a false bridge across a branch change. func OpenBoundarySweep(samples []OpenBoundarySweepSample) ([][]GeoPoint, error) { groups, err := openBoundarySweepGroups(samples) if err != nil { return nil, err } polygons := make([][]GeoPoint, 0, len(groups)) for _, group := range groups { groupPolygons, groupErr := sweepOpenBoundaryGroup(group) if groupErr != nil { return nil, groupErr } polygons = append(polygons, groupPolygons...) } return polygons, nil } // MonotoneOpenBoundarySweep 连接端点轨迹和中间弧已知单调的开放弧。 // MonotoneOpenBoundarySweep joins open arcs whose endpoint tracks and // intermediate arcs are already known to be monotone. It avoids the general // containment audit used by OpenBoundarySweep and is intended for tightly // sampled, event-local contact caps. func MonotoneOpenBoundarySweep(samples []OpenBoundarySweepSample) ([][]GeoPoint, error) { groups, err := openBoundarySweepGroups(samples) if err != nil { return nil, err } polygons := make([][]GeoPoint, 0, len(groups)) for index, group := range groups { if !sweepEndpointTracksMonotone(group) { return nil, fmt.Errorf("%w in group %d", ErrOpenBoundaryEndpointTrackReversal, index) } outline := sweepOpenBoundaryOutline(group) if len(outline) < 3 { return nil, fmt.Errorf("monotone open boundary sweep group %d has no usable outline", index) } if !sweepOutlineContainsRepresentativeSamples(outline, group) { return nil, fmt.Errorf("monotone open boundary sweep group %d folds outside its endpoint tracks", index) } polygons = append(polygons, outline) } return polygons, nil } func sweepEndpointTracksMonotone(group []openBoundarySweepSample) bool { if len(group) < 3 { return true } for _, endpoint := range []func(openBoundarySweepSample) GeoPoint{ func(sample openBoundarySweepSample) GeoPoint { return sample.start }, func(sample openBoundarySweepSample) GeoPoint { return sample.end }, } { first := geoPointUnitVector(endpoint(group[0])) last := geoPointUnitVector(endpoint(group[len(group)-1])) direction := geoPointTangentDirection(first, last) if vectorNorm(direction) <= 1e-12 { continue } previousProgress := 0.0 previousVector := first var previousStep [3]float64 for index := 1; index < len(group); index++ { current := geoPointUnitVector(endpoint(group[index])) step := [3]float64{ current[0] - previousVector[0], current[1] - previousVector[1], current[2] - previousVector[2], } if index > 1 { previousLength := vectorNorm(previousStep) currentLength := vectorNorm(step) if math.Min(previousLength, currentLength)*6371.0088 > openBoundaryEndpointStepNoiseKM && vectorDot(previousStep, step) < -0.1*previousLength*currentLength { return false } } progress := vectorDot(current, direction) // A long spherical track can deviate slightly from its first-to-last // tangent chord even while every local step remains forward. Reserve a // few kilometres for that projection effect; larger gradual reversals // still invalidate the endpoint envelope. if progress+openBoundaryEndpointProgressBackKM/6371.0088 < previousProgress { return false } previousProgress = progress previousVector = current previousStep = step } } return true } func geoPointUnitVector(point GeoPoint) [3]float64 { latitude := point.Latitude * math.Pi / 180 longitude := point.Longitude * math.Pi / 180 cosLatitude := math.Cos(latitude) return [3]float64{ cosLatitude * math.Cos(longitude), cosLatitude * math.Sin(longitude), math.Sin(latitude), } } func geoPointTangentDirection(first, last [3]float64) [3]float64 { dot := vectorDot(first, last) return [3]float64{ last[0] - dot*first[0], last[1] - dot*first[1], last[2] - dot*first[2], } } func vectorDot(first, second [3]float64) float64 { return first[0]*second[0] + first[1]*second[1] + first[2]*second[2] } func vectorNorm(value [3]float64) float64 { return math.Sqrt(vectorDot(value, value)) } // OpenBoundaryEndpointOutlines 返回各开放边界分组的低成本端点轨迹轮廓。 // OpenBoundaryEndpointOutlines returns the inexpensive endpoint-track outline // for each open-boundary group. The outlines intentionally omit intermediate // arc bulges, so callers must combine them with the source footprint polygons; // they are not a standalone geometric union. func OpenBoundaryEndpointOutlines(samples []OpenBoundarySweepSample) ([][]GeoPoint, error) { groups, err := openBoundarySweepGroups(samples) if err != nil { return nil, err } polygons := make([][]GeoPoint, 0, len(groups)) for index, group := range groups { if len(group) < 2 { continue } outline := sweepOpenBoundaryOutline(group) if len(outline) < 3 { return nil, fmt.Errorf("open boundary endpoint group %d has no usable outline", index) } polygons = append(polygons, outline) } if len(polygons) == 0 { return nil, fmt.Errorf("open boundary samples contain no endpoint track") } return polygons, nil } // DecimateOpenBoundarySweepSamples 在保留端点和分组的前提下限制通用带状 union 的采样成本。 // DecimateOpenBoundarySweepSamples bounds the cost of the general ribbon // union while retaining every group endpoint and closed-sample separator. func DecimateOpenBoundarySweepSamples( samples []OpenBoundarySweepSample, maximumGroupSamples int, targetSpacingKM float64, ) []OpenBoundarySweepSample { if maximumGroupSamples < 2 { maximumGroupSamples = 2 } result := make([]OpenBoundarySweepSample, 0, len(samples)) flush := func(group []OpenBoundarySweepSample) { if len(group) == 0 { return } selected := group if len(group) > maximumGroupSamples { selected = make([]OpenBoundarySweepSample, 0, maximumGroupSamples) last := len(group) - 1 lastPosition := -1 for index := 0; index < maximumGroupSamples; index++ { position := int(math.Round(float64(index*last) / float64(maximumGroupSamples-1))) if position != lastPosition { selected = append(selected, group[position]) lastPosition = position } } } for _, sample := range selected { copySample := OpenBoundarySweepSample{Closed: sample.Closed} copySample.Boundaries = make([][]GeoPoint, len(sample.Boundaries)) for index, boundary := range sample.Boundaries { copySample.Boundaries[index] = decimateOpenBoundaryPoints(boundary, targetSpacingKM) } result = append(result, copySample) } } group := make([]OpenBoundarySweepSample, 0, maximumGroupSamples) for _, sample := range samples { if sample.Closed { flush(group) group = group[:0] if len(result) == 0 || !result[len(result)-1].Closed { result = append(result, OpenBoundarySweepSample{Closed: true}) } continue } group = append(group, sample) } flush(group) return result } // OpenBoundarySweepInnerCaps 闭合紧邻内部闭合阶段的小地平缺口。 // OpenBoundarySweepInnerCaps closes the small horizon gaps immediately next // to a closed-sample separator. Those nearly closed arcs overlap the paired // middle band; leaving their gap open can retain a narrow inward notch after // polygon union. func OpenBoundarySweepInnerCaps( samples []OpenBoundarySweepSample, maximumGapKM float64, ) [][]GeoPoint { if maximumGapKM <= 0 { return nil } indices := make([]int, 0, 2) for index := 0; index < len(samples); index++ { if !samples[index].Closed { continue } if index > 0 && !samples[index-1].Closed { indices = append(indices, index-1) } for index+1 < len(samples) && samples[index+1].Closed { index++ } if index+1 < len(samples) && !samples[index+1].Closed { indices = append(indices, index+1) } } caps := make([][]GeoPoint, 0, len(indices)) for _, index := range indices { boundary := openGeoRing(JoinPolylineSegments(samples[index].Boundaries)) if len(boundary) < 3 || geoPointDistanceKM(boundary[0], boundary[len(boundary)-1]) > maximumGapKM { continue } caps = append(caps, boundary) } return caps } func decimateOpenBoundaryPoints(points []GeoPoint, targetSpacingKM float64) []GeoPoint { if len(points) < 3 || targetSpacingKM <= 0 { return append([]GeoPoint(nil), points...) } result := make([]GeoPoint, 1, len(points)) result[0] = points[0] for _, point := range points[1 : len(points)-1] { if geoPointDistanceKM(result[len(result)-1], point) >= targetSpacingKM { result = append(result, point) } } if !SameGeoPoint(result[len(result)-1], points[len(points)-1]) { result = append(result, points[len(points)-1]) } return result } func sweepOutlineContainsRepresentativeSamples( outline []GeoPoint, group []openBoundarySweepSample, ) bool { const checksPerArc = 5 for _, sample := range group { for check := 0; check < checksPerArc; check++ { index := check * (len(sample.boundary) - 1) / (checksPerArc - 1) if !sweepPointInPolygon(outline, sample.boundary[index]) { return false } } } return true } func openBoundarySweepGroups(samples []OpenBoundarySweepSample) ([][]openBoundarySweepSample, error) { groups := make([][]openBoundarySweepSample, 0, 2) current := make([]openBoundarySweepSample, 0, len(samples)) var previous openBoundarySweepSample for _, source := range samples { // A closed footprint is an instantaneous cap, not an open arc. It // terminates the current open branch so later samples cannot bridge // across a visibility/type transition. if source.Closed { if len(current) > 0 { groups = append(groups, current) current = nil } previous = openBoundarySweepSample{} continue } boundary := JoinPolylineSegments(source.Boundaries) boundary = openGeoRing(boundary) if len(boundary) < 2 { continue } value := openBoundarySweepSample{boundary: boundary, start: boundary[0], end: boundary[len(boundary)-1]} if len(current) > 0 { keep := geoPointDistanceKM(previous.start, value.start) + geoPointDistanceKM(previous.end, value.end) reverse := geoPointDistanceKM(previous.start, value.end) + geoPointDistanceKM(previous.end, value.start) if reverse < keep { reverseSweepGeoPoints(value.boundary) value.start, value.end = value.end, value.start } if math.Max( geoPointDistanceKM(previous.start, value.start), geoPointDistanceKM(previous.end, value.end), ) > 2000 { groups = append(groups, current) current = nil } } current = append(current, value) previous = value } if len(current) > 0 { groups = append(groups, current) } if len(groups) == 0 { return nil, fmt.Errorf("open boundary samples contain no usable arcs") } return groups, nil } // sweepOpenBoundaryGroup constructs the union swept by a sequence of open // arcs. Each adjacent pair forms a ribbon whose caps are the two sampled arcs // and whose sides follow their endpoints. Unioning the ribbons retains any // intermediate arc that becomes part of a non-monotone outer boundary; a // convex hull would replace that boundary with visibly incorrect chords. func sweepOpenBoundaryGroup(group []openBoundarySweepSample) ([][]GeoPoint, error) { if len(group) == 0 { return nil, fmt.Errorf("open boundary sweep group is empty") } if len(group) == 1 { return [][]GeoPoint{append([]GeoPoint(nil), group[0].boundary...)}, nil } if outline := sweepOpenBoundaryOutline(group); len(outline) >= 3 && sweepOutlineContainsSamples(outline, group) { return [][]GeoPoint{outline}, nil } ribbons := make([][]GeoPoint, 0, len(group)-1) for index := 1; index < len(group); index++ { first := group[index-1].boundary second := group[index].boundary ring := make([]GeoPoint, 0, len(first)+len(second)) ring = append(ring, first...) for pointIndex := len(second) - 1; pointIndex >= 0; pointIndex-- { ring = append(ring, second[pointIndex]) } ring = sweepDeduplicateAdjacent(ring) if len(ring) < 3 { return nil, fmt.Errorf("open boundary sweep ribbon %d has no usable boundary", index-1) } ribbons = append(ribbons, ring) } polygons, err := UnionPolygons(ribbons) if err != nil { return nil, fmt.Errorf("open boundary sweep ribbons: %w", err) } polygons, err = sweepBridgeTouchingPolygons(polygons) if err != nil { return nil, fmt.Errorf("open boundary sweep touching ribbons: %w", err) } return polygons, nil } // sweepOpenBoundaryOutline is the simple outer envelope for a non-crossing // sequence of arcs. The two endpoint tracks are the lateral edges of the // swept band; using them directly avoids retaining every overlapping ribbon // edge as a false inward spike near a tangent horizon. func sweepOpenBoundaryOutline(group []openBoundarySweepSample) []GeoPoint { if len(group) < 2 { return nil } first, last := group[0].boundary, group[len(group)-1].boundary if len(first) < 2 || len(last) < 2 { return nil } ring := make([]GeoPoint, 0, len(first)+len(last)+2*len(group)) ring = appendUnwrappedSweepPoints(ring, first) for index := 1; index < len(group); index++ { ring = appendUnwrappedSweepPoint(ring, group[index].end) } for index := len(last) - 1; index >= 0; index-- { ring = appendUnwrappedSweepPoint(ring, last[index]) } for index := len(group) - 2; index >= 0; index-- { ring = appendUnwrappedSweepPoint(ring, group[index].start) } return sweepDeduplicateAdjacent(ring) } func appendUnwrappedSweepPoints(result []GeoPoint, points []GeoPoint) []GeoPoint { for _, point := range points { result = appendUnwrappedSweepPoint(result, point) } return result } func appendUnwrappedSweepPoint(result []GeoPoint, point GeoPoint) []GeoPoint { if len(result) == 0 { return append(result, point) } longitude := point.Longitude for longitude-result[len(result)-1].Longitude > 180 { longitude -= 360 } for longitude-result[len(result)-1].Longitude < -180 { longitude += 360 } return append(result, GeoPoint{Longitude: longitude, Latitude: point.Latitude}) } func sweepOutlineContainsSamples(outline []GeoPoint, group []openBoundarySweepSample) bool { if len(outline) < 3 { return false } for _, sample := range group { for _, point := range sample.boundary { if !sweepPointInPolygon(outline, point) { return false } } } return true } func sweepPointInPolygon(polygon []GeoPoint, point GeoPoint) bool { if len(polygon) < 3 { return false } longitude := point.Longitude for longitude-polygon[0].Longitude > 180 { longitude -= 360 } for longitude-polygon[0].Longitude < -180 { longitude += 360 } probe := GeoPoint{Longitude: longitude, Latitude: point.Latitude} for current, previous := 0, len(polygon)-1; current < len(polygon); previous, current = current, current+1 { if sweepPointOnSegment(probe, polygon[previous], polygon[current]) { return true } } inside := false for current, previous := 0, len(polygon)-1; current < len(polygon); previous, current = current, current+1 { first, second := polygon[previous], polygon[current] if (first.Latitude > point.Latitude) == (second.Latitude > point.Latitude) { continue } intersection := first.Longitude + (point.Latitude-first.Latitude)*(second.Longitude-first.Longitude)/(second.Latitude-first.Latitude) if intersection >= longitude { inside = !inside } } return inside } func sweepPointOnSegment(point, start, end GeoPoint) bool { deltaLongitude := end.Longitude - start.Longitude deltaLatitude := end.Latitude - start.Latitude length := math.Hypot(deltaLongitude, deltaLatitude) if length <= 1e-12 { return math.Hypot(point.Longitude-start.Longitude, point.Latitude-start.Latitude) <= 1e-9 } cross := deltaLongitude*(point.Latitude-start.Latitude) - deltaLatitude*(point.Longitude-start.Longitude) if math.Abs(cross) > 1e-9*length { return false } dot := (point.Longitude-start.Longitude)*deltaLongitude + (point.Latitude-start.Latitude)*deltaLatitude return dot >= -1e-9*length && dot <= length*length+1e-9*length } func sweepBridgeTouchingPolygons(polygons [][]GeoPoint) ([][]GeoPoint, error) { const ( touchingDistanceKM = 0.01 bridgeHalfSizeDeg = 1e-4 ) for len(polygons) > 1 { firstIndex, secondIndex, touchingPoint, ok := sweepTouchingPolygonPair(polygons, touchingDistanceKM) if !ok { return polygons, nil } bridge := []GeoPoint{ {Longitude: touchingPoint.Longitude - bridgeHalfSizeDeg, Latitude: touchingPoint.Latitude - bridgeHalfSizeDeg}, {Longitude: touchingPoint.Longitude + bridgeHalfSizeDeg, Latitude: touchingPoint.Latitude - bridgeHalfSizeDeg}, {Longitude: touchingPoint.Longitude + bridgeHalfSizeDeg, Latitude: touchingPoint.Latitude + bridgeHalfSizeDeg}, {Longitude: touchingPoint.Longitude - bridgeHalfSizeDeg, Latitude: touchingPoint.Latitude + bridgeHalfSizeDeg}, } // 输入是并集输出,环之间互不重叠,因此只需合并这一对。 merged, err := UnionPolygons([][]GeoPoint{polygons[firstIndex], polygons[secondIndex], bridge}) if err != nil { return nil, err } if len(merged) >= 2 { return polygons, nil } polygons = sweepReplaceTouchingPolygonPair(polygons, firstIndex, secondIndex, merged) } return polygons, nil } const sweepTouchingCellSize = 2e-6 type sweepTouchingVertex struct { polygon int point GeoPoint } // sweepTouchingPolygonPair 返回扫描顺序最前的一对近邻顶点:先按多边形下标,再按顶点下标。 func sweepTouchingPolygonPair( polygons [][]GeoPoint, thresholdKM float64, ) (int, int, GeoPoint, bool) { cells := make(map[int64][]sweepTouchingVertex) for polygonIndex, polygon := range polygons { for _, point := range polygon { key := sweepTouchingCellKey(geoPointVector(point)) cells[key] = append(cells[key], sweepTouchingVertex{polygon: polygonIndex, point: point}) } } pairs := make(map[[2]int]struct{}) for polygonIndex, polygon := range polygons { for _, point := range polygon { x, y, z := sweepTouchingCellCoordinates(geoPointVector(point)) for deltaX := int64(-1); deltaX <= 1; deltaX++ { for deltaY := int64(-1); deltaY <= 1; deltaY++ { for deltaZ := int64(-1); deltaZ <= 1; deltaZ++ { key := sweepTouchingCellKeyFromCoordinates(x+deltaX, y+deltaY, z+deltaZ) for _, candidate := range cells[key] { if candidate.polygon <= polygonIndex || geoPointDistanceKM(point, candidate.point) > thresholdKM { continue } pairs[[2]int{polygonIndex, candidate.polygon}] = struct{}{} } } } } } } firstIndex, secondIndex := -1, -1 for pair := range pairs { if firstIndex < 0 || pair[0] < firstIndex || (pair[0] == firstIndex && pair[1] < secondIndex) { firstIndex, secondIndex = pair[0], pair[1] } } if firstIndex < 0 { return 0, 0, GeoPoint{}, false } for _, first := range polygons[firstIndex] { for _, second := range polygons[secondIndex] { if !(geoPointDistanceKM(first, second) > thresholdKM) { return firstIndex, secondIndex, GeoPoint{ Longitude: first.Longitude + math.Remainder(second.Longitude-first.Longitude, 360)/2, Latitude: (first.Latitude + second.Latitude) / 2, }, true } } } return 0, 0, GeoPoint{}, false } // 网格边长 2e-6 弧度,大于 0.01 公里的球面弦长,近邻顶点必落在相邻格。 func sweepTouchingCellCoordinates(vector geoVector3) (int64, int64, int64) { const offset = 1 << 19 return int64(math.Floor(vector.x/sweepTouchingCellSize)) + offset, int64(math.Floor(vector.y/sweepTouchingCellSize)) + offset, int64(math.Floor(vector.z/sweepTouchingCellSize)) + offset } func sweepTouchingCellKey(vector geoVector3) int64 { x, y, z := sweepTouchingCellCoordinates(vector) return sweepTouchingCellKeyFromCoordinates(x, y, z) } func sweepTouchingCellKeyFromCoordinates(x, y, z int64) int64 { return x<<40 | y<<20 | z } func sweepReplaceTouchingPolygonPair( polygons [][]GeoPoint, first, second int, merged [][]GeoPoint, ) [][]GeoPoint { if first > second { first, second = second, first } result := make([][]GeoPoint, 0, len(polygons)-1+len(merged)) result = append(result, polygons[:first]...) result = append(result, merged...) result = append(result, polygons[first+1:second]...) result = append(result, polygons[second+1:]...) return result } func sweepDeduplicateAdjacent(points []GeoPoint) []GeoPoint { if len(points) < 2 { return points } result := make([]GeoPoint, 0, len(points)) for _, point := range points { if len(result) == 0 || !SameGeoPoint(result[len(result)-1], point) { result = append(result, point) } } if len(result) > 1 && SameGeoPoint(result[0], result[len(result)-1]) { result = result[:len(result)-1] } return result } func openGeoRing(points []GeoPoint) []GeoPoint { if len(points) > 1 && SameGeoPoint(points[0], points[len(points)-1]) { return points[:len(points)-1] } return points } func reverseSweepGeoPoints(points []GeoPoint) { for left, right := 0, len(points)-1; left < right; left, right = left+1, right-1 { points[left], points[right] = points[right], points[left] } } func geoPointDistanceKM(first, second GeoPoint) float64 { firstLatitude := first.Latitude * math.Pi / 180 secondLatitude := second.Latitude * math.Pi / 180 deltaLatitude := secondLatitude - firstLatitude deltaLongitude := math.Remainder((second.Longitude-first.Longitude)*math.Pi/180, 2*math.Pi) haversine := math.Sin(deltaLatitude/2)*math.Sin(deltaLatitude/2) + math.Cos(firstLatitude)*math.Cos(secondLatitude)*math.Sin(deltaLongitude/2)*math.Sin(deltaLongitude/2) return 2 * 6378.1366 * math.Asin(math.Sqrt(math.Min(1, haversine))) }