044f2f4381
- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算 - 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询 - 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出 - 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解 - 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤 - 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持 - 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理 - 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
154 lines
6.0 KiB
Go
154 lines
6.0 KiB
Go
package eclipse
|
|
|
|
import (
|
|
"math"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"b612.me/astro/basic"
|
|
"b612.me/astro/internal/ephem"
|
|
)
|
|
|
|
type p3LunarShiftEngine struct {
|
|
base *ephem.Instance
|
|
offset float64
|
|
}
|
|
|
|
func (engine p3LunarShiftEngine) State(target, center ephem.Body, jde float64) ([3]float64, [3]float64, error) {
|
|
return engine.base.State(target, center, jde+engine.offset)
|
|
}
|
|
|
|
func (engine p3LunarShiftEngine) Apparent(target ephem.Body, jde float64) ([3]float64, float64, float64, error) {
|
|
return engine.base.Apparent(target, jde+engine.offset)
|
|
}
|
|
|
|
func (engine p3LunarShiftEngine) Coverage(target ephem.Body) (float64, float64, bool) {
|
|
start, end, ok := engine.base.Coverage(target)
|
|
return start - engine.offset, end - engine.offset, ok
|
|
}
|
|
|
|
func TestP3LunarQueriesPreserveNilAndLegacy(t *testing.T) {
|
|
previous := ephem.Active()
|
|
t.Cleanup(func() { ephem.SetActive(previous) })
|
|
date := time.Date(2022, 11, 8, 12, 0, 0, 0, time.UTC)
|
|
for _, model := range []ephem.Model{ephem.Legacy, ephem.Semianalytic} {
|
|
ephem.SetActive(model)
|
|
inst := ephem.NewModelInstance(model)
|
|
for _, query := range []struct {
|
|
old func(time.Time) (LunarEclipseInfo, bool)
|
|
new func(time.Time, *ephem.Instance) (LunarEclipseInfo, bool)
|
|
}{
|
|
{LunarEclipseOnDate, LunarEclipseOnDateWithInstance},
|
|
{LunarEclipseOnDateDanjon, LunarEclipseOnDateDanjonWithInstance},
|
|
{LunarEclipseOnDateChauvenet, LunarEclipseOnDateChauvenetWithInstance},
|
|
} {
|
|
want, wantOK := query.old(date)
|
|
got, gotOK := query.new(date, nil)
|
|
if gotOK != wantOK || !reflect.DeepEqual(got, want) {
|
|
t.Error("nil changed local-date query")
|
|
}
|
|
if model == ephem.Legacy {
|
|
got, gotOK = query.new(date, inst)
|
|
if gotOK != wantOK || !reflect.DeepEqual(got, want) {
|
|
t.Error("explicit Legacy changed local-date query")
|
|
}
|
|
}
|
|
}
|
|
for _, query := range []struct {
|
|
old func(time.Time) LunarEclipseInfo
|
|
new func(time.Time, *ephem.Instance) LunarEclipseInfo
|
|
}{
|
|
{LastLunarEclipse, LastLunarEclipseWithInstance},
|
|
{LastLunarEclipseDanjon, LastLunarEclipseDanjonWithInstance},
|
|
{LastLunarEclipseChauvenet, LastLunarEclipseChauvenetWithInstance},
|
|
{NextLunarEclipse, NextLunarEclipseWithInstance},
|
|
{NextLunarEclipseDanjon, NextLunarEclipseDanjonWithInstance},
|
|
{NextLunarEclipseChauvenet, NextLunarEclipseChauvenetWithInstance},
|
|
{ClosestLunarEclipse, ClosestLunarEclipseWithInstance},
|
|
{ClosestLunarEclipseDanjon, ClosestLunarEclipseDanjonWithInstance},
|
|
{ClosestLunarEclipseChauvenet, ClosestLunarEclipseChauvenetWithInstance},
|
|
} {
|
|
want := query.old(date)
|
|
if got := query.new(date, nil); !reflect.DeepEqual(got, want) {
|
|
t.Error("nil changed directional search")
|
|
}
|
|
if model == ephem.Legacy {
|
|
if got := query.new(date, inst); !reflect.DeepEqual(got, want) {
|
|
t.Error("explicit Legacy changed directional search")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestP3LunarQueriesUseInstanceThroughout(t *testing.T) {
|
|
previous := ephem.Active()
|
|
t.Cleanup(func() { ephem.SetActive(previous) })
|
|
base := ephem.NewModelInstance(ephem.Semianalytic)
|
|
first := ephem.NewInstance(p3LunarShiftEngine{base: base, offset: 0.1})
|
|
second := ephem.NewInstance(p3LunarShiftEngine{base: base, offset: 0.2})
|
|
date := time.Date(2022, 11, 8, 12, 0, 0, 0, time.FixedZone("UTC+8", 8*3600))
|
|
global, model, generation := ephem.Global(), ephem.Active(), ephem.Generation()
|
|
firstInfo, firstOK := LunarEclipseOnDateWithInstance(date, first)
|
|
secondInfo, secondOK := LunarEclipseOnDateWithInstance(date, second)
|
|
if !firstOK || !secondOK || !firstInfo.HasTotal || !secondInfo.HasTotal {
|
|
t.Fatalf("explicit query lost total eclipse: %+v / %+v", firstInfo, secondInfo)
|
|
}
|
|
if firstInfo.HasSaros || secondInfo.HasSaros {
|
|
t.Error("custom engines must not inherit built-in Saros metadata")
|
|
}
|
|
if math.Abs(firstInfo.GreatestJDE-secondInfo.GreatestJDE-0.1) > 0.001 {
|
|
t.Errorf("event time did not follow instances: %v / %v", firstInfo.GreatestJDE, secondInfo.GreatestJDE)
|
|
}
|
|
if firstInfo.Maximum.Location() != date.Location() || len(firstInfo.ContactPoints) != 6 {
|
|
t.Error("local timezone or contacts were lost")
|
|
}
|
|
if ephem.Global() != global || ephem.Active() != model || ephem.Generation() != generation {
|
|
t.Error("query changed global ephemeris state")
|
|
}
|
|
for _, active := range []ephem.Model{ephem.Legacy, ephem.Semianalytic} {
|
|
ephem.SetActive(active)
|
|
got, ok := LunarEclipseOnDateWithInstance(date, first)
|
|
if !ok || !reflect.DeepEqual(got, firstInfo) {
|
|
t.Error("result, contact orientation or shadow geometry read the global model")
|
|
}
|
|
for _, query := range []func(time.Time, *ephem.Instance) LunarEclipseInfo{LastLunarEclipseWithInstance, NextLunarEclipseWithInstance, ClosestLunarEclipseWithInstance} {
|
|
got := query(firstInfo.Maximum.Add(-time.Minute), first)
|
|
if got.Type == LunarEclipseNone || got.Maximum.IsZero() {
|
|
t.Error("directional search lost instance")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestP3LunarSarosSelectsInstanceModel(t *testing.T) {
|
|
previous := ephem.Active()
|
|
t.Cleanup(func() { ephem.SetActive(previous) })
|
|
for _, model := range []ephem.Model{ephem.Legacy, ephem.Semianalytic} {
|
|
inst := ephem.NewModelInstance(model)
|
|
ephem.SetActive(model)
|
|
for _, seed := range []float64{basic.JDCalc(2022, 11, 8), basic.JDCalc(7000, 1, 1), basic.JDCalc(-5000, 1, 1)} {
|
|
want, wantOK := lunarSarosInfoWithInstance(seed, inst)
|
|
ephem.SetActive(ephem.Model(1 - int(model)))
|
|
got, gotOK := lunarSarosInfoWithInstance(seed, inst)
|
|
if gotOK != wantOK || got != want {
|
|
t.Errorf("Saros read active model instead of instance: %+v / %+v", got, want)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestP3LunarQueriesStopOnEngineFailure(t *testing.T) {
|
|
inst := ephem.NewInstance(nil)
|
|
date := time.Date(2022, 11, 8, 12, 0, 0, 0, time.UTC)
|
|
if info, ok := LunarEclipseOnDateWithInstance(date, inst); ok || info.Type != "" {
|
|
t.Error("failed engine produced a local-date eclipse")
|
|
}
|
|
for _, query := range []func(time.Time, *ephem.Instance) LunarEclipseInfo{LastLunarEclipseWithInstance, NextLunarEclipseWithInstance, ClosestLunarEclipseWithInstance} {
|
|
if info := query(date, inst); !info.Maximum.IsZero() {
|
|
t.Error("failed engine fell back during directional search")
|
|
}
|
|
}
|
|
}
|