feat: 完善日月食与月掩几何链路并扩展历法接口

- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑
- 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口
- 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界
- 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名
- 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验
- 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口
- 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
This commit is contained in:
2026-09-17 12:27:40 +08:00
parent 9ee2163cc7
commit 2bf8478639
428 changed files with 85981 additions and 7998 deletions
+162 -15
View File
@@ -39,8 +39,10 @@ type JupiterGalileanPhenomenonEvent struct {
}
type jupiterGalileanMetricSample struct {
active bool
metric float64
active bool
metric float64
// signed 是该现象相对木星视面中心线的有符号偏移(木星半径),每次过零对应一次现象;未定义时为 NaN。
signed float64
phenomenon JupiterGalileanPhenomenon
}
@@ -55,18 +57,24 @@ type jupiterGalileanShadowPoint struct {
}
// LastJupiterGalileanPhenomenonEvent 上一次伽利略卫星现象 / previous Galilean-satellite event.
//
// jd 与返回时刻都是 UTC/UT 儒略日;需要瞬时状态时先用 TD2UT(jd, true) 换成 TT。
func LastJupiterGalileanPhenomenonEvent(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType) JupiterGalileanPhenomenonEvent {
event, _ := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, -1, true)
return event
}
// NextJupiterGalileanPhenomenonEvent 下一次伽利略卫星现象 / next Galilean-satellite event.
//
// jd 与返回时刻都是 UTC/UT 儒略日;需要瞬时状态时先用 TD2UT(jd, true) 换成 TT。
func NextJupiterGalileanPhenomenonEvent(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType) JupiterGalileanPhenomenonEvent {
event, _ := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, 1, false)
return event
}
// ClosestJupiterGalileanPhenomenonEvent 最近一次伽利略卫星现象 / closest Galilean-satellite event.
//
// jd 与返回时刻都是 UTC/UT 儒略日;需要瞬时状态时先用 TD2UT(jd, true) 换成 TT。
func ClosestJupiterGalileanPhenomenonEvent(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType) JupiterGalileanPhenomenonEvent {
last, hasLast := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, -1, true)
next, hasNext := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, 1, false)
@@ -121,14 +129,11 @@ func searchJupiterGalileanPhenomenonEvent(
for i := 2; i <= maxSteps; i++ {
nextTime := jd + sign*float64(i)*stepDays
nextSample := jupiterGalileanPhenomenonMetricAt(nextTime, satellite, phenomenonType)
if isFinite(midSample.metric) &&
midSample.metric <= prevSample.metric &&
midSample.metric <= nextSample.metric {
candidate := refineJupiterGalileanMetricMinimum(prevTime, nextTime, satellite, phenomenonType)
event := findJupiterGalileanPhenomenonEventAround(candidate, satellite, phenomenonType)
if event.Valid && jupiterGalileanEventMatchesDirection(event.Greatest, jd, direction, includeCurrent) {
return event, true
}
if event, ok := jupiterGalileanIntervalCandidate(
prevTime, nextTime, prevSample, midSample, nextSample,
jd, satellite, phenomenonType, direction, includeCurrent,
); ok {
return event, true
}
prevTime, prevSample = midTime, midSample
midTime, midSample = nextTime, nextSample
@@ -137,6 +142,132 @@ func searchJupiterGalileanPhenomenonEvent(
return invalidJupiterGalileanPhenomenonEvent(), false
}
// jupiterGalileanIntervalCandidate 在粗扫区间内确认现象:度量局部极小,或有符号偏移过零。
func jupiterGalileanIntervalCandidate(
prevTime, nextTime float64,
prevSample, midSample, nextSample jupiterGalileanMetricSample,
jd float64,
satellite int,
phenomenonType JupiterGalileanPhenomenonType,
direction int,
includeCurrent bool,
) (JupiterGalileanPhenomenonEvent, bool) {
var best JupiterGalileanPhenomenonEvent
found := false
consider := func(event JupiterGalileanPhenomenonEvent) {
if !event.Valid ||
!jupiterGalileanEventMatchesDirection(event.Greatest, jd, direction, includeCurrent) {
return
}
if !found ||
(direction > 0 && event.Greatest < best.Greatest) ||
(direction < 0 && event.Greatest > best.Greatest) {
best, found = event, true
}
}
if isFinite(midSample.metric) &&
midSample.metric <= prevSample.metric &&
midSample.metric <= nextSample.metric {
consider(findJupiterGalileanPhenomenonEventAround(
refineJupiterGalileanMetricMinimum(prevTime, nextTime, satellite, phenomenonType),
satellite, phenomenonType,
))
}
if jupiterGalileanSignedCrosses(prevSample.signed, nextSample.signed) &&
jupiterGalileanNearDisk(prevSample, midSample, nextSample) {
crossing := refineJupiterGalileanSignedCrossing(prevTime, nextTime, satellite, phenomenonType)
// 过零点只说明"靠近木星",只在它已贴近视面时按事件尺度细扫一次。
if sample := jupiterGalileanPhenomenonMetricAt(crossing, satellite, phenomenonType); isFinite(sample.metric) &&
sample.metric <= jupiterGalileanCrossingProbeMetric {
if event, ok := jupiterGalileanFineScanAround(crossing, satellite, phenomenonType); ok {
consider(event)
}
}
}
return best, found
}
const (
// jupiterGalileanCrossingProbeMetric 是细扫门限(椭球度量,1 为视面边缘)。
jupiterGalileanCrossingProbeMetric = 2.5
// jupiterGalileanCrossingProbeDays 是过零点两侧的细扫半宽。
jupiterGalileanCrossingProbeDays = 15.0 / 1440.0
// jupiterGalileanCrossingProbeStepDays 是细扫步长(30 秒),短于最短的擦边事件。
jupiterGalileanCrossingProbeStepDays = 0.5 / 1440.0
)
// jupiterGalileanNearDisk 报告区间内是否已有贴近木星视面的采样点。
func jupiterGalileanNearDisk(prevSample, midSample, nextSample jupiterGalileanMetricSample) bool {
for _, metric := range [3]float64{prevSample.metric, midSample.metric, nextSample.metric} {
if isFinite(metric) && metric <= jupiterGalileanCrossingProbeMetric {
return true
}
}
return false
}
// jupiterGalileanFineScanAround 在过零点邻域按事件尺度取样,命中后交给事件求解。
func jupiterGalileanFineScanAround(
jd float64,
satellite int,
phenomenonType JupiterGalileanPhenomenonType,
) (JupiterGalileanPhenomenonEvent, bool) {
// 从过零点向两侧交替外扩,通常几步内就能落进事件窗口。
steps := int(math.Round(jupiterGalileanCrossingProbeDays / jupiterGalileanCrossingProbeStepDays))
for step := 0; step <= steps; step++ {
for _, sign := range [2]float64{1, -1} {
if step == 0 && sign < 0 {
continue
}
candidate := jd + sign*float64(step)*jupiterGalileanCrossingProbeStepDays
if !jupiterGalileanPhenomenonMetricAt(candidate, satellite, phenomenonType).active {
continue
}
if event := findJupiterGalileanPhenomenonEventAround(candidate, satellite, phenomenonType); event.Valid {
return event, true
}
}
}
return JupiterGalileanPhenomenonEvent{}, false
}
// jupiterGalileanSignedCrosses 判断两次采样的有符号偏移是否变号(端点为 0 也算)。
func jupiterGalileanSignedCrosses(first, second float64) bool {
if !isFinite(first) || !isFinite(second) {
return false
}
if first == 0 || second == 0 {
return true
}
return (first < 0) != (second < 0)
}
// refineJupiterGalileanSignedCrossing 用二分把过零时刻收敛到事件容差内。
func refineJupiterGalileanSignedCrossing(
left, right float64,
satellite int,
phenomenonType JupiterGalileanPhenomenonType,
) float64 {
leftValue := jupiterGalileanPhenomenonMetricAt(left, satellite, phenomenonType).signed
rightValue := jupiterGalileanPhenomenonMetricAt(right, satellite, phenomenonType).signed
if !isFinite(leftValue) || !isFinite(rightValue) || (leftValue < 0) == (rightValue < 0) {
return (left + right) / 2
}
for i := 0; i < 80 && right-left > jupiterGalileanEventEpsilonDays; i++ {
middle := (left + right) / 2
value := jupiterGalileanPhenomenonMetricAt(middle, satellite, phenomenonType).signed
if !isFinite(value) {
return middle
}
if (value < 0) == (leftValue < 0) {
left, leftValue = middle, value
} else {
right, rightValue = middle, value
}
}
return (left + right) / 2
}
func findJupiterGalileanPhenomenonEventAround(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType) JupiterGalileanPhenomenonEvent {
sample := jupiterGalileanPhenomenonMetricAt(jd, satellite, phenomenonType)
if !sample.active {
@@ -292,6 +423,7 @@ func jupiterGalileanPhenomenonMetricAt(
if !isFinite(jd) || satellite < 1 || satellite > 4 || !isValidJupiterGalileanPhenomenonType(phenomenonType) {
return jupiterGalileanMetricSample{
metric: math.Inf(1),
signed: math.NaN(),
phenomenon: invalidJupiterGalileanPhenomenon(),
}
}
@@ -301,6 +433,7 @@ func jupiterGalileanPhenomenonMetricAt(
if context.jupiterDistance == 0 {
return jupiterGalileanMetricSample{
metric: math.Inf(1),
signed: math.NaN(),
phenomenon: invalidJupiterGalileanPhenomenon(),
}
}
@@ -360,27 +493,41 @@ func jupiterGalileanPhenomenonMetricAt(
if !observation.InFrontOfJupiter {
metric += 4
}
return jupiterGalileanMetricSample{active: phenomenon.Transit, metric: metric, phenomenon: phenomenon}
return jupiterGalileanMetricSample{
active: phenomenon.Transit, metric: metric, signed: xEarth, phenomenon: phenomenon,
}
case JupiterGalileanOccultation:
metric := earthMetric
if observation.InFrontOfJupiter {
metric += 4
}
return jupiterGalileanMetricSample{active: phenomenon.Occultation, metric: metric, phenomenon: phenomenon}
return jupiterGalileanMetricSample{
active: phenomenon.Occultation, metric: metric, signed: xEarth, phenomenon: phenomenon,
}
case JupiterGalileanEclipse:
metric := sunMetric
if zSunAU <= 0 {
metric += 4
}
return jupiterGalileanMetricSample{active: phenomenon.Eclipse, metric: metric, phenomenon: phenomenon}
return jupiterGalileanMetricSample{
active: phenomenon.Eclipse, metric: metric, signed: xSun, phenomenon: phenomenon,
}
case JupiterGalileanShadowTransit:
metric := shadowMetric
if shadowPoint.hasIntersection && !shadowPoint.visible {
metric += 4
}
return jupiterGalileanMetricSample{active: phenomenon.ShadowTransit, metric: metric, phenomenon: phenomenon}
signed := math.NaN()
if shadowPoint.hasIntersection {
signed = shadowPoint.xJupiterRadii
}
return jupiterGalileanMetricSample{
active: phenomenon.ShadowTransit, metric: metric, signed: signed, phenomenon: phenomenon,
}
default:
return jupiterGalileanMetricSample{metric: math.Inf(1), phenomenon: invalidJupiterGalileanPhenomenon()}
return jupiterGalileanMetricSample{
metric: math.Inf(1), signed: math.NaN(), phenomenon: invalidJupiterGalileanPhenomenon(),
}
}
}