feat: 引入可切换星历模型并扩展天象观测链路

- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算
- 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询
- 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出
- 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解
- 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤
- 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持
- 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理
- 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
This commit is contained in:
2026-10-15 23:54:15 +08:00
parent 16c62a97d5
commit 044f2f4381
674 changed files with 62983 additions and 8396 deletions
+124 -61
View File
@@ -42,8 +42,16 @@ type SolarEclipseOptions struct {
RadiusModel SolarEclipseRadiusModel
// SunRadiusModel 太阳半径口径,零值为标准档, solar radius convention.
SunRadiusModel SolarEclipseSunRadiusModel
// LunarLimb 月缘剖面,nil 沿用模块设置;修正站心接触、食分与遮蔽率,以及全球始末与食甚点中心食时长 / lunar limb profile for local contacts and values, global P1/P4 and central duration.
LunarLimb LunarLimb
}
// LunarLimb 月缘剖面接口,内置实现见 b612.me/astro/moon/limb, lunar limb profile interface.
type LunarLimb = basic.LunarLimb
// SolarEclipseLimbContact 月缘修正后的全球偏食始末接触点位置 / limb-corrected global contact location.
type SolarEclipseLimbContact = basic.SolarEclipseLimbContact
// SolarEclipseType 全局日食食型, global solar eclipse type.
type SolarEclipseType string
@@ -85,6 +93,11 @@ type SolarEclipseInfo struct {
Type SolarEclipseType
// Centrality 中心性, eclipse centrality.
Centrality SolarEclipseCentrality
// LunarLimbApplied 全球偏食始末与食甚点中心食时长已按月缘剖面修正;剖面失败时为 false 且结果保持圆月面 / contacts refined with a lunar limb profile.
LunarLimbApplied bool
// PartialBeginContact / PartialEndContact 月缘修正后的偏食始末接触点位置,未修正时 Defined 为 false / limb-corrected contact locations.
PartialBeginContact SolarEclipseLimbContact
PartialEndContact SolarEclipseLimbContact
// HasSaros 存在沙罗序列信息(可能是锚点外推结果), has Saros series metadata (possibly extrapolated).
HasSaros bool
// Saros 是沙罗序列信息,包括系列号、系列内序号和总成员数。
@@ -148,48 +161,54 @@ type SolarEclipseInfo struct {
}
// SolarEclipseOnDate 当地自然日全局日食查询 / local-date global solar eclipse query.
// Determine whether a global solar eclipse overlaps the local date, using NASA bulletin Split-K by default.
func SolarEclipseOnDate(date time.Time) (SolarEclipseInfo, bool) {
return SolarEclipseOnDateNASABulletinSplitK(date)
func (module *Eclipse) SolarEclipseOnDate(date time.Time) (SolarEclipseInfo, bool) {
return module.SolarEclipseOnDateNASABulletinSplitK(date)
}
// SolarEclipseOnDateNASABulletinSplitK 当地自然日全局日食查询(NASA bulletin Split-K) / local-date global solar eclipse query with NASA bulletin Split-K.
// Determine whether a global solar eclipse overlaps the local date with the NASA bulletin Split-K model.
func SolarEclipseOnDateNASABulletinSplitK(date time.Time) (SolarEclipseInfo, bool) {
return solarEclipseOnDate(date, basic.SolarEclipseNASABulletinSplitK)
func (module *Eclipse) SolarEclipseOnDateNASABulletinSplitK(date time.Time) (SolarEclipseInfo, bool) {
return solarEclipseOnDate(date, func(seedJDE float64) basic.SolarEclipseResult {
return module.solarResult(seedJDE, basic.SolarEclipseModelNASABulletinSplitK)
}, module)
}
// SolarEclipseOnDateIAUSingleK 当地自然日全局日食查询(IAU Single-K) / local-date global solar eclipse query with IAU Single-K.
// Determine whether a global solar eclipse overlaps the local date with the IAU Single-K model.
func SolarEclipseOnDateIAUSingleK(date time.Time) (SolarEclipseInfo, bool) {
return solarEclipseOnDate(date, basic.SolarEclipseIAUSingleK)
func (module *Eclipse) SolarEclipseOnDateIAUSingleK(date time.Time) (SolarEclipseInfo, bool) {
return solarEclipseOnDate(date, func(seedJDE float64) basic.SolarEclipseResult {
return module.solarResult(seedJDE, basic.SolarEclipseModelIAUSingleK)
}, module)
}
// SolarEclipseOnDateWithOptions 当地自然日全局日食查询(自定义半径口径) / local-date global solar eclipse query with custom radius conventions.
func SolarEclipseOnDateWithOptions(date time.Time, options SolarEclipseOptions) (SolarEclipseInfo, bool) {
return solarEclipseOnDate(date, solarEclipseCalculatorFor(options))
func (module *Eclipse) SolarEclipseOnDateWithOptions(date time.Time, options SolarEclipseOptions) (SolarEclipseInfo, bool) {
return solarEclipseOnDate(date, solarEclipseCalculatorFor(options, module), module)
}
// LastSolarEclipseWithOptions 上次日食(自定义半径口径) / previous solar eclipse with custom radius conventions.
func LastSolarEclipseWithOptions(date time.Time, options SolarEclipseOptions) SolarEclipseInfo {
info, _ := searchSolarEclipse(date, -1, true, solarEclipseCalculatorFor(options))
func (module *Eclipse) LastSolarEclipseWithOptions(date time.Time, options SolarEclipseOptions) SolarEclipseInfo {
info, _ := searchSolarEclipse(date, -1, true, solarEclipseCalculatorFor(options, module), module)
return info
}
// NextSolarEclipseWithOptions 下次日食(自定义半径口径) / next solar eclipse with custom radius conventions.
func NextSolarEclipseWithOptions(date time.Time, options SolarEclipseOptions) SolarEclipseInfo {
info, _ := searchSolarEclipse(date, 1, false, solarEclipseCalculatorFor(options))
func (module *Eclipse) NextSolarEclipseWithOptions(date time.Time, options SolarEclipseOptions) SolarEclipseInfo {
info, _ := searchSolarEclipse(date, 1, false, solarEclipseCalculatorFor(options, module), module)
return info
}
// ClosestSolarEclipseWithOptions 最近一次日食(自定义半径口径) / closest solar eclipse with custom radius conventions.
func ClosestSolarEclipseWithOptions(date time.Time, options SolarEclipseOptions) SolarEclipseInfo {
last, hasLast := searchSolarEclipse(date, -1, true, solarEclipseCalculatorFor(options))
next, hasNext := searchSolarEclipse(date, 1, false, solarEclipseCalculatorFor(options))
func (module *Eclipse) ClosestSolarEclipseWithOptions(date time.Time, options SolarEclipseOptions) SolarEclipseInfo {
last, hasLast := searchSolarEclipse(date, -1, true, solarEclipseCalculatorFor(options, module), module)
next, hasNext := searchSolarEclipse(date, 1, false, solarEclipseCalculatorFor(options, module), module)
return closestSolarEclipse(date, last, hasLast, next, hasNext)
}
func solarEclipseCalculatorFor(options SolarEclipseOptions) solarEclipseCalculator {
func solarEclipseCalculatorFor(options SolarEclipseOptions, modules ...*Eclipse) solarEclipseCalculator {
module := &defaultModule
if len(modules) > 0 {
module = modules[0]
}
basicOptions := basic.SolarEclipseOptions{
RadiusModel: basic.SolarEclipseRadiusModel(options.RadiusModel),
SunRadiusModel: basic.SolarEclipseSunRadiusModel(options.SunRadiusModel),
@@ -197,22 +216,43 @@ func solarEclipseCalculatorFor(options SolarEclipseOptions) solarEclipseCalculat
if basicOptions.RadiusModel != basic.SolarEclipseModelIAUSingleK {
basicOptions.RadiusModel = basic.SolarEclipseModelNASABulletinSplitK
}
basicOptions.LunarLimb = options.LunarLimb
if basicOptions.LunarLimb == nil {
basicOptions.LunarLimb = module.lunarLimb
}
return func(seed float64) basic.SolarEclipseResult {
return basic.SolarEclipseWithOptions(seed, basicOptions)
return basic.SolarEclipseWithOptionsWithInstance(seed, basicOptions, module.inst)
}
}
func solarEclipseOnDate(date time.Time, calculator solarEclipseCalculator) (SolarEclipseInfo, bool) {
// solarResult 按模块星历与月缘计算一次全球日食;未设置月缘时与包级计算相同。
func (module *Eclipse) solarResult(seedJDE float64, model basic.SolarEclipseRadiusModel) basic.SolarEclipseResult {
if module.lunarLimb == nil {
if model == basic.SolarEclipseModelIAUSingleK {
return basic.SolarEclipseIAUSingleKWithInstance(seedJDE, module.inst)
}
return basic.SolarEclipseNASABulletinSplitKWithInstance(seedJDE, module.inst)
}
return basic.SolarEclipseWithOptionsWithInstance(seedJDE,
basic.SolarEclipseOptions{RadiusModel: model, LunarLimb: module.lunarLimb}, module.inst)
}
func solarEclipseOnDate(date time.Time, calculator solarEclipseCalculator, modules ...*Eclipse) (SolarEclipseInfo, bool) {
module := &defaultModule
if len(modules) > 0 {
module = modules[0]
}
location := date.Location()
dayStart, dayMid, dayEnd := solarEclipseLocalDayBounds(date)
candidateTT := basic.CalcMoonSHByJDE(solarEclipseTimeToTTJDE(dayMid), 0)
candidateTT := basic.CalcMoonSHByJDEWithInstance(solarEclipseTimeToTTJDE(dayMid), 0, module.inst)
result := calculator(candidateTT)
if result.Type == basic.SolarEclipseNone {
return SolarEclipseInfo{}, false
}
info := solarEclipseInfoFromBasic(result, location)
info := solarEclipseInfoFromBasic(result, location, module)
if !solarEclipseOverlapsDate(info, dayStart, dayEnd) {
return SolarEclipseInfo{}, false
}
@@ -220,64 +260,71 @@ func solarEclipseOnDate(date time.Time, calculator solarEclipseCalculator) (Sola
}
// LastSolarEclipse 上次日食 / previous solar eclipse.
// Previous solar eclipse, using NASA bulletin Split-K by default.
func LastSolarEclipse(date time.Time) SolarEclipseInfo {
return LastSolarEclipseNASABulletinSplitK(date)
func (module *Eclipse) LastSolarEclipse(date time.Time) SolarEclipseInfo {
return module.LastSolarEclipseNASABulletinSplitK(date)
}
// LastSolarEclipseNASABulletinSplitK 上次日食(NASA bulletin Split-K) / previous solar eclipse with NASA bulletin Split-K.
// Previous solar eclipse with the NASA bulletin Split-K model.
func LastSolarEclipseNASABulletinSplitK(date time.Time) SolarEclipseInfo {
info, _ := searchSolarEclipse(date, -1, true, basic.SolarEclipseNASABulletinSplitK)
func (module *Eclipse) LastSolarEclipseNASABulletinSplitK(date time.Time) SolarEclipseInfo {
info, _ := searchSolarEclipse(date, -1, true, func(seedJDE float64) basic.SolarEclipseResult {
return module.solarResult(seedJDE, basic.SolarEclipseModelNASABulletinSplitK)
}, module)
return info
}
// LastSolarEclipseIAUSingleK 上次日食(IAU Single-K) / previous solar eclipse with IAU Single-K.
// Previous solar eclipse with the IAU Single-K model.
func LastSolarEclipseIAUSingleK(date time.Time) SolarEclipseInfo {
info, _ := searchSolarEclipse(date, -1, true, basic.SolarEclipseIAUSingleK)
func (module *Eclipse) LastSolarEclipseIAUSingleK(date time.Time) SolarEclipseInfo {
info, _ := searchSolarEclipse(date, -1, true, func(seedJDE float64) basic.SolarEclipseResult {
return module.solarResult(seedJDE, basic.SolarEclipseModelIAUSingleK)
}, module)
return info
}
// NextSolarEclipse 下次日食 / next solar eclipse.
// Next solar eclipse, using NASA bulletin Split-K by default.
func NextSolarEclipse(date time.Time) SolarEclipseInfo {
return NextSolarEclipseNASABulletinSplitK(date)
func (module *Eclipse) NextSolarEclipse(date time.Time) SolarEclipseInfo {
return module.NextSolarEclipseNASABulletinSplitK(date)
}
// NextSolarEclipseNASABulletinSplitK 下次日食(NASA bulletin Split-K) / next solar eclipse with NASA bulletin Split-K.
// Next solar eclipse with the NASA bulletin Split-K model.
func NextSolarEclipseNASABulletinSplitK(date time.Time) SolarEclipseInfo {
info, _ := searchSolarEclipse(date, 1, false, basic.SolarEclipseNASABulletinSplitK)
func (module *Eclipse) NextSolarEclipseNASABulletinSplitK(date time.Time) SolarEclipseInfo {
info, _ := searchSolarEclipse(date, 1, false, func(seedJDE float64) basic.SolarEclipseResult {
return module.solarResult(seedJDE, basic.SolarEclipseModelNASABulletinSplitK)
}, module)
return info
}
// NextSolarEclipseIAUSingleK 下次日食(IAU Single-K) / next solar eclipse with IAU Single-K.
// Next solar eclipse with the IAU Single-K model.
func NextSolarEclipseIAUSingleK(date time.Time) SolarEclipseInfo {
info, _ := searchSolarEclipse(date, 1, false, basic.SolarEclipseIAUSingleK)
func (module *Eclipse) NextSolarEclipseIAUSingleK(date time.Time) SolarEclipseInfo {
info, _ := searchSolarEclipse(date, 1, false, func(seedJDE float64) basic.SolarEclipseResult {
return module.solarResult(seedJDE, basic.SolarEclipseModelIAUSingleK)
}, module)
return info
}
// ClosestSolarEclipse 最近一次日食 / closest solar eclipse.
// Closest solar eclipse, using NASA bulletin Split-K by default.
func ClosestSolarEclipse(date time.Time) SolarEclipseInfo {
return ClosestSolarEclipseNASABulletinSplitK(date)
func (module *Eclipse) ClosestSolarEclipse(date time.Time) SolarEclipseInfo {
return module.ClosestSolarEclipseNASABulletinSplitK(date)
}
// ClosestSolarEclipseNASABulletinSplitK 最近一次日食(NASA bulletin Split-K) / closest solar eclipse with NASA bulletin Split-K.
// Closest solar eclipse with the NASA bulletin Split-K model.
func ClosestSolarEclipseNASABulletinSplitK(date time.Time) SolarEclipseInfo {
last, hasLast := searchSolarEclipse(date, -1, true, basic.SolarEclipseNASABulletinSplitK)
next, hasNext := searchSolarEclipse(date, 1, false, basic.SolarEclipseNASABulletinSplitK)
func (module *Eclipse) ClosestSolarEclipseNASABulletinSplitK(date time.Time) SolarEclipseInfo {
last, hasLast := searchSolarEclipse(date, -1, true, func(seedJDE float64) basic.SolarEclipseResult {
return module.solarResult(seedJDE, basic.SolarEclipseModelNASABulletinSplitK)
}, module)
next, hasNext := searchSolarEclipse(date, 1, false, func(seedJDE float64) basic.SolarEclipseResult {
return module.solarResult(seedJDE, basic.SolarEclipseModelNASABulletinSplitK)
}, module)
return closestSolarEclipse(date, last, hasLast, next, hasNext)
}
// ClosestSolarEclipseIAUSingleK 最近一次日食(IAU Single-K) / closest solar eclipse with IAU Single-K.
// Closest solar eclipse with the IAU Single-K model.
func ClosestSolarEclipseIAUSingleK(date time.Time) SolarEclipseInfo {
last, hasLast := searchSolarEclipse(date, -1, true, basic.SolarEclipseIAUSingleK)
next, hasNext := searchSolarEclipse(date, 1, false, basic.SolarEclipseIAUSingleK)
func (module *Eclipse) ClosestSolarEclipseIAUSingleK(date time.Time) SolarEclipseInfo {
last, hasLast := searchSolarEclipse(date, -1, true, func(seedJDE float64) basic.SolarEclipseResult {
return module.solarResult(seedJDE, basic.SolarEclipseModelIAUSingleK)
}, module)
next, hasNext := searchSolarEclipse(date, 1, false, func(seedJDE float64) basic.SolarEclipseResult {
return module.solarResult(seedJDE, basic.SolarEclipseModelIAUSingleK)
}, module)
return closestSolarEclipse(date, last, hasLast, next, hasNext)
}
@@ -309,26 +356,34 @@ func searchSolarEclipse(
date time.Time,
direction int,
includeCurrent bool,
calculator solarEclipseCalculator,
) (SolarEclipseInfo, bool) {
calculator solarEclipseCalculator, modules ...*Eclipse) (SolarEclipseInfo, bool) {
module := &defaultModule
if len(modules) > 0 {
module = modules[0]
}
targetTT := solarEclipseTimeToTTJDE(date)
candidateTT := basic.CalcMoonSHByJDE(targetTT, 0)
candidateTT := basic.CalcMoonSHByJDEWithInstance(targetTT, 0, module.inst)
for i := 0; i < solarEclipseSearchLimit; i++ {
if isPotentialSolarEclipse(candidateTT) {
if module.isPotentialSolarEclipse(candidateTT) {
result := calculator(candidateTT)
if result.Type != basic.SolarEclipseNone && solarEclipseMatchesDirection(result.GreatestEclipse, targetTT, direction, includeCurrent) {
return solarEclipseInfoFromBasic(result, date.Location()), true
return solarEclipseInfoFromBasic(result, date.Location(), module), true
}
}
candidateTT = nextEclipseSearchCandidateTT(candidateTT, 0, direction, solarEclipseSynodicMonthDays)
candidateTT = nextEclipseSearchCandidateTT(candidateTT, 0, direction, solarEclipseSynodicMonthDays, module)
}
return SolarEclipseInfo{}, false
}
func isPotentialSolarEclipse(newMoonTT float64) bool {
return math.Abs(basic.HMoonTrueBo(newMoonTT)) <= solarEclipseLatitudeLimitDeg
return defaultModule.isPotentialSolarEclipse(newMoonTT)
}
func (module *Eclipse) isPotentialSolarEclipse(newMoonTT float64) bool {
return math.Abs(basic.HMoonTrueBoWithInstance(newMoonTT, module.inst)) <= solarEclipseLatitudeLimitDeg
}
func solarEclipseMatchesDirection(greatestTT, targetTT float64, direction int, includeCurrent bool) bool {
@@ -355,13 +410,21 @@ func nextSolarEclipseLocalDayStart(dayStart time.Time) time.Time {
return time.Date(dayStart.Year(), dayStart.Month(), dayStart.Day()+1, 0, 0, 0, 0, location)
}
func solarEclipseInfoFromBasic(result basic.SolarEclipseResult, location *time.Location) SolarEclipseInfo {
saros, hasSaros := solarSarosInfo(result.GreatestEclipse)
func solarEclipseInfoFromBasic(result basic.SolarEclipseResult, location *time.Location, modules ...*Eclipse) SolarEclipseInfo {
module := &defaultModule
if len(modules) > 0 {
module = modules[0]
}
saros, hasSaros := solarSarosInfo(result.GreatestEclipse, module)
return SolarEclipseInfo{
Model: mapBasicSolarEclipseModel(result.Model),
SunRadiusModel: result.SunRadiusModel,
Type: mapBasicSolarEclipseType(result.Type),
Centrality: mapBasicSolarEclipseCentrality(result.Centrality),
LunarLimbApplied: result.LunarLimbApplied,
PartialBeginContact: result.PartialBeginContact,
PartialEndContact: result.PartialEndContact,
HasSaros: hasSaros,
Saros: saros,
GreatestEclipse: solarEclipseTTJDEToTime(result.GreatestEclipse, location),