package eclipse import ( "math" "time" "b612.me/astro/basic" ) const localLunarEclipseSearchLimit = 6000 type localLunarEclipseQueryMode int const ( localLunarEclipseQueryVisible localLunarEclipseQueryMode = iota localLunarEclipseQueryGeometric ) // LocalLunarEclipseInfo 站点月食信息, local lunar eclipse information. // // 所有时刻字段都保持用户输入的时区。 // 不存在的阶段使用零值 time.Time。 type LocalLunarEclipseInfo 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 // Longitude 观测点经度,东正西负, observer longitude, east positive. Longitude float64 // Latitude 观测点纬度,北正南负, observer latitude, north positive. Latitude float64 // Height 观测点海拔高度,单位米, observer height in meters. Height float64 // 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 // MoonAltitude 食甚时月亮高度角,单位度, Moon altitude at maximum in degrees. MoonAltitude float64 // MoonAzimuth 食甚时月亮方位角,单位度, Moon azimuth at maximum in degrees. MoonAzimuth float64 // VisibleAtMaximum 食甚时月亮中心在本地几何地平线上方, Moon center above the local geometric horizon at maximum. VisibleAtMaximum bool // HasPenumbral 有半影阶段, has penumbral phase. HasPenumbral bool // HasPartial 有偏食阶段, has partial phase. HasPartial bool // HasTotal 有全食阶段, has total phase. HasTotal bool } // LocalLunarEclipseOnDate 当地可见月食查询 / local visible lunar eclipse query. // Determine whether a visible local lunar eclipse occurs on the local date, using Danjon by default. func LocalLunarEclipseOnDate(date time.Time, lon, lat, height float64) (LocalLunarEclipseInfo, bool) { return LocalLunarEclipseOnDateDanjon(date, lon, lat, height) } // LocalLunarEclipseOnDateDanjon 当地可见月食查询(Danjon) / local visible lunar eclipse query with Danjon model. // Determine whether a visible local lunar eclipse occurs on the local date with the Danjon model. func LocalLunarEclipseOnDateDanjon(date time.Time, lon, lat, height float64) (LocalLunarEclipseInfo, bool) { return localLunarEclipseOnDate(date, lon, lat, height, basic.LunarEclipseDanjon, localLunarEclipseQueryVisible) } // LocalLunarEclipseOnDateChauvenet 当地可见月食查询(Chauvenet) / local visible lunar eclipse query with Chauvenet model. // Determine whether a visible local lunar eclipse occurs on the local date with the Chauvenet model. func LocalLunarEclipseOnDateChauvenet(date time.Time, lon, lat, height float64) (LocalLunarEclipseInfo, bool) { return localLunarEclipseOnDate(date, lon, lat, height, basic.LunarEclipseChauvenet, localLunarEclipseQueryVisible) } // GeometricLocalLunarEclipseOnDate 当地几何月食查询 / local geometric lunar eclipse query. // Determine whether a geometric local lunar eclipse occurs on the local date, using Danjon by default. func GeometricLocalLunarEclipseOnDate(date time.Time, lon, lat, height float64) (LocalLunarEclipseInfo, bool) { return GeometricLocalLunarEclipseOnDateDanjon(date, lon, lat, height) } // GeometricLocalLunarEclipseOnDateDanjon 当地几何月食查询(Danjon) / local geometric lunar eclipse query with Danjon model. // Determine whether a geometric local lunar eclipse occurs on the local date with the Danjon model. func GeometricLocalLunarEclipseOnDateDanjon(date time.Time, lon, lat, height float64) (LocalLunarEclipseInfo, bool) { return localLunarEclipseOnDate(date, lon, lat, height, basic.LunarEclipseDanjon, localLunarEclipseQueryGeometric) } // GeometricLocalLunarEclipseOnDateChauvenet 当地几何月食查询(Chauvenet) / local geometric lunar eclipse query with Chauvenet model. // Determine whether a geometric local lunar eclipse occurs on the local date with the Chauvenet model. func GeometricLocalLunarEclipseOnDateChauvenet(date time.Time, lon, lat, height float64) (LocalLunarEclipseInfo, bool) { return localLunarEclipseOnDate(date, lon, lat, height, basic.LunarEclipseChauvenet, localLunarEclipseQueryGeometric) } func localLunarEclipseOnDate( date time.Time, lon, lat, height float64, calculator lunarEclipseCalculator, mode localLunarEclipseQueryMode, ) (LocalLunarEclipseInfo, bool) { location := date.Location() dayStart, dayMid, dayEnd := lunarEclipseLocalDayBounds(date) phaseDiff := moonSunLoDiff(dayMid) if phaseDiff < lunarEclipseDayPhaseMin || phaseDiff > lunarEclipseDayPhaseMax { return LocalLunarEclipseInfo{}, false } candidateTT := basic.CalcMoonSHByJDE(timeToTTJDE(dayMid), 1) if !isPotentialLunarEclipse(candidateTT) { return LocalLunarEclipseInfo{}, false } result := calculator(candidateTT) if result.Type == basic.LunarEclipseNone { return LocalLunarEclipseInfo{}, false } info := localLunarEclipseInfoFromBasic(result, lon, lat, height, location) if !localLunarEclipseOverlapsDate(info, dayStart, dayEnd) { return LocalLunarEclipseInfo{}, false } if mode == localLunarEclipseQueryVisible && !localLunarEclipseVisibleOnDate(info, dayStart, dayEnd) { return LocalLunarEclipseInfo{}, false } return info, true } // LastLocalLunarEclipse 上次可见月食 / previous visible local lunar eclipse. // Previous visible local lunar eclipse, using Danjon by default. func LastLocalLunarEclipse(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo { return LastLocalLunarEclipseDanjon(date, lon, lat, height) } // LastLocalTotalLunarEclipse 上次可见月全食 / previous visible local total lunar eclipse. // Previous visible local total lunar eclipse, using Danjon by default. func LastLocalTotalLunarEclipse(date time.Time, lon, lat, height float64) (LocalLunarEclipseInfo, bool) { return searchLocalTotalLunarEclipse(date, lon, lat, height, -1, true, basic.LunarEclipseDanjon, localLunarEclipseQueryVisible) } // LastLocalLunarEclipseDanjon 上次可见月食(Danjon) / previous visible local lunar eclipse with Danjon model. // Previous visible local lunar eclipse with the Danjon model. func LastLocalLunarEclipseDanjon(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo { info, _ := searchLocalLunarEclipse(date, lon, lat, height, -1, true, basic.LunarEclipseDanjon, localLunarEclipseQueryVisible) return info } // LastLocalLunarEclipseChauvenet 上次可见月食(Chauvenet) / previous visible local lunar eclipse with Chauvenet model. // Previous visible local lunar eclipse with the Chauvenet model. func LastLocalLunarEclipseChauvenet(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo { info, _ := searchLocalLunarEclipse(date, lon, lat, height, -1, true, basic.LunarEclipseChauvenet, localLunarEclipseQueryVisible) return info } // LastGeometricLocalLunarEclipse 上次几何月食 / previous geometric local lunar eclipse. // Previous geometric local lunar eclipse, using Danjon by default. func LastGeometricLocalLunarEclipse(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo { return LastGeometricLocalLunarEclipseDanjon(date, lon, lat, height) } // LastGeometricLocalLunarEclipseDanjon 上次几何月食(Danjon) / previous geometric local lunar eclipse with Danjon model. // Previous geometric local lunar eclipse with the Danjon model. func LastGeometricLocalLunarEclipseDanjon(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo { info, _ := searchLocalLunarEclipse(date, lon, lat, height, -1, true, basic.LunarEclipseDanjon, localLunarEclipseQueryGeometric) return info } // LastGeometricLocalLunarEclipseChauvenet 上次几何月食(Chauvenet) / previous geometric local lunar eclipse with Chauvenet model. // Previous geometric local lunar eclipse with the Chauvenet model. func LastGeometricLocalLunarEclipseChauvenet(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo { info, _ := searchLocalLunarEclipse(date, lon, lat, height, -1, true, basic.LunarEclipseChauvenet, localLunarEclipseQueryGeometric) return info } // NextLocalLunarEclipse 下次可见月食 / next visible local lunar eclipse. // Next visible local lunar eclipse, using Danjon by default. func NextLocalLunarEclipse(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo { return NextLocalLunarEclipseDanjon(date, lon, lat, height) } // NextLocalTotalLunarEclipse 下次可见月全食 / next visible local total lunar eclipse. // Next visible local total lunar eclipse, using Danjon by default. func NextLocalTotalLunarEclipse(date time.Time, lon, lat, height float64) (LocalLunarEclipseInfo, bool) { return searchLocalTotalLunarEclipse(date, lon, lat, height, 1, false, basic.LunarEclipseDanjon, localLunarEclipseQueryVisible) } // NextLocalLunarEclipseDanjon 下次可见月食(Danjon) / next visible local lunar eclipse with Danjon model. // Next visible local lunar eclipse with the Danjon model. func NextLocalLunarEclipseDanjon(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo { info, _ := searchLocalLunarEclipse(date, lon, lat, height, 1, false, basic.LunarEclipseDanjon, localLunarEclipseQueryVisible) return info } // NextLocalLunarEclipseChauvenet 下次可见月食(Chauvenet) / next visible local lunar eclipse with Chauvenet model. // Next visible local lunar eclipse with the Chauvenet model. func NextLocalLunarEclipseChauvenet(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo { info, _ := searchLocalLunarEclipse(date, lon, lat, height, 1, false, basic.LunarEclipseChauvenet, localLunarEclipseQueryVisible) return info } // NextGeometricLocalLunarEclipse 下次几何月食 / next geometric local lunar eclipse. // Next geometric local lunar eclipse, using Danjon by default. func NextGeometricLocalLunarEclipse(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo { return NextGeometricLocalLunarEclipseDanjon(date, lon, lat, height) } // NextGeometricLocalLunarEclipseDanjon 下次几何月食(Danjon) / next geometric local lunar eclipse with Danjon model. // Next geometric local lunar eclipse with the Danjon model. func NextGeometricLocalLunarEclipseDanjon(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo { info, _ := searchLocalLunarEclipse(date, lon, lat, height, 1, false, basic.LunarEclipseDanjon, localLunarEclipseQueryGeometric) return info } // NextGeometricLocalLunarEclipseChauvenet 下次几何月食(Chauvenet) / next geometric local lunar eclipse with Chauvenet model. // Next geometric local lunar eclipse with the Chauvenet model. func NextGeometricLocalLunarEclipseChauvenet(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo { info, _ := searchLocalLunarEclipse(date, lon, lat, height, 1, false, basic.LunarEclipseChauvenet, localLunarEclipseQueryGeometric) return info } // ClosestLocalLunarEclipse 最近一次可见月食 / closest visible local lunar eclipse. // Closest visible local lunar eclipse, using Danjon by default. func ClosestLocalLunarEclipse(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo { return ClosestLocalLunarEclipseDanjon(date, lon, lat, height) } // ClosestLocalTotalLunarEclipse 最近一次可见月全食 / closest visible local total lunar eclipse. // Closest visible local total lunar eclipse, using Danjon by default. func ClosestLocalTotalLunarEclipse(date time.Time, lon, lat, height float64) (LocalLunarEclipseInfo, bool) { last, hasLast := searchLocalTotalLunarEclipse(date, lon, lat, height, -1, true, basic.LunarEclipseDanjon, localLunarEclipseQueryVisible) next, hasNext := searchLocalTotalLunarEclipse(date, lon, lat, height, 1, false, basic.LunarEclipseDanjon, localLunarEclipseQueryVisible) return closestLocalLunarEclipseResult(date, last, hasLast, next, hasNext) } // ClosestLocalLunarEclipseDanjon 最近一次可见月食(Danjon) / closest visible local lunar eclipse with Danjon model. // Closest visible local lunar eclipse with the Danjon model. func ClosestLocalLunarEclipseDanjon(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo { last, hasLast := searchLocalLunarEclipse(date, lon, lat, height, -1, true, basic.LunarEclipseDanjon, localLunarEclipseQueryVisible) next, hasNext := searchLocalLunarEclipse(date, lon, lat, height, 1, false, basic.LunarEclipseDanjon, localLunarEclipseQueryVisible) return closestLocalLunarEclipse(date, last, hasLast, next, hasNext) } // ClosestLocalLunarEclipseChauvenet 最近一次可见月食(Chauvenet) / closest visible local lunar eclipse with Chauvenet model. // Closest visible local lunar eclipse with the Chauvenet model. func ClosestLocalLunarEclipseChauvenet(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo { last, hasLast := searchLocalLunarEclipse(date, lon, lat, height, -1, true, basic.LunarEclipseChauvenet, localLunarEclipseQueryVisible) next, hasNext := searchLocalLunarEclipse(date, lon, lat, height, 1, false, basic.LunarEclipseChauvenet, localLunarEclipseQueryVisible) return closestLocalLunarEclipse(date, last, hasLast, next, hasNext) } // ClosestGeometricLocalLunarEclipse 最近一次几何月食 / closest geometric local lunar eclipse. // Closest geometric local lunar eclipse, using Danjon by default. func ClosestGeometricLocalLunarEclipse(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo { return ClosestGeometricLocalLunarEclipseDanjon(date, lon, lat, height) } // ClosestGeometricLocalLunarEclipseDanjon 最近一次几何月食(Danjon) / closest geometric local lunar eclipse with Danjon model. // Closest geometric local lunar eclipse with the Danjon model. func ClosestGeometricLocalLunarEclipseDanjon(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo { last, hasLast := searchLocalLunarEclipse(date, lon, lat, height, -1, true, basic.LunarEclipseDanjon, localLunarEclipseQueryGeometric) next, hasNext := searchLocalLunarEclipse(date, lon, lat, height, 1, false, basic.LunarEclipseDanjon, localLunarEclipseQueryGeometric) return closestLocalLunarEclipse(date, last, hasLast, next, hasNext) } // ClosestGeometricLocalLunarEclipseChauvenet 最近一次几何月食(Chauvenet) / closest geometric local lunar eclipse with Chauvenet model. // Closest geometric local lunar eclipse with the Chauvenet model. func ClosestGeometricLocalLunarEclipseChauvenet(date time.Time, lon, lat, height float64) LocalLunarEclipseInfo { last, hasLast := searchLocalLunarEclipse(date, lon, lat, height, -1, true, basic.LunarEclipseChauvenet, localLunarEclipseQueryGeometric) next, hasNext := searchLocalLunarEclipse(date, lon, lat, height, 1, false, basic.LunarEclipseChauvenet, localLunarEclipseQueryGeometric) return closestLocalLunarEclipse(date, last, hasLast, next, hasNext) } func closestLocalLunarEclipse( date time.Time, last LocalLunarEclipseInfo, hasLast bool, next LocalLunarEclipseInfo, hasNext bool, ) LocalLunarEclipseInfo { info, _ := closestLocalLunarEclipseResult(date, last, hasLast, next, hasNext) return info } func closestLocalLunarEclipseResult( date time.Time, last LocalLunarEclipseInfo, hasLast bool, next LocalLunarEclipseInfo, hasNext bool, ) (LocalLunarEclipseInfo, bool) { switch { case hasLast && !hasNext: return last, true case !hasLast && hasNext: return next, true case !hasLast && !hasNext: return LocalLunarEclipseInfo{}, false } lastDistance := math.Abs(date.Sub(last.Maximum).Seconds()) nextDistance := math.Abs(next.Maximum.Sub(date).Seconds()) if lastDistance <= nextDistance { return last, true } return next, true } func searchLocalLunarEclipse( date time.Time, lon, lat, height float64, direction int, includeCurrent bool, calculator lunarEclipseCalculator, mode localLunarEclipseQueryMode, ) (LocalLunarEclipseInfo, bool) { targetTT := timeToTTJDE(date) candidateTT := basic.CalcMoonSHByJDE(targetTT, 1) for i := 0; i < localLunarEclipseSearchLimit; i++ { if isPotentialLunarEclipse(candidateTT) { result := calculator(candidateTT) if result.Type != basic.LunarEclipseNone { info := localLunarEclipseInfoFromBasic(result, lon, lat, height, date.Location()) if (mode != localLunarEclipseQueryVisible || localLunarEclipseVisible(info)) && lunarEclipseMatchesDirection(result.Maximum, targetTT, direction, includeCurrent) { return info, true } } } candidateTT = nextEclipseSearchCandidateTT(candidateTT, 1, direction, lunarEclipseSynodicMonthDays) } return LocalLunarEclipseInfo{}, false } func searchLocalTotalLunarEclipse( date time.Time, lon, lat, height float64, direction int, includeCurrent bool, calculator lunarEclipseCalculator, mode localLunarEclipseQueryMode, ) (LocalLunarEclipseInfo, bool) { targetTT := timeToTTJDE(date) candidateTT := basic.CalcMoonSHByJDE(targetTT, 1) for i := 0; i < localLunarEclipseSearchLimit; i++ { if isPotentialLunarEclipse(candidateTT) { result := calculator(candidateTT) if result.HasTotal { info := localLunarEclipseInfoFromBasic(result, lon, lat, height, date.Location()) if (mode != localLunarEclipseQueryVisible || localTotalLunarEclipseVisible(info)) && lunarEclipseMatchesDirection(result.Maximum, targetTT, direction, includeCurrent) { return info, true } } } candidateTT = nextEclipseSearchCandidateTT(candidateTT, 1, direction, lunarEclipseSynodicMonthDays) } return LocalLunarEclipseInfo{}, false } func localLunarEclipseInfoFromBasic( result basic.LunarEclipseResult, lon, lat, height float64, location *time.Location, ) LocalLunarEclipseInfo { maximum := ttJDEToTime(result.Maximum, location) visibleThreshold := localLunarEclipseVisibilityThreshold(height, lat) moonAltitude := lunarAltitude(maximum, lon, lat) saros, hasSaros := lunarSarosInfo(result.Maximum) return LocalLunarEclipseInfo{ HasSaros: hasSaros, Saros: saros, Type: mapBasicLunarEclipseType(result.Type), Longitude: lon, Latitude: lat, Height: height, PenumbralMagnitude: result.PenumbralMagnitude, UmbralMagnitude: result.Magnitude, PenumbralStart: ttJDEToTime(result.PenumbralStart, location), PartialStart: ttJDEToTime(result.PartialStart, location), TotalStart: ttJDEToTime(result.TotalStart, location), Maximum: maximum, TotalEnd: ttJDEToTime(result.TotalEnd, location), PartialEnd: ttJDEToTime(result.PartialEnd, location), PenumbralEnd: ttJDEToTime(result.PenumbralEnd, location), MoonAltitude: moonAltitude, MoonAzimuth: lunarAzimuth(maximum, lon, lat), VisibleAtMaximum: moonAltitude > visibleThreshold, HasPenumbral: result.HasPenumbral, HasPartial: result.HasPartial, HasTotal: result.HasTotal, } } func localLunarEclipseOverlapsDate(info LocalLunarEclipseInfo, dayStart, dayEnd time.Time) bool { eventStart, eventEnd, ok := localLunarEclipseRange(info) if !ok { return false } return !eventEnd.Before(dayStart) && eventStart.Before(dayEnd) } func localLunarEclipseRange(info LocalLunarEclipseInfo) (time.Time, time.Time, bool) { if !info.HasPenumbral { return time.Time{}, time.Time{}, false } return info.PenumbralStart, info.PenumbralEnd, true } func localLunarEclipseVisible(info LocalLunarEclipseInfo) bool { eventStart, eventEnd, ok := localLunarEclipseRange(info) if !ok { return false } return localLunarEclipseVisibleDuring(info, eventStart, eventEnd) } func localTotalLunarEclipseVisible(info LocalLunarEclipseInfo) bool { if !info.HasTotal || info.TotalStart.IsZero() || info.TotalEnd.IsZero() { return false } return localLunarEclipseVisibleDuring(info, info.TotalStart, info.TotalEnd) } func localLunarEclipseVisibleOnDate(info LocalLunarEclipseInfo, dayStart, dayEnd time.Time) bool { eventStart, eventEnd, ok := localLunarEclipseRange(info) if !ok { return false } segmentStart := maxLocalLunarTime(eventStart, dayStart) segmentEnd := minLocalLunarTime(eventEnd, dayEnd) if !segmentStart.Before(segmentEnd) { return false } return localLunarEclipseVisibleDuring(info, segmentStart, segmentEnd) } func localLunarEclipseVisibleDuring(info LocalLunarEclipseInfo, start, end time.Time) bool { if localLunarEclipseAltitudeVisible(start, info) || localLunarEclipseAltitudeVisible(end, info) { return true } for dayStart, _, _ := lunarEclipseLocalDayBounds(start); !dayStart.After(end); dayStart = nextLunarEclipseLocalDayStart(dayStart) { _, culminationSeed, _ := lunarEclipseLocalDayBounds(dayStart) culmination := lunarCulminationTime(culminationSeed, info.Longitude, info.Latitude) if culmination.Before(start) || culmination.After(end) { continue } if localLunarEclipseAltitudeVisible(culmination, info) { return true } } return false } func localLunarEclipseAltitudeVisible(date time.Time, info LocalLunarEclipseInfo) bool { return lunarAltitude(date, info.Longitude, info.Latitude) > localLunarEclipseVisibilityThreshold(info.Height, info.Latitude) } func localLunarEclipseVisibilityThreshold(height, latitude float64) float64 { if height <= 0 { return 0 } return -basic.HeightDegreeByLat(height, latitude) } func maxLocalLunarTime(a, b time.Time) time.Time { if a.After(b) { return a } return b } func minLocalLunarTime(a, b time.Time) time.Time { if a.Before(b) { return a } return b }