9ee2163cc7
- 新增月掩恒星和行星:支持搜索、掩甚点、全球掩带及固定地点轨迹计算 - 支持恒星星表坐标转换、有限盘面行星接触事件和月掩 SVG 输出 - 新增日月食及月掩全球投影图、时间标记和 GeoJSON 地理数据接口 - 扩展日食中心线、南北界及偏食足迹采样,支持极区投影 - 修正站心时角、月出月落、月球视半径、折射和恒星自行计算 - 优化内外行星事件搜索、边界选择、极端输入处理和计算稳定性
254 lines
9.2 KiB
Go
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,
|
|
}
|
|
}
|