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
+142
View File
@@ -0,0 +1,142 @@
package eclipse
import (
"math"
"time"
"b612.me/astro/basic"
"b612.me/astro/internal/ephem"
)
// LunarEclipseOnDateWithInstance 指定星历实例的当地日月食查询,自定义引擎不推断沙罗 / local-date lunar eclipse with an explicit instance; custom engines omit Saros metadata.
func LunarEclipseOnDateWithInstance(date time.Time, inst *ephem.Instance) (LunarEclipseInfo, bool) {
return lunarEclipseOnDateWithInstance(date, lunarCalculatorWithInstance(false, inst), true, inst)
}
// LunarEclipseOnDateDanjonWithInstance 指定星历实例的 Danjon 当地日月食查询 / local-date Danjon lunar eclipse with an explicit instance.
func LunarEclipseOnDateDanjonWithInstance(date time.Time, inst *ephem.Instance) (LunarEclipseInfo, bool) {
return lunarEclipseOnDateWithInstance(date, lunarCalculatorWithInstance(false, inst), false, inst)
}
// LunarEclipseOnDateChauvenetWithInstance 指定星历实例的 Chauvenet 当地日月食查询 / local-date Chauvenet lunar eclipse with an explicit instance.
func LunarEclipseOnDateChauvenetWithInstance(date time.Time, inst *ephem.Instance) (LunarEclipseInfo, bool) {
return lunarEclipseOnDateWithInstance(date, lunarCalculatorWithInstance(true, inst), false, inst)
}
// LastLunarEclipseWithInstance 指定星历实例的上次月食 / previous lunar eclipse with an explicit instance.
func LastLunarEclipseWithInstance(date time.Time, inst *ephem.Instance) LunarEclipseInfo {
info, _ := searchLunarEclipseWithInstance(date, -1, true, lunarCalculatorWithInstance(false, inst), true, inst)
return info
}
// LastLunarEclipseDanjonWithInstance 指定星历实例的 Danjon 上次月食 / previous Danjon lunar eclipse with an explicit instance.
func LastLunarEclipseDanjonWithInstance(date time.Time, inst *ephem.Instance) LunarEclipseInfo {
info, _ := searchLunarEclipseWithInstance(date, -1, true, lunarCalculatorWithInstance(false, inst), false, inst)
return info
}
// LastLunarEclipseChauvenetWithInstance 指定星历实例的 Chauvenet 上次月食 / previous Chauvenet lunar eclipse with an explicit instance.
func LastLunarEclipseChauvenetWithInstance(date time.Time, inst *ephem.Instance) LunarEclipseInfo {
info, _ := searchLunarEclipseWithInstance(date, -1, true, lunarCalculatorWithInstance(true, inst), false, inst)
return info
}
// NextLunarEclipseWithInstance 指定星历实例的下次月食 / next lunar eclipse with an explicit instance.
func NextLunarEclipseWithInstance(date time.Time, inst *ephem.Instance) LunarEclipseInfo {
info, _ := searchLunarEclipseWithInstance(date, 1, false, lunarCalculatorWithInstance(false, inst), true, inst)
return info
}
// NextLunarEclipseDanjonWithInstance 指定星历实例的 Danjon 下次月食 / next Danjon lunar eclipse with an explicit instance.
func NextLunarEclipseDanjonWithInstance(date time.Time, inst *ephem.Instance) LunarEclipseInfo {
info, _ := searchLunarEclipseWithInstance(date, 1, false, lunarCalculatorWithInstance(false, inst), false, inst)
return info
}
// NextLunarEclipseChauvenetWithInstance 指定星历实例的 Chauvenet 下次月食 / next Chauvenet lunar eclipse with an explicit instance.
func NextLunarEclipseChauvenetWithInstance(date time.Time, inst *ephem.Instance) LunarEclipseInfo {
info, _ := searchLunarEclipseWithInstance(date, 1, false, lunarCalculatorWithInstance(true, inst), false, inst)
return info
}
// ClosestLunarEclipseWithInstance 指定星历实例的最近月食 / closest lunar eclipse with an explicit instance.
func ClosestLunarEclipseWithInstance(date time.Time, inst *ephem.Instance) LunarEclipseInfo {
return closestLunarEclipseWithInstance(date, false, true, inst)
}
// ClosestLunarEclipseDanjonWithInstance 指定星历实例的 Danjon 最近月食 / closest Danjon lunar eclipse with an explicit instance.
func ClosestLunarEclipseDanjonWithInstance(date time.Time, inst *ephem.Instance) LunarEclipseInfo {
return closestLunarEclipseWithInstance(date, false, false, inst)
}
// ClosestLunarEclipseChauvenetWithInstance 指定星历实例的 Chauvenet 最近月食 / closest Chauvenet lunar eclipse with an explicit instance.
func ClosestLunarEclipseChauvenetWithInstance(date time.Time, inst *ephem.Instance) LunarEclipseInfo {
return closestLunarEclipseWithInstance(date, true, false, inst)
}
func lunarCalculatorWithInstance(chauvenet bool, inst *ephem.Instance) lunarEclipseCalculator {
if chauvenet {
return func(jde float64) basic.LunarEclipseResult { return basic.LunarEclipseChauvenetWithInstance(jde, inst) }
}
return func(jde float64) basic.LunarEclipseResult { return basic.LunarEclipseDanjonWithInstance(jde, inst) }
}
func closestLunarEclipseWithInstance(date time.Time, chauvenet, allowDefaultFallback bool, inst *ephem.Instance) LunarEclipseInfo {
calculator := lunarCalculatorWithInstance(chauvenet, inst)
last, hasLast := searchLunarEclipseWithInstance(date, -1, true, calculator, allowDefaultFallback, inst)
next, hasNext := searchLunarEclipseWithInstance(date, 1, false, calculator, allowDefaultFallback, inst)
return closestLunarEclipse(date, last, hasLast, next, hasNext)
}
func moonSunLoDiffWithInstance(date time.Time, inst *ephem.Instance) float64 {
if inst == nil {
return moonSunLoDiff(date)
}
jde := timeToTTJDE(date)
return normalizeDegree360(basic.HMoonApparentLoWithInstance(jde, inst) - basic.HSunApparentLoWithInstance(jde, inst))
}
func nextLunarEclipseSearchCandidateTTWithInstance(candidateTT float64, direction int, inst *ephem.Instance) float64 {
if inst == nil {
return nextEclipseSearchCandidateTT(candidateTT, 1, direction, lunarEclipseSynodicMonthDays)
}
step := 1
if inst.Model() != ephem.Custom {
step = eclipseSearchStep(candidateTT, direction, lunarEclipseSynodicMonthDays)
}
return basic.CalcMoonSHByJDEWithInstance(candidateTT+float64(direction*step)*lunarEclipseSynodicMonthDays, 1, inst)
}
func lunarEclipseMoonCenterPositionAngleWithInstance(ttJDE float64, inst *ephem.Instance) float64 {
if inst == nil {
return lunarEclipseMoonCenterPositionAngle(ttJDE)
}
sunRA, sunDec := basic.HSunApparentRaDecWithInstance(ttJDE, inst)
moonRA, moonDec := basic.HMoonTrueRaDecWithInstance(ttJDE, inst)
return positionAngle(normalizeDegree360(sunRA+180), -sunDec, moonRA, moonDec)
}
func lunarSarosInfoWithInstance(ttJDE float64, inst *ephem.Instance) (SarosInfo, bool) {
if inst == nil {
return lunarSarosInfo(ttJDE)
}
if inst.Model() == ephem.Custom {
return SarosInfo{}, false
}
if info, ok := matchSarosMagic(lunarSarosAnchors[:], 1, lunarSarosHeadOverrides[:], ttJDE); ok {
return info, true
}
info, ok := extendedSarosInfoForModel(ttJDE, 1, inst.Model(), func(lunation, phase int) (float64, bool, bool) {
seed := sarosLunationEpoch + (float64(lunation)+float64(phase)/2)*solarEclipseSynodicMonthDays
result := basic.LunarEclipseDanjonWithInstance(seed, inst)
if result.Type == basic.LunarEclipseNone {
result = basic.LunarEclipseChauvenetWithInstance(seed, inst)
}
valid := !math.IsNaN(result.Maximum) && !math.IsInf(result.Maximum, 0) && math.Abs(result.Maximum-seed) < sarosEclipseSeedToleranceDays
return result.Maximum, result.Type != basic.LunarEclipseNone, valid
})
if !ok {
return SarosInfo{}, false
}
return reconcileSarosSpan(info, ttJDE, 1)
}