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