2026-05-01 22:38:44 +08:00
package basic
import (
"math"
"sort"
2026-10-15 23:54:15 +08:00
"b612.me/astro/internal/geodata"
2026-05-01 22:38:44 +08:00
)
const (
localSolarEclipseDiagramDefaultStepDays = 5.0 / 1440.0
localSolarEclipseDiagramMinStepDays = 1.0 / 86400.0
localSolarEclipseDiagramMaxSamples = 2000
localSolarEclipseDiagramDuplicateDays = 1e-10
)
// LocalSolarEclipseDiagramOptions 控制站心日食视圆图采样。
// LocalSolarEclipseDiagramOptions controls local solar eclipse disk diagram sampling.
type LocalSolarEclipseDiagramOptions struct {
// StepDays 是路径采样步长,单位为日;<=0 时使用 5 分钟。
// StepDays is the path sampling step in days; values <= 0 use five minutes.
StepDays float64
2026-09-23 18:55:12 +08:00
// SunRadiusModel 太阳半径口径,零值为标准档 / solar radius convention, standard when zero.
SunRadiusModel SolarEclipseSunRadiusModel
2026-10-15 23:54:15 +08:00
// LunarLimb 月缘剖面,nil 为圆月面;修正接触时刻、站心食分与遮蔽率,食甚时刻不变,月盘仍按圆面采样 / lunar limb profile for contacts, magnitude and obscuration.
LunarLimb LunarLimb
2026-05-01 22:38:44 +08:00
}
// LocalSolarEclipseDiagramFrame 表示一个时刻的站心日月视圆几何。
// LocalSolarEclipseDiagramFrame is local Sun-Moon disk geometry at one instant.
type LocalSolarEclipseDiagramFrame struct {
// JDE 是力学时儒略日, TT Julian ephemeris day.
JDE float64
// MoonX / MoonY 是以太阳视半径为单位的月心相对日心坐标,X 向东为正,Y 向北为正。
// MoonX/MoonY are Moon-center coordinates relative to the Sun center in Sun-radius units; X east, Y north.
MoonX float64
MoonY float64
// SunRadius 是太阳视半径,单位为图上太阳半径;固定为 1。
// SunRadius is the Sun radius in diagram Sun-radius units; always 1.
SunRadius float64
// MoonRadius 是月球视半径,单位为图上太阳半径。
// MoonRadius is the Moon apparent radius in Sun-radius units.
MoonRadius float64
// Separation 是日月中心角距,单位为度。
// Separation is the Sun-Moon center separation in degrees.
Separation float64
// PositionAngle 是月心相对日心的位置角,从北点起向东量,单位为度。
// PositionAngle is the Moon-center position angle from north toward east, in degrees.
PositionAngle float64
// SunAltitude / SunAzimuth 是太阳站心高度角 / 方位角,单位为度。
// SunAltitude/SunAzimuth are local Sun altitude/azimuth in degrees.
SunAltitude float64
SunAzimuth float64
// Label 是关键阶段标签,如 C1/Greatest/C4。
// Label is a key phase label such as C1/Greatest/C4.
Label string
// Labels 是该点对应的全部关键阶段标签;若事件重合,这里会有多个值。
// Labels contains all phase labels attached to this point.
Labels [] string
}
// LocalSolarEclipseDiagramResult 表示站心日食视圆图几何结果。
// LocalSolarEclipseDiagramResult is the geometry result for a local solar eclipse disk diagram.
type LocalSolarEclipseDiagramResult struct {
// Eclipse 是对应的站心日食结果。
// Eclipse is the local eclipse result used for the diagram.
Eclipse LocalSolarEclipseResult
// Frames 是采样帧,太阳固定在 (0,0),月球按 MoonX/MoonY 绘制。
// Frames are sampled frames; the Sun is fixed at (0,0), and the Moon uses MoonX/MoonY.
Frames [] LocalSolarEclipseDiagramFrame
// StepDays 是实际采用的路径采样步长,单位为日。
// StepDays is the effective path sampling step in days.
StepDays float64
}
type localSolarEclipseDiagramTime struct {
jde float64
labels [] string
}
// LocalSolarEclipseDiagram 计算站心日食视圆图几何数据,默认使用 NASA bulletin Split-K 模型。
// LocalSolarEclipseDiagram computes local solar eclipse disk diagram geometry, using NASA bulletin Split-K by default.
func LocalSolarEclipseDiagram ( seedJDE , lon , lat , height float64 , options LocalSolarEclipseDiagramOptions ) LocalSolarEclipseDiagramResult {
return LocalSolarEclipseDiagramNASABulletinSplitK ( seedJDE , lon , lat , height , options )
}
2026-10-15 23:54:15 +08:00
// LocalSolarEclipseDiagramIAUSingleKWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LocalSolarEclipseDiagramIAUSingleKWithInstance ( seedJDE , lon , lat , height float64 , options LocalSolarEclipseDiagramOptions , inst * Instance ) LocalSolarEclipseDiagramResult {
return localSolarEclipseDiagram ( seedJDE , lon , lat , height , SolarEclipseModelIAUSingleK , options , inst )
2026-05-01 22:38:44 +08:00
}
2026-10-15 23:54:15 +08:00
// LocalSolarEclipseDiagramNASABulletinSplitKWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LocalSolarEclipseDiagramNASABulletinSplitKWithInstance ( seedJDE , lon , lat , height float64 , options LocalSolarEclipseDiagramOptions , inst * Instance ) LocalSolarEclipseDiagramResult {
return localSolarEclipseDiagram ( seedJDE , lon , lat , height , SolarEclipseModelNASABulletinSplitK , options , inst )
2026-05-01 22:38:44 +08:00
}
func localSolarEclipseDiagram (
seedJDE , lonDeg , latDeg , heightMeters float64 ,
model SolarEclipseRadiusModel ,
2026-10-15 23:54:15 +08:00
options LocalSolarEclipseDiagramOptions , instances ...* Instance ) LocalSolarEclipseDiagramResult {
inst := instanceValue ( instances )
2026-05-01 22:38:44 +08:00
options = normalizeLocalSolarEclipseDiagramOptions ( options )
2026-09-23 18:55:12 +08:00
eclipse := localSolarEclipse ( seedJDE , lonDeg , latDeg , heightMeters , SolarEclipseOptions {
RadiusModel : model ,
SunRadiusModel : options . SunRadiusModel ,
2026-10-15 23:54:15 +08:00
LunarLimb : options . LunarLimb ,
}, inst )
2026-05-01 22:38:44 +08:00
result := LocalSolarEclipseDiagramResult {
Eclipse : eclipse ,
StepDays : options . StepDays ,
}
if ! eclipse . HasPartial {
return result
}
lonRad := lonDeg * rad
latRad := latDeg * rad
heightKM := heightMeters / 1000.0
2026-09-23 18:55:12 +08:00
params := solarEclipseModelParams ( model , options . SunRadiusModel )
2026-05-01 22:38:44 +08:00
times , stepDays := localSolarEclipseDiagramTimes ( eclipse , options . StepDays )
result . StepDays = stepDays
result . Frames = make ([] LocalSolarEclipseDiagramFrame , 0 , len ( times ))
for _ , item := range times {
2026-10-15 23:54:15 +08:00
frame := localSolarEclipseDiagramFrameAt ( item . jde , lonRad , latRad , heightKM , params , inst )
2026-05-01 22:38:44 +08:00
frame . Label = localSolarEclipseDiagramPrimaryLabel ( item . labels )
frame . Labels = append ([] string ( nil ), item . labels ... )
result . Frames = append ( result . Frames , frame )
}
return result
}
func normalizeLocalSolarEclipseDiagramOptions ( options LocalSolarEclipseDiagramOptions ) LocalSolarEclipseDiagramOptions {
if options . StepDays <= 0 || math . IsNaN ( options . StepDays ) || math . IsInf ( options . StepDays , 0 ) {
options . StepDays = localSolarEclipseDiagramDefaultStepDays
}
if options . StepDays < localSolarEclipseDiagramMinStepDays {
options . StepDays = localSolarEclipseDiagramMinStepDays
}
return options
}
func localSolarEclipseDiagramTimes (
eclipse LocalSolarEclipseResult ,
stepDays float64 ,
) ([] localSolarEclipseDiagramTime , float64 ) {
startJDE := eclipse . PartialStart
endJDE := eclipse . PartialEnd
if startJDE == 0 || endJDE == 0 || endJDE <= startJDE {
return nil , stepDays
}
if sampleCount := int ( math . Ceil (( endJDE - startJDE ) / stepDays )) + 1 ; sampleCount > localSolarEclipseDiagramMaxSamples {
stepDays = ( endJDE - startJDE ) / float64 ( localSolarEclipseDiagramMaxSamples - 1 )
}
times := [] localSolarEclipseDiagramTime {
{ jde : startJDE , labels : [] string { "C1" }},
{ jde : eclipse . GreatestEclipse , labels : [] string { "Greatest" }},
{ jde : endJDE , labels : [] string { "C4" }},
}
if eclipse . HasCentral {
times = append ( times ,
localSolarEclipseDiagramTime { jde : eclipse . CentralStart , labels : [] string { "C2" }},
localSolarEclipseDiagramTime { jde : eclipse . CentralEnd , labels : [] string { "C3" }},
)
}
for jde := startJDE + stepDays ; jde < endJDE ; jde += stepDays {
times = append ( times , localSolarEclipseDiagramTime { jde : jde })
}
sort . SliceStable ( times , func ( i , j int ) bool {
if times [ i ]. jde == times [ j ]. jde {
return localSolarEclipseDiagramLabelPriority ( times [ i ]. labels ) < localSolarEclipseDiagramLabelPriority ( times [ j ]. labels )
}
return times [ i ]. jde < times [ j ]. jde
})
return uniqueLocalSolarEclipseDiagramTimes ( times ), stepDays
}
func uniqueLocalSolarEclipseDiagramTimes ( times [] localSolarEclipseDiagramTime ) [] localSolarEclipseDiagramTime {
if len ( times ) < 2 {
return times
}
unique := times [: 0 ]
for _ , item := range times {
if item . jde == 0 {
continue
}
if len ( unique ) == 0 || math . Abs ( item . jde - unique [ len ( unique ) - 1 ]. jde ) > localSolarEclipseDiagramDuplicateDays {
item . labels = append ([] string ( nil ), item . labels ... )
unique = append ( unique , item )
continue
}
unique [ len ( unique ) - 1 ]. labels = mergeLocalSolarEclipseDiagramLabels ( unique [ len ( unique ) - 1 ]. labels , item . labels )
}
return unique
}
func mergeLocalSolarEclipseDiagramLabels ( existing , incoming [] string ) [] string {
if len ( incoming ) == 0 {
return existing
}
if len ( existing ) == 0 {
return append ([] string ( nil ), incoming ... )
}
for _ , label := range incoming {
found := false
for _ , current := range existing {
if current == label {
found = true
break
}
}
if ! found {
existing = append ( existing , label )
}
}
return existing
}
func localSolarEclipseDiagramPrimaryLabel ( labels [] string ) string {
for _ , label := range labels {
if label == "Greatest" {
return label
}
}
if len ( labels ) == 0 {
return ""
}
return labels [ 0 ]
}
func localSolarEclipseDiagramLabelPriority ( labels [] string ) int {
if len ( labels ) == 0 {
return 99
}
switch labels [ 0 ] {
case "C1" :
return 0
case "C2" :
return 1
case "Greatest" :
return 2
case "C3" :
return 3
case "C4" :
return 4
default :
return 99
}
}
func localSolarEclipseDiagramFrameAt (
jdTT , lonRad , latRad , heightKM float64 ,
2026-10-15 23:54:15 +08:00
params solarEclipseModelParameters , instances ...* Instance ) LocalSolarEclipseDiagramFrame {
inst := instanceValue ( instances )
sunEquatorial , moonEquatorial := solarEclipseSunMoonEquatorial ( jdTT , inst )
2026-05-01 22:38:44 +08:00
sunXYZ := solarEclipseLLRToXYZ ( sunEquatorial [ 0 ], sunEquatorial [ 1 ], sunEquatorial [ 2 ])
moonXYZ := solarEclipseLLRToXYZ ( moonEquatorial [ 0 ], moonEquatorial [ 1 ], moonEquatorial [ 2 ])
2026-09-23 18:55:12 +08:00
ut1JDE := TT2UT1 ( jdTT )
gst := ApparentSiderealTime ( ut1JDE ) * 15 * rad
2026-05-01 22:38:44 +08:00
observerXYZ := localSolarEclipseObserverXYZ ( gst , lonRad , latRad , heightKM )
sunTopocentric := solarEclipseXYZToLLR (
sunXYZ [ 0 ] - observerXYZ [ 0 ],
sunXYZ [ 1 ] - observerXYZ [ 1 ],
sunXYZ [ 2 ] - observerXYZ [ 2 ],
)
moonTopocentric := solarEclipseXYZToLLR (
moonXYZ [ 0 ] - observerXYZ [ 0 ],
moonXYZ [ 1 ] - observerXYZ [ 1 ],
moonXYZ [ 2 ] - observerXYZ [ 2 ],
)
sunUnit := solarEclipseLLRToXYZ ( sunTopocentric [ 0 ], sunTopocentric [ 1 ], 1 )
moonUnit := solarEclipseLLRToXYZ ( moonTopocentric [ 0 ], moonTopocentric [ 1 ], 1 )
dot := localSolarEclipseClampUnit (
sunUnit [ 0 ] * moonUnit [ 0 ] + sunUnit [ 1 ] * moonUnit [ 1 ] + sunUnit [ 2 ] * moonUnit [ 2 ],
)
sunRadiusRad := math . Asin ( localSolarEclipseClampUnit (
2026-10-15 23:54:15 +08:00
geodata . EarthEquatorialRadiusKM * params . sunRadiusRatio / sunTopocentric [ 2 ],
2026-05-01 22:38:44 +08:00
))
moonRadiusRad := math . Asin ( localSolarEclipseClampUnit (
2026-09-23 18:55:12 +08:00
solarEclipsePenumbralRadiusNumerator ( params ) / moonTopocentric [ 2 ],
2026-05-01 22:38:44 +08:00
))
2026-09-23 18:55:12 +08:00
if params . umbralK > params . penumbralK {
2026-05-01 22:38:44 +08:00
moonRadiusRad = math . Asin ( localSolarEclipseClampUnit (
2026-10-15 23:54:15 +08:00
geodata . EarthEquatorialRadiusKM * params . umbralK * localSolarMoonRadiusScale / moonTopocentric [ 2 ],
2026-05-01 22:38:44 +08:00
))
}
east := [ 3 ] float64 { - math . Sin ( sunTopocentric [ 0 ]), math . Cos ( sunTopocentric [ 0 ]), 0 }
north := [ 3 ] float64 {
- math . Cos ( sunTopocentric [ 0 ]) * math . Sin ( sunTopocentric [ 1 ]),
- math . Sin ( sunTopocentric [ 0 ]) * math . Sin ( sunTopocentric [ 1 ]),
math . Cos ( sunTopocentric [ 1 ]),
}
denominator := dot
if math . Abs ( denominator ) < 1e-12 {
denominator = 1
}
xRad := ( moonUnit [ 0 ] * east [ 0 ] + moonUnit [ 1 ] * east [ 1 ] + moonUnit [ 2 ] * east [ 2 ]) / denominator
yRad := ( moonUnit [ 0 ] * north [ 0 ] + moonUnit [ 1 ] * north [ 1 ] + moonUnit [ 2 ] * north [ 2 ]) / denominator
positionAngle := math . Atan2 ( xRad , yRad ) / rad
if positionAngle < 0 {
positionAngle += 360
}
sunHorizontal := solarEclipseEquatorialToHorizontal (
sunTopocentric [ 0 ],
sunTopocentric [ 1 ],
sunTopocentric [ 2 ],
lonRad ,
latRad ,
gst ,
)
return LocalSolarEclipseDiagramFrame {
JDE : jdTT ,
MoonX : xRad / sunRadiusRad ,
MoonY : yRad / sunRadiusRad ,
SunRadius : 1 ,
MoonRadius : moonRadiusRad / sunRadiusRad ,
Separation : math . Acos ( dot ) / rad ,
PositionAngle : positionAngle ,
SunAltitude : sunHorizontal [ 1 ] / rad ,
SunAzimuth : solarEclipseNormalizeRadians ( sunHorizontal [ 0 ] + math . Pi ) / rad ,
}
}