feat: 完善日月食与月掩几何链路并扩展历法接口
- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑 - 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口 - 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界 - 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名 - 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验 - 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口 - 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
package svg
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 本文件钉住全球掩带图的画布下限与"所有文本都在画布内":480x360、600x400 曾因图例压页脚、经纬刻度出框而被放行。
|
||||
|
||||
func TestOccultationMapRejectsCanvasBelowMinimum(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
width, height int
|
||||
}{
|
||||
{name: "reference too small", width: 480, height: 360},
|
||||
{name: "legend footer collision", width: 600, height: 400},
|
||||
{name: "width just below", width: 639, height: 480},
|
||||
{name: "height just below", width: 640, height: 479},
|
||||
}
|
||||
starPath := occultationTestStarPath(t)
|
||||
planetPath := occultationTestSaturnPath(t)
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
options := StarOccultationSVGOptions{Width: test.width, Height: test.height}
|
||||
if _, err := StarOccultationPathSVG(starPath, options); !errors.Is(err, ErrInvalidStarOccultationSVGOptions) {
|
||||
t.Fatalf("stellar %dx%d error = %v, want ErrInvalidStarOccultationSVGOptions", test.width, test.height, err)
|
||||
}
|
||||
if _, err := PlanetOccultationPathSVG(planetPath, options); !errors.Is(err, ErrInvalidPlanetOccultationSVGOptions) {
|
||||
t.Fatalf("planetary %dx%d error = %v, want ErrInvalidPlanetOccultationSVGOptions", test.width, test.height, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOccultationMapKeepsEveryTextInsideCanvas(t *testing.T) {
|
||||
sizes := [][2]int{{640, 480}, {800, 600}, {1200, 800}, {1000, 1414}}
|
||||
projections := []MapProjection{MapProjectionAuto, MapProjectionNorthPolar, MapProjectionOrthographic}
|
||||
starPath := occultationTestStarPath(t)
|
||||
planetPath := occultationTestSaturnPath(t)
|
||||
for _, size := range sizes {
|
||||
for _, projection := range projections {
|
||||
name := strconv.Itoa(size[0]) + "x" + strconv.Itoa(size[1]) + "-" + string(projection)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
options := StarOccultationSVGOptions{
|
||||
Width: size[0], Height: size[1], Location: time.UTC, Projection: projection,
|
||||
}
|
||||
diagrams := map[string]string{}
|
||||
star, err := StarOccultationPathSVG(starPath, options)
|
||||
if err != nil {
|
||||
t.Fatalf("stellar render: %v", err)
|
||||
}
|
||||
diagrams["stellar"] = star
|
||||
planet, err := PlanetOccultationPathSVG(planetPath, options)
|
||||
if err != nil {
|
||||
t.Fatalf("planetary render: %v", err)
|
||||
}
|
||||
diagrams["planetary"] = planet
|
||||
for label, diagram := range diagrams {
|
||||
texts := occultationSVGTexts(t, diagram)
|
||||
if len(texts) == 0 {
|
||||
t.Fatalf("%s SVG has no text", label)
|
||||
}
|
||||
if outside := occultationSVGTextsOutsideCanvas(texts, float64(size[0]), float64(size[1])); len(outside) > 0 {
|
||||
t.Fatalf("%s SVG has %d texts outside %s: %v", label, len(outside), name, outside)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOccultationMapLegendDoesNotOverlapFooter(t *testing.T) {
|
||||
sizes := [][2]int{{640, 480}, {800, 600}, {1200, 800}}
|
||||
starPath := occultationTestStarPath(t)
|
||||
planetPath := occultationTestSaturnPath(t)
|
||||
for _, size := range sizes {
|
||||
t.Run(strconv.Itoa(size[0])+"x"+strconv.Itoa(size[1]), func(t *testing.T) {
|
||||
options := StarOccultationSVGOptions{Width: size[0], Height: size[1], Location: time.UTC}
|
||||
for _, item := range []struct {
|
||||
label string
|
||||
path func() (string, error)
|
||||
}{
|
||||
{label: "stellar", path: func() (string, error) { return StarOccultationPathSVG(starPath, options) }},
|
||||
{label: "planetary", path: func() (string, error) { return PlanetOccultationPathSVG(planetPath, options) }},
|
||||
} {
|
||||
diagram, err := item.path()
|
||||
if err != nil {
|
||||
t.Fatalf("%s render: %v", item.label, err)
|
||||
}
|
||||
texts := occultationSVGTexts(t, diagram)
|
||||
legend := []occultationSVGText{}
|
||||
footer := []occultationSVGText{}
|
||||
for _, text := range texts {
|
||||
if text.class == "occultation-legend-text" {
|
||||
legend = append(legend, text)
|
||||
}
|
||||
if text.parent == "svg" && text.class == "" && text.fontSize == 11 && text.x == 40 {
|
||||
footer = append(footer, text)
|
||||
}
|
||||
}
|
||||
if len(legend) < 3 || len(footer) == 0 {
|
||||
t.Fatalf("%s SVG legend=%d footer=%d, want both present", item.label, len(legend), len(footer))
|
||||
}
|
||||
for _, legendText := range legend {
|
||||
for _, footerText := range footer {
|
||||
if occultationSVGTextBox(legendText)[3] > occultationSVGTextBox(footerText)[1] {
|
||||
t.Fatalf("%s legend %q overlaps footer %q at %dx%d",
|
||||
item.label, legendText.value, footerText.value, size[0], size[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user