feat: 完善日月食与月掩几何链路并扩展历法接口
- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑 - 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口 - 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界 - 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名 - 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验 - 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口 - 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
This commit is contained in:
@@ -0,0 +1,672 @@
|
||||
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)))
|
||||
}
|
||||
Reference in New Issue
Block a user