Files
astro/eclipse/solar_local.go
T
b612 044f2f4381 feat: 引入可切换星历模型并扩展天象观测链路
- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算
- 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询
- 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出
- 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解
- 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤
- 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持
- 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理
- 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
2026-10-15 23:54:15 +08:00

728 lines
33 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
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
// Type 站心食型, local eclipse type.
Type SolarEclipseType
// LunarLimbApplied 接触时刻已按月缘剖面修正;剖面失败时为 false 且结果保持圆月面 / contacts refined with a lunar limb profile.
LunarLimbApplied bool
// HasSaros 存在沙罗序列信息(可能是锚点外推结果), has Saros series metadata (possibly extrapolated).
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.
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
// 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)
}
// 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)
}
// 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)
}
// 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)
}
// 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)
}
// 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)
}
// 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)
},
}
}
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]
}
location := date.Location()
dayStart, dayMid, dayEnd := solarEclipseLocalDayBounds(date)
candidateTT := basic.CalcMoonSHByJDEWithInstance(solarEclipseTimeToTTJDE(dayMid), 0, module.inst)
if !isPotentialLocalSolarEclipse(candidateTT, module) {
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)
if !localSolarEclipseOverlapsDate(info, dayStart, dayEnd) {
return LocalSolarEclipseInfo{}, false
}
if mode == localSolarEclipseQueryVisible && !localSolarEclipseVisibleOnDate(info, dayStart, dayEnd, module) {
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)
}
// 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)
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)
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)
}
// 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)
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)
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)
}
// NextLocalSolarEclipse 下次站心日食 / next local solar eclipse.
func (module *Eclipse) NextLocalSolarEclipse(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo {
return module.NextLocalSolarEclipseNASABulletinSplitK(date, lon, lat, height)
}
// 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)
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)
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)
}
// 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)
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)
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)
}
// ClosestLocalSolarEclipse 最近一次站心日食 / closest local solar eclipse.
func (module *Eclipse) ClosestLocalSolarEclipse(date time.Time, lon, lat, height float64) LocalSolarEclipseInfo {
return module.ClosestLocalSolarEclipseNASABulletinSplitK(date, lon, lat, height)
}
// 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)
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)
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)
}
// 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)
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)
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)
}
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, 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
}
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
}
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)
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]
}
visibleThreshold := localSolarEclipseVisibilityThreshold(height, lat)
saros, hasSaros := solarSarosInfo(result.GreatestEclipse, module)
return LocalSolarEclipseInfo{
Model: mapBasicSolarEclipseModel(result.Model),
LunarLimbApplied: result.LunarLimbApplied,
SunRadiusModel: result.SunRadiusModel,
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),
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]
}
if !result.HasPartial {
return nil
}
options := basic.LocalSolarEclipseDiagramOptions{StepDays: 1, SunRadiusModel: result.SunRadiusModel, LunarLimb: limb}
var diagram basic.LocalSolarEclipseDiagramResult
if result.Model == basic.SolarEclipseModelIAUSingleK {
diagram = basic.LocalSolarEclipseDiagramIAUSingleKWithInstance(result.GreatestEclipse, lon, lat, height, options, module.inst)
} else {
diagram = basic.LocalSolarEclipseDiagramNASABulletinSplitKWithInstance(result.GreatestEclipse, lon, lat, height, options, module.inst)
}
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]
}
eventStart, eventEnd, ok := localSolarEclipseRange(info)
if !ok {
return false
}
return localSolarEclipseVisibleDuring(info, eventStart, eventEnd, module)
}
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]
}
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)
}
func localSolarEclipseVisibleDuring(info LocalSolarEclipseInfo, start, end time.Time, modules ...*Eclipse) bool {
module := &defaultModule
if len(modules) > 0 {
module = modules[0]
}
if !start.Before(end) && !start.Equal(end) {
return false
}
if localSolarEclipseAltitudeVisible(start, info, module) || localSolarEclipseAltitudeVisible(end, info, module) {
return true
}
for dayStart, _, _ := solarEclipseLocalDayBounds(start); !dayStart.After(end); dayStart = nextSolarEclipseLocalDayStart(dayStart) {
_, culminationSeed, _ := solarEclipseLocalDayBounds(dayStart)
culmination := solarCulminationTime(culminationSeed, info.Longitude, module)
if culmination.Before(start) || culmination.After(end) {
continue
}
if localSolarEclipseAltitudeVisible(culmination, info, module) {
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)
}
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
}