- 新增日食、月食、本地可见性、中心线、半影区域、SVG 图示与沙罗周期信息 - 新增行星冲合、留、方照、物理星历、视直径、相位、亮肢角、轨道节点等计算 - 新增木星伽利略卫星位置、现象与接触事件计算 - 新增恒星星表、星座判定、自行修正与观测辅助能力 - 新增 coord、formula、orbit、sundial、lite/sun、lite/moon 等扩展包 - 完善农历年号、月相英文别名、视差角、大气质量、折射、日晷与双星计算 - 增加 NASA、JPL Horizons、IMCCE 等回归测试数据与基线测试 - 重构基础算法文件组织,补充大量公开 API 注释和语义回归测试 - 更新中文和英文 README,补充示例、精度说明、SVG 配图
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,
|
||
}
|
||
}
|