177 lines
6.1 KiB
Go
177 lines
6.1 KiB
Go
|
|
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)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|