16c62a97d5
- 新增时标、ΔT 模型、质心时间与 UT1 支持 - 改进日月食、月掩、行星事件及路径边界计算 - 完善恒星三维自行与动态距离传播 - 扩展 SVG、GeoJSON、KML 输出与底层距离换算工具 - 整理中英文手册、示例资源及回归测试
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
|
||
}
|