feat: 完善时标与天象几何计算并扩展输出接口

- 新增时标、ΔT 模型、质心时间与 UT1 支持
- 改进日月食、月掩、行星事件及路径边界计算
- 完善恒星三维自行与动态距离传播
- 扩展 SVG、GeoJSON、KML 输出与底层距离换算工具
- 整理中英文手册、示例资源及回归测试
This commit is contained in:
2026-09-23 18:55:12 +08:00
parent 1f31a9b5b5
commit 16c62a97d5
503 changed files with 33290 additions and 9471 deletions
+25 -17
View File
@@ -16,6 +16,8 @@ const (
type SolarEclipseShadowSolverOptions struct {
// Model 月亮半径模型,零值为 NASA bulletin Split-K / lunar radius model.
Model SolarEclipseRadiusModel
// SunRadiusModel 太阳半径口径,零值为标准档 / solar radius convention, standard when zero.
SunRadiusModel SolarEclipseSunRadiusModel
// DeltaTSeconds 显式 ΔT(秒),<=0 用进程级模型,只改变地球自转相位 / explicit ΔT in seconds.
DeltaTSeconds float64
// BoundaryPoints 边界角向采样点数,<=0 用 96 / boundary sample count.
@@ -100,6 +102,8 @@ type SolarEclipseShadowInstant struct {
JDE float64
// Model 本次使用的月亮半径模型 / lunar radius model used.
Model SolarEclipseRadiusModel
// SunRadiusModel 本次使用的太阳半径口径 / solar radius convention used.
SunRadiusModel SolarEclipseSunRadiusModel
// DeltaTSeconds 实际使用的 ΔT / ΔT actually used.
DeltaTSeconds float64
// Kind 本次计算的阴影类型 / shadow kind of this computation.
@@ -130,9 +134,8 @@ type SolarEclipseShadowSolver struct {
// NewSolarEclipseShadowSolver 构造单时刻求解器 / builds a single-instant solver.
func NewSolarEclipseShadowSolver(options SolarEclipseShadowSolverOptions) *SolarEclipseShadowSolver {
if options.Model != SolarEclipseModelIAUSingleK {
options.Model = SolarEclipseModelNASABulletinSplitK
}
options.Model = normalizeSolarEclipseRadiusModel(options.Model)
options.SunRadiusModel = normalizeSolarEclipseSunRadiusModel(options.SunRadiusModel)
if options.BoundaryPoints <= 0 {
options.BoundaryPoints = solarEclipseShadowDefaultBoundaryPoints
}
@@ -170,19 +173,20 @@ func (solver *SolarEclipseShadowSolver) ShadowAtJDE(jdeTT float64) (SolarEclipse
)
if len(footprint.Boundaries) == 0 {
return SolarEclipseShadowInstant{
JDE: jdeTT, Model: solver.options.Model,
JDE: jdeTT, Model: solver.options.Model, SunRadiusModel: solver.options.SunRadiusModel,
Kind: solver.options.Kind, DeltaTSeconds: deltaT,
}, false
}
return SolarEclipseShadowInstant{
JDE: jdeTT,
Model: solver.options.Model,
Kind: solver.options.Kind,
DeltaTSeconds: deltaT,
Closed: footprint.Closed,
Boundaries: footprint.Boundaries,
HorizonEnds: footprint.HorizonEnds,
Topology: solarEclipseShadowFootprintTopology(footprint, solver.options.Kind),
JDE: jdeTT,
Model: solver.options.Model,
SunRadiusModel: solver.options.SunRadiusModel,
Kind: solver.options.Kind,
DeltaTSeconds: deltaT,
Closed: footprint.Closed,
Boundaries: footprint.Boundaries,
HorizonEnds: footprint.HorizonEnds,
Topology: solarEclipseShadowFootprintTopology(footprint, solver.options.Kind),
}, true
}
@@ -196,8 +200,8 @@ func (options SolarEclipseShadowSolverOptions) shadowKind() solarEclipseShadowKi
func (solver *SolarEclipseShadowSolver) effectiveDeltaT(jdeTT float64) float64 {
override := solver.options.DeltaTSeconds
if override <= 0 {
// 选项 0/负值表示"未覆盖",用模型;显式 ΔT=0 需走 DeltaTSecondsAt 的直接调用。
override = math.NaN()
// ΔT 模型的自变量是 UT1,须先从输入的 TT 反解。
return ut1ToTTOffsetSeconds(ttToUT1JDE(jdeTT))
}
return DeltaTSecondsAt(jdeTT, override)
}
@@ -209,7 +213,10 @@ func (solver *SolarEclipseShadowSolver) solverFor(jdeTT float64) solarEclipseSol
anchor := CalcMoonSHByJDE(jdeTT, 0)
solver.anchorSet = true
solver.anchorJDE = anchor
solver.anchorSolver = newSolarEclipseSolver(anchor, solver.options.Model)
solver.anchorSolver = newSolarEclipseSolverWithOptions(anchor, SolarEclipseOptions{
RadiusModel: solver.options.Model,
SunRadiusModel: solver.options.SunRadiusModel,
})
return solver.anchorSolver
}
@@ -289,7 +296,7 @@ type SolarEclipseStationState struct {
// HasTotalPhase 与 HasAnnularPhase 表示该瞬时是否处于全食或环食 / total or annular now.
HasTotalPhase bool
HasAnnularPhase bool
// Visible 太阳中心高于几何地平,海拔用俯仰角修正阈值 / Sun center above the horizon.
// Visible 太阳中心高于几何地平,高度用俯仰角修正阈值 / Sun center above the horizon.
Visible bool
}
@@ -303,7 +310,8 @@ func (solver *SolarEclipseShadowSolver) StationStateAtJDE(
deltaT := solver.effectiveDeltaT(jdeTT)
heightKM := heightMeters / 1000
state := localSolarEclipseStateAtWithDeltaT(
jdeTT, deltaT, lonDeg*rad, latDeg*rad, heightKM, solarEclipseModelParams(solver.options.Model),
jdeTT, deltaT, lonDeg*rad, latDeg*rad, heightKM,
solarEclipseModelParams(solver.options.Model, solver.options.SunRadiusModel),
)
contact := state.movingDiskContactState()
central := contact.internalContactGap() <= 0