astro/basic/lunar_eclipse_diagram.go

254 lines
8.2 KiB
Go
Raw Normal View History

package basic
import (
"math"
"sort"
)
const (
lunarEclipseDiagramDefaultStepDays = 5.0 / 1440.0
lunarEclipseDiagramMinStepDays = 1.0 / 86400.0
lunarEclipseDiagramMaxSamples = 2000
lunarEclipseDiagramDuplicateDays = 1e-10
)
// LunarEclipseDiagramOptions 控制月食穿影图采样。
// LunarEclipseDiagramOptions controls lunar eclipse shadow-path diagram sampling.
type LunarEclipseDiagramOptions struct {
// StepDays 是路径采样步长,单位为日;<=0 时使用 5 分钟。
// StepDays is the path sampling step in days; values <= 0 use five minutes.
StepDays float64
}
// LunarEclipseDiagramPoint 表示月食穿影图上的一个月心位置。
// LunarEclipseDiagramPoint is one Moon-center point in a lunar eclipse diagram.
type LunarEclipseDiagramPoint struct {
// JDE 是力学时儒略日, TT Julian ephemeris day.
JDE float64
// X / Y 是以月球半径为单位的月心相对地影中心坐标。
// X/Y are Moon-center coordinates relative to the shadow center, in Moon-radius units.
X float64
Y float64
// Label 是关键接触标签,如 P1/U1/U2/Greatest/U3/U4/P4。
// Label is a key contact label such as P1/U1/U2/Greatest/U3/U4/P4.
Label string
// Labels 是该点对应的全部关键接触标签;若事件重合,这里会有多个值。
// Labels contains all contact labels attached to this point.
Labels []string
}
// LunarEclipseDiagramResult 表示月食穿影图几何结果。
// LunarEclipseDiagramResult is the geometry result for a lunar eclipse diagram.
type LunarEclipseDiagramResult struct {
// Eclipse 是对应的月食结果。
// Eclipse is the eclipse result used for the diagram.
Eclipse LunarEclipseResult
// MoonRadius 是月球半径,单位为图上月球半径;固定为 1。
// MoonRadius is the Moon radius in diagram Moon-radius units; always 1.
MoonRadius float64
// UmbraRadius 是本影半径,单位为图上月球半径。
// UmbraRadius is the umbral shadow radius in Moon-radius units.
UmbraRadius float64
// PenumbraRadius 是半影半径,单位为图上月球半径。
// PenumbraRadius is the penumbral shadow radius in Moon-radius units.
PenumbraRadius float64
// Points 是月心路径点,包含接触点与采样点。
// Points are Moon-center path points, including contact and sampled points.
Points []LunarEclipseDiagramPoint
// StepDays 是实际采用的路径采样步长,单位为日。
// StepDays is the effective path sampling step in days.
StepDays float64
}
type lunarEclipseDiagramTime struct {
jde float64
labels []string
}
// LunarEclipseDiagram 计算月食穿影图几何数据,默认使用 Danjon 影半径模型。
// LunarEclipseDiagram computes lunar eclipse diagram geometry, using the Danjon shadow model by default.
func LunarEclipseDiagram(seedJDE float64, options LunarEclipseDiagramOptions) LunarEclipseDiagramResult {
return LunarEclipseDiagramDanjon(seedJDE, options)
}
// LunarEclipseDiagramDanjon 计算月食穿影图几何数据,使用 Danjon 影半径模型。
// LunarEclipseDiagramDanjon computes lunar eclipse diagram geometry with the Danjon shadow model.
func LunarEclipseDiagramDanjon(seedJDE float64, options LunarEclipseDiagramOptions) LunarEclipseDiagramResult {
return lunarEclipseDiagram(seedJDE, lunarEclipseShadowDanjon, options)
}
// LunarEclipseDiagramChauvenet 计算月食穿影图几何数据,使用 Chauvenet 影半径模型。
// LunarEclipseDiagramChauvenet computes lunar eclipse diagram geometry with the Chauvenet shadow model.
func LunarEclipseDiagramChauvenet(seedJDE float64, options LunarEclipseDiagramOptions) LunarEclipseDiagramResult {
return lunarEclipseDiagram(seedJDE, lunarEclipseShadowChauvenet, options)
}
func lunarEclipseDiagram(
seedJDE float64,
shadowModel lunarEclipseShadowModel,
options LunarEclipseDiagramOptions,
) LunarEclipseDiagramResult {
options = normalizeLunarEclipseDiagramOptions(options)
eclipse := lunarEclipse(seedJDE, shadowModel)
result := LunarEclipseDiagramResult{
Eclipse: eclipse,
StepDays: options.StepDays,
}
if !eclipse.HasPenumbral {
return result
}
maximumState := computeLunarShadowState(eclipse.Maximum, shadowModel)
if maximumState.moonRadiusRad <= 0 {
return result
}
result.MoonRadius = 1
result.UmbraRadius = maximumState.umbraRadiusRad / maximumState.moonRadiusRad
result.PenumbraRadius = maximumState.penumbraRadiusRad / maximumState.moonRadiusRad
times, stepDays := lunarEclipseDiagramTimes(eclipse, options.StepDays)
result.StepDays = stepDays
result.Points = make([]LunarEclipseDiagramPoint, 0, len(times))
for _, item := range times {
state := computeLunarShadowState(item.jde, shadowModel)
result.Points = append(result.Points, LunarEclipseDiagramPoint{
JDE: item.jde,
X: state.x / maximumState.moonRadiusRad,
Y: state.y / maximumState.moonRadiusRad,
Label: lunarEclipseDiagramPrimaryLabel(item.labels),
Labels: append([]string(nil), item.labels...),
})
}
return result
}
func normalizeLunarEclipseDiagramOptions(options LunarEclipseDiagramOptions) LunarEclipseDiagramOptions {
if options.StepDays <= 0 || math.IsNaN(options.StepDays) || math.IsInf(options.StepDays, 0) {
options.StepDays = lunarEclipseDiagramDefaultStepDays
}
if options.StepDays < lunarEclipseDiagramMinStepDays {
options.StepDays = lunarEclipseDiagramMinStepDays
}
return options
}
func lunarEclipseDiagramTimes(eclipse LunarEclipseResult, stepDays float64) ([]lunarEclipseDiagramTime, float64) {
startJDE := eclipse.PenumbralStart
endJDE := eclipse.PenumbralEnd
if startJDE == 0 || endJDE == 0 || endJDE <= startJDE {
return nil, stepDays
}
if sampleCount := int(math.Ceil((endJDE-startJDE)/stepDays)) + 1; sampleCount > lunarEclipseDiagramMaxSamples {
stepDays = (endJDE - startJDE) / float64(lunarEclipseDiagramMaxSamples-1)
}
times := []lunarEclipseDiagramTime{
{jde: startJDE, labels: []string{"P1"}},
{jde: eclipse.Maximum, labels: []string{"Greatest"}},
{jde: endJDE, labels: []string{"P4"}},
}
if eclipse.HasPartial {
times = append(times,
lunarEclipseDiagramTime{jde: eclipse.PartialStart, labels: []string{"U1"}},
lunarEclipseDiagramTime{jde: eclipse.PartialEnd, labels: []string{"U4"}},
)
}
if eclipse.HasTotal {
times = append(times,
lunarEclipseDiagramTime{jde: eclipse.TotalStart, labels: []string{"U2"}},
lunarEclipseDiagramTime{jde: eclipse.TotalEnd, labels: []string{"U3"}},
)
}
for jde := startJDE + stepDays; jde < endJDE; jde += stepDays {
times = append(times, lunarEclipseDiagramTime{jde: jde})
}
sort.SliceStable(times, func(i, j int) bool {
if times[i].jde == times[j].jde {
return lunarEclipseDiagramLabelPriority(times[i].labels) < lunarEclipseDiagramLabelPriority(times[j].labels)
}
return times[i].jde < times[j].jde
})
return uniqueLunarEclipseDiagramTimes(times), stepDays
}
func uniqueLunarEclipseDiagramTimes(times []lunarEclipseDiagramTime) []lunarEclipseDiagramTime {
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) > lunarEclipseDiagramDuplicateDays {
item.labels = append([]string(nil), item.labels...)
unique = append(unique, item)
continue
}
unique[len(unique)-1].labels = mergeLunarEclipseDiagramLabels(unique[len(unique)-1].labels, item.labels)
}
return unique
}
func mergeLunarEclipseDiagramLabels(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 lunarEclipseDiagramPrimaryLabel(labels []string) string {
for _, label := range labels {
if label == "Greatest" {
return label
}
}
if len(labels) == 0 {
return ""
}
return labels[0]
}
func lunarEclipseDiagramLabelPriority(labels []string) int {
if len(labels) == 0 {
return 99
}
switch labels[0] {
case "P1":
return 0
case "U1":
return 1
case "U2":
return 2
case "Greatest":
return 3
case "U3":
return 4
case "U4":
return 5
case "P4":
return 6
default:
return 99
}
}