2026-05-01 22:38:44 +08:00
|
|
|
|
package eclipse
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"math"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"b612.me/astro/basic"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
|
lunarEclipseSynodicMonthDays = 29.530588853
|
|
|
|
|
|
lunarEclipseSearchLimit = 24
|
|
|
|
|
|
lunarEclipseSearchEpsilonDay = 1e-8
|
|
|
|
|
|
// 默认口径仍以 Danjon 为主,但对五千年目录边界那类“Danjon 判无食、Chauvenet 判极浅半影食”的个例,
|
|
|
|
|
|
// 允许在公开默认包装层回退到 Chauvenet,避免把目录中确有记录的边缘半影食整个跳过。
|
|
|
|
|
|
lunarEclipseDefaultFallbackMaxPenumbralMagnitude = 0.03
|
|
|
|
|
|
|
|
|
|
|
|
// 当天判断的第一层只做粗筛:
|
|
|
|
|
|
// 如果本地中午的日月黄经差明显不在满月附近,则这一天不可能发生月食。
|
|
|
|
|
|
lunarEclipseDayPhaseMin = 120.0
|
|
|
|
|
|
lunarEclipseDayPhaseMax = 240.0
|
|
|
|
|
|
|
|
|
|
|
|
// 月食只会发生在月球接近黄道节点时。
|
|
|
|
|
|
// 这里保守放宽到 2 度,作为“是否值得进入精算”的预筛条件。
|
|
|
|
|
|
lunarEclipseLatitudeLimitDeg = 2.0
|
|
|
|
|
|
lunarEclipseLongitudeLimitDeg = 15.0
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
type lunarEclipseCalculator func(float64) basic.LunarEclipseResult
|
|
|
|
|
|
|
|
|
|
|
|
// LunarEclipseType 月食类型, lunar eclipse type.
|
|
|
|
|
|
type LunarEclipseType string
|
|
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
|
// LunarEclipseNone 无月食, no lunar eclipse.
|
|
|
|
|
|
LunarEclipseNone LunarEclipseType = "none"
|
|
|
|
|
|
// LunarEclipsePenumbral 半影月食, penumbral lunar eclipse.
|
|
|
|
|
|
LunarEclipsePenumbral LunarEclipseType = "penumbral"
|
|
|
|
|
|
// LunarEclipsePartial 月偏食, partial lunar eclipse.
|
|
|
|
|
|
LunarEclipsePartial LunarEclipseType = "partial"
|
|
|
|
|
|
// LunarEclipseTotal 月全食, total lunar eclipse.
|
|
|
|
|
|
LunarEclipseTotal LunarEclipseType = "total"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// LunarEclipseContactPoint 表示月食接触点在月面上的方位。
|
|
|
|
|
|
// LunarEclipseContactPoint describes a lunar eclipse contact point on the Moon limb.
|
|
|
|
|
|
type LunarEclipseContactPoint struct {
|
|
|
|
|
|
// Label 是接触标签,如 P1/U1/U2/U3/U4/P4。
|
|
|
|
|
|
// Label is the contact label, such as P1/U1/U2/U3/U4/P4.
|
|
|
|
|
|
Label string
|
|
|
|
|
|
// Time 是该接触时刻,保持用户输入时区。
|
|
|
|
|
|
// Time is the contact time, preserving the input timezone.
|
|
|
|
|
|
Time time.Time
|
|
|
|
|
|
// ContactPositionAngle 是月面接触点位置角,从天球北点起向东量,单位度。
|
|
|
|
|
|
// ContactPositionAngle is the Moon-limb contact position angle from celestial north toward east, in degrees.
|
|
|
|
|
|
ContactPositionAngle float64
|
|
|
|
|
|
// ContactClockwiseAngle 是图面上从北点顺时针量到接触点的角度,单位度。
|
|
|
|
|
|
// ContactClockwiseAngle is the chart clockwise angle from north to the contact point, in degrees.
|
|
|
|
|
|
ContactClockwiseAngle float64
|
|
|
|
|
|
// MoonCenterPositionAngle 是月心相对地影中心的位置角,从北点起向东量,单位度。
|
|
|
|
|
|
// MoonCenterPositionAngle is the Moon-center position angle from the shadow center, in degrees.
|
|
|
|
|
|
MoonCenterPositionAngle float64
|
|
|
|
|
|
// ShadowCenterPositionAngle 是地影中心相对月心的位置角,从北点起向东量,单位度。
|
|
|
|
|
|
// ShadowCenterPositionAngle is the shadow-center position angle from the Moon center, in degrees.
|
|
|
|
|
|
ShadowCenterPositionAngle float64
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// LunarEclipseInfo 月食信息, lunar eclipse information.
|
|
|
|
|
|
//
|
|
|
|
|
|
// 所有时刻字段都保持用户输入的时区。
|
|
|
|
|
|
// 不存在的阶段使用零值 time.Time。
|
|
|
|
|
|
type LunarEclipseInfo struct {
|
|
|
|
|
|
// Type 月食类型, eclipse type.
|
|
|
|
|
|
Type LunarEclipseType
|
|
|
|
|
|
// HasSaros 存在沙罗序列信息, has Saros series metadata.
|
|
|
|
|
|
HasSaros bool
|
|
|
|
|
|
// Saros 是沙罗序列信息,包括系列号、系列内序号和总成员数。
|
|
|
|
|
|
// Saros is Saros series metadata with the series number, member index, and total member count.
|
|
|
|
|
|
Saros SarosInfo
|
|
|
|
|
|
|
|
|
|
|
|
// PenumbralMagnitude 半影食分, penumbral magnitude.
|
|
|
|
|
|
PenumbralMagnitude float64
|
|
|
|
|
|
// UmbralMagnitude 本影食分;纯半影月食时可为负值, umbral magnitude; can be negative for purely penumbral eclipses.
|
|
|
|
|
|
UmbralMagnitude float64
|
|
|
|
|
|
|
|
|
|
|
|
// PenumbralStart 半影始, penumbral eclipse begins.
|
|
|
|
|
|
PenumbralStart time.Time
|
|
|
|
|
|
// PartialStart 初亏, partial eclipse begins.
|
|
|
|
|
|
PartialStart time.Time
|
|
|
|
|
|
// TotalStart 食既, total eclipse begins.
|
|
|
|
|
|
TotalStart time.Time
|
|
|
|
|
|
// Maximum 食甚, greatest eclipse.
|
|
|
|
|
|
Maximum time.Time
|
|
|
|
|
|
// TotalEnd 生光, total eclipse ends.
|
|
|
|
|
|
TotalEnd time.Time
|
|
|
|
|
|
// PartialEnd 复圆, partial eclipse ends.
|
|
|
|
|
|
PartialEnd time.Time
|
|
|
|
|
|
// PenumbralEnd 半影终, penumbral eclipse ends.
|
|
|
|
|
|
PenumbralEnd time.Time
|
|
|
|
|
|
|
|
|
|
|
|
// ContactPoints 是各接触时刻在月面上的接触点方位。
|
|
|
|
|
|
// ContactPoints are Moon-limb contact position angles at eclipse contacts.
|
|
|
|
|
|
ContactPoints []LunarEclipseContactPoint
|
|
|
|
|
|
|
|
|
|
|
|
// HasPenumbral 有半影阶段, has penumbral phase.
|
|
|
|
|
|
HasPenumbral bool
|
|
|
|
|
|
// HasPartial 有偏食阶段, has partial phase.
|
|
|
|
|
|
HasPartial bool
|
|
|
|
|
|
// HasTotal 有全食阶段, has total phase.
|
|
|
|
|
|
HasTotal bool
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// LunarEclipseOnDate 当地自然日月食查询 / local-date lunar eclipse query.
|
|
|
|
|
|
// Determine whether a lunar eclipse occurs on the local date.
|
|
|
|
|
|
// The default path uses Danjon and falls back to Chauvenet for ultra-shallow penumbral edge cases.
|
|
|
|
|
|
//
|
|
|
|
|
|
// 只要该自然日内有任意一个接触时刻,或整场月食与该自然日有时间重叠,就返回 true。
|
|
|
|
|
|
func LunarEclipseOnDate(date time.Time) (LunarEclipseInfo, bool) {
|
|
|
|
|
|
return lunarEclipseOnDateWithFallback(date, basic.LunarEclipseDanjon, true)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// LunarEclipseOnDateDanjon 当地自然日月食查询(Danjon) / local-date lunar eclipse query with Danjon model.
|
|
|
|
|
|
// Determine whether a lunar eclipse occurs on the local date with the Danjon model.
|
|
|
|
|
|
func LunarEclipseOnDateDanjon(date time.Time) (LunarEclipseInfo, bool) {
|
|
|
|
|
|
return lunarEclipseOnDateWithFallback(date, basic.LunarEclipseDanjon, false)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// LunarEclipseOnDateChauvenet 当地自然日月食查询(Chauvenet) / local-date lunar eclipse query with Chauvenet model.
|
|
|
|
|
|
// Determine whether a lunar eclipse occurs on the local date with the Chauvenet model.
|
|
|
|
|
|
func LunarEclipseOnDateChauvenet(date time.Time) (LunarEclipseInfo, bool) {
|
|
|
|
|
|
return lunarEclipseOnDateWithFallback(date, basic.LunarEclipseChauvenet, false)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func lunarEclipseOnDateWithFallback(date time.Time, calculator lunarEclipseCalculator, allowDefaultFallback bool) (LunarEclipseInfo, bool) {
|
|
|
|
|
|
location := date.Location()
|
|
|
|
|
|
dayStart, dayMid, dayEnd := lunarEclipseLocalDayBounds(date)
|
|
|
|
|
|
|
|
|
|
|
|
phaseDiff := moonSunLoDiff(dayMid)
|
|
|
|
|
|
if phaseDiff < lunarEclipseDayPhaseMin || phaseDiff > lunarEclipseDayPhaseMax {
|
|
|
|
|
|
return LunarEclipseInfo{}, false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
candidateTT := basic.CalcMoonSHByJDE(timeToTTJDE(dayMid), 1)
|
|
|
|
|
|
if !isPotentialLunarEclipse(candidateTT) {
|
|
|
|
|
|
return LunarEclipseInfo{}, false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
result := calculator(candidateTT)
|
|
|
|
|
|
if result.Type == basic.LunarEclipseNone && allowDefaultFallback {
|
|
|
|
|
|
if fallback, ok := lunarEclipseDefaultFallback(candidateTT); ok {
|
|
|
|
|
|
result = fallback
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if result.Type == basic.LunarEclipseNone {
|
|
|
|
|
|
return LunarEclipseInfo{}, false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
info := lunarEclipseInfoFromBasic(result, location)
|
|
|
|
|
|
if !lunarEclipseOverlapsDate(info, dayStart, dayEnd) {
|
|
|
|
|
|
return LunarEclipseInfo{}, false
|
|
|
|
|
|
}
|
|
|
|
|
|
return info, true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// LastLunarEclipse 上次月食 / previous lunar eclipse.
|
|
|
|
|
|
// Previous lunar eclipse.
|
|
|
|
|
|
// The default path uses Danjon and falls back to Chauvenet for ultra-shallow penumbral edge cases.
|
|
|
|
|
|
func LastLunarEclipse(date time.Time) LunarEclipseInfo {
|
|
|
|
|
|
info, _ := searchLunarEclipse(date, -1, true, basic.LunarEclipseDanjon, true)
|
|
|
|
|
|
return info
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// LastLunarEclipseDanjon 上次月食(Danjon) / previous lunar eclipse with Danjon model.
|
|
|
|
|
|
// Previous lunar eclipse with the Danjon model.
|
|
|
|
|
|
func LastLunarEclipseDanjon(date time.Time) LunarEclipseInfo {
|
|
|
|
|
|
info, _ := searchLunarEclipse(date, -1, true, basic.LunarEclipseDanjon, false)
|
|
|
|
|
|
return info
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// LastLunarEclipseChauvenet 上次月食(Chauvenet) / previous lunar eclipse with Chauvenet model.
|
|
|
|
|
|
// Previous lunar eclipse with the Chauvenet model.
|
|
|
|
|
|
func LastLunarEclipseChauvenet(date time.Time) LunarEclipseInfo {
|
|
|
|
|
|
info, _ := searchLunarEclipse(date, -1, true, basic.LunarEclipseChauvenet, false)
|
|
|
|
|
|
return info
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// NextLunarEclipse 下次月食 / next lunar eclipse.
|
|
|
|
|
|
// Next lunar eclipse.
|
|
|
|
|
|
// The default path uses Danjon and falls back to Chauvenet for ultra-shallow penumbral edge cases.
|
|
|
|
|
|
func NextLunarEclipse(date time.Time) LunarEclipseInfo {
|
|
|
|
|
|
info, _ := searchLunarEclipse(date, 1, false, basic.LunarEclipseDanjon, true)
|
|
|
|
|
|
return info
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// NextLunarEclipseDanjon 下次月食(Danjon) / next lunar eclipse with Danjon model.
|
|
|
|
|
|
// Next lunar eclipse with the Danjon model.
|
|
|
|
|
|
func NextLunarEclipseDanjon(date time.Time) LunarEclipseInfo {
|
|
|
|
|
|
info, _ := searchLunarEclipse(date, 1, false, basic.LunarEclipseDanjon, false)
|
|
|
|
|
|
return info
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// NextLunarEclipseChauvenet 下次月食(Chauvenet) / next lunar eclipse with Chauvenet model.
|
|
|
|
|
|
// Next lunar eclipse with the Chauvenet model.
|
|
|
|
|
|
func NextLunarEclipseChauvenet(date time.Time) LunarEclipseInfo {
|
|
|
|
|
|
info, _ := searchLunarEclipse(date, 1, false, basic.LunarEclipseChauvenet, false)
|
|
|
|
|
|
return info
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ClosestLunarEclipse 最近一次月食 / closest lunar eclipse.
|
|
|
|
|
|
// Closest lunar eclipse.
|
|
|
|
|
|
// The default path uses Danjon and falls back to Chauvenet for ultra-shallow penumbral edge cases.
|
|
|
|
|
|
func ClosestLunarEclipse(date time.Time) LunarEclipseInfo {
|
|
|
|
|
|
last, hasLast := searchLunarEclipse(date, -1, true, basic.LunarEclipseDanjon, true)
|
|
|
|
|
|
next, hasNext := searchLunarEclipse(date, 1, false, basic.LunarEclipseDanjon, true)
|
|
|
|
|
|
return closestLunarEclipse(date, last, hasLast, next, hasNext)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ClosestLunarEclipseDanjon 最近一次月食(Danjon) / closest lunar eclipse with Danjon model.
|
|
|
|
|
|
// Closest lunar eclipse with the Danjon model.
|
|
|
|
|
|
func ClosestLunarEclipseDanjon(date time.Time) LunarEclipseInfo {
|
|
|
|
|
|
last, hasLast := searchLunarEclipse(date, -1, true, basic.LunarEclipseDanjon, false)
|
|
|
|
|
|
next, hasNext := searchLunarEclipse(date, 1, false, basic.LunarEclipseDanjon, false)
|
|
|
|
|
|
|
|
|
|
|
|
return closestLunarEclipse(date, last, hasLast, next, hasNext)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ClosestLunarEclipseChauvenet 最近一次月食(Chauvenet) / closest lunar eclipse with Chauvenet model.
|
|
|
|
|
|
// Closest lunar eclipse with the Chauvenet model.
|
|
|
|
|
|
func ClosestLunarEclipseChauvenet(date time.Time) LunarEclipseInfo {
|
|
|
|
|
|
last, hasLast := searchLunarEclipse(date, -1, true, basic.LunarEclipseChauvenet, false)
|
|
|
|
|
|
next, hasNext := searchLunarEclipse(date, 1, false, basic.LunarEclipseChauvenet, false)
|
|
|
|
|
|
|
|
|
|
|
|
return closestLunarEclipse(date, last, hasLast, next, hasNext)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func closestLunarEclipse(
|
|
|
|
|
|
date time.Time,
|
|
|
|
|
|
last LunarEclipseInfo,
|
|
|
|
|
|
hasLast bool,
|
|
|
|
|
|
next LunarEclipseInfo,
|
|
|
|
|
|
hasNext bool,
|
|
|
|
|
|
) LunarEclipseInfo {
|
|
|
|
|
|
switch {
|
|
|
|
|
|
case hasLast && !hasNext:
|
|
|
|
|
|
return last
|
|
|
|
|
|
case !hasLast && hasNext:
|
|
|
|
|
|
return next
|
|
|
|
|
|
case !hasLast && !hasNext:
|
|
|
|
|
|
return LunarEclipseInfo{}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
lastDistance := math.Abs(date.Sub(last.Maximum).Seconds())
|
|
|
|
|
|
nextDistance := math.Abs(next.Maximum.Sub(date).Seconds())
|
|
|
|
|
|
if lastDistance <= nextDistance {
|
|
|
|
|
|
return last
|
|
|
|
|
|
}
|
|
|
|
|
|
return next
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func searchLunarEclipse(
|
|
|
|
|
|
date time.Time,
|
|
|
|
|
|
direction int,
|
|
|
|
|
|
includeCurrent bool,
|
|
|
|
|
|
calculator lunarEclipseCalculator,
|
|
|
|
|
|
allowDefaultFallback bool,
|
|
|
|
|
|
) (LunarEclipseInfo, bool) {
|
|
|
|
|
|
targetTT := timeToTTJDE(date)
|
|
|
|
|
|
candidateTT := basic.CalcMoonSHByJDE(targetTT, 1)
|
|
|
|
|
|
|
|
|
|
|
|
for i := 0; i < lunarEclipseSearchLimit; i++ {
|
|
|
|
|
|
if isPotentialLunarEclipse(candidateTT) {
|
|
|
|
|
|
result := calculator(candidateTT)
|
|
|
|
|
|
if result.Type == basic.LunarEclipseNone && allowDefaultFallback {
|
|
|
|
|
|
if fallback, ok := lunarEclipseDefaultFallback(candidateTT); ok {
|
|
|
|
|
|
result = fallback
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if result.Type != basic.LunarEclipseNone && lunarEclipseMatchesDirection(result.Maximum, targetTT, direction, includeCurrent) {
|
|
|
|
|
|
return lunarEclipseInfoFromBasic(result, date.Location()), true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-03 19:00:08 +08:00
|
|
|
|
candidateTT = nextEclipseSearchCandidateTT(candidateTT, 1, direction, lunarEclipseSynodicMonthDays)
|
2026-05-01 22:38:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return LunarEclipseInfo{}, false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func lunarEclipseDefaultFallback(candidateTT float64) (basic.LunarEclipseResult, bool) {
|
|
|
|
|
|
result := basic.LunarEclipseChauvenet(candidateTT)
|
|
|
|
|
|
if result.Type != basic.LunarEclipsePenumbral {
|
|
|
|
|
|
return basic.LunarEclipseResult{}, false
|
|
|
|
|
|
}
|
|
|
|
|
|
if result.HasPartial || result.HasTotal {
|
|
|
|
|
|
return basic.LunarEclipseResult{}, false
|
|
|
|
|
|
}
|
|
|
|
|
|
if result.PenumbralMagnitude <= 0 || result.PenumbralMagnitude > lunarEclipseDefaultFallbackMaxPenumbralMagnitude {
|
|
|
|
|
|
return basic.LunarEclipseResult{}, false
|
|
|
|
|
|
}
|
|
|
|
|
|
return result, true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func lunarEclipseMatchesDirection(maximumTT, targetTT float64, direction int, includeCurrent bool) bool {
|
|
|
|
|
|
delta := maximumTT - targetTT
|
|
|
|
|
|
if math.Abs(delta) <= lunarEclipseSearchEpsilonDay {
|
|
|
|
|
|
return direction < 0 && includeCurrent
|
|
|
|
|
|
}
|
|
|
|
|
|
if direction > 0 {
|
|
|
|
|
|
return delta > 0
|
|
|
|
|
|
}
|
|
|
|
|
|
return delta < 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func isPotentialLunarEclipse(fullMoonTT float64) bool {
|
|
|
|
|
|
moonLatitude := math.Abs(basic.HMoonTrueBo(fullMoonTT))
|
|
|
|
|
|
if moonLatitude > lunarEclipseLatitudeLimitDeg {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
phaseDiff := math.Abs(normalizeDegree180(basic.HMoonApparentLo(fullMoonTT) - basic.HSunApparentLo(fullMoonTT) - 180))
|
|
|
|
|
|
return phaseDiff <= lunarEclipseLongitudeLimitDeg
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func lunarEclipseInfoFromBasic(result basic.LunarEclipseResult, location *time.Location) LunarEclipseInfo {
|
|
|
|
|
|
saros, hasSaros := lunarSarosInfo(result.Maximum)
|
|
|
|
|
|
return LunarEclipseInfo{
|
|
|
|
|
|
HasSaros: hasSaros,
|
|
|
|
|
|
Saros: saros,
|
|
|
|
|
|
Type: mapBasicLunarEclipseType(result.Type),
|
|
|
|
|
|
PenumbralMagnitude: result.PenumbralMagnitude,
|
|
|
|
|
|
UmbralMagnitude: result.Magnitude,
|
|
|
|
|
|
PenumbralStart: ttJDEToTime(result.PenumbralStart, location),
|
|
|
|
|
|
PartialStart: ttJDEToTime(result.PartialStart, location),
|
|
|
|
|
|
TotalStart: ttJDEToTime(result.TotalStart, location),
|
|
|
|
|
|
Maximum: ttJDEToTime(result.Maximum, location),
|
|
|
|
|
|
TotalEnd: ttJDEToTime(result.TotalEnd, location),
|
|
|
|
|
|
PartialEnd: ttJDEToTime(result.PartialEnd, location),
|
|
|
|
|
|
PenumbralEnd: ttJDEToTime(result.PenumbralEnd, location),
|
|
|
|
|
|
ContactPoints: lunarEclipseContactPointsFromBasic(result, location),
|
|
|
|
|
|
HasPenumbral: result.HasPenumbral,
|
|
|
|
|
|
HasPartial: result.HasPartial,
|
|
|
|
|
|
HasTotal: result.HasTotal,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func lunarEclipseContactPointsFromBasic(
|
|
|
|
|
|
result basic.LunarEclipseResult,
|
|
|
|
|
|
location *time.Location,
|
|
|
|
|
|
) []LunarEclipseContactPoint {
|
|
|
|
|
|
if !result.HasPenumbral {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
contacts := []LunarEclipseContactPoint{
|
|
|
|
|
|
lunarEclipseContactPoint("P1", result.PenumbralStart, location, false),
|
|
|
|
|
|
}
|
|
|
|
|
|
if result.HasPartial {
|
|
|
|
|
|
contacts = append(contacts, lunarEclipseContactPoint("U1", result.PartialStart, location, false))
|
|
|
|
|
|
}
|
|
|
|
|
|
if result.HasTotal {
|
|
|
|
|
|
contacts = append(contacts, lunarEclipseContactPoint("U2", result.TotalStart, location, true))
|
|
|
|
|
|
}
|
|
|
|
|
|
if result.HasTotal {
|
|
|
|
|
|
contacts = append(contacts, lunarEclipseContactPoint("U3", result.TotalEnd, location, true))
|
|
|
|
|
|
}
|
|
|
|
|
|
if result.HasPartial {
|
|
|
|
|
|
contacts = append(contacts, lunarEclipseContactPoint("U4", result.PartialEnd, location, false))
|
|
|
|
|
|
}
|
|
|
|
|
|
contacts = append(contacts, lunarEclipseContactPoint("P4", result.PenumbralEnd, location, false))
|
|
|
|
|
|
return contacts
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func lunarEclipseContactPoint(
|
|
|
|
|
|
label string,
|
|
|
|
|
|
ttJDE float64,
|
|
|
|
|
|
location *time.Location,
|
|
|
|
|
|
internalContact bool,
|
|
|
|
|
|
) LunarEclipseContactPoint {
|
|
|
|
|
|
moonCenterPA := lunarEclipseMoonCenterPositionAngle(ttJDE)
|
|
|
|
|
|
shadowCenterPA := normalizeDegree360(moonCenterPA + 180)
|
|
|
|
|
|
contactPA := shadowCenterPA
|
|
|
|
|
|
if internalContact {
|
|
|
|
|
|
contactPA = moonCenterPA
|
|
|
|
|
|
}
|
|
|
|
|
|
return LunarEclipseContactPoint{
|
|
|
|
|
|
Label: label,
|
|
|
|
|
|
Time: ttJDEToTime(ttJDE, location),
|
|
|
|
|
|
ContactPositionAngle: contactPA,
|
|
|
|
|
|
ContactClockwiseAngle: normalizeDegree360(360 - contactPA),
|
|
|
|
|
|
MoonCenterPositionAngle: moonCenterPA,
|
|
|
|
|
|
ShadowCenterPositionAngle: shadowCenterPA,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func lunarEclipseMoonCenterPositionAngle(ttJDE float64) float64 {
|
|
|
|
|
|
shadowRA, shadowDec := lunarEclipseShadowCenterRaDec(ttJDE)
|
|
|
|
|
|
moonRA, moonDec := basic.HMoonTrueRaDec(ttJDE)
|
|
|
|
|
|
return positionAngle(shadowRA, shadowDec, moonRA, moonDec)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func lunarEclipseShadowCenterRaDec(ttJDE float64) (float64, float64) {
|
|
|
|
|
|
sunRA, sunDec := basic.HSunApparentRaDec(ttJDE)
|
|
|
|
|
|
return normalizeDegree360(sunRA + 180), -sunDec
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func positionAngle(fromRA, fromDec, toRA, toDec float64) float64 {
|
|
|
|
|
|
dRA := (toRA - fromRA) * math.Pi / 180
|
|
|
|
|
|
fromDecRad := fromDec * math.Pi / 180
|
|
|
|
|
|
toDecRad := toDec * math.Pi / 180
|
|
|
|
|
|
angle := math.Atan2(
|
|
|
|
|
|
math.Sin(dRA),
|
|
|
|
|
|
math.Cos(fromDecRad)*math.Tan(toDecRad)-math.Sin(fromDecRad)*math.Cos(dRA),
|
|
|
|
|
|
) * 180 / math.Pi
|
|
|
|
|
|
return normalizeDegree360(angle)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func mapBasicLunarEclipseType(eclipseType basic.LunarEclipseType) LunarEclipseType {
|
|
|
|
|
|
switch eclipseType {
|
|
|
|
|
|
case basic.LunarEclipsePenumbral:
|
|
|
|
|
|
return LunarEclipsePenumbral
|
|
|
|
|
|
case basic.LunarEclipsePartial:
|
|
|
|
|
|
return LunarEclipsePartial
|
|
|
|
|
|
case basic.LunarEclipseTotal:
|
|
|
|
|
|
return LunarEclipseTotal
|
|
|
|
|
|
default:
|
|
|
|
|
|
return LunarEclipseNone
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func lunarEclipseOverlapsDate(info LunarEclipseInfo, dayStart, dayEnd time.Time) bool {
|
|
|
|
|
|
eventStart, eventEnd, ok := lunarEclipseRange(info)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
return !eventEnd.Before(dayStart) && eventStart.Before(dayEnd)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func lunarEclipseRange(info LunarEclipseInfo) (time.Time, time.Time, bool) {
|
|
|
|
|
|
if !info.HasPenumbral {
|
|
|
|
|
|
return time.Time{}, time.Time{}, false
|
|
|
|
|
|
}
|
|
|
|
|
|
return info.PenumbralStart, info.PenumbralEnd, true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func ttJDEToTime(ttJDE float64, location *time.Location) time.Time {
|
|
|
|
|
|
if ttJDE == 0 {
|
|
|
|
|
|
return time.Time{}
|
|
|
|
|
|
}
|
|
|
|
|
|
utcJDE := basic.TD2UT(ttJDE, false)
|
|
|
|
|
|
return basic.JDE2DateByZone(utcJDE, location, false)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func timeToTTJDE(date time.Time) float64 {
|
|
|
|
|
|
utcJDE := basic.Date2JDE(date.UTC())
|
|
|
|
|
|
return basic.TD2UT(utcJDE, true)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func normalizeDegree180(angle float64) float64 {
|
|
|
|
|
|
angle = math.Mod(angle, 360)
|
|
|
|
|
|
if angle > 180 {
|
|
|
|
|
|
angle -= 360
|
|
|
|
|
|
}
|
|
|
|
|
|
if angle <= -180 {
|
|
|
|
|
|
angle += 360
|
|
|
|
|
|
}
|
|
|
|
|
|
return angle
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func normalizeDegree360(angle float64) float64 {
|
|
|
|
|
|
angle = math.Mod(angle, 360)
|
|
|
|
|
|
if angle < 0 {
|
|
|
|
|
|
angle += 360
|
|
|
|
|
|
}
|
|
|
|
|
|
return angle
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func lunarEclipseLocalDayBounds(date time.Time) (time.Time, time.Time, time.Time) {
|
|
|
|
|
|
location := date.Location()
|
|
|
|
|
|
dayStart := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, location)
|
|
|
|
|
|
dayMid := time.Date(date.Year(), date.Month(), date.Day(), 12, 0, 0, 0, location)
|
|
|
|
|
|
dayEnd := time.Date(date.Year(), date.Month(), date.Day()+1, 0, 0, 0, 0, location)
|
|
|
|
|
|
return dayStart, dayMid, dayEnd
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func nextLunarEclipseLocalDayStart(dayStart time.Time) time.Time {
|
|
|
|
|
|
location := dayStart.Location()
|
|
|
|
|
|
return time.Date(dayStart.Year(), dayStart.Month(), dayStart.Day()+1, 0, 0, 0, 0, location)
|
|
|
|
|
|
}
|