044f2f4381
- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算 - 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询 - 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出 - 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解 - 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤 - 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持 - 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理 - 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
152 lines
6.1 KiB
Go
152 lines
6.1 KiB
Go
package svg
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
"b612.me/astro"
|
||
"b612.me/astro/basic"
|
||
"b612.me/astro/moon"
|
||
)
|
||
|
||
// 月掩五族的 UT1 选项:渲染成功、详细图声明尺度、非 UTC 时区报错,且绝不改写调用方传进来的 path。
|
||
func TestOccultationSVGTimeScaleUT1(t *testing.T) {
|
||
cst := time.FixedZone("CST", 8*3600)
|
||
path := occultationTestStarPath(t)
|
||
before := path.Start.Time
|
||
|
||
starSVG, err := StarOccultationDetailedSVG(path, hr4799StarCoordinate(), OccultationDetailedSVGOptions{TimeScale: astro.TimeScaleUT1})
|
||
if err != nil {
|
||
t.Fatalf("恒星月掩 UT1 详细图渲染失败: %v", err)
|
||
}
|
||
assertOccultationUT1Declaration(t, "恒星月掩详细图", starSVG)
|
||
if !path.Start.Time.Equal(before) {
|
||
t.Error("渲染器不得改写调用方传入的 path")
|
||
}
|
||
|
||
starPathSVG, err := StarOccultationPathSVG(path, StarOccultationSVGOptions{TimeScale: astro.TimeScaleUT1})
|
||
if err != nil {
|
||
t.Fatalf("恒星月掩 UT1 路径图渲染失败: %v", err)
|
||
}
|
||
assertOccultationUT1Declaration(t, "恒星月掩全球路径图", starPathSVG)
|
||
if _, err := StarOccultationPathSVG(path, StarOccultationSVGOptions{TimeScale: astro.TimeScaleUT1, Location: cst}); err == nil {
|
||
t.Error("UT1 配非 UTC 时区应报错")
|
||
}
|
||
if _, err := StarOccultationDetailedSVG(path, hr4799StarCoordinate(), OccultationDetailedSVGOptions{TimeScale: astro.TimeScaleUT1, Location: cst}); err == nil {
|
||
t.Error("UT1 配非 UTC 时区应报错(详细图)")
|
||
}
|
||
|
||
planetPath := samplePlanetOccultationPath()
|
||
if _, err := PlanetOccultationDetailedSVG(planetPath, OccultationDetailedSVGOptions{TimeScale: astro.TimeScaleUT1}); err != nil {
|
||
t.Fatalf("行星月掩 UT1 详细图渲染失败: %v", err)
|
||
}
|
||
if _, err := PlanetOccultationDetailedSVG(planetPath, OccultationDetailedSVGOptions{TimeScale: astro.TimeScaleUT1, Location: cst}); err == nil {
|
||
t.Error("行星月掩 UT1 配非 UTC 时区应报错")
|
||
}
|
||
// 行星全球路径图与恒星侧同契约:UT1 配非 UTC 必须报错,配 UTC 必须声明时标。
|
||
if _, err := PlanetOccultationPathSVG(planetPath, PlanetOccultationSVGOptions{TimeScale: astro.TimeScaleUT1, Location: cst}); err == nil {
|
||
t.Error("行星月掩全球路径图 UT1 配非 UTC 时区应报错")
|
||
}
|
||
planetPathSVG, err := PlanetOccultationPathSVG(planetPath, PlanetOccultationSVGOptions{TimeScale: astro.TimeScaleUT1})
|
||
if err != nil {
|
||
t.Fatalf("行星月掩 UT1 全球路径图渲染失败: %v", err)
|
||
}
|
||
assertOccultationUT1Declaration(t, "行星月掩全球路径图", planetPathSVG)
|
||
|
||
info := localHR4799Occultation(t)
|
||
if _, err := LocalStarOccultationSVG(info, hr4799StarCoordinate(), LocalStarOccultationSVGOptions{TimeScale: astro.TimeScaleUT1, Location: time.UTC}); err != nil {
|
||
t.Fatalf("地方月掩 UT1 渲染失败: %v", err)
|
||
}
|
||
if _, err := LocalStarOccultationSVG(info, hr4799StarCoordinate(), LocalStarOccultationSVGOptions{TimeScale: astro.TimeScaleUT1, Location: cst}); err == nil {
|
||
t.Error("地方月掩 UT1 配非 UTC 时区应报错")
|
||
}
|
||
}
|
||
|
||
// assertOccultationUT1Declaration UT1 声明须是可解析的 <text> 元素并写出 DUT1。
|
||
func assertOccultationUT1Declaration(t *testing.T, name, diagram string) {
|
||
t.Helper()
|
||
for _, text := range occultationSVGTexts(t, diagram) {
|
||
if strings.Contains(text.value, "DUT1 = UT1−UTC = ") {
|
||
return
|
||
}
|
||
}
|
||
t.Errorf("%s 应在图内的 <text> 元素里声明 UT1 与 DUT1 差值", name)
|
||
}
|
||
|
||
// 换算器必须保留"该阶段不存在"的零值语义。
|
||
func TestOccultationInfoConvertersPreserveZero(t *testing.T) {
|
||
info := moon.StarOccultationInfo{Greatest: time.Date(2025, 1, 5, 17, 0, 0, 0, time.UTC)}
|
||
got := moon.StarOccultationInfoInUT1(info)
|
||
if !got.Immersion.IsZero() || !got.Emersion.IsZero() {
|
||
t.Error("不存在的阶段应保持零值")
|
||
}
|
||
if !got.Greatest.After(info.Greatest) {
|
||
t.Error("UT1 时刻应领先民用时刻")
|
||
}
|
||
}
|
||
|
||
// 详细版历表面板(ΔT、月距、天平动、月亮地心坐标)必须与输出时标无关:注入一个偏移 1 小时的
|
||
// TT−UTC 覆盖,让"把 UT1 读数当民用时刻"的写法必然算出不同历表值;修复后两次渲染的
|
||
// 非时刻文字必须逐个相同。
|
||
func TestOccultationDetailedPanelIgnoresTimeScale(t *testing.T) {
|
||
astro.SetTTMinusUTC(func(jd float64) float64 {
|
||
return basic.TTMinusUTCSecondsDefault(jd) + 3600
|
||
})
|
||
defer astro.SetTTMinusUTC(nil)
|
||
|
||
path := occultationTestStarPath(t)
|
||
utc, err := StarOccultationDetailedSVG(path, hr4799StarCoordinate(), OccultationDetailedSVGOptions{})
|
||
if err != nil {
|
||
t.Fatalf("UTC 详细图渲染失败: %v", err)
|
||
}
|
||
ut1, err := StarOccultationDetailedSVG(path, hr4799StarCoordinate(), OccultationDetailedSVGOptions{TimeScale: astro.TimeScaleUT1})
|
||
if err != nil {
|
||
t.Fatalf("UT1 详细图渲染失败: %v", err)
|
||
}
|
||
utcValues := occultationPanelValues(t, utc)
|
||
ut1Values := occultationPanelValues(t, ut1)
|
||
if len(utcValues) == 0 {
|
||
t.Fatal("未解析到面板文字")
|
||
}
|
||
if len(utcValues) != len(ut1Values) {
|
||
t.Fatalf("两图文字数不同: %d vs %d", len(utcValues), len(ut1Values))
|
||
}
|
||
for index := range utcValues {
|
||
if utcValues[index] != ut1Values[index] {
|
||
t.Errorf("第 %d 条文字随时标改变: %q vs %q", index, utcValues[index], ut1Values[index])
|
||
}
|
||
}
|
||
}
|
||
|
||
// occultationPanelValues 取出不随时标变化的文字:跳过时刻、时区声明与包含它们的行。
|
||
func occultationPanelValues(t *testing.T, diagram string) []string {
|
||
t.Helper()
|
||
values := make([]string, 0, 32)
|
||
for _, text := range occultationSVGTexts(t, diagram) {
|
||
value := strings.TrimSpace(text.value)
|
||
if value == "" || strings.Contains(value, "UT1") || strings.Contains(value, "DUT1") ||
|
||
strings.Contains(value, "UTC") || strings.Contains(value, "时区") || strings.Contains(value, "图中时刻") {
|
||
continue
|
||
}
|
||
if occultationTimeLike(value) {
|
||
continue
|
||
}
|
||
values = append(values, value)
|
||
}
|
||
return values
|
||
}
|
||
|
||
func occultationTimeLike(value string) bool {
|
||
for _, field := range strings.Fields(value) {
|
||
trimmed := strings.Trim(field, "(),")
|
||
if len(trimmed) >= 5 && trimmed[2] == ':' && trimmed[0] >= '0' && trimmed[0] <= '9' {
|
||
return true
|
||
}
|
||
if len(trimmed) >= 8 && trimmed[4] == '-' && trimmed[7] == '-' {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|