Files
astro/eclipse/svg/solar_map_landscape_layout_test.go
T
b612 2bf8478639 feat: 完善日月食与月掩几何链路并扩展历法接口
- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑
- 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口
- 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界
- 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名
- 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验
- 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口
- 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
2026-09-17 12:27:40 +08:00

119 lines
4.7 KiB
Go

package svg
import (
"regexp"
"strconv"
"testing"
"time"
eclipsecore "b612.me/astro/eclipse"
"b612.me/astro/internal/svgmap"
)
var solarEclipsePanelRectPattern = regexp.MustCompile(
`class="solar-detailed-panel"><rect x="([-0-9.]+)" y="([-0-9.]+)" width="([-0-9.]+)" height="([-0-9.]+)"`)
// 横版日食版式必须容下地图框与数据网格:地图框与面板矩形不得相交,面板行距不得塌陷,
// 低于文档下限的画布按文档回落到 960x640。
func TestSolarEclipseMapLandscapeLayoutKeepsPanelsClearOfMap(t *testing.T) {
date := time.Date(2024, time.April, 8, 0, 0, 0, 0, time.UTC)
info, ok := eclipsecore.SolarEclipseOnDateNASABulletinSplitK(date)
if !ok {
t.Fatal("missing 2024-04-08 eclipse")
}
center := svgmap.GeoPoint{Longitude: info.GreatestLongitude, Latitude: info.GreatestLatitude}
for _, test := range []struct {
name string
width, height int
}{
{name: "documented minimum", width: 800, height: 560},
{name: "minimum landscape", width: 800, height: 600},
{name: "default", width: 960, height: 640},
{name: "wide", width: 1200, height: 800},
} {
t.Run(test.name, func(t *testing.T) {
options := SolarEclipseMapSVGOptions{Width: test.width, Height: test.height, Location: time.UTC, PartialStep: 10 * time.Minute}
normalized := normalizeSolarEclipseMapSVGOptions(date, options)
if normalized.Width != test.width || normalized.Height != test.height {
t.Fatalf("canvas %dx%d was resized to %dx%d", test.width, test.height, normalized.Width, normalized.Height)
}
layout := solarEclipseMapLayoutFor(normalized, svgmap.ProjectionEquirectangular, center)
if !layout.landscape {
t.Fatal("expected the landscape composition")
}
frameRight := layout.frame.X + layout.frame.Width
if frameRight > layout.gridX {
t.Fatalf("map frame right edge %.3f overlaps the panel grid at x=%.3f by %.3f px",
frameRight, layout.gridX, frameRight-layout.gridX)
}
// 数据块最多六行,行高按 (cellHeight-30)/行数 等分。
rowSpacing := (layout.gridCellHeight - 30) / 6
if rowSpacing < 8 {
t.Fatalf("panel row spacing = %.2f px, want at least 8", rowSpacing)
}
rendered, ok := SolarEclipseMapSVG(date, options)
if !ok {
t.Fatal("missing map")
}
rects := solarEclipsePanelRectPattern.FindAllStringSubmatch(rendered, -1)
if len(rects) == 0 {
t.Fatal("landscape map renders no data panels")
}
for _, rect := range rects {
values := make([]float64, 4)
for index := range values {
parsed, err := strconv.ParseFloat(rect[index+1], 64)
if err != nil {
t.Fatalf("bad panel rect %v: %v", rect, err)
}
values[index] = parsed
}
x, y, width, height := values[0], values[1], values[2], values[3]
if x < 0 || y < 0 || x+width > float64(test.width) || y+height > float64(test.height) {
t.Fatalf("panel rect (%.1f,%.1f,%.1f,%.1f) leaves the %dx%d canvas", x, y, width, height, test.width, test.height)
}
if x < frameRight && layout.frame.X < x+width &&
y < layout.frame.Y+layout.frame.Height && layout.frame.Y < y+height {
t.Fatalf("panel rect (%.1f,%.1f,%.1f,%.1f) intersects the map frame", x, y, width, height)
}
}
})
}
// 低于下限的那一维单独回落到默认值,另一维保持请求值。
for _, test := range []struct {
name string
width, height int
wantWidth, wantHeight int
}{
{name: "legacy minimum", width: 640, height: 420, wantWidth: 960, wantHeight: 640},
{name: "narrow", width: 700, height: 600, wantWidth: 960, wantHeight: 600},
{name: "short", width: 1000, height: 500, wantWidth: 1000, wantHeight: 640},
} {
t.Run("fallback/"+test.name, func(t *testing.T) {
normalized := normalizeSolarEclipseMapSVGOptions(date, SolarEclipseMapSVGOptions{Width: test.width, Height: test.height})
if normalized.Width != test.wantWidth || normalized.Height != test.wantHeight {
t.Fatalf("%dx%d normalized to %dx%d, want %dx%d",
test.width, test.height, normalized.Width, normalized.Height,
test.wantWidth, test.wantHeight)
}
})
}
}
// 画布内闭合:文档下限的横版画布与同宽度的竖版画布都不得让 text/rect 越出画布。
func TestSolarEclipseMapKeepsEveryElementInCanvas(t *testing.T) {
date := time.Date(2024, time.April, 8, 0, 0, 0, 0, time.UTC)
for _, size := range [][2]int{{800, 560}, {800, 801}} {
rendered, ok := SolarEclipseMapSVG(date, SolarEclipseMapSVGOptions{
Width: size[0], Height: size[1], Location: time.UTC, PartialStep: 10 * time.Minute,
GreatestTimeStep: 30 * time.Minute, PenumbralOutlineStep: time.Hour, CentralShadowStep: 10 * time.Minute,
})
if !ok {
t.Fatalf("%dx%d: missing map", size[0], size[1])
}
assertSVGElementsInCanvas(t, rendered, size[0], size[1])
}
}