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
+153
View File
@@ -0,0 +1,153 @@
// Package limbtable decodes lunar limb profile tables without embedding a dataset.
package limbtable
import (
"bytes"
"compress/zlib"
"encoding/binary"
"errors"
"io"
"math"
)
const HeaderSize = 4 + 3*4 + 7*8
const maxCells = 1 << 24
// Profile is a lunar limb profile table.
type Profile struct {
lonCount, latCount, angleCount int
lonStart, lonStep float64
latStart, latStep float64
angleStep float64
referenceRadiusKM float64
minHeight, maxHeight float64
heights []float32
}
// Parse decodes an LLB1 profile table.
func Parse(data []byte) (*Profile, error) {
if len(data) < HeaderSize || string(data[:4]) != "LLB1" {
return nil, errors.New("limbtable: not an LLB1 table")
}
le := binary.LittleEndian
counts := [3]int{}
for index := range counts {
counts[index] = int(le.Uint32(data[4+4*index:]))
}
values := [7]float64{}
for index := range values {
values[index] = math.Float64frombits(le.Uint64(data[16+8*index:]))
}
profile := &Profile{
lonCount: counts[0], latCount: counts[1], angleCount: counts[2],
lonStart: values[0], lonStep: values[1],
latStart: values[2], latStep: values[3],
angleStep: values[4], referenceRadiusKM: values[5],
}
quantum := values[6]
finite := true
for _, value := range values {
finite = finite && isFinite(value)
}
if !finite || profile.lonCount < 2 || profile.latCount < 2 || profile.angleCount < 2 ||
!(profile.lonStep > 0) || !(profile.latStep > 0) || !(quantum > 0) || !(profile.referenceRadiusKM > 0) ||
math.Abs(profile.angleStep*float64(profile.angleCount)-360) > 1e-9 ||
profile.lonCount > maxCells/profile.latCount/profile.angleCount {
return nil, errors.New("limbtable: invalid LLB1 header")
}
reader, err := zlib.NewReader(bytes.NewReader(data[HeaderSize:]))
if err != nil {
return nil, err
}
defer reader.Close()
total := profile.lonCount * profile.latCount * profile.angleCount
raw := make([]byte, 2*total)
if _, err := io.ReadFull(reader, raw); err != nil {
return nil, err
}
row := profile.lonCount * profile.angleCount
levels := make([]int32, total)
for index := range levels {
levels[index] = int32(int16(le.Uint16(raw[2*index:])))
}
for index := row; index < total; index++ {
levels[index] += levels[index-row]
}
for lat := 0; lat < profile.latCount; lat++ {
base := lat * row
for index := base + profile.angleCount; index < base+row; index++ {
levels[index] += levels[index-profile.angleCount]
}
}
profile.heights = make([]float32, total)
profile.minHeight, profile.maxHeight = math.Inf(1), math.Inf(-1)
for index, level := range levels {
height := float32(float64(level) * quantum)
profile.heights[index] = height
profile.minHeight = math.Min(profile.minHeight, float64(height))
profile.maxHeight = math.Max(profile.maxHeight, float64(height))
}
return profile, nil
}
// LimbRadiusKM returns the Moon-centre to limb-point distance in kilometres.
func (profile *Profile) LimbRadiusKM(librationLongitude, librationLatitude, positionAngle float64) (float64, bool) {
if profile == nil || len(profile.heights) == 0 ||
!isFinite(librationLongitude) || !isFinite(librationLatitude) || !isFinite(positionAngle) {
return 0, false
}
lon, lonWeight := profile.cell(librationLongitude, profile.lonStart, profile.lonStep, profile.lonCount)
lat, latWeight := profile.cell(librationLatitude, profile.latStart, profile.latStep, profile.latCount)
angle := math.Mod(positionAngle/profile.angleStep, float64(profile.angleCount))
if angle < 0 {
angle += float64(profile.angleCount)
}
angle0 := int(angle)
if angle0 >= profile.angleCount {
angle0 = profile.angleCount - 1
}
angleWeight := angle - float64(angle0)
angle1 := (angle0 + 1) % profile.angleCount
height := 0.0
for dLat, wLat := range [2]float64{1 - latWeight, latWeight} {
for dLon, wLon := range [2]float64{1 - lonWeight, lonWeight} {
base := ((lat+dLat)*profile.lonCount + lon + dLon) * profile.angleCount
height += wLat * wLon * ((1-angleWeight)*float64(profile.heights[base+angle0]) +
angleWeight*float64(profile.heights[base+angle1]))
}
}
return profile.referenceRadiusKM + height/1000, true
}
// AngleStepDeg 位置角网格步长(度) / position-angle grid step in degrees.
func (profile *Profile) AngleStepDeg() float64 {
if profile == nil {
return 0
}
return profile.angleStep
}
// RadiusRangeKM 表内可能取到的最小、最大月缘半径(km) / smallest and largest limb radius.
func (profile *Profile) RadiusRangeKM() (float64, float64) {
if profile == nil || len(profile.heights) == 0 {
return 0, 0
}
return profile.referenceRadiusKM + profile.minHeight/1000, profile.referenceRadiusKM + profile.maxHeight/1000
}
func (profile *Profile) cell(value, start, step float64, count int) (int, float64) {
position := (value - start) / step
if position <= 0 {
return 0, 0
}
if position >= float64(count-1) {
return count - 2, 1
}
index := int(position)
return index, position - float64(index)
}
func isFinite(value float64) bool {
return !math.IsNaN(value) && !math.IsInf(value, 0)
}