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
+186
View File
@@ -0,0 +1,186 @@
package eclipse
import (
"b612.me/astro/basic"
"b612.me/astro/internal/ephem"
"reflect"
"testing"
"time"
)
type contextAPI interface {
ClosestGeometricLocalLunarEclipse(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo
ClosestGeometricLocalLunarEclipseChauvenet(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo
ClosestGeometricLocalLunarEclipseDanjon(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo
ClosestGeometricLocalSolarEclipse(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo
ClosestGeometricLocalSolarEclipseIAUSingleK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo
ClosestGeometricLocalSolarEclipseNASABulletinSplitK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo
ClosestLocalAnnularSolarEclipse(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool)
ClosestLocalLunarEclipse(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo
ClosestLocalLunarEclipseChauvenet(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo
ClosestLocalLunarEclipseDanjon(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo
ClosestLocalSolarEclipse(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo
ClosestLocalSolarEclipseIAUSingleK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo
ClosestLocalSolarEclipseNASABulletinSplitK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo
ClosestLocalTotalLunarEclipse(date time.Time, lon, lat, height float64) (LocalLunarEclipseInfo, bool)
ClosestLocalTotalSolarEclipse(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool)
ClosestLunarEclipse(date time.Time) LunarEclipseInfo
ClosestLunarEclipseChauvenet(date time.Time) LunarEclipseInfo
ClosestLunarEclipseDanjon(date time.Time) LunarEclipseInfo
ClosestSolarEclipse(date time.Time) SolarEclipseInfo
ClosestSolarEclipseIAUSingleK(date time.Time) SolarEclipseInfo
ClosestSolarEclipseNASABulletinSplitK(date time.Time) SolarEclipseInfo
ClosestSolarEclipseWithOptions(date time.Time, options SolarEclipseOptions) SolarEclipseInfo
GeometricLocalLunarEclipseOnDate(date time.Time, lon, lat, height float64) (LocalLunarEclipseInfo, bool)
GeometricLocalLunarEclipseOnDateChauvenet(date time.Time, lon, lat, height float64) (LocalLunarEclipseInfo, bool)
GeometricLocalLunarEclipseOnDateDanjon(date time.Time, lon, lat, height float64) (LocalLunarEclipseInfo, bool)
GeometricLocalSolarEclipseOnDate(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool)
GeometricLocalSolarEclipseOnDateIAUSingleK(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool)
GeometricLocalSolarEclipseOnDateNASABulletinSplitK(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool)
LastGeometricLocalLunarEclipse(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo
LastGeometricLocalLunarEclipseChauvenet(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo
LastGeometricLocalLunarEclipseDanjon(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo
LastGeometricLocalSolarEclipse(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo
LastGeometricLocalSolarEclipseIAUSingleK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo
LastGeometricLocalSolarEclipseNASABulletinSplitK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo
LastLocalAnnularSolarEclipse(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool)
LastLocalLunarEclipse(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo
LastLocalLunarEclipseChauvenet(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo
LastLocalLunarEclipseDanjon(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo
LastLocalSolarEclipse(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo
LastLocalSolarEclipseIAUSingleK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo
LastLocalSolarEclipseNASABulletinSplitK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo
LastLocalTotalLunarEclipse(date time.Time, lon, lat, height float64) (LocalLunarEclipseInfo, bool)
LastLocalTotalSolarEclipse(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool)
LastLunarEclipse(date time.Time) LunarEclipseInfo
LastLunarEclipseChauvenet(date time.Time) LunarEclipseInfo
LastLunarEclipseDanjon(date time.Time) LunarEclipseInfo
LastSolarEclipse(date time.Time) SolarEclipseInfo
LastSolarEclipseIAUSingleK(date time.Time) SolarEclipseInfo
LastSolarEclipseNASABulletinSplitK(date time.Time) SolarEclipseInfo
LastSolarEclipseWithOptions(date time.Time, options SolarEclipseOptions) SolarEclipseInfo
LocalLunarEclipseOnDate(date time.Time, lon, lat, height float64) (LocalLunarEclipseInfo, bool)
LocalLunarEclipseOnDateChauvenet(date time.Time, lon, lat, height float64) (LocalLunarEclipseInfo, bool)
LocalLunarEclipseOnDateDanjon(date time.Time, lon, lat, height float64) (LocalLunarEclipseInfo, bool)
LocalSolarEclipseInfoInUT1(info LocalSolarEclipseInfo) LocalSolarEclipseInfo
LocalSolarEclipseOnDate(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool)
LocalSolarEclipseOnDateIAUSingleK(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool)
LocalSolarEclipseOnDateNASABulletinSplitK(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool)
LocalSolarEclipseOnDateWithOptions(date time.Time, lon, lat, height float64, options SolarEclipseOptions) (LocalSolarEclipseInfo, bool)
LunarEclipseGeocentricPanelAt(date time.Time) (LunarEclipseGeocentricPanel, bool)
LunarEclipseGeocentricPanelChauvenet(date time.Time) (LunarEclipseGeocentricPanel, bool)
LunarEclipseInfoInUT1(info LunarEclipseInfo) LunarEclipseInfo
LunarEclipseOnDate(date time.Time) (LunarEclipseInfo, bool)
LunarEclipseOnDateChauvenet(date time.Time) (LunarEclipseInfo, bool)
LunarEclipseOnDateDanjon(date time.Time) (LunarEclipseInfo, bool)
NewSolarEclipseShadowSolver(options SolarEclipseShadowSolverOptions) *SolarEclipseShadowSolver
NextGeometricLocalLunarEclipse(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo
NextGeometricLocalLunarEclipseChauvenet(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo
NextGeometricLocalLunarEclipseDanjon(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo
NextGeometricLocalSolarEclipse(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo
NextGeometricLocalSolarEclipseIAUSingleK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo
NextGeometricLocalSolarEclipseNASABulletinSplitK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo
NextLocalAnnularSolarEclipse(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool)
NextLocalLunarEclipse(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo
NextLocalLunarEclipseChauvenet(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo
NextLocalLunarEclipseDanjon(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo
NextLocalSolarEclipse(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo
NextLocalSolarEclipseIAUSingleK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo
NextLocalSolarEclipseNASABulletinSplitK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo
NextLocalTotalLunarEclipse(date time.Time, lon, lat, height float64) (LocalLunarEclipseInfo, bool)
NextLocalTotalSolarEclipse(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool)
NextLunarEclipse(date time.Time) LunarEclipseInfo
NextLunarEclipseChauvenet(date time.Time) LunarEclipseInfo
NextLunarEclipseDanjon(date time.Time) LunarEclipseInfo
NextSolarEclipse(date time.Time) SolarEclipseInfo
NextSolarEclipseIAUSingleK(date time.Time) SolarEclipseInfo
NextSolarEclipseNASABulletinSplitK(date time.Time) SolarEclipseInfo
NextSolarEclipseWithOptions(date time.Time, options SolarEclipseOptions) SolarEclipseInfo
SearchLocalCentralSolarEclipse(
date time.Time, lon, lat, height float64,
options SolarEclipseLocalSearchOptions,
) (LocalSolarEclipseInfo, SolarEclipseLocalSearchStatus)
SolarEclipseBesselianElements(
seedJDE float64, options SolarEclipseBesselianElementsOptions,
) (SolarEclipseBesselianElementsResult, bool)
SolarEclipseBesselianMuForPublishedTable(
mu SolarEclipseBesselianPolynomial, deltaTSeconds float64,
) SolarEclipseBesselianPolynomial
SolarEclipseCandidates(
start, end time.Time, options SolarEclipseCandidateOptions,
) []SolarEclipseCandidate
SolarEclipseCentralPath(date time.Time, options SolarEclipsePathOptions) (SolarEclipsePath, bool)
SolarEclipseCentralPathIAUSingleK(date time.Time, options SolarEclipsePathOptions) (SolarEclipsePath, bool)
SolarEclipseCentralPathNASABulletinSplitK(date time.Time, options SolarEclipsePathOptions) (SolarEclipsePath, bool)
SolarEclipseGeocentricPanelAt(date time.Time) (SolarEclipseGeocentricPanel, bool)
SolarEclipseGeocentricPanelIAUSingleK(date time.Time) (SolarEclipseGeocentricPanel, bool)
SolarEclipseGeocentricPanelInUT1(panel SolarEclipseGeocentricPanel) SolarEclipseGeocentricPanel
SolarEclipseGeocentricPanelWithOptions(date time.Time, options SolarEclipseOptions) (SolarEclipseGeocentricPanel, bool)
SolarEclipseInfoInUT1(info SolarEclipseInfo) SolarEclipseInfo
SolarEclipseOnDate(date time.Time) (SolarEclipseInfo, bool)
SolarEclipseOnDateIAUSingleK(date time.Time) (SolarEclipseInfo, bool)
SolarEclipseOnDateNASABulletinSplitK(date time.Time) (SolarEclipseInfo, bool)
SolarEclipseOnDateWithOptions(date time.Time, options SolarEclipseOptions) (SolarEclipseInfo, bool)
SolarEclipsePartialArea(date time.Time, options SolarEclipsePartialAreaOptions) (SolarEclipsePartialAreaInfo, bool)
SolarEclipsePartialAreaIAUSingleK(date time.Time, options SolarEclipsePartialAreaOptions) (SolarEclipsePartialAreaInfo, bool)
SolarEclipsePartialAreaNASABulletinSplitK(date time.Time, options SolarEclipsePartialAreaOptions) (SolarEclipsePartialAreaInfo, bool)
SolarEclipsePartialFootprints(date time.Time, options SolarEclipsePartialFootprintOptions) (SolarEclipsePartialFootprintsInfo, bool)
SolarEclipsePartialFootprintsIAUSingleK(date time.Time, options SolarEclipsePartialFootprintOptions) (SolarEclipsePartialFootprintsInfo, bool)
SolarEclipsePartialFootprintsInUT1(info SolarEclipsePartialFootprintsInfo) SolarEclipsePartialFootprintsInfo
SolarEclipsePartialFootprintsNASABulletinSplitK(date time.Time, options SolarEclipsePartialFootprintOptions) (SolarEclipsePartialFootprintsInfo, bool)
SolarEclipsePathInUT1(path SolarEclipsePath) SolarEclipsePath
SolarEclipseShadowAt(value time.Time) (SolarEclipseShadowInstant, bool)
SolarEclipseShadowBetween(
start, end time.Time, step time.Duration,
) []SolarEclipseShadowInstant
SolarEclipseShadowBetweenWithOptions(
start, end time.Time, step time.Duration, options SolarEclipseShadowSolverOptions,
) []SolarEclipseShadowInstant
SolarEclipseStationStateAt(value time.Time, lon, lat, height float64) SolarEclipseStationState
TimeLabelsInUT1(times []time.Time) []time.Time
}
var _ contextAPI = (*Eclipse)(nil)
func TestContextModuleIsolation(t *testing.T) {
previous := basic.GetEphemerisModel()
t.Cleanup(func() { basic.SetEphemerisModel(previous) })
date := time.Date(2024, 4, 8, 0, 0, 0, 0, time.UTC)
for _, model := range []ephem.Model{ephem.Legacy, ephem.Semianalytic} {
basic.SetEphemerisModel(basic.EphemerisModel(model))
want, wantOK := SolarEclipseOnDate(date)
module := NewWithInstance(ephem.NewModelInstance(model))
basic.SetEphemerisModel(basic.EphemerisModel(1 - int(model)))
got, gotOK := module.SolarEclipseOnDate(date)
if !wantOK || gotOK != wantOK || got != want {
t.Fatalf("model %d: solar eclipse escaped instance", model)
}
}
}
// 站心日食、候选枚举与中心食搜索必须使用模块实例,切换全局模型不得改变结果。
func TestContextLocalSolarEclipseIsolation(t *testing.T) {
previous := basic.GetEphemerisModel()
t.Cleanup(func() { basic.SetEphemerisModel(previous) })
date := time.Date(2024, 3, 1, 0, 0, 0, 0, time.UTC)
for _, model := range []ephem.Model{ephem.Legacy, ephem.Semianalytic} {
module := NewWithInstance(ephem.NewModelInstance(model))
run := func() []interface{} {
next := module.NextLocalSolarEclipse(date, -97, 32, 0)
iau := module.NextLocalSolarEclipseIAUSingleK(date, -97, 32, 0)
onDate, ok := module.LocalSolarEclipseOnDate(time.Date(2024, 4, 8, 0, 0, 0, 0, time.UTC), -97, 32, 0)
candidates := module.SolarEclipseCandidates(date, date.AddDate(1, 0, 0), SolarEclipseCandidateOptions{})
central, status := module.SearchLocalCentralSolarEclipse(date, -97, 32, 0, SolarEclipseLocalSearchOptions{MaxYears: 2})
return []interface{}{next, iau, onDate, ok, candidates, central, status}
}
basic.SetEphemerisModel(basic.EphemerisModel(model))
want := run()
basic.SetEphemerisModel(basic.EphemerisModel(1 - int(model)))
got := run()
for i := range want {
if !reflect.DeepEqual(got[i], want[i]) {
t.Fatalf("model %d: result %d escaped instance:\n got %+v\nwant %+v", model, i, got[i], want[i])
}
}
}
}