package eclipse import ( "math" "time" "b612.me/astro/basic" ) const ( solarEclipseSynodicMonthDays = 29.530588853 solarEclipseSearchLimit = 36 solarEclipseSearchEpsilonDay = 1e-8 solarEclipseLatitudeLimitDeg = 2.0 ) type solarEclipseCalculator func(float64) basic.SolarEclipseResult // SolarEclipseRadiusModel 日食月亮半径模型, lunar radius model for solar eclipses. type SolarEclipseRadiusModel string const ( // SolarEclipseModelIAUSingleK IAU 单一 k 值模型, IAU single-k model. SolarEclipseModelIAUSingleK SolarEclipseRadiusModel = "iau_single_k" // SolarEclipseModelNASABulletinSplitK NASA bulletin 分裂 k 值模型, NASA bulletin split-k model. SolarEclipseModelNASABulletinSplitK SolarEclipseRadiusModel = "nasa_bulletin_split_k" ) // SolarEclipseType 全局日食食型, global solar eclipse type. type SolarEclipseType string const ( // SolarEclipseNone 无日食, no solar eclipse. SolarEclipseNone SolarEclipseType = "none" // SolarEclipsePartial 日偏食, partial solar eclipse. SolarEclipsePartial SolarEclipseType = "partial" // SolarEclipseAnnular 日环食, annular solar eclipse. SolarEclipseAnnular SolarEclipseType = "annular" // SolarEclipseTotal 日全食, total solar eclipse. SolarEclipseTotal SolarEclipseType = "total" // SolarEclipseHybrid 全环食, hybrid solar eclipse. SolarEclipseHybrid SolarEclipseType = "hybrid" ) // SolarEclipseCentrality 中心线进入地球的方式, global eclipse centrality. type SolarEclipseCentrality string const ( // SolarEclipseNonCentral 非中心食, non-central eclipse. SolarEclipseNonCentral SolarEclipseCentrality = "non_central" // SolarEclipseCentralOneLimit 单界中心食, central eclipse with one limit. SolarEclipseCentralOneLimit SolarEclipseCentrality = "central_one_limit" // SolarEclipseCentralTwoLimits 双界中心食, central eclipse with two limits. SolarEclipseCentralTwoLimits SolarEclipseCentrality = "central_two_limits" ) // SolarEclipseInfo 全局日食信息, global solar eclipse information. // // 所有时刻字段都保持用户输入的时区。 // 不存在的阶段使用零值 time.Time。 type SolarEclipseInfo struct { // Model 日食月亮半径模型, eclipse lunar radius model. Model SolarEclipseRadiusModel // Type 全局食型, global eclipse type. Type SolarEclipseType // Centrality 中心性, eclipse centrality. Centrality SolarEclipseCentrality // 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 // GreatestEclipse 食甚时刻, greatest eclipse. GreatestEclipse time.Time // PartialBeginOnEarth 地球范围偏食始, partial eclipse begins on Earth. PartialBeginOnEarth time.Time // PartialEndOnEarth 地球范围偏食终, partial eclipse ends on Earth. PartialEndOnEarth time.Time // CentralBeginOnEarth 地球范围中心食始, central eclipse begins on Earth. CentralBeginOnEarth time.Time // CentralEndOnEarth 地球范围中心食终, central eclipse ends on Earth. CentralEndOnEarth time.Time // Magnitude 全局食分, global eclipse magnitude. Magnitude float64 // Gamma 食甚时影轴到地心的距离, gamma at greatest eclipse. Gamma float64 // PathWidthKM 食甚处中心食带宽度, central path width at greatest eclipse. PathWidthKM float64 // GreatestLongitude 食甚点经度,东正西负, longitude of greatest eclipse, east positive. GreatestLongitude float64 // GreatestLatitude 食甚点纬度,北正南负, latitude of greatest eclipse, north positive. GreatestLatitude float64 // HasPartial 存在偏食阶段, has partial phase. HasPartial bool // HasCentral 存在中心食阶段, has central phase. HasCentral bool // HasAnnular 存在环食阶段, has annular phase. HasAnnular bool // HasTotal 存在全食阶段, has total phase. HasTotal bool // HasHybrid 为混合食, is hybrid eclipse. HasHybrid bool } // SolarEclipseOnDate 当地自然日全局日食查询 / local-date global solar eclipse query. // Determine whether a global solar eclipse overlaps the local date, using NASA bulletin Split-K by default. func SolarEclipseOnDate(date time.Time) (SolarEclipseInfo, bool) { return SolarEclipseOnDateNASABulletinSplitK(date) } // SolarEclipseOnDateNASABulletinSplitK 当地自然日全局日食查询(NASA bulletin Split-K) / local-date global solar eclipse query with NASA bulletin Split-K. // Determine whether a global solar eclipse overlaps the local date with the NASA bulletin Split-K model. func SolarEclipseOnDateNASABulletinSplitK(date time.Time) (SolarEclipseInfo, bool) { return solarEclipseOnDate(date, basic.SolarEclipseNASABulletinSplitK) } // SolarEclipseOnDateIAUSingleK 当地自然日全局日食查询(IAU Single-K) / local-date global solar eclipse query with IAU Single-K. // Determine whether a global solar eclipse overlaps the local date with the IAU Single-K model. func SolarEclipseOnDateIAUSingleK(date time.Time) (SolarEclipseInfo, bool) { return solarEclipseOnDate(date, basic.SolarEclipseIAUSingleK) } func solarEclipseOnDate(date time.Time, calculator solarEclipseCalculator) (SolarEclipseInfo, bool) { location := date.Location() dayStart, dayMid, dayEnd := solarEclipseLocalDayBounds(date) candidateTT := basic.CalcMoonSHByJDE(solarEclipseTimeToTTJDE(dayMid), 0) result := calculator(candidateTT) if result.Type == basic.SolarEclipseNone { return SolarEclipseInfo{}, false } info := solarEclipseInfoFromBasic(result, location) if !solarEclipseOverlapsDate(info, dayStart, dayEnd) { return SolarEclipseInfo{}, false } return info, true } // LastSolarEclipse 上次日食 / previous solar eclipse. // Previous solar eclipse, using NASA bulletin Split-K by default. func LastSolarEclipse(date time.Time) SolarEclipseInfo { return LastSolarEclipseNASABulletinSplitK(date) } // LastSolarEclipseNASABulletinSplitK 上次日食(NASA bulletin Split-K) / previous solar eclipse with NASA bulletin Split-K. // Previous solar eclipse with the NASA bulletin Split-K model. func LastSolarEclipseNASABulletinSplitK(date time.Time) SolarEclipseInfo { info, _ := searchSolarEclipse(date, -1, true, basic.SolarEclipseNASABulletinSplitK) return info } // LastSolarEclipseIAUSingleK 上次日食(IAU Single-K) / previous solar eclipse with IAU Single-K. // Previous solar eclipse with the IAU Single-K model. func LastSolarEclipseIAUSingleK(date time.Time) SolarEclipseInfo { info, _ := searchSolarEclipse(date, -1, true, basic.SolarEclipseIAUSingleK) return info } // NextSolarEclipse 下次日食 / next solar eclipse. // Next solar eclipse, using NASA bulletin Split-K by default. func NextSolarEclipse(date time.Time) SolarEclipseInfo { return NextSolarEclipseNASABulletinSplitK(date) } // NextSolarEclipseNASABulletinSplitK 下次日食(NASA bulletin Split-K) / next solar eclipse with NASA bulletin Split-K. // Next solar eclipse with the NASA bulletin Split-K model. func NextSolarEclipseNASABulletinSplitK(date time.Time) SolarEclipseInfo { info, _ := searchSolarEclipse(date, 1, false, basic.SolarEclipseNASABulletinSplitK) return info } // NextSolarEclipseIAUSingleK 下次日食(IAU Single-K) / next solar eclipse with IAU Single-K. // Next solar eclipse with the IAU Single-K model. func NextSolarEclipseIAUSingleK(date time.Time) SolarEclipseInfo { info, _ := searchSolarEclipse(date, 1, false, basic.SolarEclipseIAUSingleK) return info } // ClosestSolarEclipse 最近一次日食 / closest solar eclipse. // Closest solar eclipse, using NASA bulletin Split-K by default. func ClosestSolarEclipse(date time.Time) SolarEclipseInfo { return ClosestSolarEclipseNASABulletinSplitK(date) } // ClosestSolarEclipseNASABulletinSplitK 最近一次日食(NASA bulletin Split-K) / closest solar eclipse with NASA bulletin Split-K. // Closest solar eclipse with the NASA bulletin Split-K model. func ClosestSolarEclipseNASABulletinSplitK(date time.Time) SolarEclipseInfo { last, hasLast := searchSolarEclipse(date, -1, true, basic.SolarEclipseNASABulletinSplitK) next, hasNext := searchSolarEclipse(date, 1, false, basic.SolarEclipseNASABulletinSplitK) return closestSolarEclipse(date, last, hasLast, next, hasNext) } // ClosestSolarEclipseIAUSingleK 最近一次日食(IAU Single-K) / closest solar eclipse with IAU Single-K. // Closest solar eclipse with the IAU Single-K model. func ClosestSolarEclipseIAUSingleK(date time.Time) SolarEclipseInfo { last, hasLast := searchSolarEclipse(date, -1, true, basic.SolarEclipseIAUSingleK) next, hasNext := searchSolarEclipse(date, 1, false, basic.SolarEclipseIAUSingleK) return closestSolarEclipse(date, last, hasLast, next, hasNext) } func closestSolarEclipse( date time.Time, last SolarEclipseInfo, hasLast bool, next SolarEclipseInfo, hasNext bool, ) SolarEclipseInfo { switch { case hasLast && !hasNext: return last case !hasLast && hasNext: return next case !hasLast && !hasNext: return SolarEclipseInfo{} } lastDistance := math.Abs(date.Sub(last.GreatestEclipse).Seconds()) nextDistance := math.Abs(next.GreatestEclipse.Sub(date).Seconds()) if lastDistance <= nextDistance { return last } return next } func searchSolarEclipse( date time.Time, direction int, includeCurrent bool, calculator solarEclipseCalculator, ) (SolarEclipseInfo, bool) { targetTT := solarEclipseTimeToTTJDE(date) candidateTT := basic.CalcMoonSHByJDE(targetTT, 0) for i := 0; i < solarEclipseSearchLimit; i++ { if isPotentialSolarEclipse(candidateTT) { result := calculator(candidateTT) if result.Type != basic.SolarEclipseNone && solarEclipseMatchesDirection(result.GreatestEclipse, targetTT, direction, includeCurrent) { return solarEclipseInfoFromBasic(result, date.Location()), true } } candidateTT = nextEclipseSearchCandidateTT(candidateTT, 0, direction, solarEclipseSynodicMonthDays) } return SolarEclipseInfo{}, false } func isPotentialSolarEclipse(newMoonTT float64) bool { return math.Abs(basic.HMoonTrueBo(newMoonTT)) <= solarEclipseLatitudeLimitDeg } func solarEclipseMatchesDirection(greatestTT, targetTT float64, direction int, includeCurrent bool) bool { delta := greatestTT - targetTT if math.Abs(delta) <= solarEclipseSearchEpsilonDay { return direction < 0 && includeCurrent } if direction > 0 { return delta > 0 } return delta < 0 } func solarEclipseLocalDayBounds(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 nextSolarEclipseLocalDayStart(dayStart time.Time) time.Time { location := dayStart.Location() return time.Date(dayStart.Year(), dayStart.Month(), dayStart.Day()+1, 0, 0, 0, 0, location) } func solarEclipseInfoFromBasic(result basic.SolarEclipseResult, location *time.Location) SolarEclipseInfo { saros, hasSaros := solarSarosInfo(result.GreatestEclipse) return SolarEclipseInfo{ Model: mapBasicSolarEclipseModel(result.Model), Type: mapBasicSolarEclipseType(result.Type), Centrality: mapBasicSolarEclipseCentrality(result.Centrality), HasSaros: hasSaros, Saros: saros, GreatestEclipse: solarEclipseTTJDEToTime(result.GreatestEclipse, location), PartialBeginOnEarth: solarEclipseTTJDEToTime(result.PartialBeginOnEarth, location), PartialEndOnEarth: solarEclipseTTJDEToTime(result.PartialEndOnEarth, location), CentralBeginOnEarth: solarEclipseTTJDEToTime(result.CentralBeginOnEarth, location), CentralEndOnEarth: solarEclipseTTJDEToTime(result.CentralEndOnEarth, location), Magnitude: result.Magnitude, Gamma: result.Gamma, PathWidthKM: result.PathWidthKM, GreatestLongitude: result.GreatestLongitude, GreatestLatitude: result.GreatestLatitude, HasPartial: result.HasPartial, HasCentral: result.HasCentral, HasAnnular: result.HasAnnular, HasTotal: result.HasTotal, HasHybrid: result.HasHybrid, } } func mapBasicSolarEclipseModel(model basic.SolarEclipseRadiusModel) SolarEclipseRadiusModel { switch model { case basic.SolarEclipseModelIAUSingleK: return SolarEclipseModelIAUSingleK default: return SolarEclipseModelNASABulletinSplitK } } func mapBasicSolarEclipseType(eclipseType basic.SolarEclipseType) SolarEclipseType { switch eclipseType { case basic.SolarEclipsePartial: return SolarEclipsePartial case basic.SolarEclipseAnnular: return SolarEclipseAnnular case basic.SolarEclipseTotal: return SolarEclipseTotal case basic.SolarEclipseHybrid: return SolarEclipseHybrid default: return SolarEclipseNone } } func mapBasicSolarEclipseCentrality(centrality basic.SolarEclipseCentrality) SolarEclipseCentrality { switch centrality { case basic.SolarEclipseCentralOneLimit: return SolarEclipseCentralOneLimit case basic.SolarEclipseCentralTwoLimits: return SolarEclipseCentralTwoLimits default: return SolarEclipseNonCentral } } func solarEclipseOverlapsDate(info SolarEclipseInfo, dayStart, dayEnd time.Time) bool { eventStart, eventEnd, ok := solarEclipseRange(info) if !ok { return false } return !eventEnd.Before(dayStart) && eventStart.Before(dayEnd) } func solarEclipseRange(info SolarEclipseInfo) (time.Time, time.Time, bool) { if !info.HasPartial { return time.Time{}, time.Time{}, false } return info.PartialBeginOnEarth, info.PartialEndOnEarth, true } func solarEclipseTTJDEToTime(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 solarEclipseTimeToTTJDE(date time.Time) float64 { utcJDE := basic.Date2JDE(date.UTC()) return basic.TD2UT(utcJDE, true) }