Files
astro/moon/svg/occultation_planet_test.go
T
b612 9ee2163cc7 feat: 新增月掩与日月食地理绘图并提升观测计算精度
- 新增月掩恒星和行星:支持搜索、掩甚点、全球掩带及固定地点轨迹计算
- 支持恒星星表坐标转换、有限盘面行星接触事件和月掩 SVG 输出
- 新增日月食及月掩全球投影图、时间标记和 GeoJSON 地理数据接口
- 扩展日食中心线、南北界及偏食足迹采样,支持极区投影
- 修正站心时角、月出月落、月球视半径、折射和恒星自行计算
- 优化内外行星事件搜索、边界选择、极端输入处理和计算稳定性
2026-08-06 12:00:56 +08:00

254 lines
9.2 KiB
Go

package svg
import (
"encoding/xml"
"errors"
"io"
"math"
"strconv"
"strings"
"testing"
"time"
"b612.me/astro/moon"
)
func TestFindPlanetOccultationSVGsSaturnFiniteDisk(t *testing.T) {
diagrams, err := FindPlanetOccultationSVGs(
time.Date(2024, time.August, 20, 0, 0, 0, 0, time.UTC),
time.Date(2024, time.August, 22, 0, 0, 0, 0, time.UTC),
moon.OccultationSaturn,
moon.OccultationPathOptions{Step: 5 * time.Minute, TargetSpacingKM: 200},
PlanetOccultationSVGOptions{Width: 720, Height: 520},
)
if err != nil {
t.Fatalf("FindPlanetOccultationSVGs() error = %v", err)
}
if len(diagrams) != 1 {
t.Fatalf("FindPlanetOccultationSVGs() returned %d diagrams, want 1", len(diagrams))
}
diagram := diagrams[0]
for _, want := range []string{
`<svg`, `width="720"`, `height="520"`, "2024-08-21", "土星",
"全球掩带", "外掩始", "全掩始", "掩甚", "全掩终", "外掩终", "外切锥面", "内切锥面",
"部分掩带", "全掩带", `class="occultation-band"`, `class="total-occultation-band"`,
`fill-rule="nonzero"`, `class="center-line"`,
`class="occultation-time-marker"`,
`class="event-marker event-total-start"`, `class="event-marker event-greatest"`,
`class="event-marker event-total-end"`, `class="event-marker-leader"`,
} {
if !strings.Contains(diagram, want) {
t.Fatalf("planetary SVG missing %q", want)
}
}
for _, obsolete := range []string{
`class="northern-limit"`, `class="southern-limit"`,
`class="northern-total-limit"`, `class="southern-total-limit"`,
} {
if strings.Contains(diagram, obsolete) {
t.Fatalf("planetary footprint sweep retains obsolete approximate line %q", obsolete)
}
}
if strings.Contains(diagram, "月掩Saturn") {
t.Fatal("Chinese planetary SVG title contains the English default target name")
}
if err := validateXML(diagram); err != nil {
t.Fatalf("generated planetary SVG is not valid XML: %v", err)
}
for _, pair := range [][2]string{{"外掩始", "全掩始"}, {"全掩终", "外掩终"}} {
_, firstY := planetOccultationSVGTextPosition(t, diagram, pair[0])
_, secondY := planetOccultationSVGTextPosition(t, diagram, pair[1])
if math.Abs(firstY-secondY) < 13 {
t.Fatalf("planetary SVG labels %q and %q overlap vertically at y %.3f / %.3f", pair[0], pair[1], firstY, secondY)
}
}
if got := strings.Count(diagram, `class="event-marker-leader"`); got != 4 {
t.Fatalf("planetary SVG close-contact leader count = %d, want 4", got)
}
if got := strings.Count(diagram, `fill="#087f8c" stroke="#ffffff" stroke-width="1.0"`); got < 2 {
t.Fatalf("planetary SVG total-contact marker count = %d, want at least 2", got)
}
}
func TestPlanetOccultationPathSVGWrapsFivePhaseSummaryAtRenderedFontSize(t *testing.T) {
path := samplePlanetOccultationPath()
options := normalizeStarOccultationSVGOptions(PlanetOccultationSVGOptions{
Width: 1100,
SummaryText: strings.Repeat("A", 130),
})
options = planetOccultationSVGDefaults(path, options)
lines := starOccultationSVGHeaderLines(planetOccultationStarShape(path, path.TargetID), options)
if len(lines) < 3 {
t.Fatalf("planetary SVG header lines = %d, want wrapped summary plus greatest line", len(lines))
}
if width := starOccultationTextWidth(lines[0], 14); width > float64(options.Width)-80 {
t.Fatalf("planetary SVG first header line width = %.1f, canvas allowance %.1f", width, float64(options.Width)-80)
}
}
func TestPlanetOccultationPathSVGUsesLargeGlobalMapDefault(t *testing.T) {
diagram, err := PlanetOccultationPathSVG(samplePlanetOccultationPath(), PlanetOccultationSVGOptions{})
if err != nil {
t.Fatalf("PlanetOccultationPathSVG() error = %v", err)
}
for _, want := range []string{
`width="1200" height="800"`,
`preserveAspectRatio="xMidYMid meet"`,
`style="max-width:100%;height:auto;display:block"`,
} {
if !strings.Contains(diagram, want) {
t.Fatalf("planetary SVG missing large responsive canvas marker %q", want)
}
}
}
func planetOccultationSVGTextPosition(t *testing.T, diagram, label string) (float64, float64) {
t.Helper()
decoder := xml.NewDecoder(strings.NewReader(diagram))
for {
token, err := decoder.Token()
if err == io.EOF {
break
}
if err != nil {
t.Fatalf("decode planetary SVG: %v", err)
}
start, ok := token.(xml.StartElement)
if !ok || start.Name.Local != "text" {
continue
}
var text string
if err := decoder.DecodeElement(&text, &start); err != nil {
t.Fatalf("decode planetary SVG text: %v", err)
}
if text != label {
continue
}
values := map[string]float64{}
for _, attribute := range start.Attr {
if attribute.Name.Local != "x" && attribute.Name.Local != "y" {
continue
}
value, parseErr := strconv.ParseFloat(attribute.Value, 64)
if parseErr != nil {
t.Fatalf("parse %s coordinate %q: %v", attribute.Name.Local, attribute.Value, parseErr)
}
values[attribute.Name.Local] = value
}
return values["x"], values["y"]
}
t.Fatalf("planetary SVG label %q not found", label)
return 0, 0
}
func TestPlanetOccultationPathSVGRejectsInvalidCanvasWithPlanetError(t *testing.T) {
path := samplePlanetOccultationPath()
_, err := PlanetOccultationPathSVG(path, PlanetOccultationSVGOptions{Width: 1})
if !errors.Is(err, ErrInvalidPlanetOccultationSVGOptions) {
t.Fatalf("PlanetOccultationPathSVG() error = %v, want ErrInvalidPlanetOccultationSVGOptions", err)
}
}
func TestPlanetOccultationPathSVGEnglishAndCustomText(t *testing.T) {
diagram, err := PlanetOccultationPathSVG(samplePlanetOccultationPath(), PlanetOccultationSVGOptions{
Language: "en",
Location: time.UTC,
Title: "Custom planetary occultation",
SummaryText: "Custom summary",
GreatestText: "Custom greatest",
FooterNote: "Custom footer",
})
if err != nil {
t.Fatalf("PlanetOccultationPathSVG() error = %v", err)
}
for _, want := range []string{
"Custom planetary occultation", "Custom summary", "Custom greatest", "Custom footer",
"Partial begins", "Total begins", "Greatest", "Total ends", "Partial ends",
} {
if !strings.Contains(diagram, want) {
t.Fatalf("English planetary SVG missing %q", want)
}
}
if err := validateXML(diagram); err != nil {
t.Fatalf("English planetary SVG is not valid XML: %v", err)
}
}
func TestPlanetOccultationPathSVGRejectsInvalidPath(t *testing.T) {
_, err := PlanetOccultationPathSVG(moon.PlanetOccultationPath{Planet: moon.OccultationSaturn}, PlanetOccultationSVGOptions{})
if !errors.Is(err, ErrInvalidPlanetOccultationPath) {
t.Fatalf("PlanetOccultationPathSVG() error = %v, want ErrInvalidPlanetOccultationPath", err)
}
path := samplePlanetOccultationPath()
path.TotalComplete = false
_, err = PlanetOccultationPathSVG(path, PlanetOccultationSVGOptions{})
if !errors.Is(err, ErrInvalidPlanetOccultationPath) {
t.Fatalf("incomplete total band error = %v, want ErrInvalidPlanetOccultationPath", err)
}
}
func TestPlanetOccultationPathSVGSupportsPartialOnlyGlobalBand(t *testing.T) {
path := samplePlanetOccultationPath()
path.HasTotalBand = false
path.TotalStart = moon.OccultationPathPoint{}
path.TotalEnd = moon.OccultationPathPoint{}
path.TotalComplete = false
path.NorthernTotalLimit = nil
path.SouthernTotalLimit = nil
path.GreatestTotalWidthKM = 0
diagram, err := PlanetOccultationPathSVG(path, PlanetOccultationSVGOptions{})
if err != nil {
t.Fatalf("PlanetOccultationPathSVG() partial-only error = %v", err)
}
if !strings.Contains(diagram, "部分掩带") || strings.Contains(diagram, `class="total-occultation-band"`) ||
strings.Contains(diagram, "全掩始") || strings.Contains(diagram, "全掩终") {
t.Fatal("partial-only planetary SVG contains inconsistent total-band content")
}
if got := strings.Count(diagram, `class="event-marker `); got != 3 {
t.Fatalf("partial-only global marker count = %d, want 3", got)
}
}
func samplePlanetOccultationPath() moon.PlanetOccultationPath {
starPath := sampleStarOccultationPath()
totalStart := starOccultationInterpolatePathPoint(
starPath.CenterLine[0], starPath.CenterLine[1], 0.5, 167.5,
)
totalEnd := starOccultationInterpolatePathPoint(
starPath.CenterLine[2], starPath.CenterLine[3], 0.5, -167.5,
)
northernTotal := []moon.OccultationPathPoint{totalStart}
southernTotal := []moon.OccultationPathPoint{totalStart}
for _, point := range starPath.CenterLine[1:3] {
north, south := point, point
north.Latitude += 10
south.Latitude -= 10
north.WidthKM = 0
south.WidthKM = 0
northernTotal = append(northernTotal, north)
southernTotal = append(southernTotal, south)
}
northernTotal = append(northernTotal, totalEnd)
southernTotal = append(southernTotal, totalEnd)
return moon.PlanetOccultationPath{
Planet: moon.OccultationSaturn,
TargetID: "Saturn",
Start: starPath.Start,
Greatest: starPath.Greatest,
End: starPath.End,
Complete: true,
CenterLine: starPath.CenterLine,
NorthernLimit: starPath.NorthernLimit,
SouthernLimit: starPath.SouthernLimit,
HasTotalBand: true,
TotalStart: totalStart,
TotalEnd: totalEnd,
TotalComplete: true,
NorthernTotalLimit: northernTotal,
SouthernTotalLimit: southernTotal,
GreatestTotalWidthKM: 3100,
Step: starPath.Step,
TargetSpacingKM: starPath.TargetSpacingKM,
}
}