Files
astro/eclipse/svg/solar_map_limb_panel_test.go
T
b612 044f2f4381 feat: 引入可切换星历模型并扩展天象观测链路
- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算
- 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询
- 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出
- 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解
- 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤
- 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持
- 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理
- 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
2026-10-15 23:54:15 +08:00

204 lines
8.3 KiB
Go

package svg
// 全球日食图开月缘时,面板数值与画出的界线同源;剖面失败时整张图保持圆月面。
import (
"fmt"
"math"
"strings"
"testing"
"time"
eclipsecore "b612.me/astro/eclipse"
"b612.me/astro/moon/limb"
)
type failingEclipseLimb struct{}
func (failingEclipseLimb) LimbRadiusKM(_, _, _ float64) (float64, bool) { return 0, false }
func TestSolarEclipseMapLunarLimbPanelMatchesLimbResults(t *testing.T) {
date := time.Date(2024, time.April, 8, 0, 0, 0, 0, time.UTC)
profile := limb.Default()
circular, ok := SolarEclipseMapSVG(date, SolarEclipseMapSVGOptions{Location: time.UTC})
if !ok {
t.Fatal("circular map missing")
}
corrected, ok := SolarEclipseMapSVG(date, SolarEclipseMapSVGOptions{Location: time.UTC, LunarLimb: profile})
if !ok {
t.Fatal("limb map missing")
}
module := eclipsecore.NewWithInstance(nil).WithLunarLimb(profile)
global, _ := module.SolarEclipseOnDateNASABulletinSplitK(date)
if !global.LunarLimbApplied {
t.Fatal("global limb result not flagged")
}
// 与地图同一组路径选项,带宽才和画出的南北界同源。
path, ok := module.SolarEclipseCentralPathNASABulletinSplitK(date, eclipsecore.SolarEclipsePathOptions{
Step: 2 * time.Minute, TargetSpacingKM: 150, SkipCentralBand: true,
})
if !ok || !path.PathWidthDefined {
t.Fatal("limb central path missing")
}
limbWidth := fmt.Sprintf("%.1f km", path.Greatest.WidthKM)
circularGlobal, _ := eclipsecore.SolarEclipseOnDateNASABulletinSplitK(date)
circularWidth := fmt.Sprintf("%.1f km", circularGlobal.PathWidthKM)
if limbWidth == circularWidth {
t.Fatalf("limb width %s equals circular width; fixture no longer distinguishes them", limbWidth)
}
if !strings.Contains(corrected, limbWidth) || strings.Contains(corrected, circularWidth) {
t.Fatalf("limb map must show width %s and not %s", limbWidth, circularWidth)
}
if !strings.Contains(circular, circularWidth) {
t.Fatalf("circular map must show width %s", circularWidth)
}
local, ok := module.GeometricLocalSolarEclipseOnDateNASABulletinSplitK(date, global.GreatestLongitude, global.GreatestLatitude, 0)
if !ok || !local.LunarLimbApplied {
t.Fatal("limb local panel result missing or not flagged")
}
if magnitude := fmt.Sprintf("%.4f", local.Magnitude); !strings.Contains(corrected, magnitude) {
t.Fatalf("limb map must show the limb local magnitude %s", magnitude)
}
begin := global.PartialBeginOnEarth.UTC().Format("15:04:05")
if begin == circularGlobal.PartialBeginOnEarth.UTC().Format("15:04:05") {
t.Fatal("limb P1 rounds to the circular value; pick a fixture where it moves")
}
if !strings.Contains(corrected, begin) || strings.Contains(circular, begin) {
t.Fatalf("limb P1 %s must appear only in the limb map", begin)
}
if !strings.Contains(corrected, "按月缘剖面修正全球始末、食甚点站心值、中心带界线、可见域边界") {
t.Fatal("limb map does not declare every corrected layer")
}
}
// selectiveEclipseLimb 只在位置角 [low, high) 内给出剖面值并报告半径范围;外切扫描靠范围剪枝停在日心方向附近,需要全方向的判据退回圆月面。
type selectiveEclipseLimb struct{ low, high float64 }
func (s selectiveEclipseLimb) LimbRadiusKM(longitude, latitude, angle float64) (float64, bool) {
if angle < s.low || angle >= s.high {
return 0, false
}
return limb.Default().LimbRadiusKM(longitude, latitude, angle)
}
func (selectiveEclipseLimb) RadiusRangeKM() (float64, float64) { return limb.Default().RadiusRangeKM() }
func (selectiveEclipseLimb) AngleStepDeg() float64 { return limb.Default().AngleStepDeg() }
func TestSolarEclipseMapFooterDeclaresOnlyCorrectedLayers(t *testing.T) {
date := time.Date(2024, time.April, 8, 0, 0, 0, 0, time.UTC)
circular, ok := SolarEclipseMapSVG(date, SolarEclipseMapSVGOptions{Location: time.UTC})
if !ok {
t.Fatal("circular map missing")
}
// 位置角 45°~90° 只覆盖偏食外边界的接触点:全球始末、站心面板与中心界线都退回圆月面。
selective, ok := SolarEclipseMapSVG(date, SolarEclipseMapSVGOptions{Location: time.UTC, LunarLimb: selectiveEclipseLimb{45, 90}})
if !ok {
t.Fatal("selective-limb map missing")
}
if !strings.Contains(selective, "按月缘剖面修正可见域边界(食分线与等时线仍按圆月面)") {
t.Fatal("selective map must declare only the visibility boundary")
}
for _, layer := range []string{"全球始末", "食甚点站心值", "中心带界线"} {
if strings.Contains(selective, layer) {
t.Errorf("selective map declares uncorrected layer %s", layer)
}
}
circularGlobal, _ := eclipsecore.SolarEclipseOnDateNASABulletinSplitK(date)
if width := fmt.Sprintf("%.1f km", circularGlobal.PathWidthKM); !strings.Contains(selective, width) {
t.Errorf("selective map must keep the circular width %s while the limits stay circular", width)
}
if strings.Contains(circular, "按月缘剖面修正") {
t.Error("circular map declares a limb correction")
}
}
func TestEclipseSVGsWithFailingLimbStayCircular(t *testing.T) {
solarDate := time.Date(2024, time.April, 8, 0, 0, 0, 0, time.UTC)
lunarDate := time.Date(2025, time.September, 7, 0, 0, 0, 0, time.UTC)
failing := failingEclipseLimb{}
pairs := map[string][2]func() (string, bool){
"local solar": {
func() (string, bool) {
return LocalSolarEclipseSVG(solarDate, -96.8, 32.78, 0, LocalSolarEclipseSVGOptions{Location: time.UTC})
},
func() (string, bool) {
return LocalSolarEclipseSVG(solarDate, -96.8, 32.78, 0, LocalSolarEclipseSVGOptions{Location: time.UTC, LunarLimb: failing})
},
},
"solar map": {
func() (string, bool) {
return SolarEclipseMapSVG(solarDate, SolarEclipseMapSVGOptions{Location: time.UTC})
},
func() (string, bool) {
return SolarEclipseMapSVG(solarDate, SolarEclipseMapSVGOptions{Location: time.UTC, LunarLimb: failing})
},
},
"lunar": {
func() (string, bool) { return LunarEclipseSVG(lunarDate, LunarEclipseSVGOptions{Location: time.UTC}) },
func() (string, bool) {
return LunarEclipseSVG(lunarDate, LunarEclipseSVGOptions{Location: time.UTC, LunarLimb: failing})
},
},
"lunar map": {
func() (string, bool) {
return LunarEclipseMapSVG(lunarDate, LunarEclipseMapSVGOptions{Location: time.UTC})
},
func() (string, bool) {
return LunarEclipseMapSVG(lunarDate, LunarEclipseMapSVGOptions{Location: time.UTC, LunarLimb: failing})
},
},
"lunar detailed": {
func() (string, bool) {
return LunarEclipseDetailedSVG(lunarDate, LunarEclipseDetailedSVGOptions{Location: time.UTC})
},
func() (string, bool) {
return LunarEclipseDetailedSVG(lunarDate, LunarEclipseDetailedSVGOptions{Location: time.UTC, LunarLimb: failing})
},
},
}
for name, pair := range pairs {
circular, ok := pair[0]()
if !ok {
t.Fatalf("%s: circular chart missing", name)
}
failed, ok := pair[1]()
if !ok {
t.Fatalf("%s: failing-limb chart missing", name)
}
if failed != circular {
t.Errorf("%s: failing limb profile changed the chart", name)
}
}
}
// TestSolarEclipseMapContactMarkerMatchesCorrectedTime 检查全球图的 P1/P4 标记与页脚时刻同源:
// 月缘修正过时刻时,标记位置必须取修正后的接触点,而不是圆月面接触点。
func TestSolarEclipseMapContactMarkerMatchesCorrectedTime(t *testing.T) {
date := time.Date(2024, time.April, 8, 0, 0, 0, 0, time.UTC)
module := eclipsecore.NewWithInstance(nil).WithLunarLimb(limb.Default())
global, ok := module.SolarEclipseOnDateNASABulletinSplitK(date)
if !ok {
t.Fatal("global result missing")
}
if !global.LunarLimbApplied || !global.PartialBeginContact.Defined {
t.Fatal("the default limb profile must correct the global contacts")
}
circular, ok := eclipsecore.SolarEclipsePartialFootprintsNASABulletinSplitK(date, eclipsecore.SolarEclipsePartialFootprintOptions{})
if !ok {
t.Fatal("circular footprints missing")
}
// 修正后的接触点必须真的离开圆月面接触点,否则这条回归就测不到东西。
moved := math.Hypot(
(global.PartialBeginContact.Longitude-circular.P1.Longitude)*math.Cos(circular.P1.Latitude*math.Pi/180),
global.PartialBeginContact.Latitude-circular.P1.Latitude)
if moved < 1e-4 {
t.Fatalf("corrected P1 did not move (%.6f deg)", moved)
}
if delta := global.PartialBeginOnEarth.Sub(circular.P1.Time).Seconds(); math.Abs(delta) < 0.5 {
t.Fatalf("corrected P1 time moved by only %.3f s", delta)
}
}