feat: 引入可切换星历模型并扩展天象观测链路

- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算
- 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询
- 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出
- 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解
- 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤
- 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持
- 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理
- 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
This commit is contained in:
2026-10-15 23:54:15 +08:00
parent 16c62a97d5
commit 044f2f4381
674 changed files with 62983 additions and 8396 deletions
+24 -19
View File
@@ -72,28 +72,26 @@ type planetOccultationDiagramTime struct {
labels []string
}
// PlanetOccultationDiagram 为已求解的固定地点行星月掩计算以月心为原点的切平面轨迹。事件数据无效或不完整时,结果不含帧。
// PlanetOccultationDiagram computes a Moon-centered tangent-plane track for an already solved fixed-site planetary occultation. Invalid or incomplete event data produces a result without frames.
func PlanetOccultationDiagram(
// PlanetOccultationDiagramWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func PlanetOccultationDiagramWithInstance(
info PlanetOccultationInfo,
options PlanetOccultationDiagramOptions,
) PlanetOccultationDiagramResult {
options PlanetOccultationDiagramOptions, inst *Instance) PlanetOccultationDiagramResult {
options = normalizePlanetOccultationDiagramOptions(options)
result := PlanetOccultationDiagramResult{Occultation: info, StepDays: options.StepDays}
if !planetOccultationDiagramInputValid(info) {
return result
}
config, ok := planetOccultationConfigFor(info.Planet)
config, ok := planetOccultationConfigFor(info.Planet, inst)
if !ok {
return result
}
startTT := occultationTimeToTT(info.ExternalImmersion)
greatestTT := occultationTimeToTT(info.Greatest)
endTT := occultationTimeToTT(info.ExternalEmersion)
externalImmersionFrame, externalImmersionOK := planetOccultationDiagramFrameAt(startTT, config, info.Observer)
greatestFrame, greatestOK := planetOccultationDiagramFrameAt(greatestTT, config, info.Observer)
externalEmersionFrame, externalEmersionOK := planetOccultationDiagramFrameAt(endTT, config, info.Observer)
externalImmersionFrame, externalImmersionOK := planetOccultationDiagramFrameAt(startTT, config, info.Observer, inst)
greatestFrame, greatestOK := planetOccultationDiagramFrameAt(greatestTT, config, info.Observer, inst)
externalEmersionFrame, externalEmersionOK := planetOccultationDiagramFrameAt(endTT, config, info.Observer, inst)
if !externalImmersionOK || !greatestOK || !externalEmersionOK {
return result
}
@@ -103,10 +101,10 @@ func PlanetOccultationDiagram(
)
if info.HasInternalContacts {
internalImmersionFrame, internalImmersionOK = planetOccultationDiagramFrameAt(
occultationTimeToTT(info.InternalImmersion), config, info.Observer,
occultationTimeToTT(info.InternalImmersion), config, info.Observer, inst,
)
internalEmersionFrame, internalEmersionOK = planetOccultationDiagramFrameAt(
occultationTimeToTT(info.InternalEmersion), config, info.Observer,
occultationTimeToTT(info.InternalEmersion), config, info.Observer, inst,
)
}
if (info.HasInternalContacts && (!internalImmersionOK || !internalEmersionOK)) ||
@@ -120,7 +118,7 @@ func PlanetOccultationDiagram(
result.StepDays = stepDays
result.Frames = make([]PlanetOccultationDiagramFrame, 0, len(times))
for _, item := range times {
frame, frameOK := planetOccultationDiagramFrameAt(item.jde, config, info.Observer)
frame, frameOK := planetOccultationDiagramFrameAt(item.jde, config, info.Observer, inst)
if !frameOK {
return PlanetOccultationDiagramResult{Occultation: info, StepDays: stepDays}
}
@@ -142,8 +140,10 @@ func planetOccultationDiagramMatchesInfo(
!finite(info.PlanetSemidiameterArcsec) || info.PlanetSemidiameterArcsec <= 0 {
return false
}
if math.Abs(externalImmersion.SeparationArcsec-externalImmersion.MoonRadiusArcsec-externalImmersion.PlanetRadiusArcsec) > planetOccultationDiagramGeometryArcsec ||
math.Abs(externalEmersion.SeparationArcsec-externalEmersion.MoonRadiusArcsec-externalEmersion.PlanetRadiusArcsec) > planetOccultationDiagramGeometryArcsec {
// 已按月缘剖面收敛的接触不在圆月面上,圆月面接触判据只适用于未修正的结果。
checkContacts := !info.LunarLimbApplied
if checkContacts && (math.Abs(externalImmersion.SeparationArcsec-externalImmersion.MoonRadiusArcsec-externalImmersion.PlanetRadiusArcsec) > planetOccultationDiagramGeometryArcsec ||
math.Abs(externalEmersion.SeparationArcsec-externalEmersion.MoonRadiusArcsec-externalEmersion.PlanetRadiusArcsec) > planetOccultationDiagramGeometryArcsec) {
return false
}
if math.Abs(greatest.SeparationArcsec-info.MinimumSeparationArcsec) > planetOccultationDiagramGeometryArcsec ||
@@ -153,6 +153,10 @@ func planetOccultationDiagramMatchesInfo(
return false
}
// 月缘结果的食型按剖面判定,圆月面掩甚间隙不再适用。
if info.LunarLimbApplied {
return info.Type == OccultationPartial || info.Type == OccultationTotal || info.Type == OccultationGrazing
}
externalMetric := greatest.SeparationArcsec - greatest.MoonRadiusArcsec - greatest.PlanetRadiusArcsec
internalMetric := greatest.SeparationArcsec - greatest.MoonRadiusArcsec + greatest.PlanetRadiusArcsec
switch info.Type {
@@ -164,8 +168,8 @@ func planetOccultationDiagramMatchesInfo(
math.Abs(startTT-greatestTT) <= planetOccultationDiagramContactTimeDays &&
math.Abs(endTT-greatestTT) <= planetOccultationDiagramContactTimeDays
case OccultationTotal:
if math.Abs(internalImmersion.SeparationArcsec-internalImmersion.MoonRadiusArcsec+internalImmersion.PlanetRadiusArcsec) > planetOccultationDiagramGeometryArcsec ||
math.Abs(internalEmersion.SeparationArcsec-internalEmersion.MoonRadiusArcsec+internalEmersion.PlanetRadiusArcsec) > planetOccultationDiagramGeometryArcsec {
if checkContacts && (math.Abs(internalImmersion.SeparationArcsec-internalImmersion.MoonRadiusArcsec+internalImmersion.PlanetRadiusArcsec) > planetOccultationDiagramGeometryArcsec ||
math.Abs(internalEmersion.SeparationArcsec-internalEmersion.MoonRadiusArcsec+internalEmersion.PlanetRadiusArcsec) > planetOccultationDiagramGeometryArcsec) {
return false
}
return externalMetric < -planetOccultationGrazingToleranceArcsec &&
@@ -291,9 +295,10 @@ func planetOccultationDiagramLabelPriority(labels []string) int {
func planetOccultationDiagramFrameAt(
tt float64,
config planetOccultationConfig,
observer Observer,
) (PlanetOccultationDiagramFrame, bool) {
state := planetOccultationStateAt(tt, config, &observer, -1)
observer Observer, instances ...*Instance) (PlanetOccultationDiagramFrame, bool) {
inst := instanceValue(instances)
state := planetOccultationStateAt(tt, config, &observer, -1, inst)
if !state.valid {
return PlanetOccultationDiagramFrame{}, false
}