feat: 引入可切换星历模型并扩展天象观测链路
- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算 - 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询 - 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出 - 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解 - 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤 - 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持 - 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理 - 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
This commit is contained in:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user