Files
astro/eclipse/solar_shadow.go
T
b612 16c62a97d5 feat: 完善时标与天象几何计算并扩展输出接口
- 新增时标、ΔT 模型、质心时间与 UT1 支持
- 改进日月食、月掩、行星事件及路径边界计算
- 完善恒星三维自行与动态距离传播
- 扩展 SVG、GeoJSON、KML 输出与底层距离换算工具
- 整理中英文手册、示例资源及回归测试
2026-09-23 18:55:12 +08:00

314 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package eclipse
import (
"math"
"time"
"b612.me/astro/basic"
)
// SolarEclipseShadowKind 阴影类型 / shadow kind.
type SolarEclipseShadowKind int
const (
// SolarEclipseShadowUmbra 本影与反本影 / umbra and antumbra.
SolarEclipseShadowUmbra SolarEclipseShadowKind = iota
// SolarEclipseShadowPenumbra 半影,即偏食区 / penumbra, the partial-eclipse region.
SolarEclipseShadowPenumbra
)
// SolarEclipseShadowSolverOptions 单时刻阴影求解器配置 / single-instant shadow solver options.
type SolarEclipseShadowSolverOptions struct {
// Model 月亮半径模型,零值为 NASA bulletin Split-K / lunar radius model.
Model SolarEclipseRadiusModel
// DeltaTSeconds 显式 ΔT(秒),<=0 用进程级模型,只改变地球自转相位 / explicit ΔT in seconds.
DeltaTSeconds float64
// BoundaryPoints 边界角向采样点数,<=0 用 96 / boundary sample count.
BoundaryPoints int
// Kind 本影或半影,零值为本影 / umbra or penumbra, zero is the umbra.
Kind SolarEclipseShadowKind
// TargetSpacingKM 边界加密目标间距(千米),0 用该类型的整包采样默认值,负值表示不加密 / boundary refinement spacing in km.
TargetSpacingKM float64
}
// SolarEclipseShadowTopology 瞬时足迹拓扑签名,用于插值门控 / footprint topology signature.
type SolarEclipseShadowTopology struct {
// Kind 阴影类型 / shadow kind.
Kind SolarEclipseShadowKind
// Vertices、Segments 与 Closed 描述边界形状 / boundary shape.
Vertices int
Segments int
Closed bool
// EnclosesPole 环绕过极点,经度已展开,不能按下标插值 / pole-winding ring.
EnclosesPole bool
}
// Signature 稳定签名字符串,可直接作为能否插值的比较键 / stable interpolation key.
func (topology SolarEclipseShadowTopology) Signature() string {
return basic.SolarEclipseShadowTopology{
Kind: basic.SolarEclipseShadowKind(topology.Kind),
Vertices: topology.Vertices,
Segments: topology.Segments,
Closed: topology.Closed,
EnclosesPole: topology.EnclosesPole,
}.Signature()
}
// SolarEclipseShadowInstant 某瞬时的全球本影或半影足迹 / instantaneous shadow footprint.
type SolarEclipseShadowInstant struct {
// Time 记录对应的时刻:UTC 入口为调用方所给,TT 入口回填民用时刻(TT2UTC),与句柄 ΔT 无关。
// Time is the instant this record describes: the caller's value for the UTC entry, and the civil
// time recovered with TT2UTC for the TT entry, independently of the handle's ΔT.
Time time.Time
// JDE 几何使用的力学时儒略日,ΔT 不改变它 / TT instant used by the geometry.
JDE float64
// DeltaTSeconds 实际使用的 ΔT(秒),来自句柄显式值或进程级模型 / ΔT actually used.
DeltaTSeconds float64
// Model 本次使用的月亮半径模型 / lunar radius model used.
Model SolarEclipseRadiusModel
// Kind 本次计算的阴影类型 / shadow kind of this computation.
Kind SolarEclipseShadowKind
// Closed 边界由阴影自身闭合,false 表示被地平线切断 / self-closed or horizon-cut.
Closed bool
// Boundaries 物理边界分段,HorizonEnds 是两端的地平擦地点 / segments and grazing points.
Boundaries [][]SolarEclipsePathPoint
HorizonEnds []SolarEclipsePathPoint
// Topology 插值判定用的拓扑签名 / interpolation signature.
Topology SolarEclipseShadowTopology
}
// Empty 该时刻阴影未落在地球表面 / no footprint on the Earth.
func (instant SolarEclipseShadowInstant) Empty() bool {
return len(instant.Boundaries) == 0
}
// SolarEclipseStationState 某瞬时的站心日月几何 / topocentric geometry at one instant.
type SolarEclipseStationState struct {
// Time 是该状态对应的时刻。
// Time is the instant this state describes.
Time time.Time
// JDE 本次计算使用的力学时儒略日 / TT instant used.
JDE float64
// DeltaTSeconds 实际使用的 ΔT(秒) / ΔT actually used.
DeltaTSeconds float64
// SeparationDeg 与 SeparationArcsec 是日月中心的站心角距 / topocentric separation.
SeparationDeg float64
SeparationArcsec float64
// 三个半径字段是日月站心视半径 / topocentric apparent radii.
SunRadiusDeg float64
MoonOuterRadiusDeg float64
MoonInnerRadiusDeg float64
// SunAltitudeDeg 与 SunAzimuthDeg 是站心太阳高度角与方位角,方位角自北向东 / solar altitude and azimuth.
SunAltitudeDeg float64
SunAzimuthDeg float64
// Magnitude 瞬时食分,Obscuration 太阳视面积遮蔽率 / instantaneous magnitude and obscuration.
Magnitude float64
Obscuration float64
// 中心食标志:位于本影或反本影内,以及全食或环食 / central phase flags.
InCentralPhase bool
HasTotalPhase bool
HasAnnularPhase bool
// CentralPhaseType 中心食类型,非中心食为 SolarEclipseNone / central phase kind.
CentralPhaseType SolarEclipseType
// Visible 太阳中心在地平线上,高度用俯仰角修正 / Sun center above the horizon.
Visible bool
}
// SolarEclipseShadowSolver 可复用的单时刻求解器;非并发安全,宿主每条 lane 各持一个 / reusable solver, one per lane.
type SolarEclipseShadowSolver struct {
options SolarEclipseShadowSolverOptions
inner *basic.SolarEclipseShadowSolver
}
// NewSolarEclipseShadowSolver 构造单时刻求解器 / builds a single-instant solver.
func NewSolarEclipseShadowSolver(options SolarEclipseShadowSolverOptions) *SolarEclipseShadowSolver {
return &SolarEclipseShadowSolver{
options: options,
inner: basic.NewSolarEclipseShadowSolver(basic.SolarEclipseShadowSolverOptions{
Model: basic.SolarEclipseRadiusModel(options.Model),
DeltaTSeconds: options.DeltaTSeconds,
BoundaryPoints: options.BoundaryPoints,
Kind: basic.SolarEclipseShadowKind(options.Kind),
TargetSpacingKM: options.TargetSpacingKM,
}),
}
}
func (solver *SolarEclipseShadowSolver) ttJDE(value time.Time) float64 {
// 入参是民用时刻:TT = UTC + (TT−UTC),与闰秒表一致。显式 ΔT 是 TT−UT1,
// 只改自转相位,不能掺进这条换算,否则报告值与实用偏移会差一个 DUT1。
return basic.UTC2TT(basic.Date2JD(value.UTC()))
}
// ShadowAt 给定 UTC 时刻的阴影足迹;不在地球上时返回 (零值, false) / footprint at one UTC instant.
func (solver *SolarEclipseShadowSolver) ShadowAt(value time.Time) (SolarEclipseShadowInstant, bool) {
return solver.shadowAt(value, solver.ttJDE(value))
}
// ShadowAtJDE 以 TT 时刻取足迹,ΔT 只改变地面位置 / footprint at one TT instant.
func (solver *SolarEclipseShadowSolver) ShadowAtJDE(jdeTT float64) (SolarEclipseShadowInstant, bool) {
if !finiteEclipseValue(jdeTT) {
return SolarEclipseShadowInstant{}, false
}
return solver.shadowAt(time.Time{}, jdeTT)
}
func (solver *SolarEclipseShadowSolver) shadowAt(
value time.Time,
jdeTT float64,
) (SolarEclipseShadowInstant, bool) {
instant, ok := solver.inner.ShadowAtJDE(jdeTT)
return solarEclipseShadowInstantFromBasic(value, instant), ok
}
func solarEclipseShadowInstantFromBasic(
value time.Time, instant basic.SolarEclipseShadowInstant,
) SolarEclipseShadowInstant {
location := value.Location()
if value.IsZero() {
value = solarEclipseTTJDEToTime(instant.JDE, time.UTC)
location = time.UTC
}
return SolarEclipseShadowInstant{
Time: value,
JDE: instant.JDE,
DeltaTSeconds: instant.DeltaTSeconds,
Model: mapBasicSolarEclipseModel(instant.Model),
Kind: SolarEclipseShadowKind(instant.Kind),
Closed: instant.Closed,
Boundaries: solarEclipsePathSegmentsFromBasic(instant.Boundaries, location),
HorizonEnds: solarEclipsePathPointsFromBasic(instant.HorizonEnds, location),
Topology: SolarEclipseShadowTopology{
Kind: SolarEclipseShadowKind(instant.Topology.Kind),
Vertices: instant.Topology.Vertices,
Segments: instant.Topology.Segments,
Closed: instant.Topology.Closed,
EnclosesPole: instant.Topology.EnclosesPole,
},
}
}
// StationStateAt 给定 UTC 时刻与站点的站心情形,任何时刻可调用 / topocentric state at one instant.
func (solver *SolarEclipseShadowSolver) StationStateAt(
value time.Time, lon, lat, height float64,
) SolarEclipseStationState {
return solver.stationState(value, solver.ttJDE(value), lon, lat, height)
}
// StationStateAtJDE 以 TT 时刻取站心情形,ΔT 只影响地球自转相位 / topocentric state at one TT instant.
func (solver *SolarEclipseShadowSolver) StationStateAtJDE(
jdeTT, lon, lat, height float64,
) SolarEclipseStationState {
if !finiteEclipseValue(jdeTT) {
return SolarEclipseStationState{}
}
state := solver.inner.StationStateAtJDE(jdeTT, lon, lat, height)
result := solarEclipseStationStateFromBasic(time.Time{}, state)
result.Time = solarEclipseTTJDEToTime(jdeTT, time.UTC)
return result
}
func (solver *SolarEclipseShadowSolver) stationState(
value time.Time, jdeTT, lon, lat, height float64,
) SolarEclipseStationState {
return solarEclipseStationStateFromBasic(value, solver.inner.StationStateAtJDE(jdeTT, lon, lat, height))
}
func solarEclipseStationStateFromBasic(
value time.Time, state basic.SolarEclipseStationState,
) SolarEclipseStationState {
return SolarEclipseStationState{
Time: value,
JDE: state.JDE,
DeltaTSeconds: state.DeltaTSeconds,
SeparationDeg: state.SeparationDeg,
SeparationArcsec: state.SeparationArcsec,
SunRadiusDeg: state.SunRadiusDeg,
MoonOuterRadiusDeg: state.MoonOuterRadiusDeg,
MoonInnerRadiusDeg: state.MoonInnerRadiusDeg,
SunAltitudeDeg: state.SunAltitudeDeg,
SunAzimuthDeg: state.SunAzimuthDeg,
Magnitude: state.Magnitude,
Obscuration: state.Obscuration,
InCentralPhase: state.InCentralPhase,
HasTotalPhase: state.HasTotalPhase,
HasAnnularPhase: state.HasAnnularPhase,
CentralPhaseType: mapBasicSolarEclipseType(state.CentralPhaseType),
Visible: state.Visible,
}
}
// SolarEclipseShadowAt 无状态便捷入口,可并发调用 / stateless convenience entry point.
func SolarEclipseShadowAt(value time.Time) (SolarEclipseShadowInstant, bool) {
return NewSolarEclipseShadowSolver(SolarEclipseShadowSolverOptions{}).ShadowAt(value)
}
// SolarEclipseStationStateAt 无状态便捷入口,可并发调用 / stateless convenience entry point.
func SolarEclipseStationStateAt(value time.Time, lon, lat, height float64) SolarEclipseStationState {
return NewSolarEclipseShadowSolver(SolarEclipseShadowSolverOptions{}).StationStateAt(value, lon, lat, height)
}
func finiteEclipseValue(value float64) bool {
return !math.IsNaN(value) && !math.IsInf(value, 0)
}
// ShadowBetween 区间内等步长逐时刻的阴影足迹,超过内部上限时返回 nil / per-step footprints over a range.
func (solver *SolarEclipseShadowSolver) ShadowBetween(
start, end time.Time, step time.Duration,
) []SolarEclipseShadowInstant {
if step <= 0 || !end.After(start) {
return nil
}
startTT := solver.ttJDE(start)
endTT := solver.ttJDE(end)
stepDays := step.Hours() / 24
if stepDays <= 0 {
return nil
}
inner := solver.inner.ShadowBetweenJDE(startTT, endTT, stepDays)
result := make([]SolarEclipseShadowInstant, 0, len(inner))
// 逐条累加而非 index*step:Duration 是 int64 纳秒,长窗口下乘法会溢出。
instantTime := start
for _, instant := range inner {
// 直接映射,避免为每个时刻重复解算一次足迹。
result = append(result, solarEclipseShadowInstantFromBasic(instantTime, instant))
instantTime = instantTime.Add(step)
}
return result
}
// StationStatesBetween 区间内等步长逐时刻的站心情形 / per-step station states.
func (solver *SolarEclipseShadowSolver) StationStatesBetween(
start, end time.Time, step time.Duration, lon, lat, height float64,
) []SolarEclipseStationState {
if step <= 0 || !end.After(start) {
return nil
}
startTT := solver.ttJDE(start)
endTT := solver.ttJDE(end)
stepDays := step.Hours() / 24
inner := solver.inner.StationStatesBetweenJDE(startTT, endTT, stepDays, lon, lat, height)
result := make([]SolarEclipseStationState, 0, len(inner))
// 同上:累加避免长窗口下的纳秒溢出。
stateTime := start
for _, state := range inner {
result = append(result, solarEclipseStationStateFromBasic(stateTime, state))
stateTime = stateTime.Add(step)
}
return result
}
// SolarEclipseShadowBetween 无状态批量入口,使用进程级 ΔT / stateless batch entry point.
func SolarEclipseShadowBetween(
start, end time.Time, step time.Duration,
) []SolarEclipseShadowInstant {
return SolarEclipseShadowBetweenWithOptions(start, end, step, SolarEclipseShadowSolverOptions{})
}
// SolarEclipseShadowBetweenWithOptions 可显式指定 ΔT 与模型 / with explicit ΔT and model.
func SolarEclipseShadowBetweenWithOptions(
start, end time.Time, step time.Duration, options SolarEclipseShadowSolverOptions,
) []SolarEclipseShadowInstant {
return NewSolarEclipseShadowSolver(options).ShadowBetween(start, end, step)
}