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
+52 -24
View File
@@ -7,8 +7,11 @@ import "math"
// 所有时刻字段都使用力学时儒略日(JDE, TT)。
// 输入 seedJDE 只需要落在目标朔月附近,允许相差数天。
type LocalSolarEclipseResult struct {
Model SolarEclipseRadiusModel
Type SolarEclipseType
// 下列字段是决定上述数值的口径,随结果一起保留。
// The fields below are the conventions that fix the numbers above.
Model SolarEclipseRadiusModel
SunRadiusModel SolarEclipseSunRadiusModel
Type SolarEclipseType
// GreatestEclipse 是站心盘面中心角距最小的时刻。
GreatestEclipse float64
@@ -91,27 +94,40 @@ const (
// LocalSolarEclipse 计算给定近朔时刻附近的一次站心日食,默认使用 NASA bulletin Split-K 模型。
//
// seedJDE 为力学时儒略日(TT),只需落在目标朔月附近,允许相差数天。
// lon 为经度,东正西负;lat 为纬度,北正南负;height 为海拔高度,单位米。
// LocalSolarEclipse 计算给定近朔时刻附近的一次站心日食,使用 NASA bulletin Split-K 模型与标准太阳半径。
//
// lon 为经度,东正西负;lat 为纬度,北正南负;height 为观测点高度,单位米,按椭球高(大地高)解读:
// 只有正高 H 时须由调用方先加上大地水准面差距 N,即 height = H + N;本库不建模 N。
// LocalSolarEclipse computes one local solar eclipse with the NASA bulletin Split-K model and
// the standard solar radius. height is the ellipsoidal height in metres; convert an orthometric
// height H with height = H + N, since the geoid undulation N is not modelled here.
func LocalSolarEclipse(seedJDE, lon, lat, height float64) LocalSolarEclipseResult {
return LocalSolarEclipseNASABulletinSplitK(seedJDE, lon, lat, height)
}
// LocalSolarEclipseWithOptions 计算给定近朔时刻附近的一次站心日食,半径口径由 options 指定 / computes one local solar eclipse with the given radius conventions.
func LocalSolarEclipseWithOptions(seedJDE, lon, lat, height float64, options SolarEclipseOptions) LocalSolarEclipseResult {
return localSolarEclipse(seedJDE, lon, lat, height, options)
}
// LocalSolarEclipseIAUSingleK 计算给定近朔时刻附近的一次站心日食,使用 IAU Single-K 模型。
func LocalSolarEclipseIAUSingleK(seedJDE, lon, lat, height float64) LocalSolarEclipseResult {
return localSolarEclipse(seedJDE, lon, lat, height, SolarEclipseModelIAUSingleK)
return localSolarEclipse(seedJDE, lon, lat, height, SolarEclipseOptions{RadiusModel: SolarEclipseModelIAUSingleK})
}
// LocalSolarEclipseNASABulletinSplitK 计算给定近朔时刻附近的一次站心日食,使用 NASA bulletin Split-K 模型。
func LocalSolarEclipseNASABulletinSplitK(seedJDE, lon, lat, height float64) LocalSolarEclipseResult {
return localSolarEclipse(seedJDE, lon, lat, height, SolarEclipseModelNASABulletinSplitK)
return localSolarEclipse(seedJDE, lon, lat, height, SolarEclipseOptions{RadiusModel: SolarEclipseModelNASABulletinSplitK})
}
func localSolarEclipse(seedJDE, lonDeg, latDeg, heightMeters float64, model SolarEclipseRadiusModel) LocalSolarEclipseResult {
func localSolarEclipse(seedJDE, lonDeg, latDeg, heightMeters float64, options SolarEclipseOptions) LocalSolarEclipseResult {
newMoonJDE := CalcMoonSHByJDE(seedJDE, 0)
lonRad := lonDeg * rad
latRad := latDeg * rad
heightKM := heightMeters / 1000.0
params := solarEclipseModelParams(model)
options.RadiusModel = normalizeSolarEclipseRadiusModel(options.RadiusModel)
options.SunRadiusModel = normalizeSolarEclipseSunRadiusModel(options.SunRadiusModel)
params := solarEclipseModelParams(options.RadiusModel, options.SunRadiusModel)
greatestEclipseJDE := localSolarEclipseGreatest(newMoonJDE, lonRad, latRad, heightKM, params)
state := localSolarEclipseStateAt(greatestEclipseJDE, lonRad, latRad, heightKM, params)
@@ -122,7 +138,8 @@ func localSolarEclipse(seedJDE, lonDeg, latDeg, heightMeters float64, model Sola
}
result := LocalSolarEclipseResult{
Model: model,
Model: options.RadiusModel,
SunRadiusModel: options.SunRadiusModel,
Type: SolarEclipseNone,
GreatestEclipse: greatestEclipseJDE,
Separation: state.separationRad / rad,
@@ -181,13 +198,24 @@ func localSolarEclipse(seedJDE, lonDeg, latDeg, heightMeters float64, model Sola
return result
}
func solarEclipseModelParams(model SolarEclipseRadiusModel) solarEclipseModelParameters {
params := solarEclipseModelParameters{
penumbralK: solarEclipsePenumbralK,
umbralK: solarEclipsePenumbralK,
// solarEclipsePenumbralRadiusNumerator 是半影外半径公式的分子(地球赤道半径 × k1 × 月亮半径尺度)。
// Split-K 的 k1 是编译期常量,这里保持常量折叠后的末位,使该模型的既有输出逐字节不变;IAU Single-K 只能走运行时值。
func solarEclipsePenumbralRadiusNumerator(params solarEclipseModelParameters) float64 {
if params.penumbralK == solarEclipsePenumbralK {
return solarEclipseEarthEquatorialRadiusKM * solarEclipsePenumbralK * localSolarMoonRadiusScale
}
if model == SolarEclipseModelNASABulletinSplitK {
params.umbralK = solarEclipseUmbralK
return solarEclipseEarthEquatorialRadiusKM * params.penumbralK * localSolarMoonRadiusScale
}
func solarEclipseModelParams(model SolarEclipseRadiusModel, sunRadiusModel SolarEclipseSunRadiusModel) solarEclipseModelParameters {
params := solarEclipseModelParameters{
penumbralK: solarEclipsePenumbralK,
umbralK: solarEclipseUmbralK,
sunRadiusRatio: solarEclipseSunRadiusRatio(sunRadiusModel),
}
if model == SolarEclipseModelIAUSingleK {
params.penumbralK = solarEclipseIAUSingleRadiusK
params.umbralK = solarEclipseIAUSingleRadiusK
}
return params
}
@@ -218,19 +246,19 @@ func localSolarEclipseGreatestWith(
// centralPhaseDurationDaysAt 用事件局部插值星历求解某点的中心相时长(日):先求局部食甚,
// 再解本影/反本影的内切接触。没有中心相或接触退化时返回 0。
func (solver solarEclipseSolver) centralPhaseDurationDaysAt(jd, lonDeg, latDeg float64) float64 {
func (solver solarEclipseSolver) centralPhaseDurationDaysAt(jde, lonDeg, latDeg float64) float64 {
solver = solver.withLocalEphemeris()
lonRad, latRad := lonDeg*rad, latDeg*rad
stateAt := func(jd float64) localSolarEclipseState {
return solver.localStateContextCandidateAt(jd).stateAt(lonRad, latRad, 0)
stateAt := func(jde float64) localSolarEclipseState {
return solver.localStateContextCandidateAt(jde).stateAt(lonRad, latRad, 0)
}
greatestJDE := localSolarEclipseGreatestWith(solver.newMoonJDE, stateAt)
contactState := stateAt(greatestJDE).movingDiskContactState()
if !contactState.valid || contactState.internalContactGap() > 0 {
return 0
}
evaluator := newMovingDiskContactEvaluator(func(jd float64) (movingDiskContactState, bool) {
contact := stateAt(jd).movingDiskContactState()
evaluator := newMovingDiskContactEvaluator(func(jde float64) (movingDiskContactState, bool) {
contact := stateAt(jde).movingDiskContactState()
return contact, contact.valid
})
evaluator.prime(greatestJDE, contactState, true)
@@ -311,14 +339,14 @@ func newLocalSolarEclipseStateContextWithOverride(
params solarEclipseModelParameters,
) localSolarEclipseStateContext {
sunEquatorial, moonEquatorial := solarEclipseSunMoonEquatorial(jdTT)
utJDE := TD2UT(jdTT, false)
ut1JDE := TT2UT1(jdTT)
if !math.IsNaN(deltaTSeconds) {
utJDE = jdTT - deltaTSeconds/86400
ut1JDE = jdTT - deltaTSeconds/86400
}
return localSolarEclipseStateContext{
sunXYZ: solarEclipseLLRToXYZ(sunEquatorial[0], sunEquatorial[1], sunEquatorial[2]),
moonXYZ: solarEclipseLLRToXYZ(moonEquatorial[0], moonEquatorial[1], moonEquatorial[2]),
gst: ApparentSiderealTime(utJDE) * 15 * rad,
gst: ApparentSiderealTime(ut1JDE) * 15 * rad,
params: params,
}
}
@@ -348,10 +376,10 @@ func (context localSolarEclipseStateContext) stateAt(lonRad, latRad, heightKM fl
}
sunRadiusRad := math.Asin(localSolarEclipseClampUnit(
solarEclipseEarthEquatorialRadiusKM * solarEclipseSolarRadiusRatio / sunTopocentric[2],
solarEclipseEarthEquatorialRadiusKM * context.params.sunRadiusRatio / sunTopocentric[2],
))
moonOuterRadiusRad := math.Asin(localSolarEclipseClampUnit(
solarEclipseEarthEquatorialRadiusKM * solarEclipsePenumbralK * localSolarMoonRadiusScale / moonTopocentric[2],
solarEclipsePenumbralRadiusNumerator(context.params) / moonTopocentric[2],
))
moonInnerRadiusRad := math.Asin(localSolarEclipseClampUnit(
solarEclipseEarthEquatorialRadiusKM * context.params.umbralK * localSolarMoonRadiusScale / moonTopocentric[2],