2bf8478639
- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑 - 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口 - 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界 - 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名 - 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验 - 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口 - 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
223 lines
8.3 KiB
Go
223 lines
8.3 KiB
Go
package svg
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"b612.me/astro/moon"
|
|
)
|
|
|
|
func TestFindLocalStarOccultationSVGsHR4799(t *testing.T) {
|
|
location := time.FixedZone("UTC+8", 8*3600)
|
|
diagrams, err := FindLocalStarOccultationSVGs(
|
|
time.Date(2025, 6, 5, 0, 0, 0, 0, location),
|
|
time.Date(2025, 6, 6, 0, 0, 0, 0, location),
|
|
hr4799StarCoordinate(),
|
|
121.56601, 6.80706, 0,
|
|
moon.OccultationSearchOptions{},
|
|
LocalStarOccultationSVGOptions{Width: 720, Height: 560, Location: location},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("FindLocalStarOccultationSVGs() error = %v", err)
|
|
}
|
|
if len(diagrams) != 1 {
|
|
t.Fatalf("FindLocalStarOccultationSVGs() returned %d diagrams, want 1", len(diagrams))
|
|
}
|
|
diagram := diagrams[0]
|
|
for _, want := range []string{
|
|
`<svg`, `width="720"`, `height="560"`, "2025-06-05", "HR 4799",
|
|
"指定地点月掩", "观测点", "站心恒星轨迹", "掩始、掩甚与掩终视圆",
|
|
"本地接触时刻", "掩始", "掩甚", "掩终", "左东右西", "UTC+8",
|
|
`class="local-star-track"`, `class="overview-moon"`, `class="overview-star"`,
|
|
`class="lunar-path-line"`, `class="lunar-path-label"`, "白道",
|
|
`<symbol id="le-moon"`, `href="#le-moon"`,
|
|
} {
|
|
if !strings.Contains(diagram, want) {
|
|
t.Fatalf("local SVG missing %q", want)
|
|
}
|
|
}
|
|
if strings.Contains(diagram, "local-occultation-moon") || strings.Contains(diagram, "<radialGradient") {
|
|
t.Fatalf("local SVG still contains the temporary synthetic Moon rendering")
|
|
}
|
|
if strings.Contains(diagram, "全球掩带") || strings.Contains(diagram, "全球中心线") {
|
|
t.Fatalf("local SVG unexpectedly contains global-path labels")
|
|
}
|
|
if got := strings.Count(diagram, `class="stage-moon"`); got != 3 {
|
|
t.Fatalf("stage Moon count = %d, want 3", got)
|
|
}
|
|
if got := strings.Count(diagram, `class="stage-star"`); got != 3 {
|
|
t.Fatalf("stage star count = %d, want 3", got)
|
|
}
|
|
if err := validateXML(diagram); err != nil {
|
|
t.Fatalf("generated local SVG is not valid XML: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLocalStarOccultationSVGUsesGreatestMoonPhase(t *testing.T) {
|
|
info := localHR4799Occultation(t)
|
|
diagram, err := LocalStarOccultationSVG(info, hr4799StarCoordinate(), LocalStarOccultationSVGOptions{
|
|
Location: time.UTC,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("LocalStarOccultationSVG() error = %v", err)
|
|
}
|
|
appearance := localOccultationMoonAppearanceAt(info.Greatest, info.Observer)
|
|
want := fmt.Sprintf(`data-illumination="%.6f" data-bright-limb-position-angle="%.3f"`,
|
|
appearance.illumination, appearance.brightLimbPA)
|
|
if !strings.Contains(diagram, want) {
|
|
t.Fatalf("local SVG does not carry greatest-time Moon appearance %q", want)
|
|
}
|
|
if appearance.illumination > 1e-6 && appearance.illumination < 1-1e-6 &&
|
|
!strings.Contains(diagram, `<clipPath id="local-star-occultation-moon-overview">`) {
|
|
t.Fatal("non-full Moon overview is missing its phase clip path")
|
|
}
|
|
}
|
|
|
|
func TestLocalOccultationMoonPhasePathSelectsIlluminatedArea(t *testing.T) {
|
|
gibbous := localOccultationMoonPhasePath(100, 0.75)
|
|
if !strings.Contains(gibbous, `A 50.000 100.000 0 0 1 0 -100.000`) {
|
|
t.Fatalf("waxing or waning gibbous terminator uses the wrong arc: %s", gibbous)
|
|
}
|
|
crescent := localOccultationMoonPhasePath(100, 0.25)
|
|
if !strings.Contains(crescent, `A 50.000 100.000 0 0 0 0 -100.000`) {
|
|
t.Fatalf("crescent terminator uses the wrong arc: %s", crescent)
|
|
}
|
|
}
|
|
|
|
func TestLocalOccultationMoonSVGRotationUsesNorthUpEastLeft(t *testing.T) {
|
|
tests := []struct {
|
|
positionAngle float64
|
|
rotation float64
|
|
}{
|
|
{positionAngle: 0, rotation: -90},
|
|
{positionAngle: 90, rotation: -180},
|
|
{positionAngle: 180, rotation: -270},
|
|
{positionAngle: 270, rotation: -360},
|
|
}
|
|
for _, test := range tests {
|
|
if got := localOccultationMoonSVGRotation(test.positionAngle); got != test.rotation {
|
|
t.Fatalf("bright-limb PA %.1f rotation = %.1f, want %.1f", test.positionAngle, got, test.rotation)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLocalStarOccultationSVGEnglishAndCustomText(t *testing.T) {
|
|
info := localHR4799Occultation(t)
|
|
diagram, err := LocalStarOccultationSVG(info, hr4799StarCoordinate(), LocalStarOccultationSVGOptions{
|
|
Language: "en",
|
|
Location: time.UTC,
|
|
Title: "Custom local title",
|
|
SummaryText: "Custom local summary",
|
|
GreatestText: "Custom local greatest",
|
|
OverviewTitle: "Custom local overview",
|
|
PhasePanelsTitle: "Custom local stages",
|
|
ContactsTitle: "Custom local contacts",
|
|
DirectionText: "Custom local direction",
|
|
FooterNote: "Custom local footer",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("LocalStarOccultationSVG() error = %v", err)
|
|
}
|
|
for _, want := range []string{
|
|
"Custom local title", "Custom local summary", "Custom local greatest",
|
|
"Custom local overview", "Custom local stages", "Custom local contacts",
|
|
"Custom local direction", "Custom local footer", "Immersion", "Greatest", "Emersion", "Lunar path",
|
|
} {
|
|
if !strings.Contains(diagram, want) {
|
|
t.Fatalf("English local SVG missing %q", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLocalStarOccultationSVGLunarPathUsesGreatestTangent(t *testing.T) {
|
|
frames := []moon.StarOccultationDiagramFrame{
|
|
{StarXArcsec: 100, StarYArcsec: 100},
|
|
{StarXArcsec: -1, StarYArcsec: -2},
|
|
{StarXArcsec: 0, StarYArcsec: 0, Label: "Greatest", Labels: []string{"Greatest"}},
|
|
{StarXArcsec: 1, StarYArcsec: 2},
|
|
{StarXArcsec: -100, StarYArcsec: 100},
|
|
}
|
|
unitX, unitY, ok := localStarOccultationSVGLunarPathDirection(frames)
|
|
if !ok {
|
|
t.Fatalf("localStarOccultationSVGLunarPathDirection() returned no direction")
|
|
}
|
|
if math.Abs(unitX-1/math.Sqrt(5)) > 1e-12 || math.Abs(unitY-2/math.Sqrt(5)) > 1e-12 {
|
|
t.Fatalf("lunar path direction = %.12f %.12f, want greatest tangent", unitX, unitY)
|
|
}
|
|
}
|
|
|
|
func TestFindLocalStarOccultationSVGsNoEvent(t *testing.T) {
|
|
location := time.FixedZone("CST", 8*3600)
|
|
star := hr4799StarCoordinate()
|
|
star.Dec = 80
|
|
diagrams, err := FindLocalStarOccultationSVGs(
|
|
time.Date(2025, 6, 5, 0, 0, 0, 0, location),
|
|
time.Date(2025, 6, 6, 0, 0, 0, 0, location),
|
|
star, 121.56601, 6.80706, 0,
|
|
moon.OccultationSearchOptions{}, LocalStarOccultationSVGOptions{},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("FindLocalStarOccultationSVGs() error = %v", err)
|
|
}
|
|
if len(diagrams) != 0 {
|
|
t.Fatalf("FindLocalStarOccultationSVGs() returned %d diagrams, want none", len(diagrams))
|
|
}
|
|
}
|
|
|
|
func TestLocalStarOccultationSVGRejectsInvalidInput(t *testing.T) {
|
|
info := localHR4799Occultation(t)
|
|
_, err := LocalStarOccultationSVG(info, hr4799StarCoordinate(), LocalStarOccultationSVGOptions{Width: 1})
|
|
if !errors.Is(err, ErrInvalidLocalStarOccultationSVGOptions) {
|
|
t.Fatalf("invalid canvas error = %v, want ErrInvalidLocalStarOccultationSVGOptions", err)
|
|
}
|
|
info.ContactsComplete = false
|
|
_, err = LocalStarOccultationSVG(info, hr4799StarCoordinate(), LocalStarOccultationSVGOptions{})
|
|
if !errors.Is(err, ErrInvalidLocalStarOccultationInfo) {
|
|
t.Fatalf("incomplete event error = %v, want ErrInvalidLocalStarOccultationInfo", err)
|
|
}
|
|
|
|
info = localHR4799Occultation(t)
|
|
otherStar := hr4799StarCoordinate()
|
|
otherStar.ID = "another-star"
|
|
_, err = LocalStarOccultationSVG(info, otherStar, LocalStarOccultationSVGOptions{})
|
|
if !errors.Is(err, ErrInvalidLocalStarOccultationInfo) {
|
|
t.Fatalf("mismatched target error = %v, want ErrInvalidLocalStarOccultationInfo", err)
|
|
}
|
|
|
|
info = localHR4799Occultation(t)
|
|
wrongCoordinate := hr4799StarCoordinate()
|
|
wrongCoordinate.RA += 30
|
|
_, err = LocalStarOccultationSVG(info, wrongCoordinate, LocalStarOccultationSVGOptions{})
|
|
if !errors.Is(err, ErrInvalidLocalStarOccultationInfo) {
|
|
t.Fatalf("same-ID wrong coordinate error = %v, want ErrInvalidLocalStarOccultationInfo", err)
|
|
}
|
|
|
|
info.TargetID = ""
|
|
wrongCoordinate.ID = ""
|
|
_, err = LocalStarOccultationSVG(info, wrongCoordinate, LocalStarOccultationSVGOptions{})
|
|
if !errors.Is(err, ErrInvalidLocalStarOccultationInfo) {
|
|
t.Fatalf("empty-ID wrong coordinate error = %v, want ErrInvalidLocalStarOccultationInfo", err)
|
|
}
|
|
}
|
|
|
|
func localHR4799Occultation(t *testing.T) moon.StarOccultationInfo {
|
|
t.Helper()
|
|
location := time.FixedZone("CST", 8*3600)
|
|
events, err := moon.FindStarOccultations(
|
|
time.Date(2025, 6, 5, 0, 0, 0, 0, location),
|
|
time.Date(2025, 6, 6, 0, 0, 0, 0, location),
|
|
hr4799StarCoordinate(), 121.56601, 6.80706, 0, moon.OccultationSearchOptions{},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("FindStarOccultations() error = %v", err)
|
|
}
|
|
if len(events) != 1 {
|
|
t.Fatalf("FindStarOccultations() returned %d events, want 1", len(events))
|
|
}
|
|
return events[0]
|
|
}
|