Files
astro/eclipse/solar_local.go
T

728 lines
33 KiB
Go
Raw Permalink Normal View History

2026-05-01 22:38:44 +08:00
package eclipse
import (
"math"
"time"
"b612.me/astro/basic"
)
const (
localSolarEclipseSynodicMonthDays = 29.530588853
localSolarEclipseSearchEpsilonDay = 1e-8
localSolarEclipseLatitudeLimitDeg = 2.0
)
type localSolarEclipseCalculator struct {
global func(float64) basic.SolarEclipseResult
local func(float64, float64, float64, float64) basic.LocalSolarEclipseResult
// limb 是 local 实际使用的月缘剖面,接触点图须与之一致。
limb basic.LunarLimb
2026-05-01 22:38:44 +08:00
}
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
// SunRadiusModel 日食太阳半径口径, solar radius convention.
SunRadiusModel SolarEclipseSunRadiusModel
2026-05-01 22:38:44 +08:00
// Type 站心食型, local eclipse type.
Type SolarEclipseType
// LunarLimbApplied 接触时刻已按月缘剖面修正;剖面失败时为 false 且结果保持圆月面 / contacts refined with a lunar limb profile.
LunarLimbApplied bool
// 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
// Longitude 观测点经度,东正西负, observer longitude, east positive.
Longitude float64
// Latitude 观测点纬度,北正南负, observer latitude, north positive.
Latitude float64
// Height 观测点椭球高(大地高),单位米;只有正高 H 时须先加大地水准面差距 N, observer ellipsoidal height in meters.
2026-05-01 22:38:44 +08:00
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
// 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 站心食分, 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.
func (module *Eclipse) LocalSolarEclipseOnDate(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) {
return module.LocalSolarEclipseOnDateNASABulletinSplitK(date, lon, lat, height)
2026-05-01 22:38:44 +08:00
}
// LocalSolarEclipseOnDateNASABulletinSplitK 当地站心日食查询(NASA bulletin Split-K) / local topocentric solar eclipse query with NASA bulletin Split-K.
func (module *Eclipse) LocalSolarEclipseOnDateNASABulletinSplitK(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) {
return localSolarEclipseOnDate(date, lon, lat, height, localSolarEclipseCalculatorForModel(SolarEclipseModelNASABulletinSplitK, module), localSolarEclipseQueryVisible, module)
2026-05-01 22:38:44 +08:00
}
// LocalSolarEclipseOnDateIAUSingleK 当地站心日食查询(IAU Single-K) / local topocentric solar eclipse query with IAU Single-K.
func (module *Eclipse) LocalSolarEclipseOnDateIAUSingleK(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) {
return localSolarEclipseOnDate(date, lon, lat, height, localSolarEclipseCalculatorForModel(SolarEclipseModelIAUSingleK, module), localSolarEclipseQueryVisible, module)
2026-05-01 22:38:44 +08:00
}
// GeometricLocalSolarEclipseOnDate 当地站心几何日食查询 / local geometric solar eclipse query.
func (module *Eclipse) GeometricLocalSolarEclipseOnDate(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) {
return module.GeometricLocalSolarEclipseOnDateNASABulletinSplitK(date, lon, lat, height)
2026-05-01 22:38:44 +08:00
}
// GeometricLocalSolarEclipseOnDateNASABulletinSplitK 当地站心几何日食查询(NASA bulletin Split-K) / local geometric solar eclipse query with NASA bulletin Split-K.
func (module *Eclipse) GeometricLocalSolarEclipseOnDateNASABulletinSplitK(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) {
return localSolarEclipseOnDate(date, lon, lat, height, localSolarEclipseCalculatorForModel(SolarEclipseModelNASABulletinSplitK, module), localSolarEclipseQueryGeometric, module)
2026-05-01 22:38:44 +08:00
}
// GeometricLocalSolarEclipseOnDateIAUSingleK 当地站心几何日食查询(IAU Single-K) / local geometric solar eclipse query with IAU Single-K.
func (module *Eclipse) GeometricLocalSolarEclipseOnDateIAUSingleK(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) {
return localSolarEclipseOnDate(date, lon, lat, height, localSolarEclipseCalculatorForModel(SolarEclipseModelIAUSingleK, module), localSolarEclipseQueryGeometric, module)
2026-05-01 22:38:44 +08:00
}
// LocalSolarEclipseOnDateWithOptions 当地站心日食查询(自定义半径口径) / local topocentric solar eclipse query with custom radius conventions.
func (module *Eclipse) LocalSolarEclipseOnDateWithOptions(date time.Time, lon, lat, height float64, options SolarEclipseOptions) (LocalSolarEclipseInfo, bool) {
return localSolarEclipseOnDate(date, lon, lat, height, localSolarEclipseCalculatorFor(options, module), localSolarEclipseQueryVisible, module)
}
func localSolarEclipseCalculatorFor(options SolarEclipseOptions, modules ...*Eclipse) localSolarEclipseCalculator {
module := &defaultModule
if len(modules) > 0 {
module = modules[0]
}
basicOptions := basic.SolarEclipseOptions{
RadiusModel: basic.SolarEclipseRadiusModel(options.RadiusModel),
SunRadiusModel: basic.SolarEclipseSunRadiusModel(options.SunRadiusModel),
}
if basicOptions.RadiusModel != basic.SolarEclipseModelIAUSingleK {
basicOptions.RadiusModel = basic.SolarEclipseModelNASABulletinSplitK
}
// 全局食况(是否有食、食型)仍按圆月面判定,月缘只修正站心接触。
localOptions := basicOptions
localOptions.LunarLimb = options.LunarLimb
if localOptions.LunarLimb == nil {
localOptions.LunarLimb = module.lunarLimb
}
return localSolarEclipseCalculator{
limb: localOptions.LunarLimb,
global: func(seed float64) basic.SolarEclipseResult {
return basic.SolarEclipseWithOptionsWithInstance(seed, basicOptions, module.inst)
},
local: func(seed, lon, lat, height float64) basic.LocalSolarEclipseResult {
return basic.LocalSolarEclipseWithOptionsWithInstance(seed, lon, lat, height, localOptions, module.inst)
},
}
}
2026-05-01 22:38:44 +08:00
func localSolarEclipseOnDate(
date time.Time,
lon, lat, height float64,
calculator localSolarEclipseCalculator,
mode localSolarEclipseQueryMode, modules ...*Eclipse) (LocalSolarEclipseInfo, bool) {
module := &defaultModule
if len(modules) > 0 {
module = modules[0]
}
2026-05-01 22:38:44 +08:00
location := date.Location()
dayStart, dayMid, dayEnd := solarEclipseLocalDayBounds(date)
candidateTT := basic.CalcMoonSHByJDEWithInstance(solarEclipseTimeToTTJDE(dayMid), 0, module.inst)
if !isPotentialLocalSolarEclipse(candidateTT, module) {
2026-05-01 22:38:44 +08:00
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, calculator.limb, module)
2026-05-01 22:38:44 +08:00
if !localSolarEclipseOverlapsDate(info, dayStart, dayEnd) {
return LocalSolarEclipseInfo{}, false
}
if mode == localSolarEclipseQueryVisible && !localSolarEclipseVisibleOnDate(info, dayStart, dayEnd, module) {
2026-05-01 22:38:44 +08:00
return LocalSolarEclipseInfo{}, false
}
return info, true
}
// LastLocalSolarEclipse 上次站心日食 / previous local solar eclipse.
func (module *Eclipse) LastLocalSolarEclipse(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo {
return module.LastLocalSolarEclipseNASABulletinSplitK(date, lon, lat, height)
2026-05-01 22:38:44 +08:00
}
// LastLocalSolarEclipseNASABulletinSplitK 上次站心日食(NASA bulletin Split-K) / previous local solar eclipse with NASA bulletin Split-K.
func (module *Eclipse) LastLocalSolarEclipseNASABulletinSplitK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo {
info, _ := searchLocalSolarEclipse(date, lon, lat, height, -1, true, localSolarEclipseCalculatorForModel(SolarEclipseModelNASABulletinSplitK, module), localSolarEclipseQueryVisible, module)
2026-05-01 22:38:44 +08:00
return info
}
// LastLocalSolarEclipseIAUSingleK 上次站心日食(IAU Single-K) / previous local solar eclipse with IAU Single-K.
func (module *Eclipse) LastLocalSolarEclipseIAUSingleK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo {
info, _ := searchLocalSolarEclipse(date, lon, lat, height, -1, true, localSolarEclipseCalculatorForModel(SolarEclipseModelIAUSingleK, module), localSolarEclipseQueryVisible, module)
2026-05-01 22:38:44 +08:00
return info
}
// LastGeometricLocalSolarEclipse 上次站心几何日食 / previous geometric local solar eclipse.
func (module *Eclipse) LastGeometricLocalSolarEclipse(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo {
return module.LastGeometricLocalSolarEclipseNASABulletinSplitK(date, lon, lat, height)
2026-05-01 22:38:44 +08:00
}
// LastGeometricLocalSolarEclipseNASABulletinSplitK 上次站心几何日食(NASA bulletin Split-K) / previous geometric local solar eclipse with NASA bulletin Split-K.
func (module *Eclipse) LastGeometricLocalSolarEclipseNASABulletinSplitK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo {
info, _ := searchLocalSolarEclipse(date, lon, lat, height, -1, true, localSolarEclipseCalculatorForModel(SolarEclipseModelNASABulletinSplitK, module), localSolarEclipseQueryGeometric, module)
2026-05-01 22:38:44 +08:00
return info
}
// LastGeometricLocalSolarEclipseIAUSingleK 上次站心几何日食(IAU Single-K) / previous geometric local solar eclipse with IAU Single-K.
func (module *Eclipse) LastGeometricLocalSolarEclipseIAUSingleK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo {
info, _ := searchLocalSolarEclipse(date, lon, lat, height, -1, true, localSolarEclipseCalculatorForModel(SolarEclipseModelIAUSingleK, module), localSolarEclipseQueryGeometric, module)
2026-05-01 22:38:44 +08:00
return info
}
// LastLocalTotalSolarEclipse 上次站心日全食 / previous local total solar eclipse.
func (module *Eclipse) LastLocalTotalSolarEclipse(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) {
return searchLocalTotalSolarEclipse(date, lon, lat, height, -1, true, localSolarEclipseCalculatorForModel(SolarEclipseModelNASABulletinSplitK, module), localSolarEclipseQueryVisible, module)
}
// LastLocalAnnularSolarEclipse 上次站心日环食 / previous local annular solar eclipse.
func (module *Eclipse) LastLocalAnnularSolarEclipse(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) {
return searchLocalAnnularSolarEclipse(date, lon, lat, height, -1, true, localSolarEclipseCalculatorForModel(SolarEclipseModelNASABulletinSplitK, module), localSolarEclipseQueryVisible, module)
}
2026-05-01 22:38:44 +08:00
// NextLocalSolarEclipse 下次站心日食 / next local solar eclipse.
func (module *Eclipse) NextLocalSolarEclipse(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo {
return module.NextLocalSolarEclipseNASABulletinSplitK(date, lon, lat, height)
2026-05-01 22:38:44 +08:00
}
// NextLocalSolarEclipseNASABulletinSplitK 下次站心日食(NASA bulletin Split-K) / next local solar eclipse with NASA bulletin Split-K.
func (module *Eclipse) NextLocalSolarEclipseNASABulletinSplitK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo {
info, _ := searchLocalSolarEclipse(date, lon, lat, height, 1, false, localSolarEclipseCalculatorForModel(SolarEclipseModelNASABulletinSplitK, module), localSolarEclipseQueryVisible, module)
2026-05-01 22:38:44 +08:00
return info
}
// NextLocalSolarEclipseIAUSingleK 下次站心日食(IAU Single-K) / next local solar eclipse with IAU Single-K.
func (module *Eclipse) NextLocalSolarEclipseIAUSingleK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo {
info, _ := searchLocalSolarEclipse(date, lon, lat, height, 1, false, localSolarEclipseCalculatorForModel(SolarEclipseModelIAUSingleK, module), localSolarEclipseQueryVisible, module)
2026-05-01 22:38:44 +08:00
return info
}
// NextGeometricLocalSolarEclipse 下次站心几何日食 / next geometric local solar eclipse.
func (module *Eclipse) NextGeometricLocalSolarEclipse(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo {
return module.NextGeometricLocalSolarEclipseNASABulletinSplitK(date, lon, lat, height)
2026-05-01 22:38:44 +08:00
}
// NextGeometricLocalSolarEclipseNASABulletinSplitK 下次站心几何日食(NASA bulletin Split-K) / next geometric local solar eclipse with NASA bulletin Split-K.
func (module *Eclipse) NextGeometricLocalSolarEclipseNASABulletinSplitK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo {
info, _ := searchLocalSolarEclipse(date, lon, lat, height, 1, false, localSolarEclipseCalculatorForModel(SolarEclipseModelNASABulletinSplitK, module), localSolarEclipseQueryGeometric, module)
2026-05-01 22:38:44 +08:00
return info
}
// NextGeometricLocalSolarEclipseIAUSingleK 下次站心几何日食(IAU Single-K) / next geometric local solar eclipse with IAU Single-K.
func (module *Eclipse) NextGeometricLocalSolarEclipseIAUSingleK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo {
info, _ := searchLocalSolarEclipse(date, lon, lat, height, 1, false, localSolarEclipseCalculatorForModel(SolarEclipseModelIAUSingleK, module), localSolarEclipseQueryGeometric, module)
2026-05-01 22:38:44 +08:00
return info
}
// NextLocalTotalSolarEclipse 下次站心日全食 / next local total solar eclipse.
func (module *Eclipse) NextLocalTotalSolarEclipse(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) {
return searchLocalTotalSolarEclipse(date, lon, lat, height, 1, false, localSolarEclipseCalculatorForModel(SolarEclipseModelNASABulletinSplitK, module), localSolarEclipseQueryVisible, module)
}
// NextLocalAnnularSolarEclipse 下次站心日环食 / next local annular solar eclipse.
func (module *Eclipse) NextLocalAnnularSolarEclipse(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) {
return searchLocalAnnularSolarEclipse(date, lon, lat, height, 1, false, localSolarEclipseCalculatorForModel(SolarEclipseModelNASABulletinSplitK, module), localSolarEclipseQueryVisible, module)
}
2026-05-01 22:38:44 +08:00
// ClosestLocalSolarEclipse 最近一次站心日食 / closest local solar eclipse.
func (module *Eclipse) ClosestLocalSolarEclipse(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo {
return module.ClosestLocalSolarEclipseNASABulletinSplitK(date, lon, lat, height)
2026-05-01 22:38:44 +08:00
}
// ClosestLocalSolarEclipseNASABulletinSplitK 最近一次站心日食(NASA bulletin Split-K) / closest local solar eclipse with NASA bulletin Split-K.
func (module *Eclipse) ClosestLocalSolarEclipseNASABulletinSplitK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo {
last, hasLast := searchLocalSolarEclipse(date, lon, lat, height, -1, true, localSolarEclipseCalculatorForModel(SolarEclipseModelNASABulletinSplitK, module), localSolarEclipseQueryVisible, module)
next, hasNext := searchLocalSolarEclipse(date, lon, lat, height, 1, false, localSolarEclipseCalculatorForModel(SolarEclipseModelNASABulletinSplitK, module), localSolarEclipseQueryVisible, module)
2026-05-01 22:38:44 +08:00
return closestLocalSolarEclipse(date, last, hasLast, next, hasNext)
}
// ClosestLocalSolarEclipseIAUSingleK 最近一次站心日食(IAU Single-K) / closest local solar eclipse with IAU Single-K.
func (module *Eclipse) ClosestLocalSolarEclipseIAUSingleK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo {
last, hasLast := searchLocalSolarEclipse(date, lon, lat, height, -1, true, localSolarEclipseCalculatorForModel(SolarEclipseModelIAUSingleK, module), localSolarEclipseQueryVisible, module)
next, hasNext := searchLocalSolarEclipse(date, lon, lat, height, 1, false, localSolarEclipseCalculatorForModel(SolarEclipseModelIAUSingleK, module), localSolarEclipseQueryVisible, module)
2026-05-01 22:38:44 +08:00
return closestLocalSolarEclipse(date, last, hasLast, next, hasNext)
}
// ClosestGeometricLocalSolarEclipse 最近一次站心几何日食 / closest geometric local solar eclipse.
func (module *Eclipse) ClosestGeometricLocalSolarEclipse(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo {
return module.ClosestGeometricLocalSolarEclipseNASABulletinSplitK(date, lon, lat, height)
2026-05-01 22:38:44 +08:00
}
// ClosestGeometricLocalSolarEclipseNASABulletinSplitK 最近一次站心几何日食(NASA bulletin Split-K) / closest geometric local solar eclipse with NASA bulletin Split-K.
func (module *Eclipse) ClosestGeometricLocalSolarEclipseNASABulletinSplitK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo {
last, hasLast := searchLocalSolarEclipse(date, lon, lat, height, -1, true, localSolarEclipseCalculatorForModel(SolarEclipseModelNASABulletinSplitK, module), localSolarEclipseQueryGeometric, module)
next, hasNext := searchLocalSolarEclipse(date, lon, lat, height, 1, false, localSolarEclipseCalculatorForModel(SolarEclipseModelNASABulletinSplitK, module), localSolarEclipseQueryGeometric, module)
2026-05-01 22:38:44 +08:00
return closestLocalSolarEclipse(date, last, hasLast, next, hasNext)
}
// ClosestGeometricLocalSolarEclipseIAUSingleK 最近一次站心几何日食(IAU Single-K) / closest geometric local solar eclipse with IAU Single-K.
func (module *Eclipse) ClosestGeometricLocalSolarEclipseIAUSingleK(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo {
last, hasLast := searchLocalSolarEclipse(date, lon, lat, height, -1, true, localSolarEclipseCalculatorForModel(SolarEclipseModelIAUSingleK, module), localSolarEclipseQueryGeometric, module)
next, hasNext := searchLocalSolarEclipse(date, lon, lat, height, 1, false, localSolarEclipseCalculatorForModel(SolarEclipseModelIAUSingleK, module), localSolarEclipseQueryGeometric, module)
2026-05-01 22:38:44 +08:00
return closestLocalSolarEclipse(date, last, hasLast, next, hasNext)
}
// ClosestLocalTotalSolarEclipse 最近一次站心日全食 / closest local total solar eclipse.
func (module *Eclipse) ClosestLocalTotalSolarEclipse(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) {
last, hasLast := searchLocalTotalSolarEclipse(date, lon, lat, height, -1, true, localSolarEclipseCalculatorForModel(SolarEclipseModelNASABulletinSplitK, module), localSolarEclipseQueryVisible, module)
next, hasNext := searchLocalTotalSolarEclipse(date, lon, lat, height, 1, false, localSolarEclipseCalculatorForModel(SolarEclipseModelNASABulletinSplitK, module), localSolarEclipseQueryVisible, module)
return closestLocalSolarEclipseResult(date, last, hasLast, next, hasNext)
}
// ClosestLocalAnnularSolarEclipse 最近一次站心日环食 / closest local annular solar eclipse.
func (module *Eclipse) ClosestLocalAnnularSolarEclipse(date time.Time, lon, lat, height float64) (LocalSolarEclipseInfo, bool) {
last, hasLast := searchLocalAnnularSolarEclipse(date, lon, lat, height, -1, true, localSolarEclipseCalculatorForModel(SolarEclipseModelNASABulletinSplitK, module), localSolarEclipseQueryVisible, module)
next, hasNext := searchLocalAnnularSolarEclipse(date, lon, lat, height, 1, false, localSolarEclipseCalculatorForModel(SolarEclipseModelNASABulletinSplitK, module), localSolarEclipseQueryVisible, module)
return closestLocalSolarEclipseResult(date, last, hasLast, next, hasNext)
}
2026-05-01 22:38:44 +08:00
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) {
2026-05-01 22:38:44 +08:00
switch {
case hasLast && !hasNext:
return last, true
2026-05-01 22:38:44 +08:00
case !hasLast && hasNext:
return next, true
2026-05-01 22:38:44 +08:00
case !hasLast && !hasNext:
return LocalSolarEclipseInfo{}, false
2026-05-01 22:38:44 +08:00
}
lastDistance := math.Abs(date.Sub(last.GreatestEclipse).Seconds())
nextDistance := math.Abs(next.GreatestEclipse.Sub(date).Seconds())
if lastDistance <= nextDistance {
return last, true
2026-05-01 22:38:44 +08:00
}
return next, true
2026-05-01 22:38:44 +08:00
}
func searchLocalSolarEclipse(
date time.Time,
lon, lat, height float64,
direction int,
includeCurrent bool,
calculator localSolarEclipseCalculator,
mode localSolarEclipseQueryMode, modules ...*Eclipse) (LocalSolarEclipseInfo, bool) {
module := &defaultModule
if len(modules) > 0 {
module = modules[0]
}
info, _, ok := searchLocalCentralSolarEclipse(
solarEclipseTimeToTTJDE(date),
solarEclipseTimeToTTJDE(date)+float64(direction)*localSolarEclipseDefaultSearchYears*365.25,
direction, includeCurrent, SolarEclipseLocalSearchAny, lon, lat, height, date.Location(),
calculator, mode, module,
)
return info, ok
}
func searchLocalTotalSolarEclipse(
date time.Time,
lon, lat, height float64,
direction int,
includeCurrent bool,
calculator localSolarEclipseCalculator,
mode localSolarEclipseQueryMode, modules ...*Eclipse) (LocalSolarEclipseInfo, bool) {
module := &defaultModule
if len(modules) > 0 {
module = modules[0]
}
info, _, ok := searchLocalCentralSolarEclipse(
solarEclipseTimeToTTJDE(date),
solarEclipseTimeToTTJDE(date)+float64(direction)*localSolarEclipseDefaultSearchYears*365.25,
direction, includeCurrent, SolarEclipseLocalSearchTotal, lon, lat, height, date.Location(),
calculator, mode, module,
)
return info, ok
}
func searchLocalAnnularSolarEclipse(
date time.Time,
lon, lat, height float64,
direction int,
includeCurrent bool,
calculator localSolarEclipseCalculator,
mode localSolarEclipseQueryMode, modules ...*Eclipse) (LocalSolarEclipseInfo, bool) {
module := &defaultModule
if len(modules) > 0 {
module = modules[0]
}
info, _, ok := searchLocalCentralSolarEclipse(
solarEclipseTimeToTTJDE(date),
solarEclipseTimeToTTJDE(date)+float64(direction)*localSolarEclipseDefaultSearchYears*365.25,
direction, includeCurrent, SolarEclipseLocalSearchAnnular, lon, lat, height, date.Location(),
calculator, mode, module,
)
return info, ok
2026-05-01 22:38:44 +08:00
}
func isPotentialLocalSolarEclipse(newMoonTT float64, modules ...*Eclipse) bool {
module := &defaultModule
if len(modules) > 0 {
module = modules[0]
}
return math.Abs(basic.HMoonTrueBoWithInstance(newMoonTT, module.inst)) <= localSolarEclipseLatitudeLimitDeg
2026-05-01 22:38:44 +08:00
}
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, limb basic.LunarLimb, modules ...*Eclipse) LocalSolarEclipseInfo {
module := &defaultModule
if len(modules) > 0 {
module = modules[0]
}
info := localSolarEclipseInfoFieldsFromBasic(result, lon, lat, height, location, module)
info.ContactPoints = localSolarEclipseContactPointsFromBasic(result, lon, lat, height, location, limb, module)
2026-05-01 22:38:44 +08:00
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, modules ...*Eclipse) LocalSolarEclipseInfo {
module := &defaultModule
if len(modules) > 0 {
module = modules[0]
}
2026-05-01 22:38:44 +08:00
visibleThreshold := localSolarEclipseVisibilityThreshold(height, lat)
saros, hasSaros := solarSarosInfo(result.GreatestEclipse, module)
2026-05-01 22:38:44 +08:00
return LocalSolarEclipseInfo{
Model: mapBasicSolarEclipseModel(result.Model),
LunarLimbApplied: result.LunarLimbApplied,
SunRadiusModel: result.SunRadiusModel,
2026-05-01 22:38:44 +08:00
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),
GreatestJDE: result.GreatestEclipse,
DeltaTSeconds: basic.DeltaT(result.GreatestEclipse, true),
2026-05-01 22:38:44 +08:00
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, limb basic.LunarLimb, modules ...*Eclipse) []LocalSolarEclipseContactPoint {
module := &defaultModule
if len(modules) > 0 {
module = modules[0]
}
2026-05-01 22:38:44 +08:00
if !result.HasPartial {
return nil
}
options := basic.LocalSolarEclipseDiagramOptions{StepDays: 1, SunRadiusModel: result.SunRadiusModel, LunarLimb: limb}
2026-05-01 22:38:44 +08:00
var diagram basic.LocalSolarEclipseDiagramResult
if result.Model == basic.SolarEclipseModelIAUSingleK {
diagram = basic.LocalSolarEclipseDiagramIAUSingleKWithInstance(result.GreatestEclipse, lon, lat, height, options, module.inst)
2026-05-01 22:38:44 +08:00
} else {
diagram = basic.LocalSolarEclipseDiagramNASABulletinSplitKWithInstance(result.GreatestEclipse, lon, lat, height, options, module.inst)
2026-05-01 22:38:44 +08:00
}
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, modules ...*Eclipse) bool {
module := &defaultModule
if len(modules) > 0 {
module = modules[0]
}
2026-05-01 22:38:44 +08:00
eventStart, eventEnd, ok := localSolarEclipseRange(info)
if !ok {
return false
}
return localSolarEclipseVisibleDuring(info, eventStart, eventEnd, module)
2026-05-01 22:38:44 +08:00
}
func localCentralSolarEclipseVisible(info LocalSolarEclipseInfo, modules ...*Eclipse) bool {
module := &defaultModule
if len(modules) > 0 {
module = modules[0]
}
if !info.HasCentral || info.CentralStart.IsZero() || info.CentralEnd.IsZero() {
return false
}
return localSolarEclipseVisibleDuring(info, info.CentralStart, info.CentralEnd, module)
}
func localSolarEclipseVisibleOnDate(info LocalSolarEclipseInfo, dayStart, dayEnd time.Time, modules ...*Eclipse) bool {
module := &defaultModule
if len(modules) > 0 {
module = modules[0]
}
2026-05-01 22:38:44 +08:00
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, module)
2026-05-01 22:38:44 +08:00
}
func localSolarEclipseVisibleDuring(info LocalSolarEclipseInfo, start, end time.Time, modules ...*Eclipse) bool {
module := &defaultModule
if len(modules) > 0 {
module = modules[0]
}
2026-05-01 22:38:44 +08:00
if !start.Before(end) && !start.Equal(end) {
return false
}
if localSolarEclipseAltitudeVisible(start, info, module) || localSolarEclipseAltitudeVisible(end, info, module) {
2026-05-01 22:38:44 +08:00
return true
}
for dayStart, _, _ := solarEclipseLocalDayBounds(start); !dayStart.After(end); dayStart = nextSolarEclipseLocalDayStart(dayStart) {
_, culminationSeed, _ := solarEclipseLocalDayBounds(dayStart)
culmination := solarCulminationTime(culminationSeed, info.Longitude, module)
2026-05-01 22:38:44 +08:00
if culmination.Before(start) || culmination.After(end) {
continue
}
if localSolarEclipseAltitudeVisible(culmination, info, module) {
2026-05-01 22:38:44 +08:00
return true
}
}
return false
}
func localSolarEclipseAltitudeVisible(date time.Time, info LocalSolarEclipseInfo, modules ...*Eclipse) bool {
module := &defaultModule
if len(modules) > 0 {
module = modules[0]
}
return solarAltitude(date, info.Longitude, info.Latitude, module) > localSolarEclipseVisibilityThreshold(info.Height, info.Latitude)
2026-05-01 22:38:44 +08:00
}
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
}