Files
astro/eclipse/solar_shadow.go
T
b612 2bf8478639 feat: 完善日月食与月掩几何链路并扩展历法接口
- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑
- 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口
- 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界
- 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名
- 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验
- 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口
- 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
2026-09-17 12:27:40 +08:00

312 lines
13 KiB
Go
Raw Permalink 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 入口按句柄 ΔT 换算 / instant this record describes.
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.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 {
utJDE := basic.Date2JDE(value.UTC())
if solver.options.DeltaTSeconds > 0 {
return utJDE + solver.options.DeltaTSeconds/86400
}
return basic.TD2UT(utJDE, true)
}
// 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 = solarEclipseTTJDEToTimeWithDeltaT(instant.JDE, instant.DeltaTSeconds, 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 = solarEclipseTTJDEToTimeWithDeltaT(jdeTT, state.DeltaTSeconds, 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)
}