- 使用压缩表加速查找日月食沙罗周期信息 - 优化日月食搜索跳步,减少非食季朔望月扫描 - 新增本地日全食、日环食、月全食搜索接口,返回 ok 区分未找到结果 - 新增水星、金星地心凌日查询及测试
275 lines
7.9 KiB
Go
275 lines
7.9 KiB
Go
package eclipse
|
|
|
|
import (
|
|
"math"
|
|
"time"
|
|
|
|
"b612.me/astro/basic"
|
|
)
|
|
|
|
const (
|
|
sarosCycleLunations = 223
|
|
sarosCycleDays = float64(sarosCycleLunations) * solarEclipseSynodicMonthDays
|
|
sarosWalkLimit = 100
|
|
|
|
sarosMagicYearOffset = 3000
|
|
sarosMagicCountMask = 0x7f
|
|
sarosMagicDayMask = 0x1f
|
|
sarosMagicMonthMask = 0x0f
|
|
sarosMagicYearMask = 0x1fff
|
|
sarosMagicCountShift = 0
|
|
sarosMagicDayShift = 7
|
|
sarosMagicMonthShift = 12
|
|
sarosMagicYearShift = 16
|
|
sarosMagicMatchLimitDay = 12.0
|
|
sarosMagicTieEpsilonDay = 1e-9
|
|
)
|
|
|
|
// 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 sarosMagic uint32
|
|
|
|
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) {
|
|
if info, ok := matchSarosMagic(solarSarosAnchors[:], 0, solarSarosHeadOverrides[:], ttJDE); ok {
|
|
return info, true
|
|
}
|
|
return solarSarosInfoByWalk(ttJDE)
|
|
}
|
|
|
|
func lunarSarosInfo(ttJDE float64) (SarosInfo, bool) {
|
|
if info, ok := matchSarosMagic(lunarSarosAnchors[:], 1, lunarSarosHeadOverrides[:], ttJDE); ok {
|
|
return info, true
|
|
}
|
|
return lunarSarosInfoByWalk(ttJDE)
|
|
}
|
|
|
|
func solarSarosInfoByWalk(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[:], 0, headTT)
|
|
if !ok || member > int(anchor.Count) {
|
|
return SarosInfo{}, false
|
|
}
|
|
return SarosInfo{
|
|
Series: int(anchor.Series),
|
|
Member: member,
|
|
Count: int(anchor.Count),
|
|
}, true
|
|
}
|
|
|
|
func lunarSarosInfoByWalk(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[:], 1, 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 matchSarosMagic(anchors []sarosMagic, seriesBase int, overrides []sarosHeadOverride, ttJDE float64) (SarosInfo, bool) {
|
|
if info, ok := matchSarosMagicOverrides(overrides, ttJDE); ok {
|
|
return info, true
|
|
}
|
|
bestDistance := math.Inf(1)
|
|
best := SarosInfo{}
|
|
for index, magic := range anchors {
|
|
anchor := decodeSarosMagic(magic, seriesBase+index)
|
|
info, distance, ok := matchSarosMagicCandidate(ttJDE, anchor, 0)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if betterSarosMagicMatch(info, distance, best, bestDistance) {
|
|
bestDistance = distance
|
|
best = info
|
|
}
|
|
}
|
|
if bestDistance <= sarosMagicMatchLimitDay {
|
|
return best, true
|
|
}
|
|
return SarosInfo{}, false
|
|
}
|
|
|
|
func matchSarosMagicOverrides(overrides []sarosHeadOverride, ttJDE float64) (SarosInfo, bool) {
|
|
bestDistance := math.Inf(1)
|
|
best := SarosInfo{}
|
|
for _, override := range overrides {
|
|
anchor := sarosAnchor{
|
|
Series: override.Series,
|
|
Count: override.Count,
|
|
Year: override.HeadYear,
|
|
Month: override.HeadMonth,
|
|
Day: override.HeadDay,
|
|
}
|
|
info, distance, ok := matchSarosMagicCandidate(ttJDE, anchor, int(override.MemberOffset))
|
|
if !ok {
|
|
continue
|
|
}
|
|
if betterSarosMagicMatch(info, distance, best, bestDistance) {
|
|
bestDistance = distance
|
|
best = info
|
|
}
|
|
}
|
|
if bestDistance <= sarosMagicMatchLimitDay {
|
|
return best, true
|
|
}
|
|
return SarosInfo{}, false
|
|
}
|
|
|
|
func matchSarosMagicCandidate(ttJDE float64, anchor sarosAnchor, memberOffset int) (SarosInfo, float64, bool) {
|
|
headTT := basic.JDECalc(int(anchor.Year), int(anchor.Month), float64(anchor.Day))
|
|
if math.IsNaN(headTT) {
|
|
return SarosInfo{}, 0, false
|
|
}
|
|
member := int(math.Round((ttJDE-headTT)/sarosCycleDays)) + 1 + memberOffset
|
|
if member < 1 || member > int(anchor.Count) {
|
|
return SarosInfo{}, 0, false
|
|
}
|
|
expectedTT := headTT + float64(member-1-memberOffset)*sarosCycleDays
|
|
return SarosInfo{
|
|
Series: int(anchor.Series),
|
|
Member: member,
|
|
Count: int(anchor.Count),
|
|
}, math.Abs(ttJDE - expectedTT), true
|
|
}
|
|
|
|
func betterSarosMagicMatch(info SarosInfo, distance float64, best SarosInfo, bestDistance float64) bool {
|
|
if distance < bestDistance-sarosMagicTieEpsilonDay {
|
|
return true
|
|
}
|
|
if math.Abs(distance-bestDistance) > sarosMagicTieEpsilonDay {
|
|
return false
|
|
}
|
|
if info.Series != best.Series {
|
|
return info.Series < best.Series
|
|
}
|
|
return info.Member < best.Member
|
|
}
|
|
|
|
func decodeSarosMagic(magic sarosMagic, series int) sarosAnchor {
|
|
value := uint32(magic)
|
|
return sarosAnchor{
|
|
Series: int16(series),
|
|
Count: uint8((value >> sarosMagicCountShift) & sarosMagicCountMask),
|
|
Year: int16(int((value>>sarosMagicYearShift)&sarosMagicYearMask) - sarosMagicYearOffset),
|
|
Month: uint8((value >> sarosMagicMonthShift) & sarosMagicMonthMask),
|
|
Day: uint8((value >> sarosMagicDayShift) & sarosMagicDayMask),
|
|
}
|
|
}
|
|
|
|
func matchSarosAnchor(anchors []sarosMagic, seriesBase int, headTT float64) (sarosAnchor, bool) {
|
|
headDate := basic.JDE2DateByZone(headTT, time.UTC, true)
|
|
year, month, day := headDate.Date()
|
|
monthNumber := int(month)
|
|
for index, magic := range anchors {
|
|
anchor := decodeSarosMagic(magic, seriesBase+index)
|
|
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
|
|
}
|