326 lines
12 KiB
Go
326 lines
12 KiB
Go
|
|
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,
|
|||
|
|
}
|
|||
|
|
}
|