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{ ` 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, } }