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
+42 -29
View File
@@ -5,6 +5,7 @@ import (
"time"
"b612.me/astro/basic"
"b612.me/astro/internal/geodata"
)
const (
@@ -13,8 +14,8 @@ const (
// brownLunationSynodicMonth 是平均朔望月长度,用于把朔的儒略日换算成月序数。
brownLunationSynodicMonth = 29.530588853
// 视半径换算地平视差用的半径比:地球赤道半径除以天体半径。
horizontalParallaxMoonRatio = 6378.137 / 1737.4
horizontalParallaxSunRatio = 6378.137 / 696000.0
horizontalParallaxMoonRatio = geodata.EarthWGS84RadiusKM / 1737.4
horizontalParallaxSunRatio = geodata.EarthWGS84RadiusKM / 696000.0
)
// SolarEclipseGeocentricPanel 详细面板所需的食甚时刻地心量 / geocentric quantities at greatest eclipse for detailed panels.
@@ -70,26 +71,23 @@ func horizontalParallaxArcsec(semidiameterArcsec, radiusRatio float64) float64 {
return math.Asin(math.Max(-1, math.Min(1, sine*radiusRatio))) * 180 / math.Pi * 3600
}
// SolarEclipseGeocentricPanelAt 计算给定日食在食甚时刻的地心量面板。
// SolarEclipseGeocentricPanelAt computes the geocentric panel of one eclipse at greatest eclipse.
func SolarEclipseGeocentricPanelAt(date time.Time) (SolarEclipseGeocentricPanel, bool) {
return SolarEclipseGeocentricPanelWithOptions(date, SolarEclipseOptions{RadiusModel: SolarEclipseModelNASABulletinSplitK})
// SolarEclipseGeocentricPanelAt 计算给定日食在食甚时刻的地心量面板 / geocentric solar eclipse panel at greatest eclipse.
func (module *Eclipse) SolarEclipseGeocentricPanelAt(date time.Time) (SolarEclipseGeocentricPanel, bool) {
return module.SolarEclipseGeocentricPanelWithOptions(date, SolarEclipseOptions{RadiusModel: SolarEclipseModelNASABulletinSplitK})
}
// SolarEclipseGeocentricPanelIAUSingleK 使用 IAU Single-K 计算地心量面板。
// SolarEclipseGeocentricPanelIAUSingleK computes the geocentric panel with the IAU Single-K model.
func SolarEclipseGeocentricPanelIAUSingleK(date time.Time) (SolarEclipseGeocentricPanel, bool) {
return SolarEclipseGeocentricPanelWithOptions(date, SolarEclipseOptions{RadiusModel: SolarEclipseModelIAUSingleK})
// SolarEclipseGeocentricPanelIAUSingleK 使用 IAU Single-K 计算地心量面板 / geocentric solar eclipse panel with IAU Single-K.
func (module *Eclipse) SolarEclipseGeocentricPanelIAUSingleK(date time.Time) (SolarEclipseGeocentricPanel, bool) {
return module.SolarEclipseGeocentricPanelWithOptions(date, SolarEclipseOptions{RadiusModel: SolarEclipseModelIAUSingleK})
}
// SolarEclipseGeocentricPanelWithOptions 使用自定义半径口径计算食甚时刻的地心量面板。
// SolarEclipseGeocentricPanelWithOptions computes the geocentric panel at greatest eclipse with custom radius conventions.
func SolarEclipseGeocentricPanelWithOptions(date time.Time, options SolarEclipseOptions) (SolarEclipseGeocentricPanel, bool) {
info, ok := SolarEclipseOnDateWithOptions(date, options)
// SolarEclipseGeocentricPanelWithOptions 使用自定义半径口径计算食甚时刻的地心量面板 / geocentric solar eclipse panel with custom radius options.
func (module *Eclipse) SolarEclipseGeocentricPanelWithOptions(date time.Time, options SolarEclipseOptions) (SolarEclipseGeocentricPanel, bool) {
info, ok := module.SolarEclipseOnDateWithOptions(date, options)
if !ok {
return SolarEclipseGeocentricPanel{}, false
}
panel := solarEclipseGeocentricPanelAt(info)
panel := solarEclipseGeocentricPanelAt(info, module)
if info.Model == SolarEclipseModelIAUSingleK {
panel.Ephemeris = "IAU Single-K"
panel.PenumbralK = basic.SolarEclipseIAUSingleRadiusK
@@ -103,10 +101,15 @@ func SolarEclipseGeocentricPanelWithOptions(date time.Time, options SolarEclipse
return panel, true
}
func solarEclipseGeocentricPanelAt(info SolarEclipseInfo) SolarEclipseGeocentricPanel {
func solarEclipseGeocentricPanelAt(info SolarEclipseInfo, modules ...*Eclipse) SolarEclipseGeocentricPanel {
module := &defaultModule
if len(modules) > 0 {
module = modules[0]
}
tt := solarEclipseTimeToTTJDE(info.GreatestEclipse)
conjunctionJDE := basic.CalcMoonSHByJDE(tt, 0)
rightAscensionJDE := solarEclipseRightAscensionConjunction(tt)
conjunctionJDE := basic.CalcMoonSHByJDEWithInstance(tt, 0, module.inst)
rightAscensionJDE := solarEclipseRightAscensionConjunction(tt, module)
panel := SolarEclipseGeocentricPanel{
Conjunction: solarEclipseTTJDEToTime(conjunctionJDE, info.GreatestEclipse.Location()),
ConjunctionJDE: conjunctionJDE,
@@ -115,14 +118,14 @@ func solarEclipseGeocentricPanelAt(info SolarEclipseInfo) SolarEclipseGeocentric
RightAscensionConjunctionJD: rightAscensionJDE - basic.DeltaT(tt, true)/86400,
DeltaTSeconds: basic.DeltaT(tt, true),
}
panel.SunRightAscensionDeg, panel.SunDeclinationDeg = basic.SunApparentRaDec(tt)
panel.MoonRightAscensionDeg, panel.MoonDeclinationDeg = basic.HMoonTrueRaDec(tt)
panel.SunRightAscensionDeg, panel.SunDeclinationDeg = basic.SunApparentRaDecWithInstance(tt, module.inst)
panel.MoonRightAscensionDeg, panel.MoonDeclinationDeg = basic.HMoonTrueRaDecWithInstance(tt, module.inst)
panel.SunRadiusModel = info.SunRadiusModel
panel.SunSemidiameterArcsec = basic.SolarEclipseSunSemidiameter(tt, info.SunRadiusModel)
panel.MoonSemidiameterArcsec = basic.MoonSemidiameter(tt)
panel.SunSemidiameterArcsec = basic.SolarEclipseSunSemidiameterWithInstance(tt, info.SunRadiusModel, module.inst)
panel.MoonSemidiameterArcsec = basic.MoonSemidiameterWithInstance(tt, module.inst)
panel.SunParallaxArcsec = horizontalParallaxArcsec(panel.SunSemidiameterArcsec, horizontalParallaxSunRatio)
panel.MoonParallaxArcsec = horizontalParallaxArcsec(panel.MoonSemidiameterArcsec, horizontalParallaxMoonRatio)
physical := basic.MoonPhysical(tt)
physical := basic.MoonPhysicalWithInstance(tt, module.inst)
panel.LibrationLongitudeDeg = physical.LibrationLongitude
panel.LibrationLatitudeDeg = physical.LibrationLatitude
panel.LibrationPositionAngleDeg = physical.PositionAngle
@@ -131,22 +134,32 @@ func solarEclipseGeocentricPanelAt(info SolarEclipseInfo) SolarEclipseGeocentric
}
// solarEclipseRightAscensionGap 返回月亮与太阳的地心视赤经差,单位弧度。
func solarEclipseRightAscensionGap(tt float64) float64 {
sunRightAscension, _ := basic.SunApparentRaDec(tt)
moonRightAscension, _ := basic.HMoonTrueRaDec(tt)
func solarEclipseRightAscensionGap(tt float64, modules ...*Eclipse) float64 {
module := &defaultModule
if len(modules) > 0 {
module = modules[0]
}
sunRightAscension, _ := basic.SunApparentRaDecWithInstance(tt, module.inst)
moonRightAscension, _ := basic.HMoonTrueRaDecWithInstance(tt, module.inst)
return math.Remainder(moonRightAscension-sunRightAscension, 360) * math.Pi / 180
}
// solarEclipseRightAscensionConjunction 求地心视赤经相等的时刻。
// 赤经差在合附近单调,用牛顿法从食甚时刻收敛即可,不需要预先求朔。
func solarEclipseRightAscensionConjunction(tt float64) float64 {
func solarEclipseRightAscensionConjunction(tt float64, modules ...*Eclipse) float64 {
module := &defaultModule
if len(modules) > 0 {
module = modules[0]
}
estimate := tt
for iteration := 0; iteration < 40; iteration++ {
value := solarEclipseRightAscensionGap(estimate)
value := solarEclipseRightAscensionGap(estimate, module)
if math.Abs(value) < 1e-10 {
break
}
derivative := (solarEclipseRightAscensionGap(estimate+1e-5) - solarEclipseRightAscensionGap(estimate-1e-5)) / 2e-5
derivative := (solarEclipseRightAscensionGap(estimate+1e-5, module) - solarEclipseRightAscensionGap(estimate-1e-5, module)) / 2e-5
if derivative == 0 || math.IsNaN(derivative) || math.IsInf(derivative, 0) {
break
}