feat: 完善日月食与月掩几何链路并扩展历法接口
- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑 - 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口 - 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界 - 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名 - 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验 - 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口 - 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
package svg
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"b612.me/astro/internal/svgchart"
|
||||
)
|
||||
|
||||
// 调用方给图题、摘要、地图标题与页脚传超长文本时,产物必须留在画布内并在截断处给出省略号;
|
||||
// 判定口径与 internal/svgchart 的 EstimatedTextWidth/EstimatedTextExtents 一致。
|
||||
|
||||
func footerFitLongText(prefix string) string {
|
||||
piece := prefix + "long custom text 自定义长文本,用于验证折行与截断;"
|
||||
value := ""
|
||||
for len([]rune(value)) < 640 {
|
||||
value += piece
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func footerFitViolations(t *testing.T, diagram string, width, height int) []string {
|
||||
t.Helper()
|
||||
violations := []string{}
|
||||
for _, text := range occultationSVGTexts(t, diagram) {
|
||||
textWidth := svgchart.EstimatedTextWidth(text.value, text.fontSize)
|
||||
left := text.x
|
||||
switch text.anchor {
|
||||
case "middle":
|
||||
left = text.x - textWidth/2
|
||||
case "end":
|
||||
left = text.x - textWidth
|
||||
}
|
||||
above, below := svgchart.EstimatedTextExtents(text.fontSize)
|
||||
if text.x < 0 || text.x > float64(width) || text.y < 0 || text.y > float64(height) {
|
||||
violations = append(violations, text.value)
|
||||
continue
|
||||
}
|
||||
if left < 0 || left+textWidth > float64(width) || text.y-above < 0 || text.y+below > float64(height) {
|
||||
violations = append(violations, text.value)
|
||||
}
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func footerFitAssert(t *testing.T, name string, diagram string, width, height int) {
|
||||
t.Helper()
|
||||
if violations := footerFitViolations(t, diagram, width, height); len(violations) > 0 {
|
||||
t.Fatalf("%s: %d texts outside the %dx%d canvas, first = %q",
|
||||
name, len(violations), width, height, violations[0])
|
||||
}
|
||||
if !strings.Contains(diagram, "…") {
|
||||
t.Fatalf("%s: truncated custom text is not marked", name)
|
||||
}
|
||||
}
|
||||
|
||||
func footerFitStarTexts(long string) StarOccultationSVGOptions {
|
||||
return StarOccultationSVGOptions{
|
||||
Title: long, SummaryText: long, GreatestText: long,
|
||||
MapTitle: long, ContactsTitle: long, FooterNote: long,
|
||||
}
|
||||
}
|
||||
|
||||
func TestStarOccultationPathSVGCustomLongTextStaysInsideCanvas(t *testing.T) {
|
||||
long := footerFitLongText("star ")
|
||||
if runes := len([]rune(long)); runes < 300 || runes > 800 {
|
||||
t.Fatalf("long text runes = %d, want 300..800", runes)
|
||||
}
|
||||
path := occultationTestStarPath(t)
|
||||
for _, size := range [][2]int{{640, 480}, {920, 720}, {1200, 800}} {
|
||||
options := footerFitStarTexts(long)
|
||||
options.Width, options.Height = size[0], size[1]
|
||||
diagram, err := StarOccultationPathSVG(path, options)
|
||||
if err != nil {
|
||||
t.Fatalf("%dx%d: %v", size[0], size[1], err)
|
||||
}
|
||||
footerFitAssert(t, "star-global", diagram, size[0], size[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanetOccultationPathSVGCustomLongTextStaysInsideCanvas(t *testing.T) {
|
||||
long := footerFitLongText("planet ")
|
||||
path := occultationTestSaturnPath(t)
|
||||
for _, size := range [][2]int{{640, 480}, {920, 720}, {1200, 800}} {
|
||||
options := PlanetOccultationSVGOptions(footerFitStarTexts(long))
|
||||
options.Width, options.Height = size[0], size[1]
|
||||
diagram, err := PlanetOccultationPathSVG(path, options)
|
||||
if err != nil {
|
||||
t.Fatalf("%dx%d: %v", size[0], size[1], err)
|
||||
}
|
||||
footerFitAssert(t, "planet-global", diagram, size[0], size[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestOccultationDetailedSVGCustomLongTextStaysInsideCanvas(t *testing.T) {
|
||||
long := footerFitLongText("detailed ")
|
||||
star := occultationTestStarPath(t)
|
||||
planetPath := occultationTestSaturnPath(t)
|
||||
for _, size := range [][2]int{{700, 990}, {1000, 1414}} {
|
||||
diagram, err := StarOccultationDetailedSVG(star, hr4799StarCoordinate(), OccultationDetailedSVGOptions{
|
||||
Width: size[0], Height: size[1], Title: long, FooterNote: long,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("star %dx%d: %v", size[0], size[1], err)
|
||||
}
|
||||
footerFitAssert(t, "star-detailed", diagram, size[0], size[1])
|
||||
diagram, err = PlanetOccultationDetailedSVG(planetPath, OccultationDetailedSVGOptions{
|
||||
Width: size[0], Height: size[1], Title: long, FooterNote: long,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("planet %dx%d: %v", size[0], size[1], err)
|
||||
}
|
||||
footerFitAssert(t, "planet-detailed", diagram, size[0], size[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalStarOccultationSVGCustomLongTextStaysInsideCanvas(t *testing.T) {
|
||||
long := footerFitLongText("local ")
|
||||
info := localHR4799Occultation(t)
|
||||
for _, size := range [][2]int{{640, 520}, {920, 720}} {
|
||||
diagram, err := LocalStarOccultationSVG(info, hr4799StarCoordinate(), LocalStarOccultationSVGOptions{
|
||||
Width: size[0], Height: size[1], Title: long, SummaryText: long, GreatestText: long,
|
||||
OverviewTitle: long, PhasePanelsTitle: long, ContactsTitle: long,
|
||||
DirectionText: long, FooterNote: long,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("%dx%d: %v", size[0], size[1], err)
|
||||
}
|
||||
footerFitAssert(t, "star-local", diagram, size[0], size[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalPlanetOccultationSVGCustomLongTextStaysInsideCanvas(t *testing.T) {
|
||||
long := footerFitLongText("local-planet ")
|
||||
info := localSaturnOccultation(t)
|
||||
for _, size := range [][2]int{{760, 600}, {920, 720}} {
|
||||
diagram, err := LocalPlanetOccultationSVG(info, LocalPlanetOccultationSVGOptions{
|
||||
Width: size[0], Height: size[1], Title: long, SummaryText: long, GreatestText: long,
|
||||
OverviewTitle: long, PhasePanelsTitle: long, ContactsTitle: long,
|
||||
DirectionText: long, FooterNote: long,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("%dx%d: %v", size[0], size[1], err)
|
||||
}
|
||||
footerFitAssert(t, "planet-local", diagram, size[0], size[1])
|
||||
}
|
||||
}
|
||||
|
||||
// 自动生成的默认文案不得进入截断分支:这些产物在改动前后逐字节一致,出现省略号即说明分流写错。
|
||||
func TestOccultationSVGDefaultTextIsNotTruncated(t *testing.T) {
|
||||
star := occultationTestStarPath(t)
|
||||
planetPath := occultationTestSaturnPath(t)
|
||||
for _, language := range []string{"zh", "en"} {
|
||||
documents := []string{}
|
||||
starGlobal, err := StarOccultationPathSVG(star, StarOccultationSVGOptions{Language: language})
|
||||
if err != nil {
|
||||
t.Fatalf("star global %s: %v", language, err)
|
||||
}
|
||||
documents = append(documents, starGlobal)
|
||||
planetGlobal, err := PlanetOccultationPathSVG(planetPath, PlanetOccultationSVGOptions{Language: language})
|
||||
if err != nil {
|
||||
t.Fatalf("planet global %s: %v", language, err)
|
||||
}
|
||||
documents = append(documents, planetGlobal)
|
||||
detailed, err := StarOccultationDetailedSVG(star, hr4799StarCoordinate(), OccultationDetailedSVGOptions{Language: language})
|
||||
if err != nil {
|
||||
t.Fatalf("star detailed %s: %v", language, err)
|
||||
}
|
||||
documents = append(documents, detailed)
|
||||
for _, document := range documents {
|
||||
if strings.Contains(document, "…") {
|
||||
t.Fatalf("%s: default text must stay unmarked", language)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user