- 新增日食、月食、本地可见性、中心线、半影区域、SVG 图示与沙罗周期信息 - 新增行星冲合、留、方照、物理星历、视直径、相位、亮肢角、轨道节点等计算 - 新增木星伽利略卫星位置、现象与接触事件计算 - 新增恒星星表、星座判定、自行修正与观测辅助能力 - 新增 coord、formula、orbit、sundial、lite/sun、lite/moon 等扩展包 - 完善农历年号、月相英文别名、视差角、大气质量、折射、日晷与双星计算 - 增加 NASA、JPL Horizons、IMCCE 等回归测试数据与基线测试 - 重构基础算法文件组织,补充大量公开 API 注释和语义回归测试 - 更新中文和英文 README,补充示例、精度说明、SVG 配图
155 lines
4.3 KiB
Go
155 lines
4.3 KiB
Go
package eclipse
|
|
|
|
import (
|
|
"time"
|
|
|
|
"b612.me/astro/basic"
|
|
)
|
|
|
|
const (
|
|
sarosCycleLunations = 223
|
|
sarosCycleDays = float64(sarosCycleLunations) * solarEclipseSynodicMonthDays
|
|
sarosWalkLimit = 100
|
|
)
|
|
|
|
// SarosInfo 沙罗序列信息, Saros series metadata.
|
|
type SarosInfo struct {
|
|
// Series 是 NASA 沙罗系列编号;太阳食可能出现 0 号系列。
|
|
// Series is the NASA Saros series number; solar eclipses may use series 0.
|
|
Series int
|
|
// Member 是本次食在该系列中的序号,从 1 开始计数。
|
|
// Member is the 1-based index of this eclipse within the series.
|
|
Member int
|
|
// Count 是该系列的总成员数。
|
|
// Count is the total number of eclipses in the series.
|
|
Count int
|
|
}
|
|
|
|
type sarosAnchor struct {
|
|
Series int16
|
|
Count uint8
|
|
Year int16
|
|
Month uint8
|
|
Day uint8
|
|
}
|
|
|
|
type sarosHeadOverride struct {
|
|
Series int16
|
|
Count uint8
|
|
HeadYear int16
|
|
HeadMonth uint8
|
|
HeadDay uint8
|
|
MemberOffset int8
|
|
}
|
|
|
|
var solarSarosHeadOverrides = [...]sarosHeadOverride{
|
|
{Series: 22, Count: 71, HeadYear: -2192, HeadMonth: 5, HeadDay: 17, MemberOffset: -1},
|
|
}
|
|
|
|
var lunarSarosHeadOverrides = [...]sarosHeadOverride{
|
|
{Series: 4, Count: 78, HeadYear: -2483, HeadMonth: 1, HeadDay: 12, MemberOffset: 2},
|
|
{Series: 8, Count: 86, HeadYear: -2494, HeadMonth: 8, HeadDay: 7, MemberOffset: 0},
|
|
{Series: 61, Count: 78, HeadYear: -762, HeadMonth: 12, HeadDay: 24, MemberOffset: 1},
|
|
}
|
|
|
|
func solarSarosInfo(ttJDE float64) (SarosInfo, bool) {
|
|
headTT, member, ok := solarSarosHead(ttJDE)
|
|
if !ok {
|
|
return SarosInfo{}, false
|
|
}
|
|
if info, ok := matchSarosHeadOverride(solarSarosHeadOverrides[:], headTT, member); ok {
|
|
return info, true
|
|
}
|
|
anchor, ok := matchSarosAnchor(solarSarosAnchors[:], headTT)
|
|
if !ok || member > int(anchor.Count) {
|
|
return SarosInfo{}, false
|
|
}
|
|
return SarosInfo{
|
|
Series: int(anchor.Series),
|
|
Member: member,
|
|
Count: int(anchor.Count),
|
|
}, true
|
|
}
|
|
|
|
func lunarSarosInfo(ttJDE float64) (SarosInfo, bool) {
|
|
headTT, member, ok := lunarSarosHead(ttJDE)
|
|
if !ok {
|
|
return SarosInfo{}, false
|
|
}
|
|
if info, ok := matchSarosHeadOverride(lunarSarosHeadOverrides[:], headTT, member); ok {
|
|
return info, true
|
|
}
|
|
anchor, ok := matchSarosAnchor(lunarSarosAnchors[:], headTT)
|
|
if !ok || member > int(anchor.Count) {
|
|
return SarosInfo{}, false
|
|
}
|
|
return SarosInfo{
|
|
Series: int(anchor.Series),
|
|
Member: member,
|
|
Count: int(anchor.Count),
|
|
}, true
|
|
}
|
|
|
|
func solarSarosHead(ttJDE float64) (float64, int, bool) {
|
|
currentTT := ttJDE
|
|
member := 1
|
|
for step := 0; step < sarosWalkLimit; step++ {
|
|
previousSeed := basic.CalcMoonSHByJDE(currentTT-sarosCycleDays, 0)
|
|
previous := basic.SolarEclipseNASABulletinSplitK(previousSeed)
|
|
if previous.Type == basic.SolarEclipseNone {
|
|
return currentTT, member, true
|
|
}
|
|
currentTT = previous.GreatestEclipse
|
|
member++
|
|
}
|
|
return 0, 0, false
|
|
}
|
|
|
|
func lunarSarosHead(ttJDE float64) (float64, int, bool) {
|
|
currentTT := ttJDE
|
|
member := 1
|
|
for step := 0; step < sarosWalkLimit; step++ {
|
|
previousSeed := basic.CalcMoonSHByJDE(currentTT-sarosCycleDays, 1)
|
|
previous := basic.LunarEclipseDanjon(previousSeed)
|
|
if previous.Type == basic.LunarEclipseNone {
|
|
return currentTT, member, true
|
|
}
|
|
currentTT = previous.Maximum
|
|
member++
|
|
}
|
|
return 0, 0, false
|
|
}
|
|
|
|
func matchSarosAnchor(anchors []sarosAnchor, headTT float64) (sarosAnchor, bool) {
|
|
headDate := basic.JDE2DateByZone(headTT, time.UTC, true)
|
|
year, month, day := headDate.Date()
|
|
monthNumber := int(month)
|
|
for _, anchor := range anchors {
|
|
if int(anchor.Year) == year && int(anchor.Month) == monthNumber && int(anchor.Day) == day {
|
|
return anchor, true
|
|
}
|
|
}
|
|
return sarosAnchor{}, false
|
|
}
|
|
|
|
func matchSarosHeadOverride(overrides []sarosHeadOverride, headTT float64, member int) (SarosInfo, bool) {
|
|
headDate := basic.JDE2DateByZone(headTT, time.UTC, true)
|
|
year, month, day := headDate.Date()
|
|
monthNumber := int(month)
|
|
for _, override := range overrides {
|
|
if int(override.HeadYear) != year || int(override.HeadMonth) != monthNumber || int(override.HeadDay) != day {
|
|
continue
|
|
}
|
|
adjustedMember := member + int(override.MemberOffset)
|
|
if adjustedMember < 1 || adjustedMember > int(override.Count) {
|
|
return SarosInfo{}, false
|
|
}
|
|
return SarosInfo{
|
|
Series: int(override.Series),
|
|
Member: adjustedMember,
|
|
Count: int(override.Count),
|
|
}, true
|
|
}
|
|
return SarosInfo{}, false
|
|
}
|