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

90 lines
3.3 KiB
Go

package svg
import (
"math"
"strings"
"testing"
"time"
"b612.me/astro/moon"
)
// 掩甚时刻等时线由路径选项请求,渲染器只在路径携带时绘制。
func TestPlanetOccultationSVGDrawsGreatestTimeIsochrones(t *testing.T) {
start := time.Date(2025, time.February, 1, 0, 0, 0, 0, time.UTC)
end := time.Date(2025, time.February, 2, 0, 0, 0, 0, time.UTC)
render := func(step time.Duration) string {
t.Helper()
paths, err := moon.FindPlanetOccultationPaths(start, end, moon.OccultationSaturn, moon.OccultationPathOptions{
Step: 2 * time.Minute, DisableFootprints: true, GreatestTimeStep: step,
})
if err != nil || len(paths) == 0 {
t.Fatalf("no occultation path: %v", err)
}
if step > 0 && len(paths[0].GreatestTimeContours) == 0 {
t.Fatal("path carries no isochrones")
}
rendered, renderErr := PlanetOccultationPathSVG(paths[0], StarOccultationSVGOptions{
Width: 1200, Height: 800, Location: time.UTC,
})
if renderErr != nil {
t.Fatalf("render: %v", renderErr)
}
return rendered
}
withIsochrones := render(30 * time.Minute)
if !strings.Contains(withIsochrones, `class="greatest-time-isoline"`) {
t.Fatal("isochrone paths missing")
}
if !strings.Contains(withIsochrones, `class="greatest-time-isoline-label"`) {
t.Fatal("isochrone labels missing")
}
if strings.Contains(render(0), "greatest-time-isoline") {
t.Fatal("a zero step must not draw isochrones")
}
}
// 等时线也要走路径校验:坏支路必须在渲染前被拦住,而不是画出 NaN 折线。
func TestPlanetOccultationSVGRejectsBrokenGreatestTimeIsochrones(t *testing.T) {
paths, err := moon.FindPlanetOccultationPaths(
time.Date(2025, time.February, 1, 0, 0, 0, 0, time.UTC),
time.Date(2025, time.February, 2, 0, 0, 0, 0, time.UTC), moon.OccultationSaturn,
moon.OccultationPathOptions{Step: 2 * time.Minute, DisableFootprints: true, GreatestTimeStep: 30 * time.Minute},
)
if err != nil || len(paths) == 0 || len(paths[0].GreatestTimeContours) == 0 {
t.Fatalf("no isochrone path: %v", err)
}
options := StarOccultationSVGOptions{Width: 1200, Height: 800, Location: time.UTC}
render := func(mutate func(path *moon.PlanetOccultationPath)) error {
path := paths[0]
path.GreatestTimeContours = append([]moon.OccultationGreatestTimeContour(nil), path.GreatestTimeContours...)
mutate(&path)
_, renderErr := PlanetOccultationPathSVG(path, options)
return renderErr
}
if err := render(func(path *moon.PlanetOccultationPath) {}); err != nil {
t.Fatalf("valid isochrones rejected: %v", err)
}
if err := render(func(path *moon.PlanetOccultationPath) {
path.GreatestTimeContours[0].JDE = 0
}); err == nil {
t.Fatal("zero JDE accepted")
}
if err := render(func(path *moon.PlanetOccultationPath) {
path.GreatestTimeContours[0].Segments[0][0].Longitude = math.NaN()
}); err == nil {
t.Fatal("NaN longitude accepted")
}
if err := render(func(path *moon.PlanetOccultationPath) {
path.GreatestTimeContours[0].Segments[0] = path.GreatestTimeContours[0].Segments[0][:1]
}); err == nil {
t.Fatal("single-point branch accepted")
}
if err := render(func(path *moon.PlanetOccultationPath) {
path.GreatestTimeContours[0].Segments[0][0].Time = path.GreatestTimeContours[0].Segments[0][0].Time.Add(2 * time.Second)
}); err == nil {
t.Fatal("branch point with a mismatched instant accepted")
}
}