044f2f4381
- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算 - 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询 - 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出 - 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解 - 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤 - 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持 - 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理 - 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
178 lines
6.5 KiB
Go
178 lines
6.5 KiB
Go
package eclipse
|
|
|
|
import (
|
|
"math"
|
|
"time"
|
|
|
|
"b612.me/astro/basic"
|
|
)
|
|
|
|
// SolarEclipseLocalSearchKind 站心搜索的目标食型 / which station eclipse to look for.
|
|
type SolarEclipseLocalSearchKind int
|
|
|
|
const (
|
|
// SolarEclipseLocalSearchAny 任意站心日食 / any local eclipse.
|
|
SolarEclipseLocalSearchAny SolarEclipseLocalSearchKind = iota
|
|
// SolarEclipseLocalSearchTotal 站心全食 / local total eclipse.
|
|
SolarEclipseLocalSearchTotal
|
|
// SolarEclipseLocalSearchAnnular 站心环食 / local annular eclipse.
|
|
SolarEclipseLocalSearchAnnular
|
|
)
|
|
|
|
// SolarEclipseLocalSearchOptions 站心搜索配置,零值为向后找任意可见日食 / station search options.
|
|
type SolarEclipseLocalSearchOptions struct {
|
|
// Kind 目标食型,零值为 Any / target kind.
|
|
Kind SolarEclipseLocalSearchKind
|
|
// Backward 为 true 时向过去搜索 / search towards the past.
|
|
Backward bool
|
|
// IncludeCurrent 起始时刻那一场也算命中 / accept an eclipse at the start instant.
|
|
IncludeCurrent bool
|
|
// Geometric 为 true 时跳过可见性门槛 / skip the visibility gate.
|
|
Geometric bool
|
|
// Model 月亮半径模型,零值为 NASA bulletin Split-K / lunar radius model.
|
|
Model SolarEclipseRadiusModel
|
|
// MaxYears 搜索跨度(年),<=0 用与旧迭代上限等价的默认跨度 / search horizon in years.
|
|
MaxYears float64
|
|
}
|
|
|
|
// SolarEclipseLocalSearchStatus 搜索结局与实际覆盖范围 / search outcome and horizon covered.
|
|
type SolarEclipseLocalSearchStatus struct {
|
|
// Found 是否找到目标事件 / whether the target was found.
|
|
Found bool
|
|
// Exhausted 已走到跨度上界仍未找到 / horizon consumed without a hit.
|
|
Exhausted bool
|
|
// YearsScanned 实际覆盖跨度(年),MonthsScanned 候选朔望月数 / covered horizon and candidates.
|
|
YearsScanned float64
|
|
MonthsScanned int
|
|
// Capped 被迭代硬上限截停,此时 Exhausted 不再代表跨度内没有 / iteration cap stopped the search.
|
|
Capped bool
|
|
// GlobalChecks 与 LocalChecks 是全局与站心解算次数 / solution counters.
|
|
GlobalChecks int
|
|
LocalChecks int
|
|
}
|
|
|
|
const (
|
|
// 默认搜索跨度 992.5 年,完整覆盖旧候选步进口径。
|
|
localSolarEclipseDefaultSearchYears = 992.5
|
|
// 时间跨度搜索的迭代硬上限,防止病态输入死循环。
|
|
localSolarEclipseSearchIterationCap = 200000
|
|
)
|
|
|
|
// SearchLocalCentralSolarEclipse 在给定跨度内搜索站心食并回报是否用尽跨度 / station search within a horizon.
|
|
func (module *Eclipse) SearchLocalCentralSolarEclipse(
|
|
date time.Time, lon, lat, height float64,
|
|
options SolarEclipseLocalSearchOptions,
|
|
) (LocalSolarEclipseInfo, SolarEclipseLocalSearchStatus) {
|
|
calculator := localSolarEclipseCalculatorForModel(options.Model, module)
|
|
mode := localSolarEclipseQueryVisible
|
|
if options.Geometric {
|
|
mode = localSolarEclipseQueryGeometric
|
|
}
|
|
maxYears := options.MaxYears
|
|
if maxYears <= 0 || math.IsNaN(maxYears) || math.IsInf(maxYears, 0) {
|
|
maxYears = localSolarEclipseDefaultSearchYears
|
|
}
|
|
direction := 1
|
|
if options.Backward {
|
|
direction = -1
|
|
}
|
|
targetTT := solarEclipseTimeToTTJDE(date)
|
|
info, status, ok := searchLocalCentralSolarEclipse(
|
|
targetTT, targetTT+float64(direction)*maxYears*365.25, direction,
|
|
options.IncludeCurrent, options.Kind, lon, lat, height, date.Location(),
|
|
calculator, mode, module,
|
|
)
|
|
status.Found = ok
|
|
status.Exhausted = !ok && !status.Capped
|
|
return info, status
|
|
}
|
|
|
|
// localSolarEclipseCalculatorForModel 构造绑定模块星历实例的计算器,非 IAU Single-K 一律按 Split-K。
|
|
func localSolarEclipseCalculatorForModel(model SolarEclipseRadiusModel, module *Eclipse) localSolarEclipseCalculator {
|
|
return localSolarEclipseCalculatorFor(SolarEclipseOptions{RadiusModel: model}, module)
|
|
}
|
|
|
|
func searchLocalCentralSolarEclipse(
|
|
targetTT, horizonTT float64,
|
|
direction int,
|
|
includeCurrent bool,
|
|
kind SolarEclipseLocalSearchKind,
|
|
lon, lat, height float64,
|
|
location *time.Location,
|
|
calculator localSolarEclipseCalculator,
|
|
mode localSolarEclipseQueryMode, modules ...*Eclipse) (LocalSolarEclipseInfo, SolarEclipseLocalSearchStatus, bool) {
|
|
module := &defaultModule
|
|
if len(modules) > 0 {
|
|
module = modules[0]
|
|
}
|
|
|
|
status := SolarEclipseLocalSearchStatus{}
|
|
candidateTT := basic.CalcMoonSHByJDEWithInstance(targetTT, 0, module.inst)
|
|
capped := true
|
|
for iteration := 0; iteration < localSolarEclipseSearchIterationCap; iteration++ {
|
|
if direction > 0 && candidateTT > horizonTT {
|
|
capped = false
|
|
break
|
|
}
|
|
if direction < 0 && candidateTT < horizonTT {
|
|
capped = false
|
|
break
|
|
}
|
|
status.MonthsScanned++
|
|
status.YearsScanned = math.Abs(candidateTT-targetTT) / 365.25
|
|
if isPotentialLocalSolarEclipse(candidateTT, module) {
|
|
globalResult := calculator.global(candidateTT)
|
|
status.GlobalChecks++
|
|
if globalResult.Type != basic.SolarEclipseNone {
|
|
if localSolarEclipseKindMatchesGlobal(kind, globalResult) {
|
|
result := calculator.local(globalResult.GreatestEclipse, lon, lat, height)
|
|
status.LocalChecks++
|
|
if localSolarEclipseKindMatchesLocal(kind, result) {
|
|
info := localSolarEclipseInfoFromBasic(result, lon, lat, height, location, calculator.limb, module)
|
|
if (mode != localSolarEclipseQueryVisible || localSolarEclipseKindVisible(kind, info, module)) &&
|
|
localSolarEclipseMatchesDirection(result.GreatestEclipse, targetTT, direction, includeCurrent) {
|
|
return info, status, true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
candidateTT = nextEclipseSearchCandidateTT(candidateTT, 0, direction, localSolarEclipseSynodicMonthDays, module)
|
|
}
|
|
// 循环自然结束说明撞上了迭代硬上限。
|
|
status.Capped = capped
|
|
return LocalSolarEclipseInfo{}, status, false
|
|
}
|
|
|
|
func localSolarEclipseKindMatchesGlobal(kind SolarEclipseLocalSearchKind, result basic.SolarEclipseResult) bool {
|
|
switch kind {
|
|
case SolarEclipseLocalSearchTotal:
|
|
return result.HasTotal || result.HasHybrid
|
|
case SolarEclipseLocalSearchAnnular:
|
|
return result.HasAnnular || result.HasHybrid
|
|
}
|
|
return true
|
|
}
|
|
|
|
func localSolarEclipseKindMatchesLocal(kind SolarEclipseLocalSearchKind, result basic.LocalSolarEclipseResult) bool {
|
|
switch kind {
|
|
case SolarEclipseLocalSearchTotal:
|
|
return result.HasTotal
|
|
case SolarEclipseLocalSearchAnnular:
|
|
return result.HasAnnular && !result.HasTotal
|
|
}
|
|
return result.Type != basic.SolarEclipseNone
|
|
}
|
|
|
|
func localSolarEclipseKindVisible(kind SolarEclipseLocalSearchKind, info LocalSolarEclipseInfo, modules ...*Eclipse) bool {
|
|
module := &defaultModule
|
|
if len(modules) > 0 {
|
|
module = modules[0]
|
|
}
|
|
|
|
if kind == SolarEclipseLocalSearchAny {
|
|
return localSolarEclipseVisible(info, module)
|
|
}
|
|
return localCentralSolarEclipseVisible(info, module)
|
|
}
|