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