Files
astro/moon/svg/occultation_local_footer_balance_test.go
T

177 lines
6.1 KiB
Go
Raw Normal View History

package svg
import (
"encoding/xml"
"io"
"math"
"strconv"
"strings"
"testing"
"time"
"b612.me/astro/internal/svgchart"
)
// 本地月掩页脚契约:方向说明与补充说明(含时标声明)都排在可用宽度内,同一块各行宽度接近,
// 不留孤立短尾行,且显示时区的偏移只写一次。
const (
localOccultationFooterDirectionFill = "#4f595b"
localOccultationFooterNoteFill = "#747c7d"
)
type localOccultationFooterText struct {
fill string
fontSize float64
value string
}
func localOccultationFooterTexts(t *testing.T, diagram string) []localOccultationFooterText {
t.Helper()
decoder := xml.NewDecoder(strings.NewReader(diagram))
texts := []localOccultationFooterText{}
for {
token, err := decoder.Token()
if err == io.EOF {
break
}
if err != nil {
t.Fatalf("decode SVG: %v", err)
}
element, ok := token.(xml.StartElement)
if !ok || element.Name.Local != "text" {
continue
}
text := localOccultationFooterText{}
for _, attribute := range element.Attr {
switch attribute.Name.Local {
case "fill":
text.fill = attribute.Value
case "font-size":
text.fontSize, _ = strconv.ParseFloat(attribute.Value, 64)
}
}
if text.fill != localOccultationFooterDirectionFill && text.fill != localOccultationFooterNoteFill {
continue
}
var content string
if err := decoder.DecodeElement(&content, &element); err != nil {
t.Fatalf("decode text element: %v", err)
}
text.value = content
texts = append(texts, text)
}
return texts
}
func localOccultationFooterWidths(lines []localOccultationFooterText) ([]string, []float64) {
values := make([]string, 0, len(lines))
widths := make([]float64, 0, len(lines))
for _, line := range lines {
values = append(values, line.value)
widths = append(widths, svgchart.EstimatedTextWidth(line.value, line.fontSize))
}
return values, widths
}
func localOccultationFooterSpread(lines []localOccultationFooterText) float64 {
_, widths := localOccultationFooterWidths(lines)
minimum, maximum := widths[0], widths[0]
for _, width := range widths {
minimum = math.Min(minimum, width)
maximum = math.Max(maximum, width)
}
return maximum / minimum
}
func localOccultationFooterGroup(lines []localOccultationFooterText, fill string) []localOccultationFooterText {
group := []localOccultationFooterText{}
for _, line := range lines {
if line.fill == fill {
group = append(group, line)
}
}
return group
}
// 说明块各行的最大最小宽度比不得超过 15%,整块最短行不得短于最长行的 60%(旧实现里时标声明独占的短尾行只有 25%~47%)。
func TestLocalOccultationFooterLinesAreEven(t *testing.T) {
location := time.FixedZone("UTC+08:00", 8*3600)
const width, height = 920, 720
for _, language := range []string{"zh", "en"} {
star, err := LocalStarOccultationSVG(localHR4799Occultation(t), hr4799StarCoordinate(), LocalStarOccultationSVGOptions{
Language: language, Location: location, Width: width, Height: height,
})
if err != nil {
t.Fatalf("star %s: %v", language, err)
}
planet, err := LocalPlanetOccultationSVG(localSaturnOccultation(t), LocalPlanetOccultationSVGOptions{
Language: language, Location: location, Width: width, Height: height,
})
if err != nil {
t.Fatalf("planet %s: %v", language, err)
}
for name, diagram := range map[string]string{"star": star, "planet": planet} {
lines := localOccultationFooterTexts(t, diagram)
if len(lines) < 2 {
t.Fatalf("%s %s: footer lines = %#v, want the direction and note blocks", name, language, lines)
}
values, widths := localOccultationFooterWidths(lines)
for index, line := range lines {
if limit := float64(width) - 80; widths[index] > limit {
t.Fatalf("%s %s: footer line %q width %.1f exceeds %.1f", name, language, line.value, widths[index], limit)
}
if strings.Contains(line.value, "…") {
t.Fatalf("%s %s: default footer text is truncated: %q", name, language, line.value)
}
}
note := localOccultationFooterGroup(lines, localOccultationFooterNoteFill)
if len(note) == 0 {
t.Fatalf("%s %s: missing the footer note block", name, language)
}
// 说明块与方向说明行字号不同(fs9 / fs10),故按块判 1.15;整块只兜"孤尾短行"(1.60)。
if spread := localOccultationFooterSpread(note); spread > 1.15 {
t.Fatalf("%s %s: note block spread = %.3f, want <= 1.15 (%q)", name, language, spread, values)
}
if spread := localOccultationFooterSpread(lines); spread > 1.60 {
t.Fatalf("%s %s: footer spread = %.3f, want no orphan short line (%q)", name, language, spread, values)
}
joined := strings.Join(values, " ")
if zone := "显示时区"; language == "en" {
if !strings.Contains(joined, "shown in") {
t.Fatalf("%s %s: footer note lost the time zone declaration: %q", name, language, joined)
}
} else if !strings.Contains(joined, zone) {
t.Fatalf("%s %s: footer note lost the time zone declaration: %q", name, language, joined)
}
}
}
}
// 偏移型时区名(UTC+08:00)不得让时标声明写成“显示时区 UTC+08:00,UTC+08:00”。
func TestLocalOccultationFooterNamesTheZoneOnce(t *testing.T) {
location := time.FixedZone("UTC+08:00", 8*3600)
for _, language := range []string{"zh", "en"} {
star, err := LocalStarOccultationSVG(localHR4799Occultation(t), hr4799StarCoordinate(), LocalStarOccultationSVGOptions{
Language: language, Location: location,
})
if err != nil {
t.Fatalf("star %s: %v", language, err)
}
planet, err := LocalPlanetOccultationSVG(localSaturnOccultation(t), LocalPlanetOccultationSVGOptions{
Language: language, Location: location,
})
if err != nil {
t.Fatalf("planet %s: %v", language, err)
}
for name, diagram := range map[string]string{"star": star, "planet": planet} {
note := localOccultationFooterGroup(localOccultationFooterTexts(t, diagram), localOccultationFooterNoteFill)
values, _ := localOccultationFooterWidths(note)
joined := strings.Join(values, " ")
if got := strings.Count(joined, "UTC+08:00"); got != 1 {
t.Fatalf("%s %s: footer note names UTC+08:00 %d times, want once: %q", name, language, got, joined)
}
}
}
}