feat: 完善日月食与月掩几何链路并扩展历法接口
- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑 - 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口 - 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界 - 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名 - 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验 - 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口 - 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
package basic
|
||||
|
||||
import "math"
|
||||
|
||||
const (
|
||||
solarEclipseCentralEnvelopeArcStepDegrees = 0.4
|
||||
solarEclipseCentralEnvelopeMinArcStepDegrees = 0.01
|
||||
solarEclipseCentralEnvelopeMaxArcSteps = 2000
|
||||
solarEclipseCentralEnvelopeMaxSpacingKM = 48.0
|
||||
solarEclipseCentralEnvelopeEndDistanceKM = 50.0
|
||||
solarEclipseTotalEnvelopeTargetSpacingKM = 200.0
|
||||
)
|
||||
|
||||
// centralTwoLimitBandEnvelope follows both continuous local-centrality limits
|
||||
// between the solved sunrise and sunset closures. Unlike a union of discrete
|
||||
// shadow footprints, every non-horizon point satisfies C=0 and dC/dt=0.
|
||||
func (solver solarEclipseSolver) centralTwoLimitBandEnvelope(
|
||||
closures [][]SolarEclipsePathPoint,
|
||||
referenceJDE float64,
|
||||
) [][]SolarEclipsePathPoint {
|
||||
branches := solver.centralBandVectorBranches(closures, referenceJDE)
|
||||
if len(branches) != 2 {
|
||||
return nil
|
||||
}
|
||||
return joinSolarEclipseCentralBandBranches(branches, closures)
|
||||
}
|
||||
|
||||
func (solver solarEclipseSolver) centralBandVectorBranches(
|
||||
closures [][]SolarEclipsePathPoint,
|
||||
referenceJDE float64,
|
||||
) [][]SolarEclipsePathPoint {
|
||||
if len(closures) != 2 || len(closures[0]) < 2 || len(closures[1]) < 2 {
|
||||
return nil
|
||||
}
|
||||
startRoots := []SolarEclipsePathPoint{closures[0][0], closures[0][len(closures[0])-1]}
|
||||
endRoots := []SolarEclipsePathPoint{closures[1][0], closures[1][len(closures[1])-1]}
|
||||
branches := make([][]SolarEclipsePathPoint, 2)
|
||||
endIndices := [2]int{-1, -1}
|
||||
solver = solver.withLocalEphemeris()
|
||||
for index, root := range startRoots {
|
||||
var transitions []SolarEclipsePathPoint
|
||||
branch, endIndex, ok := solver.traceCentralBandVectorEnvelope(root, endRoots, &transitions, referenceJDE)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
branches[index], endIndices[index] = branch, endIndex
|
||||
}
|
||||
if endIndices[0] == endIndices[1] {
|
||||
return nil
|
||||
}
|
||||
return branches
|
||||
}
|
||||
|
||||
func solarEclipseTotalBandEnvelope(
|
||||
segments, closures [][]SolarEclipsePathPoint,
|
||||
) [][]SolarEclipsePathPoint {
|
||||
if len(segments) != 2 || len(segments[0]) < 2 || len(segments[1]) < 2 {
|
||||
return nil
|
||||
}
|
||||
branches := make([][]SolarEclipsePathPoint, 2)
|
||||
for index, segment := range segments {
|
||||
branch := append([]SolarEclipsePathPoint(nil), segment...)
|
||||
if branch[0].JDE > branch[len(branch)-1].JDE {
|
||||
for left, right := 0, len(branch)-1; left < right; left, right = left+1, right-1 {
|
||||
branch[left], branch[right] = branch[right], branch[left]
|
||||
}
|
||||
}
|
||||
branches[index] = branch
|
||||
}
|
||||
if len(closures) == 2 && len(closures[0]) > 1 && len(closures[1]) > 1 &&
|
||||
solarEclipsePathDistanceKM(closures[0][0], closures[1][0]) < 0.01 &&
|
||||
solarEclipsePathDistanceKM(closures[0][len(closures[0])-1], closures[1][len(closures[1])-1]) < 0.01 {
|
||||
ring := append([]SolarEclipsePathPoint{}, branches[0]...)
|
||||
for index := len(branches[1]) - 1; index >= 0; index-- {
|
||||
ring = append(ring, branches[1][index])
|
||||
}
|
||||
ring = deduplicateSolarEclipsePathPoints(ring)
|
||||
if len(ring) >= 4 {
|
||||
ring = append(ring, ring[0])
|
||||
return [][]SolarEclipsePathPoint{ring}
|
||||
}
|
||||
}
|
||||
return joinSolarEclipseCentralBandBranches(branches, closures)
|
||||
}
|
||||
|
||||
func joinSolarEclipseCentralBandBranches(
|
||||
branches, closures [][]SolarEclipsePathPoint,
|
||||
) [][]SolarEclipsePathPoint {
|
||||
if len(branches) != 2 || len(branches[0]) == 0 || len(branches[1]) == 0 ||
|
||||
len(closures) != 2 || len(closures[0]) == 0 || len(closures[1]) == 0 {
|
||||
return nil
|
||||
}
|
||||
endClosure, ok := orientSolarEclipsePath(
|
||||
closures[1], branches[0][len(branches[0])-1], branches[1][len(branches[1])-1],
|
||||
)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
startClosure, ok := orientSolarEclipsePath(
|
||||
closures[0], branches[1][0], branches[0][0],
|
||||
)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
ring := make([]SolarEclipsePathPoint, 0,
|
||||
len(branches[0])+len(branches[1])+len(startClosure)+len(endClosure),
|
||||
)
|
||||
ring = append(ring, branches[0]...)
|
||||
ring = append(ring, endClosure[1:]...)
|
||||
for index := len(branches[1]) - 2; index >= 0; index-- {
|
||||
ring = append(ring, branches[1][index])
|
||||
}
|
||||
ring = append(ring, startClosure[1:]...)
|
||||
ring = deduplicateSolarEclipsePathPoints(ring)
|
||||
if len(ring) < 4 || solarEclipsePathDistanceKM(ring[0], ring[len(ring)-1]) > 0.01 {
|
||||
return nil
|
||||
}
|
||||
ring[len(ring)-1] = ring[0]
|
||||
return [][]SolarEclipsePathPoint{ring}
|
||||
}
|
||||
|
||||
func (solver solarEclipseSolver) traceCentralBandEnvelope(
|
||||
root SolarEclipsePathPoint,
|
||||
endRoots []SolarEclipsePathPoint,
|
||||
referenceJDE float64,
|
||||
) ([]SolarEclipsePathPoint, int, bool) {
|
||||
state, ok := solver.nonCentralBandStateAt(root, referenceJDE)
|
||||
if !ok {
|
||||
return nil, -1, false
|
||||
}
|
||||
step := solarEclipseCentralEnvelopeArcStepDegrees
|
||||
next, ok := solver.centralBandEnvelopeVisibleStart(state, step, referenceJDE)
|
||||
if !ok {
|
||||
return []SolarEclipsePathPoint{root}, -1, false
|
||||
}
|
||||
if dotSolarEclipse3(next.tangent, subtractSolarEclipse3(next.coordinates, state.coordinates)) < 0 {
|
||||
for index := range next.tangent {
|
||||
next.tangent[index] = -next.tangent[index]
|
||||
}
|
||||
}
|
||||
points := []SolarEclipsePathPoint{root, next.point}
|
||||
state = next
|
||||
for count := 0; count < solarEclipseCentralEnvelopeMaxArcSteps; count++ {
|
||||
predictor := state.coordinates
|
||||
for index := range predictor {
|
||||
predictor[index] += step * state.tangent[index]
|
||||
}
|
||||
candidate, iterations, candidateOK := solver.correctNonCentralBandBoundary(
|
||||
predictor, state.tangent, referenceJDE,
|
||||
)
|
||||
if !candidateOK {
|
||||
step /= 2
|
||||
if step < solarEclipseCentralEnvelopeMinArcStepDegrees {
|
||||
return points, -1, false
|
||||
}
|
||||
continue
|
||||
}
|
||||
if dotSolarEclipse3(candidate.tangent, state.tangent) < 0 {
|
||||
for index := range candidate.tangent {
|
||||
candidate.tangent[index] = -candidate.tangent[index]
|
||||
}
|
||||
}
|
||||
distance := solarEclipsePathDistanceKM(state.point, candidate.point)
|
||||
if distance > solarEclipseCentralEnvelopeMaxSpacingKM {
|
||||
step /= 2
|
||||
if step < solarEclipseCentralEnvelopeMinArcStepDegrees {
|
||||
return points, -1, false
|
||||
}
|
||||
continue
|
||||
}
|
||||
if candidate.point.SunAltitude < 0 {
|
||||
endIndex, endDistance := nearestSolarEclipsePathPoint(state.point, candidate.point, endRoots)
|
||||
if endIndex < 0 || endDistance > solarEclipseCentralEnvelopeEndDistanceKM {
|
||||
return points, endIndex, false
|
||||
}
|
||||
points = append(points, endRoots[endIndex])
|
||||
return points, endIndex, true
|
||||
}
|
||||
points = append(points, candidate.point)
|
||||
state = candidate
|
||||
if distance < solarEclipseCentralEnvelopeMaxSpacingKM/2 && iterations <= 4 {
|
||||
step = math.Min(solarEclipseCentralEnvelopeArcStepDegrees, step*1.5)
|
||||
}
|
||||
}
|
||||
return points, -1, false
|
||||
}
|
||||
|
||||
func (solver solarEclipseSolver) centralBandEnvelopeVisibleStart(
|
||||
state solarEclipseNonCentralBandState,
|
||||
step, referenceJDE float64,
|
||||
) (solarEclipseNonCentralBandState, bool) {
|
||||
best := solarEclipseNonCentralBandState{}
|
||||
found := false
|
||||
for trialStep := step; trialStep >= solarEclipseCentralEnvelopeMinArcStepDegrees; trialStep /= 2 {
|
||||
for _, direction := range []float64{1, -1} {
|
||||
predictor := state.coordinates
|
||||
tangent := state.tangent
|
||||
for index := range predictor {
|
||||
tangent[index] *= direction
|
||||
predictor[index] += trialStep * tangent[index]
|
||||
}
|
||||
candidate, _, ok := solver.correctNonCentralBandBoundary(predictor, tangent, referenceJDE)
|
||||
if ok && candidate.point.SunAltitude > 0 &&
|
||||
(!found || candidate.point.SunAltitude > best.point.SunAltitude) {
|
||||
best, found = candidate, true
|
||||
}
|
||||
}
|
||||
if found {
|
||||
break
|
||||
}
|
||||
}
|
||||
return best, found
|
||||
}
|
||||
|
||||
func nearestSolarEclipsePathPoint(
|
||||
first, second SolarEclipsePathPoint,
|
||||
points []SolarEclipsePathPoint,
|
||||
) (int, float64) {
|
||||
bestIndex, bestDistance := -1, math.Inf(1)
|
||||
for index, point := range points {
|
||||
distance := math.Min(
|
||||
solarEclipsePathDistanceKM(first, point),
|
||||
solarEclipsePathDistanceKM(second, point),
|
||||
)
|
||||
if distance < bestDistance {
|
||||
bestIndex, bestDistance = index, distance
|
||||
}
|
||||
}
|
||||
return bestIndex, bestDistance
|
||||
}
|
||||
|
||||
func orientSolarEclipsePath(
|
||||
points []SolarEclipsePathPoint,
|
||||
start, end SolarEclipsePathPoint,
|
||||
) ([]SolarEclipsePathPoint, bool) {
|
||||
if len(points) < 2 {
|
||||
return nil, false
|
||||
}
|
||||
oriented := append([]SolarEclipsePathPoint(nil), points...)
|
||||
direct := solarEclipsePathDistanceKM(start, oriented[0]) +
|
||||
solarEclipsePathDistanceKM(end, oriented[len(oriented)-1])
|
||||
reverse := solarEclipsePathDistanceKM(start, oriented[len(oriented)-1]) +
|
||||
solarEclipsePathDistanceKM(end, oriented[0])
|
||||
if reverse < direct {
|
||||
for left, right := 0, len(oriented)-1; left < right; left, right = left+1, right-1 {
|
||||
oriented[left], oriented[right] = oriented[right], oriented[left]
|
||||
}
|
||||
}
|
||||
if solarEclipsePathDistanceKM(start, oriented[0]) > 0.1 ||
|
||||
solarEclipsePathDistanceKM(end, oriented[len(oriented)-1]) > 0.1 {
|
||||
return nil, false
|
||||
}
|
||||
oriented[0], oriented[len(oriented)-1] = start, end
|
||||
return oriented, true
|
||||
}
|
||||
Reference in New Issue
Block a user