16c62a97d5
- 新增时标、ΔT 模型、质心时间与 UT1 支持 - 改进日月食、月掩、行星事件及路径边界计算 - 完善恒星三维自行与动态距离传播 - 扩展 SVG、GeoJSON、KML 输出与底层距离换算工具 - 整理中英文手册、示例资源及回归测试
230 lines
10 KiB
Go
230 lines
10 KiB
Go
package svg
|
|
|
|
import (
|
|
"errors"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"b612.me/astro/moon"
|
|
)
|
|
|
|
// 本文件钉住详细版式三入口的契约:确定性、画布边界、面板数值与选项校验。
|
|
|
|
func TestStarOccultationDetailedSVGDeterministicXML(t *testing.T) {
|
|
path := occultationTestStarPath(t)
|
|
options := OccultationDetailedSVGOptions{Width: 1000, Height: 1414, Location: time.UTC}
|
|
first, err := StarOccultationDetailedSVG(path, hr4799StarCoordinate(), options)
|
|
if err != nil {
|
|
t.Fatalf("StarOccultationDetailedSVG() error = %v", err)
|
|
}
|
|
if len(first) == 0 {
|
|
t.Fatal("StarOccultationDetailedSVG() returned an empty diagram")
|
|
}
|
|
second, err := StarOccultationDetailedSVG(path, hr4799StarCoordinate(), options)
|
|
if err != nil {
|
|
t.Fatalf("second StarOccultationDetailedSVG() error = %v", err)
|
|
}
|
|
if first != second {
|
|
t.Fatal("detailed stellar SVG is not byte-identical across renders")
|
|
}
|
|
if err := validateXML(first); err != nil {
|
|
t.Fatalf("detailed stellar SVG is not valid XML: %v", err)
|
|
}
|
|
for _, want := range []string{`class="occultation-detailed-panel"`, "月亮(地心坐标)", "天平动"} {
|
|
if !strings.Contains(first, want) {
|
|
t.Fatalf("detailed stellar SVG is missing %q", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPlanetOccultationDetailedSVGDeterministicXML(t *testing.T) {
|
|
path := samplePlanetOccultationPath()
|
|
options := OccultationDetailedSVGOptions{Width: 1414, Height: 1000, Location: time.UTC}
|
|
first, err := PlanetOccultationDetailedSVG(path, options)
|
|
if err != nil {
|
|
t.Fatalf("PlanetOccultationDetailedSVG() error = %v", err)
|
|
}
|
|
if len(first) == 0 {
|
|
t.Fatal("PlanetOccultationDetailedSVG() returned an empty diagram")
|
|
}
|
|
second, err := PlanetOccultationDetailedSVG(path, options)
|
|
if err != nil {
|
|
t.Fatalf("second PlanetOccultationDetailedSVG() error = %v", err)
|
|
}
|
|
if first != second {
|
|
t.Fatal("detailed planetary SVG is not byte-identical across renders")
|
|
}
|
|
if err := validateXML(first); err != nil {
|
|
t.Fatalf("detailed planetary SVG is not valid XML: %v", err)
|
|
}
|
|
if !strings.Contains(first, "全掩始") || !strings.Contains(first, "全掩带宽") {
|
|
t.Fatal("detailed planetary SVG dropped total-band content")
|
|
}
|
|
}
|
|
|
|
func TestOccultationDetailedSVGKeepsContentInsideCanvas(t *testing.T) {
|
|
sizes := [][2]int{{1000, 1414}, {1414, 1000}, {800, 600}}
|
|
starPath := occultationTestStarPath(t)
|
|
for _, size := range sizes {
|
|
size := size
|
|
t.Run(strconv.Itoa(size[0])+"x"+strconv.Itoa(size[1]), func(t *testing.T) {
|
|
options := OccultationDetailedSVGOptions{Width: size[0], Height: size[1], Location: time.UTC}
|
|
diagrams := map[string]string{}
|
|
star, err := StarOccultationDetailedSVG(starPath, hr4799StarCoordinate(), options)
|
|
if err != nil {
|
|
t.Fatalf("stellar detailed render: %v", err)
|
|
}
|
|
diagrams["stellar"] = star
|
|
planet, err := PlanetOccultationDetailedSVG(samplePlanetOccultationPath(), options)
|
|
if err != nil {
|
|
t.Fatalf("planetary detailed render: %v", err)
|
|
}
|
|
diagrams["planetary"] = planet
|
|
for name, diagram := range diagrams {
|
|
texts := occultationSVGTexts(t, diagram)
|
|
if len(texts) == 0 {
|
|
t.Fatalf("%s detailed SVG has no text", name)
|
|
}
|
|
if outside := occultationSVGTextsOutsideCanvas(texts, float64(size[0]), float64(size[1])); len(outside) > 0 {
|
|
t.Fatalf("%s detailed SVG has %d texts outside %dx%d: %v", name, len(outside), size[0], size[1], outside)
|
|
}
|
|
for _, rect := range occultationSVGPanelRects(t, diagram) {
|
|
if rect[0] < 0 || rect[1] < 0 || rect[0]+rect[2] > float64(size[0]) || rect[1]+rect[3] > float64(size[1]) {
|
|
t.Fatalf("%s detailed SVG panel rect %.1f,%.1f %.1fx%.1f leaves %dx%d",
|
|
name, rect[0], rect[1], rect[2], rect[3], size[0], size[1])
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOccultationDetailedSVGRejectsCanvasTooSmall(t *testing.T) {
|
|
starPath := sampleStarOccultationPath()
|
|
planetPath := samplePlanetOccultationPath()
|
|
tests := []struct {
|
|
name string
|
|
width, height int
|
|
}{
|
|
{name: "reference overflow", width: 400, height: 300},
|
|
{name: "narrow", width: 479, height: 1414},
|
|
{name: "short", width: 1000, height: 319},
|
|
{name: "single pixel", width: 1, height: 1},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
options := OccultationDetailedSVGOptions{Width: test.width, Height: test.height}
|
|
if _, err := StarOccultationDetailedSVG(starPath, hr4799StarCoordinate(), options); !errors.Is(err, ErrInvalidOccultationDetailedSVGOptions) {
|
|
t.Fatalf("stellar detailed %dx%d error = %v, want ErrInvalidOccultationDetailedSVGOptions", test.width, test.height, err)
|
|
}
|
|
if _, err := PlanetOccultationDetailedSVG(planetPath, options); !errors.Is(err, ErrInvalidOccultationDetailedSVGOptions) {
|
|
t.Fatalf("planetary detailed %dx%d error = %v, want ErrInvalidOccultationDetailedSVGOptions", test.width, test.height, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOccultationDetailedSVGRejectsInvalidPathOnce(t *testing.T) {
|
|
starPath := sampleStarOccultationPath()
|
|
starPath.Greatest.Longitude = math.NaN()
|
|
_, err := StarOccultationDetailedSVG(starPath, hr4799StarCoordinate(), OccultationDetailedSVGOptions{})
|
|
if !errors.Is(err, ErrInvalidStarOccultationPath) {
|
|
t.Fatalf("stellar detailed invalid path error = %v, want ErrInvalidStarOccultationPath", err)
|
|
}
|
|
if count := strings.Count(err.Error(), "invalid stellar occultation path"); count != 1 {
|
|
t.Fatalf("stellar detailed error = %q, sentinel prefix appears %d times", err, count)
|
|
}
|
|
planetPath := samplePlanetOccultationPath()
|
|
planetPath.TotalBandFootprints = []moon.PlanetOccultationFootprint{{Time: planetPath.TotalStart.Time}}
|
|
_, err = PlanetOccultationDetailedSVG(planetPath, OccultationDetailedSVGOptions{})
|
|
if !errors.Is(err, ErrInvalidPlanetOccultationPath) {
|
|
t.Fatalf("planetary detailed invalid path error = %v, want ErrInvalidPlanetOccultationPath", err)
|
|
}
|
|
if count := strings.Count(err.Error(), "invalid planetary occultation path"); count != 1 {
|
|
t.Fatalf("planetary detailed error = %q, sentinel prefix appears %d times", err, count)
|
|
}
|
|
}
|
|
|
|
func TestOccultationDetailedSVGPanelValuesGolden(t *testing.T) {
|
|
options := OccultationDetailedSVGOptions{Width: 1000, Height: 1414, Location: time.UTC}
|
|
starDiagram, err := StarOccultationDetailedSVG(sampleStarOccultationPath(), hr4799StarCoordinate(), options)
|
|
if err != nil {
|
|
t.Fatalf("StarOccultationDetailedSVG() error = %v", err)
|
|
}
|
|
star := occultationSVGDetailedPanelRows(t, starDiagram)
|
|
for _, expected := range []struct {
|
|
title, label, value string
|
|
}{
|
|
{"月亮(地心坐标)", "赤经 R.A.", "23h37m22.4s"},
|
|
{"月亮(地心坐标)", "赤纬 Dec.", "-00°01'34.1\""},
|
|
{"月亮(地心坐标)", "视半径 S.D.", "00°15'25.5\""},
|
|
{"月亮(地心坐标)", "地平视差 H.P.", "00°56'37.7\""},
|
|
{"目标天体", "目标", "HR 4799"},
|
|
{"目标天体", "赤经 R.A.", "12h36m47.4s"},
|
|
{"目标天体", "赤纬 Dec.", "-05°49'55.0\""},
|
|
{"历表与常数", "ΔT", "69.2 s"},
|
|
{"历表与常数", "月距", "387216 km"},
|
|
{"历表与常数", "部分掩带宽", "3300.0 km"},
|
|
{"天平动", "经天平动 l", "-5.86°"},
|
|
{"天平动", "纬天平动 b", "-2.79°"},
|
|
{"天平动", "自转轴位置角 c", "-21.92°"},
|
|
} {
|
|
if got := occultationSVGPanelValue(t, star, expected.title, expected.label); got != expected.value {
|
|
t.Fatalf("stellar panel %q row %q = %q, want %q", expected.title, expected.label, got, expected.value)
|
|
}
|
|
}
|
|
|
|
planetDiagram, err := PlanetOccultationDetailedSVG(samplePlanetOccultationPath(), options)
|
|
if err != nil {
|
|
t.Fatalf("PlanetOccultationDetailedSVG() error = %v", err)
|
|
}
|
|
planet := occultationSVGDetailedPanelRows(t, planetDiagram)
|
|
for _, expected := range []struct {
|
|
title, label, value string
|
|
}{
|
|
{"目标天体", "目标", "土星"},
|
|
{"目标天体", "赤经 R.A.", "00h58m06.2s"},
|
|
{"目标天体", "赤纬 Dec.", "+03°27'41.4\""},
|
|
{"目标天体", "视半径 S.D.", "00°00'09.3\""},
|
|
{"历表与常数", "ΔT", "69.2 s"},
|
|
{"历表与常数", "部分掩带宽", "3300.0 km"},
|
|
{"历表与常数", "全掩带宽", "3100.0 km"},
|
|
{"接触时刻", "全掩始", "10:30:00"},
|
|
{"接触时刻", "全掩终", "12:30:00"},
|
|
} {
|
|
if got := occultationSVGPanelValue(t, planet, expected.title, expected.label); got != expected.value {
|
|
t.Fatalf("planetary panel %q row %q = %q, want %q", expected.title, expected.label, got, expected.value)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestOccultationDetailedSVGMoonRowsFollowGeocentricEphemeris 卡住月心地心量的量级,
|
|
// 避免把 ΔT、视差常数抄错却仍然画出"看起来正常"的面板。
|
|
func TestOccultationDetailedSVGMoonRowsFollowGeocentricEphemeris(t *testing.T) {
|
|
diagram, err := StarOccultationDetailedSVG(sampleStarOccultationPath(), hr4799StarCoordinate(), OccultationDetailedSVGOptions{
|
|
Width: 1000, Height: 1414, Location: time.UTC,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("StarOccultationDetailedSVG() error = %v", err)
|
|
}
|
|
panels := occultationSVGDetailedPanelRows(t, diagram)
|
|
if deltaT := occultationSVGNumericPrefix(t, occultationSVGPanelValue(t, panels, "历表与常数", "ΔT")); deltaT < 40 || deltaT > 110 {
|
|
t.Fatalf("panel ΔT = %.1f s, want a 2026 value between 40 and 110 s", deltaT)
|
|
}
|
|
if distance := occultationSVGNumericPrefix(t, occultationSVGPanelValue(t, panels, "历表与常数", "月距")); distance < 350000 || distance > 410000 {
|
|
t.Fatalf("panel Moon distance = %.0f km, want a physical geocentric distance", distance)
|
|
}
|
|
if parallax := occultationSVGSexagesimal(t, occultationSVGPanelValue(t, panels, "月亮(地心坐标)", "地平视差 H.P.")); parallax < 0.9 || parallax > 1.02 {
|
|
t.Fatalf("panel horizontal parallax = %.4f°, want about 1° from the geocentric distance", parallax)
|
|
}
|
|
if semidiameter := occultationSVGSexagesimal(t, occultationSVGPanelValue(t, panels, "月亮(地心坐标)", "视半径 S.D.")); semidiameter < 0.23 || semidiameter > 0.28 {
|
|
t.Fatalf("panel semidiameter = %.4f°, want about 0.25°", semidiameter)
|
|
}
|
|
if rightAscension := occultationSVGSexagesimal(t, occultationSVGPanelValue(t, panels, "月亮(地心坐标)", "赤经 R.A.")); rightAscension < 350 || rightAscension > 360 {
|
|
t.Fatalf("panel Moon R.A. = %.3f°, want the same geocentric direction as the target star", rightAscension)
|
|
}
|
|
}
|