2026-05-01 22:38:44 +08:00
|
|
|
|
package eclipse
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"math"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"b612.me/astro/basic"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
|
solarEclipseSynodicMonthDays = 29.530588853
|
|
|
|
|
|
solarEclipseSearchLimit = 36
|
|
|
|
|
|
solarEclipseSearchEpsilonDay = 1e-8
|
2026-05-03 19:00:08 +08:00
|
|
|
|
solarEclipseLatitudeLimitDeg = 2.0
|
2026-05-01 22:38:44 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
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"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-09-23 18:55:12 +08:00
|
|
|
|
// SolarEclipseSunRadiusModel 日食太阳半径口径, solar radius convention for solar eclipses.
|
|
|
|
|
|
type SolarEclipseSunRadiusModel = basic.SolarEclipseSunRadiusModel
|
|
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
|
// SolarEclipseSunRadiusStandard 标准档,1 AU 处 959.639″, standard solar radius for catalogue reproduction.
|
|
|
|
|
|
SolarEclipseSunRadiusStandard = basic.SolarEclipseSunRadiusStandard
|
|
|
|
|
|
// SolarEclipseSunRadiusMeasured 边缘档,1 AU 处 959.95″, measured eclipse solar radius.
|
|
|
|
|
|
SolarEclipseSunRadiusMeasured = basic.SolarEclipseSunRadiusMeasured
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// SolarEclipseOptions 日食搜索的半径口径, radius conventions for a solar eclipse search.
|
|
|
|
|
|
type SolarEclipseOptions struct {
|
|
|
|
|
|
// RadiusModel 月亮半径模型,非 IAU Single-K 一律按 NASA bulletin Split-K, lunar radius model.
|
|
|
|
|
|
RadiusModel SolarEclipseRadiusModel
|
|
|
|
|
|
// SunRadiusModel 太阳半径口径,零值为标准档, solar radius convention.
|
|
|
|
|
|
SunRadiusModel SolarEclipseSunRadiusModel
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 22:38:44 +08:00
|
|
|
|
// 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
|
2026-09-23 18:55:12 +08:00
|
|
|
|
// SunRadiusModel 日食太阳半径口径, solar radius convention.
|
|
|
|
|
|
SunRadiusModel SolarEclipseSunRadiusModel
|
2026-05-01 22:38:44 +08:00
|
|
|
|
// Type 全局食型, global eclipse type.
|
|
|
|
|
|
Type SolarEclipseType
|
|
|
|
|
|
// Centrality 中心性, eclipse centrality.
|
|
|
|
|
|
Centrality SolarEclipseCentrality
|
2026-09-17 12:27:40 +08:00
|
|
|
|
// HasSaros 存在沙罗序列信息(可能是锚点外推结果), has Saros series metadata (possibly extrapolated).
|
2026-05-01 22:38:44 +08:00
|
|
|
|
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
|
2026-09-23 18:55:12 +08:00
|
|
|
|
// GreatestJDE 是食甚的力学时儒略日;与 DeltaTSeconds 配对即可复现上列民用时刻。
|
|
|
|
|
|
// GreatestJDE is the TT Julian ephemeris day of greatest eclipse; with DeltaTSeconds it reproduces the civil times above.
|
|
|
|
|
|
GreatestJDE float64
|
|
|
|
|
|
// DeltaTSeconds 是本次计算实际使用的 TT−UT1,单位秒。
|
|
|
|
|
|
// DeltaTSeconds is the TT−UT1 used by this computation, in seconds.
|
|
|
|
|
|
DeltaTSeconds float64
|
2026-05-01 22:38:44 +08:00
|
|
|
|
|
|
|
|
|
|
// Magnitude 全局食分, global eclipse magnitude.
|
|
|
|
|
|
Magnitude float64
|
|
|
|
|
|
// Gamma 食甚时影轴到地心的距离, gamma at greatest eclipse.
|
|
|
|
|
|
Gamma float64
|
2026-09-17 12:27:40 +08:00
|
|
|
|
// CentralDuration 是食甚点的中心食持续时间,即日食目录(如 NASA「Central Dur.」)
|
|
|
|
|
|
// 的取值口径;沿整条中心线的最大值见 SolarEclipsePath.MaxCentralDuration。
|
|
|
|
|
|
// CentralDuration is the central-phase duration at the greatest eclipse, the
|
|
|
|
|
|
// value catalogues publish; SolarEclipsePath.MaxCentralDuration is the maximum
|
|
|
|
|
|
// along the whole track.
|
|
|
|
|
|
CentralDuration time.Duration
|
2026-09-23 18:55:12 +08:00
|
|
|
|
// PathWidthKM 是食甚处中心食带宽度;非中心食为 0;单侧极限(中心带仅触及地球边缘)时该解析式失效
|
|
|
|
|
|
// 并一并置 0,此时 PathWidthDefined 为 false,NASA 目录该栏印 '-'。
|
|
|
|
|
|
// PathWidthKM is the central path width at greatest eclipse, 0 for a non-central
|
|
|
|
|
|
// event, and 0 when the analytic formula fails at a single-sided limit where the
|
|
|
|
|
|
// band only grazes the Earth's limb; PathWidthDefined is false there and
|
|
|
|
|
|
// catalogues print '-' for this column.
|
2026-05-01 22:38:44 +08:00
|
|
|
|
PathWidthKM float64
|
2026-09-23 18:55:12 +08:00
|
|
|
|
// PathWidthDefined 表示上面的带宽是否有定义:只有南北两限都存在时才为 true。
|
|
|
|
|
|
// PathWidthDefined reports whether the width above is defined: it is true only
|
|
|
|
|
|
// when both band limits exist.
|
|
|
|
|
|
PathWidthDefined bool
|
2026-05-01 22:38:44 +08:00
|
|
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-23 18:55:12 +08:00
|
|
|
|
// SolarEclipseOnDateWithOptions 当地自然日全局日食查询(自定义半径口径) / local-date global solar eclipse query with custom radius conventions.
|
|
|
|
|
|
func SolarEclipseOnDateWithOptions(date time.Time, options SolarEclipseOptions) (SolarEclipseInfo, bool) {
|
|
|
|
|
|
return solarEclipseOnDate(date, solarEclipseCalculatorFor(options))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// LastSolarEclipseWithOptions 上次日食(自定义半径口径) / previous solar eclipse with custom radius conventions.
|
|
|
|
|
|
func LastSolarEclipseWithOptions(date time.Time, options SolarEclipseOptions) SolarEclipseInfo {
|
|
|
|
|
|
info, _ := searchSolarEclipse(date, -1, true, solarEclipseCalculatorFor(options))
|
|
|
|
|
|
return info
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// NextSolarEclipseWithOptions 下次日食(自定义半径口径) / next solar eclipse with custom radius conventions.
|
|
|
|
|
|
func NextSolarEclipseWithOptions(date time.Time, options SolarEclipseOptions) SolarEclipseInfo {
|
|
|
|
|
|
info, _ := searchSolarEclipse(date, 1, false, solarEclipseCalculatorFor(options))
|
|
|
|
|
|
return info
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ClosestSolarEclipseWithOptions 最近一次日食(自定义半径口径) / closest solar eclipse with custom radius conventions.
|
|
|
|
|
|
func ClosestSolarEclipseWithOptions(date time.Time, options SolarEclipseOptions) SolarEclipseInfo {
|
|
|
|
|
|
last, hasLast := searchSolarEclipse(date, -1, true, solarEclipseCalculatorFor(options))
|
|
|
|
|
|
next, hasNext := searchSolarEclipse(date, 1, false, solarEclipseCalculatorFor(options))
|
|
|
|
|
|
return closestSolarEclipse(date, last, hasLast, next, hasNext)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func solarEclipseCalculatorFor(options SolarEclipseOptions) solarEclipseCalculator {
|
|
|
|
|
|
basicOptions := basic.SolarEclipseOptions{
|
|
|
|
|
|
RadiusModel: basic.SolarEclipseRadiusModel(options.RadiusModel),
|
|
|
|
|
|
SunRadiusModel: basic.SolarEclipseSunRadiusModel(options.SunRadiusModel),
|
|
|
|
|
|
}
|
|
|
|
|
|
if basicOptions.RadiusModel != basic.SolarEclipseModelIAUSingleK {
|
|
|
|
|
|
basicOptions.RadiusModel = basic.SolarEclipseModelNASABulletinSplitK
|
|
|
|
|
|
}
|
|
|
|
|
|
return func(seed float64) basic.SolarEclipseResult {
|
|
|
|
|
|
return basic.SolarEclipseWithOptions(seed, basicOptions)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 22:38:44 +08:00
|
|
|
|
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++ {
|
2026-05-03 19:00:08 +08:00
|
|
|
|
if isPotentialSolarEclipse(candidateTT) {
|
|
|
|
|
|
result := calculator(candidateTT)
|
|
|
|
|
|
if result.Type != basic.SolarEclipseNone && solarEclipseMatchesDirection(result.GreatestEclipse, targetTT, direction, includeCurrent) {
|
|
|
|
|
|
return solarEclipseInfoFromBasic(result, date.Location()), true
|
|
|
|
|
|
}
|
2026-05-01 22:38:44 +08:00
|
|
|
|
}
|
2026-05-03 19:00:08 +08:00
|
|
|
|
candidateTT = nextEclipseSearchCandidateTT(candidateTT, 0, direction, solarEclipseSynodicMonthDays)
|
2026-05-01 22:38:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return SolarEclipseInfo{}, false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-03 19:00:08 +08:00
|
|
|
|
func isPotentialSolarEclipse(newMoonTT float64) bool {
|
|
|
|
|
|
return math.Abs(basic.HMoonTrueBo(newMoonTT)) <= solarEclipseLatitudeLimitDeg
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 22:38:44 +08:00
|
|
|
|
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),
|
2026-09-23 18:55:12 +08:00
|
|
|
|
SunRadiusModel: result.SunRadiusModel,
|
2026-05-01 22:38:44 +08:00
|
|
|
|
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),
|
2026-09-23 18:55:12 +08:00
|
|
|
|
GreatestJDE: result.GreatestEclipse,
|
|
|
|
|
|
DeltaTSeconds: basic.DeltaT(result.GreatestEclipse, true),
|
2026-05-01 22:38:44 +08:00
|
|
|
|
Magnitude: result.Magnitude,
|
|
|
|
|
|
Gamma: result.Gamma,
|
2026-09-17 12:27:40 +08:00
|
|
|
|
CentralDuration: solarEclipseDurationFromDays(result.CentralDurationDays),
|
2026-05-01 22:38:44 +08:00
|
|
|
|
PathWidthKM: result.PathWidthKM,
|
2026-09-23 18:55:12 +08:00
|
|
|
|
PathWidthDefined: result.PathWidthDefined,
|
2026-05-01 22:38:44 +08:00
|
|
|
|
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 {
|
2026-09-23 18:55:12 +08:00
|
|
|
|
// 回填的是民用时刻,偏移必须与正向的 UTC2TT 成对(窗口内即闰秒表,窗口外按未来政策)。
|
|
|
|
|
|
// 用 ΔT(TT−UT1)会让时刻整体偏一个 DUT1,并破坏等时线网格的整刻度对齐。
|
2026-09-17 12:27:40 +08:00
|
|
|
|
if ttJDE == 0 || math.IsNaN(ttJDE) || math.IsInf(ttJDE, 0) {
|
2026-05-01 22:38:44 +08:00
|
|
|
|
return time.Time{}
|
|
|
|
|
|
}
|
2026-09-23 18:55:12 +08:00
|
|
|
|
return basic.JD2DateByZone(basic.TT2UTC(ttJDE), location, false)
|
2026-05-01 22:38:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func solarEclipseTimeToTTJDE(date time.Time) float64 {
|
2026-09-23 18:55:12 +08:00
|
|
|
|
utcJD := basic.Date2JD(date.UTC())
|
|
|
|
|
|
return basic.UTC2TT(utcJD)
|
2026-05-01 22:38:44 +08:00
|
|
|
|
}
|