astro/basic/solar_eclipse_diagram.go

326 lines
12 KiB
Go
Raw Normal View History

package basic
import (
"math"
"sort"
)
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
}
// 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)
}
// LocalSolarEclipseDiagramIAUSingleK 计算站心日食视圆图几何数据,使用 IAU Single-K 模型。
// LocalSolarEclipseDiagramIAUSingleK computes local solar eclipse disk diagram geometry with the IAU Single-K model.
func LocalSolarEclipseDiagramIAUSingleK(seedJDE, lon, lat, height float64, options LocalSolarEclipseDiagramOptions) LocalSolarEclipseDiagramResult {
return localSolarEclipseDiagram(seedJDE, lon, lat, height, SolarEclipseModelIAUSingleK, options)
}
// LocalSolarEclipseDiagramNASABulletinSplitK 计算站心日食视圆图几何数据,使用 NASA bulletin Split-K 模型。
// LocalSolarEclipseDiagramNASABulletinSplitK computes local solar eclipse disk diagram geometry with the NASA bulletin Split-K model.
func LocalSolarEclipseDiagramNASABulletinSplitK(seedJDE, lon, lat, height float64, options LocalSolarEclipseDiagramOptions) LocalSolarEclipseDiagramResult {
return localSolarEclipseDiagram(seedJDE, lon, lat, height, SolarEclipseModelNASABulletinSplitK, options)
}
func localSolarEclipseDiagram(
seedJDE, lonDeg, latDeg, heightMeters float64,
model SolarEclipseRadiusModel,
options LocalSolarEclipseDiagramOptions,
) LocalSolarEclipseDiagramResult {
options = normalizeLocalSolarEclipseDiagramOptions(options)
eclipse := localSolarEclipse(seedJDE, lonDeg, latDeg, heightMeters, model)
result := LocalSolarEclipseDiagramResult{
Eclipse: eclipse,
StepDays: options.StepDays,
}
if !eclipse.HasPartial {
return result
}
lonRad := lonDeg * rad
latRad := latDeg * rad
heightKM := heightMeters / 1000.0
params := solarEclipseModelParams(model)
times, stepDays := localSolarEclipseDiagramTimes(eclipse, options.StepDays)
result.StepDays = stepDays
result.Frames = make([]LocalSolarEclipseDiagramFrame, 0, len(times))
for _, item := range times {
frame := localSolarEclipseDiagramFrameAt(item.jde, lonRad, latRad, heightKM, params)
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,
params solarEclipseModelParameters,
) LocalSolarEclipseDiagramFrame {
sunEquatorial, moonEquatorial := solarEclipseSunMoonEquatorial(jdTT)
sunXYZ := solarEclipseLLRToXYZ(sunEquatorial[0], sunEquatorial[1], sunEquatorial[2])
moonXYZ := solarEclipseLLRToXYZ(moonEquatorial[0], moonEquatorial[1], moonEquatorial[2])
utJDE := TD2UT(jdTT, false)
gst := ApparentSiderealTime(utJDE) * 15 * rad
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(
solarEclipseEarthEquatorialRadiusKM * solarEclipseSolarRadiusRatio / sunTopocentric[2],
))
moonRadiusRad := math.Asin(localSolarEclipseClampUnit(
solarEclipseEarthEquatorialRadiusKM * solarEclipsePenumbralK * localSolarMoonRadiusScale / moonTopocentric[2],
))
if params.umbralK > solarEclipsePenumbralK {
moonRadiusRad = math.Asin(localSolarEclipseClampUnit(
solarEclipseEarthEquatorialRadiusKM * params.umbralK * localSolarMoonRadiusScale / moonTopocentric[2],
))
}
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,
}
}