package eclipse import ( "math" "time" "b612.me/astro/basic" ) const ( localSolarEclipseSynodicMonthDays = 29.530588853 localSolarEclipseSearchLimit = 6000 localSolarEclipseSearchEpsilonDay = 1e-8 localSolarEclipseLatitudeLimitDeg = 2.0 ) type localSolarEclipseCalculator struct { global func(float64) basic.SolarEclipseResult local func(float64, float64, float64, float64) basic.LocalSolarEclipseResult } type localSolarEclipseQueryMode int const ( localSolarEclipseQueryVisible localSolarEclipseQueryMode = iota localSolarEclipseQueryGeometric ) // LocalSolarEclipseContactPoint 表示站心日食在日面上的接触点方位。 // LocalSolarEclipseContactPoint describes a local solar eclipse contact point on the Sun limb. type LocalSolarEclipseContactPoint struct { // Label 是接触标签,如 C1/C2/C3/C4。 // Label is the contact label, such as C1/C2/C3/C4. Label string // Time 是该接触时刻,保持用户输入时区。 // Time is the contact time, preserving the input timezone. Time time.Time // ContactPositionAngle 是日面接触点位置角,从天球北点起向东量,单位度。 // ContactPositionAngle is the Sun-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 Sun center, in degrees. MoonCenterPositionAngle float64 } // LocalSolarEclipseInfo 站心日食信息, local solar eclipse information. // // 所有时刻字段都保持用户输入的时区。 // 不存在的阶段使用零值 time.Time。 type LocalSolarEclipseInfo struct { // Model 日食月亮半径模型, eclipse lunar radius model. Model SolarEclipseRadiusModel // Type 站心食型, local eclipse type. Type SolarEclipseType // 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 // GreatestEclipse 食甚时刻, greatest eclipse. GreatestEclipse time.Time // PartialStart 偏食始 / 初亏, partial eclipse begins. PartialStart time.Time // PartialEnd 偏食终 / 复圆, partial eclipse ends. PartialEnd time.Time // CentralStart 中心食始;对全食为食既,对环食为环食始, central eclipse begins. CentralStart time.Time // CentralEnd 中心食终;对全食为生光,对环食为环食终, central eclipse ends. CentralEnd time.Time // Magnitude 站心食分, local eclipse magnitude. Magnitude float64 // Obscuration 食甚时太阳视圆面遮蔽率, obscuration at greatest eclipse. Obscuration float64 // Separation 食甚时日月中心角距,单位度, center separation at greatest eclipse in degrees. Separation float64 // SunAltitude 食甚时太阳高度角,单位度, Sun altitude at greatest eclipse in degrees. SunAltitude float64 // SunAzimuth 食甚时太阳方位角,单位度, Sun azimuth at greatest eclipse in degrees. SunAzimuth float64 // VisibleAtGreatest 食甚时太阳中心在地平线上方, Sun center above horizon at greatest eclipse. VisibleAtGreatest bool // ContactPoints 是各接触时刻在日面上的接触点方位。 // ContactPoints are Sun-limb contact position angles at eclipse contacts. ContactPoints []LocalSolarEclipseContactPoint // HasPartial 存在偏食阶段, has partial phase. HasPartial bool // HasCentral 存在中心食阶段, has central phase. HasCentral bool // HasAnnular 存在环食阶段, has annular phase. HasAnnular bool // HasTotal 存在全食阶段, has total phase. HasTotal bool } // LocalSolarEclipseOnDate 当地站心日食查询 / local topocentric solar eclipse query. // Determine whether a visible local solar eclipse overlaps the local date, using NASA bulletin Split-K by default. func LocalSolarEclipseOnDate(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) { return LocalSolarEclipseOnDateNASABulletinSplitK(date, lon, lat, height) } // LocalSolarEclipseOnDateNASABulletinSplitK 当地站心日食查询(NASA bulletin Split-K) / local topocentric solar eclipse query with NASA bulletin Split-K. // Determine whether a visible local solar eclipse overlaps the local date with the NASA bulletin Split-K model. func LocalSolarEclipseOnDateNASABulletinSplitK(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) { return localSolarEclipseOnDate(date, lon, lat, height, localSolarEclipseNASABulletinSplitK, localSolarEclipseQueryVisible) } // LocalSolarEclipseOnDateIAUSingleK 当地站心日食查询(IAU Single-K) / local topocentric solar eclipse query with IAU Single-K. // Determine whether a visible local solar eclipse overlaps the local date with the IAU Single-K model. func LocalSolarEclipseOnDateIAUSingleK(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) { return localSolarEclipseOnDate(date, lon, lat, height, localSolarEclipseIAUSingleK, localSolarEclipseQueryVisible) } // GeometricLocalSolarEclipseOnDate 当地站心几何日食查询 / local geometric solar eclipse query. // Determine whether a geometric local solar eclipse overlaps the local date, using NASA bulletin Split-K by default. func GeometricLocalSolarEclipseOnDate(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) { return GeometricLocalSolarEclipseOnDateNASABulletinSplitK(date, lon, lat, height) } // GeometricLocalSolarEclipseOnDateNASABulletinSplitK 当地站心几何日食查询(NASA bulletin Split-K) / local geometric solar eclipse query with NASA bulletin Split-K. // Determine whether a geometric local solar eclipse overlaps the local date with the NASA bulletin Split-K model. func GeometricLocalSolarEclipseOnDateNASABulletinSplitK(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) { return localSolarEclipseOnDate(date, lon, lat, height, localSolarEclipseNASABulletinSplitK, localSolarEclipseQueryGeometric) } // GeometricLocalSolarEclipseOnDateIAUSingleK 当地站心几何日食查询(IAU Single-K) / local geometric solar eclipse query with IAU Single-K. // Determine whether a geometric local solar eclipse overlaps the local date with the IAU Single-K model. func GeometricLocalSolarEclipseOnDateIAUSingleK(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) { return localSolarEclipseOnDate(date, lon, lat, height, localSolarEclipseIAUSingleK, localSolarEclipseQueryGeometric) } func localSolarEclipseOnDate( date time.Time, lon, lat, height float64, calculator localSolarEclipseCalculator, mode localSolarEclipseQueryMode, ) (LocalSolarEclipseInfo, bool) { location := date.Location() dayStart, dayMid, dayEnd := solarEclipseLocalDayBounds(date) candidateTT := basic.CalcMoonSHByJDE(solarEclipseTimeToTTJDE(dayMid), 0) if !isPotentialLocalSolarEclipse(candidateTT) { return LocalSolarEclipseInfo{}, false } globalResult := calculator.global(candidateTT) if globalResult.Type == basic.SolarEclipseNone { return LocalSolarEclipseInfo{}, false } result := calculator.local(globalResult.GreatestEclipse, lon, lat, height) if result.Type == basic.SolarEclipseNone { return LocalSolarEclipseInfo{}, false } info := localSolarEclipseInfoFromBasic(result, lon, lat, height, location) if !localSolarEclipseOverlapsDate(info, dayStart, dayEnd) { return LocalSolarEclipseInfo{}, false } if mode == localSolarEclipseQueryVisible && !localSolarEclipseVisibleOnDate(info, dayStart, dayEnd) { return LocalSolarEclipseInfo{}, false } return info, true } // LastLocalSolarEclipse 上次站心日食 / previous local solar eclipse. // Previous visible local solar eclipse, using NASA bulletin Split-K by default. func LastLocalSolarEclipse(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo { return LastLocalSolarEclipseNASABulletinSplitK(date, lon, lat, height) } // LastLocalSolarEclipseNASABulletinSplitK 上次站心日食(NASA bulletin Split-K) / previous local solar eclipse with NASA bulletin Split-K. // Previous visible local solar eclipse with the NASA bulletin Split-K model. func LastLocalSolarEclipseNASABulletinSplitK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo { info, _ := searchLocalSolarEclipse(date, lon, lat, height, -1, true, localSolarEclipseNASABulletinSplitK, localSolarEclipseQueryVisible) return info } // LastLocalSolarEclipseIAUSingleK 上次站心日食(IAU Single-K) / previous local solar eclipse with IAU Single-K. // Previous visible local solar eclipse with the IAU Single-K model. func LastLocalSolarEclipseIAUSingleK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo { info, _ := searchLocalSolarEclipse(date, lon, lat, height, -1, true, localSolarEclipseIAUSingleK, localSolarEclipseQueryVisible) return info } // LastGeometricLocalSolarEclipse 上次站心几何日食 / previous geometric local solar eclipse. // Previous geometric local solar eclipse, using NASA bulletin Split-K by default. func LastGeometricLocalSolarEclipse(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo { return LastGeometricLocalSolarEclipseNASABulletinSplitK(date, lon, lat, height) } // LastGeometricLocalSolarEclipseNASABulletinSplitK 上次站心几何日食(NASA bulletin Split-K) / previous geometric local solar eclipse with NASA bulletin Split-K. // Previous geometric local solar eclipse with the NASA bulletin Split-K model. func LastGeometricLocalSolarEclipseNASABulletinSplitK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo { info, _ := searchLocalSolarEclipse(date, lon, lat, height, -1, true, localSolarEclipseNASABulletinSplitK, localSolarEclipseQueryGeometric) return info } // LastGeometricLocalSolarEclipseIAUSingleK 上次站心几何日食(IAU Single-K) / previous geometric local solar eclipse with IAU Single-K. // Previous geometric local solar eclipse with the IAU Single-K model. func LastGeometricLocalSolarEclipseIAUSingleK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo { info, _ := searchLocalSolarEclipse(date, lon, lat, height, -1, true, localSolarEclipseIAUSingleK, localSolarEclipseQueryGeometric) return info } // LastLocalTotalSolarEclipse 上次站心日全食 / previous local total solar eclipse. // Previous visible local total solar eclipse, using NASA bulletin Split-K by default. func LastLocalTotalSolarEclipse(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) { return searchLocalTotalSolarEclipse(date, lon, lat, height, -1, true, localSolarEclipseNASABulletinSplitK, localSolarEclipseQueryVisible) } // LastLocalAnnularSolarEclipse 上次站心日环食 / previous local annular solar eclipse. // Previous visible local annular solar eclipse, using NASA bulletin Split-K by default. func LastLocalAnnularSolarEclipse(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) { return searchLocalAnnularSolarEclipse(date, lon, lat, height, -1, true, localSolarEclipseNASABulletinSplitK, localSolarEclipseQueryVisible) } // NextLocalSolarEclipse 下次站心日食 / next local solar eclipse. // Next visible local solar eclipse, using NASA bulletin Split-K by default. func NextLocalSolarEclipse(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo { return NextLocalSolarEclipseNASABulletinSplitK(date, lon, lat, height) } // NextLocalSolarEclipseNASABulletinSplitK 下次站心日食(NASA bulletin Split-K) / next local solar eclipse with NASA bulletin Split-K. // Next visible local solar eclipse with the NASA bulletin Split-K model. func NextLocalSolarEclipseNASABulletinSplitK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo { info, _ := searchLocalSolarEclipse(date, lon, lat, height, 1, false, localSolarEclipseNASABulletinSplitK, localSolarEclipseQueryVisible) return info } // NextLocalSolarEclipseIAUSingleK 下次站心日食(IAU Single-K) / next local solar eclipse with IAU Single-K. // Next visible local solar eclipse with the IAU Single-K model. func NextLocalSolarEclipseIAUSingleK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo { info, _ := searchLocalSolarEclipse(date, lon, lat, height, 1, false, localSolarEclipseIAUSingleK, localSolarEclipseQueryVisible) return info } // NextGeometricLocalSolarEclipse 下次站心几何日食 / next geometric local solar eclipse. // Next geometric local solar eclipse, using NASA bulletin Split-K by default. func NextGeometricLocalSolarEclipse(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo { return NextGeometricLocalSolarEclipseNASABulletinSplitK(date, lon, lat, height) } // NextGeometricLocalSolarEclipseNASABulletinSplitK 下次站心几何日食(NASA bulletin Split-K) / next geometric local solar eclipse with NASA bulletin Split-K. // Next geometric local solar eclipse with the NASA bulletin Split-K model. func NextGeometricLocalSolarEclipseNASABulletinSplitK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo { info, _ := searchLocalSolarEclipse(date, lon, lat, height, 1, false, localSolarEclipseNASABulletinSplitK, localSolarEclipseQueryGeometric) return info } // NextGeometricLocalSolarEclipseIAUSingleK 下次站心几何日食(IAU Single-K) / next geometric local solar eclipse with IAU Single-K. // Next geometric local solar eclipse with the IAU Single-K model. func NextGeometricLocalSolarEclipseIAUSingleK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo { info, _ := searchLocalSolarEclipse(date, lon, lat, height, 1, false, localSolarEclipseIAUSingleK, localSolarEclipseQueryGeometric) return info } // NextLocalTotalSolarEclipse 下次站心日全食 / next local total solar eclipse. // Next visible local total solar eclipse, using NASA bulletin Split-K by default. func NextLocalTotalSolarEclipse(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) { return searchLocalTotalSolarEclipse(date, lon, lat, height, 1, false, localSolarEclipseNASABulletinSplitK, localSolarEclipseQueryVisible) } // NextLocalAnnularSolarEclipse 下次站心日环食 / next local annular solar eclipse. // Next visible local annular solar eclipse, using NASA bulletin Split-K by default. func NextLocalAnnularSolarEclipse(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) { return searchLocalAnnularSolarEclipse(date, lon, lat, height, 1, false, localSolarEclipseNASABulletinSplitK, localSolarEclipseQueryVisible) } // ClosestLocalSolarEclipse 最近一次站心日食 / closest local solar eclipse. // Closest visible local solar eclipse, using NASA bulletin Split-K by default. func ClosestLocalSolarEclipse(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo { return ClosestLocalSolarEclipseNASABulletinSplitK(date, lon, lat, height) } // ClosestLocalSolarEclipseNASABulletinSplitK 最近一次站心日食(NASA bulletin Split-K) / closest local solar eclipse with NASA bulletin Split-K. // Closest visible local solar eclipse with the NASA bulletin Split-K model. func ClosestLocalSolarEclipseNASABulletinSplitK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo { last, hasLast := searchLocalSolarEclipse(date, lon, lat, height, -1, true, localSolarEclipseNASABulletinSplitK, localSolarEclipseQueryVisible) next, hasNext := searchLocalSolarEclipse(date, lon, lat, height, 1, false, localSolarEclipseNASABulletinSplitK, localSolarEclipseQueryVisible) return closestLocalSolarEclipse(date, last, hasLast, next, hasNext) } // ClosestLocalSolarEclipseIAUSingleK 最近一次站心日食(IAU Single-K) / closest local solar eclipse with IAU Single-K. // Closest visible local solar eclipse with the IAU Single-K model. func ClosestLocalSolarEclipseIAUSingleK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo { last, hasLast := searchLocalSolarEclipse(date, lon, lat, height, -1, true, localSolarEclipseIAUSingleK, localSolarEclipseQueryVisible) next, hasNext := searchLocalSolarEclipse(date, lon, lat, height, 1, false, localSolarEclipseIAUSingleK, localSolarEclipseQueryVisible) return closestLocalSolarEclipse(date, last, hasLast, next, hasNext) } // ClosestGeometricLocalSolarEclipse 最近一次站心几何日食 / closest geometric local solar eclipse. // Closest geometric local solar eclipse, using NASA bulletin Split-K by default. func ClosestGeometricLocalSolarEclipse(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo { return ClosestGeometricLocalSolarEclipseNASABulletinSplitK(date, lon, lat, height) } // ClosestGeometricLocalSolarEclipseNASABulletinSplitK 最近一次站心几何日食(NASA bulletin Split-K) / closest geometric local solar eclipse with NASA bulletin Split-K. // Closest geometric local solar eclipse with the NASA bulletin Split-K model. func ClosestGeometricLocalSolarEclipseNASABulletinSplitK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo { last, hasLast := searchLocalSolarEclipse(date, lon, lat, height, -1, true, localSolarEclipseNASABulletinSplitK, localSolarEclipseQueryGeometric) next, hasNext := searchLocalSolarEclipse(date, lon, lat, height, 1, false, localSolarEclipseNASABulletinSplitK, localSolarEclipseQueryGeometric) return closestLocalSolarEclipse(date, last, hasLast, next, hasNext) } // ClosestGeometricLocalSolarEclipseIAUSingleK 最近一次站心几何日食(IAU Single-K) / closest geometric local solar eclipse with IAU Single-K. // Closest geometric local solar eclipse with the IAU Single-K model. func ClosestGeometricLocalSolarEclipseIAUSingleK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo { last, hasLast := searchLocalSolarEclipse(date, lon, lat, height, -1, true, localSolarEclipseIAUSingleK, localSolarEclipseQueryGeometric) next, hasNext := searchLocalSolarEclipse(date, lon, lat, height, 1, false, localSolarEclipseIAUSingleK, localSolarEclipseQueryGeometric) return closestLocalSolarEclipse(date, last, hasLast, next, hasNext) } // ClosestLocalTotalSolarEclipse 最近一次站心日全食 / closest local total solar eclipse. // Closest visible local total solar eclipse, using NASA bulletin Split-K by default. func ClosestLocalTotalSolarEclipse(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) { last, hasLast := searchLocalTotalSolarEclipse(date, lon, lat, height, -1, true, localSolarEclipseNASABulletinSplitK, localSolarEclipseQueryVisible) next, hasNext := searchLocalTotalSolarEclipse(date, lon, lat, height, 1, false, localSolarEclipseNASABulletinSplitK, localSolarEclipseQueryVisible) return closestLocalSolarEclipseResult(date, last, hasLast, next, hasNext) } // ClosestLocalAnnularSolarEclipse 最近一次站心日环食 / closest local annular solar eclipse. // Closest visible local annular solar eclipse, using NASA bulletin Split-K by default. func ClosestLocalAnnularSolarEclipse(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) { last, hasLast := searchLocalAnnularSolarEclipse(date, lon, lat, height, -1, true, localSolarEclipseNASABulletinSplitK, localSolarEclipseQueryVisible) next, hasNext := searchLocalAnnularSolarEclipse(date, lon, lat, height, 1, false, localSolarEclipseNASABulletinSplitK, localSolarEclipseQueryVisible) return closestLocalSolarEclipseResult(date, last, hasLast, next, hasNext) } func closestLocalSolarEclipse( date time.Time, last LocalSolarEclipseInfo, hasLast bool, next LocalSolarEclipseInfo, hasNext bool, ) LocalSolarEclipseInfo { info, _ := closestLocalSolarEclipseResult(date, last, hasLast, next, hasNext) return info } func closestLocalSolarEclipseResult( date time.Time, last LocalSolarEclipseInfo, hasLast bool, next LocalSolarEclipseInfo, hasNext bool, ) (LocalSolarEclipseInfo, bool) { switch { case hasLast && !hasNext: return last, true case !hasLast && hasNext: return next, true case !hasLast && !hasNext: return LocalSolarEclipseInfo{}, false } lastDistance := math.Abs(date.Sub(last.GreatestEclipse).Seconds()) nextDistance := math.Abs(next.GreatestEclipse.Sub(date).Seconds()) if lastDistance <= nextDistance { return last, true } return next, true } func searchLocalSolarEclipse( date time.Time, lon, lat, height float64, direction int, includeCurrent bool, calculator localSolarEclipseCalculator, mode localSolarEclipseQueryMode, ) (LocalSolarEclipseInfo, bool) { targetTT := solarEclipseTimeToTTJDE(date) candidateTT := basic.CalcMoonSHByJDE(targetTT, 0) for i := 0; i < localSolarEclipseSearchLimit; i++ { if isPotentialLocalSolarEclipse(candidateTT) { globalResult := calculator.global(candidateTT) if globalResult.Type != basic.SolarEclipseNone { result := calculator.local(globalResult.GreatestEclipse, lon, lat, height) if result.Type != basic.SolarEclipseNone { info := localSolarEclipseInfoFromBasic(result, lon, lat, height, date.Location()) if (mode != localSolarEclipseQueryVisible || localSolarEclipseVisible(info)) && localSolarEclipseMatchesDirection(result.GreatestEclipse, targetTT, direction, includeCurrent) { return info, true } } } } candidateTT = nextEclipseSearchCandidateTT(candidateTT, 0, direction, localSolarEclipseSynodicMonthDays) } return LocalSolarEclipseInfo{}, false } func searchLocalTotalSolarEclipse( date time.Time, lon, lat, height float64, direction int, includeCurrent bool, calculator localSolarEclipseCalculator, mode localSolarEclipseQueryMode, ) (LocalSolarEclipseInfo, bool) { targetTT := solarEclipseTimeToTTJDE(date) candidateTT := basic.CalcMoonSHByJDE(targetTT, 0) for i := 0; i < localSolarEclipseSearchLimit; i++ { if isPotentialLocalSolarEclipse(candidateTT) { globalResult := calculator.global(candidateTT) if globalResult.HasTotal || globalResult.HasHybrid { result := calculator.local(globalResult.GreatestEclipse, lon, lat, height) if result.HasTotal { info := localSolarEclipseInfoFromBasic(result, lon, lat, height, date.Location()) if (mode != localSolarEclipseQueryVisible || localCentralSolarEclipseVisible(info)) && localSolarEclipseMatchesDirection(result.GreatestEclipse, targetTT, direction, includeCurrent) { return info, true } } } } candidateTT = nextEclipseSearchCandidateTT(candidateTT, 0, direction, localSolarEclipseSynodicMonthDays) } return LocalSolarEclipseInfo{}, false } func searchLocalAnnularSolarEclipse( date time.Time, lon, lat, height float64, direction int, includeCurrent bool, calculator localSolarEclipseCalculator, mode localSolarEclipseQueryMode, ) (LocalSolarEclipseInfo, bool) { targetTT := solarEclipseTimeToTTJDE(date) candidateTT := basic.CalcMoonSHByJDE(targetTT, 0) for i := 0; i < localSolarEclipseSearchLimit; i++ { if isPotentialLocalSolarEclipse(candidateTT) { globalResult := calculator.global(candidateTT) if globalResult.HasAnnular || globalResult.HasHybrid { result := calculator.local(globalResult.GreatestEclipse, lon, lat, height) if result.HasAnnular && !result.HasTotal { info := localSolarEclipseInfoFromBasic(result, lon, lat, height, date.Location()) if (mode != localSolarEclipseQueryVisible || localCentralSolarEclipseVisible(info)) && localSolarEclipseMatchesDirection(result.GreatestEclipse, targetTT, direction, includeCurrent) { return info, true } } } } candidateTT = nextEclipseSearchCandidateTT(candidateTT, 0, direction, localSolarEclipseSynodicMonthDays) } return LocalSolarEclipseInfo{}, false } func isPotentialLocalSolarEclipse(newMoonTT float64) bool { return math.Abs(basic.HMoonTrueBo(newMoonTT)) <= localSolarEclipseLatitudeLimitDeg } func localSolarEclipseMatchesDirection(greatestTT, targetTT float64, direction int, includeCurrent bool) bool { delta := greatestTT - targetTT if math.Abs(delta) <= localSolarEclipseSearchEpsilonDay { return direction < 0 && includeCurrent } if direction > 0 { return delta > 0 } return delta < 0 } func localSolarEclipseInfoFromBasic( result basic.LocalSolarEclipseResult, lon, lat, height float64, location *time.Location, ) LocalSolarEclipseInfo { info := localSolarEclipseInfoFieldsFromBasic(result, lon, lat, height, location) info.ContactPoints = localSolarEclipseContactPointsFromBasic(result, lon, lat, height, location) return info } func localSolarEclipseInfoFromDiagram( diagram basic.LocalSolarEclipseDiagramResult, lon, lat, height float64, location *time.Location, ) LocalSolarEclipseInfo { info := localSolarEclipseInfoFieldsFromBasic(diagram.Eclipse, lon, lat, height, location) info.ContactPoints = localSolarEclipseContactPointsFromFrames(diagram.Frames, location) return info } func localSolarEclipseInfoFieldsFromBasic( result basic.LocalSolarEclipseResult, lon, lat, height float64, location *time.Location, ) LocalSolarEclipseInfo { visibleThreshold := localSolarEclipseVisibilityThreshold(height, lat) saros, hasSaros := solarSarosInfo(result.GreatestEclipse) return LocalSolarEclipseInfo{ Model: mapBasicSolarEclipseModel(result.Model), Type: mapBasicSolarEclipseType(result.Type), HasSaros: hasSaros, Saros: saros, Longitude: lon, Latitude: lat, Height: height, GreatestEclipse: solarEclipseTTJDEToTime(result.GreatestEclipse, location), PartialStart: solarEclipseTTJDEToTime(result.PartialStart, location), PartialEnd: solarEclipseTTJDEToTime(result.PartialEnd, location), CentralStart: solarEclipseTTJDEToTime(result.CentralStart, location), CentralEnd: solarEclipseTTJDEToTime(result.CentralEnd, location), Magnitude: result.Magnitude, Obscuration: result.Obscuration, Separation: result.Separation, SunAltitude: result.SunAltitude, SunAzimuth: result.SunAzimuth, VisibleAtGreatest: result.SunAltitude > visibleThreshold, HasPartial: result.HasPartial, HasCentral: result.HasCentral, HasAnnular: result.HasAnnular, HasTotal: result.HasTotal, } } func localSolarEclipseContactPointsFromBasic( result basic.LocalSolarEclipseResult, lon, lat, height float64, location *time.Location, ) []LocalSolarEclipseContactPoint { if !result.HasPartial { return nil } options := basic.LocalSolarEclipseDiagramOptions{StepDays: 1} var diagram basic.LocalSolarEclipseDiagramResult if result.Model == basic.SolarEclipseModelIAUSingleK { diagram = basic.LocalSolarEclipseDiagramIAUSingleK(result.GreatestEclipse, lon, lat, height, options) } else { diagram = basic.LocalSolarEclipseDiagramNASABulletinSplitK(result.GreatestEclipse, lon, lat, height, options) } return localSolarEclipseContactPointsFromFrames(diagram.Frames, location) } func localSolarEclipseContactPointsFromFrames( frames []basic.LocalSolarEclipseDiagramFrame, location *time.Location, ) []LocalSolarEclipseContactPoint { contacts := make([]LocalSolarEclipseContactPoint, 0, 4) for _, frame := range frames { for _, label := range localSolarEclipseFrameLabels(frame) { switch label { case "C1", "C2", "C3", "C4": contactPA := frame.PositionAngle if (label == "C2" || label == "C3") && frame.MoonRadius >= frame.SunRadius { contactPA = normalizeSolarEclipseDegree360(contactPA + 180) } contacts = append(contacts, LocalSolarEclipseContactPoint{ Label: label, Time: solarEclipseTTJDEToTime(frame.JDE, location), ContactPositionAngle: contactPA, ContactClockwiseAngle: normalizeSolarEclipseDegree360(360 - contactPA), MoonCenterPositionAngle: frame.PositionAngle, }) } } } return contacts } func localSolarEclipseFrameLabels(frame basic.LocalSolarEclipseDiagramFrame) []string { if len(frame.Labels) > 0 { return frame.Labels } if frame.Label == "" { return nil } return []string{frame.Label} } func localSolarEclipseOverlapsDate(info LocalSolarEclipseInfo, dayStart, dayEnd time.Time) bool { eventStart, eventEnd, ok := localSolarEclipseRange(info) if !ok { return false } return !eventEnd.Before(dayStart) && eventStart.Before(dayEnd) } func localSolarEclipseRange(info LocalSolarEclipseInfo) (time.Time, time.Time, bool) { if !info.HasPartial { return time.Time{}, time.Time{}, false } return info.PartialStart, info.PartialEnd, true } func localSolarEclipseVisible(info LocalSolarEclipseInfo) bool { eventStart, eventEnd, ok := localSolarEclipseRange(info) if !ok { return false } return localSolarEclipseVisibleDuring(info, eventStart, eventEnd) } func localCentralSolarEclipseVisible(info LocalSolarEclipseInfo) bool { if !info.HasCentral || info.CentralStart.IsZero() || info.CentralEnd.IsZero() { return false } return localSolarEclipseVisibleDuring(info, info.CentralStart, info.CentralEnd) } func localSolarEclipseVisibleOnDate(info LocalSolarEclipseInfo, dayStart, dayEnd time.Time) bool { eventStart, eventEnd, ok := localSolarEclipseRange(info) if !ok { return false } segmentStart := maxLocalSolarTime(eventStart, dayStart) segmentEnd := minLocalSolarTime(eventEnd, dayEnd) if !segmentStart.Before(segmentEnd) { return false } return localSolarEclipseVisibleDuring(info, segmentStart, segmentEnd) } func localSolarEclipseVisibleDuring(info LocalSolarEclipseInfo, start, end time.Time) bool { if !start.Before(end) && !start.Equal(end) { return false } if localSolarEclipseAltitudeVisible(start, info) || localSolarEclipseAltitudeVisible(end, info) { return true } for dayStart, _, _ := solarEclipseLocalDayBounds(start); !dayStart.After(end); dayStart = nextSolarEclipseLocalDayStart(dayStart) { _, culminationSeed, _ := solarEclipseLocalDayBounds(dayStart) culmination := solarCulminationTime(culminationSeed, info.Longitude) if culmination.Before(start) || culmination.After(end) { continue } if localSolarEclipseAltitudeVisible(culmination, info) { return true } } return false } func localSolarEclipseAltitudeVisible(date time.Time, info LocalSolarEclipseInfo) bool { return solarAltitude(date, info.Longitude, info.Latitude) > localSolarEclipseVisibilityThreshold(info.Height, info.Latitude) } func localSolarEclipseVisibilityThreshold(height, latitude float64) float64 { if height <= 0 { return 0 } return -basic.HeightDegreeByLat(height, latitude) } func normalizeSolarEclipseDegree360(angle float64) float64 { angle = math.Mod(angle, 360) if angle < 0 { angle += 360 } return angle } func maxLocalSolarTime(a, b time.Time) time.Time { if a.After(b) { return a } return b } func minLocalSolarTime(a, b time.Time) time.Time { if a.Before(b) { return a } return b } var ( localSolarEclipseNASABulletinSplitK = localSolarEclipseCalculator{ global: basic.SolarEclipseNASABulletinSplitK, local: basic.LocalSolarEclipseNASABulletinSplitK, } localSolarEclipseIAUSingleK = localSolarEclipseCalculator{ global: basic.SolarEclipseIAUSingleK, local: basic.LocalSolarEclipseIAUSingleK, } )