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
+162 -69
View File
@@ -5,38 +5,38 @@ import (
"compress/gzip"
_ "embed"
"encoding/binary"
"errors"
"fmt"
"io"
"io/ioutil"
"math"
)
// star_catalog.dat gzip 解压后的布局 / star_catalog.dat layout after gzip decompression:
// magic[8] | version[1] | rawDataLen[4] | rawData | stringCount[2](字符串计数) |
// repeated(stringLen[uvarint] + stringBytes)(字符串表) |
// maxHR[2] | repeated(maxHR * 6 * stringIndex[2]) | repeated(maxHR * hip[4])(星表记录)。
const starCatalogMagic = "STRCAT01"
// starCatalogMagic star_catalog.dat 为 gzip 压缩、按列存储的 uvarint 整数格式。
const starCatalogMagic = "STRCAT02"
const starCatalogVersion = 2
const starCatalogDetailFields = 6
//go:embed star_catalog.dat
var starCatalogCompressed []byte
// supplementalStarDetails 保存嵌入载荷没有对应命名条目的星表元数据 / supplementalStarDetails contains catalog metadata that is maintained in
// 源码形式 / source form when the embedded payload has no corresponding named entry.
// 六个字段按编码顺序排列:中文名、别名、拜耳命名、通用名、中文星座和 IAU 星座 / The six fields follow the encoded detail order: Chinese name, alias, Bayer
// 命名、通用名、中文星座和 IAU 星座 / designation, common name, Chinese constellation, and IAU constellation.
// supplementalStarDetails 载荷中无命名条目的星表元数据,字段依次为中文名、别名、拜耳命名、通用名、中文星座、IAU 星座。
var supplementalStarDetails = map[uint16][]string{
4799: {"进贤增九", "", "室女座25", "", "室女座", "Virgo"},
}
func initStarCatalogData() []byte {
func initStarCatalogData() []InnerStarData {
reader, err := gzip.NewReader(bytes.NewReader(starCatalogCompressed))
if err != nil {
panic(err)
}
defer reader.Close()
payload, err := io.ReadAll(reader)
payload, err := ioutil.ReadAll(reader)
if err != nil {
panic(err)
}
data, detail, hip, err := decodeStarCatalogPayload(payload)
stars, detail, hip, err := decodeStarCatalogPayload(payload)
if err != nil {
panic(err)
}
@@ -45,87 +45,180 @@ func initStarCatalogData() []byte {
}
hr2detail = detail
hr2hip = hip
return data
return stars
}
func decodeStarCatalogPayload(payload []byte) ([]byte, map[uint16][]string, map[uint16]uint32, error) {
reader := bytes.NewReader(payload)
// starCatalogReader 顺序读取 uvarint 载荷。
type starCatalogReader struct {
data []byte
pos int
err error
}
var magic [len(starCatalogMagic)]byte
if _, err := io.ReadFull(reader, magic[:]); err != nil {
return nil, nil, nil, fmt.Errorf("read star catalog magic: %w", err)
func (r *starCatalogReader) uvarint() uint64 {
if r.err != nil {
return 0
}
if string(magic[:]) != starCatalogMagic {
return nil, nil, nil, fmt.Errorf("invalid star catalog magic %q", string(magic[:]))
value, n := binary.Uvarint(r.data[r.pos:])
if n <= 0 {
r.err = errors.New("truncated star catalog")
return 0
}
r.pos += n
return value
}
version, err := reader.ReadByte()
if err != nil {
return nil, nil, nil, fmt.Errorf("read star catalog version: %w", err)
func (r *starCatalogReader) bytes(n uint64) []byte {
if r.err != nil {
return nil
}
if version != 1 {
if n > uint64(len(r.data)-r.pos) {
r.err = errors.New("truncated star catalog")
return nil
}
value := r.data[r.pos : r.pos+int(n)]
r.pos += int(n)
return value
}
// 解码公式须与编码端一致,运算顺序不可调换,否则结果不再逐位相同。
// starCatalogScaled 解码符号-幅值定点数,保留 -0。
func starCatalogScaled(v uint64, scale float64) float64 {
value := float64(v>>1) / scale
if v&1 != 0 {
return math.Copysign(value, -1)
}
return value
}
func starCatalogRa(v uint64) float64 {
ra := float64(v / 36000)
minute := (v / 600) % 60
sec := float64(v%600) / 10
ra += float64(minute)/60 + sec/3600
return ra * 15
}
func starCatalogDec(v uint64) float64 {
total := v >> 1
dec := float64(total / 3600)
dec += float64((total/60)%60)/60 + float64(total%60)/3600
if v&1 != 0 {
dec = -dec
}
return dec
}
func starCatalogMag(v uint64) float64 {
if v == 0 {
return 9999.9999
}
return starCatalogScaled(v-1, 100)
}
func starCatalogPc(v uint64) float64 {
if rc := starCatalogScaled(v, 1000); rc != 0 {
return 1 / rc
}
return 0
}
func starCatalogZigzag(v uint64) int64 {
return int64(v>>1) ^ -int64(v&1)
}
func decodeStarCatalogPayload(payload []byte) ([]InnerStarData, map[uint16][]string, map[uint16]uint32, error) {
if len(payload) < len(starCatalogMagic)+1 || string(payload[:len(starCatalogMagic)]) != starCatalogMagic {
return nil, nil, nil, errors.New("invalid star catalog magic")
}
if version := payload[len(starCatalogMagic)]; version != starCatalogVersion {
return nil, nil, nil, fmt.Errorf("unsupported star catalog version %d", version)
}
var rawLen uint32
if err := binary.Read(reader, binary.LittleEndian, &rawLen); err != nil {
return nil, nil, nil, fmt.Errorf("read star catalog data length: %w", err)
r := &starCatalogReader{data: payload, pos: len(starCatalogMagic) + 1}
count := r.uvarint()
if r.err == nil && count > math.MaxUint16 {
return nil, nil, nil, fmt.Errorf("star catalog count %d out of range", count)
}
data := make([]byte, rawLen)
if _, err := io.ReadFull(reader, data); err != nil {
return nil, nil, nil, fmt.Errorf("read star catalog data: %w", err)
stars := make([]InnerStarData, count)
for i := range stars {
stars[i].HR = uint16(i + 1)
stars[i].Name = string(r.bytes(r.uvarint()))
}
hd := int64(0)
for i := range stars {
hd += starCatalogZigzag(r.uvarint())
stars[i].HD = uint32(hd)
}
for i := range stars {
stars[i].Ra = starCatalogRa(r.uvarint())
}
for i := range stars {
stars[i].Dec = starCatalogDec(r.uvarint())
}
for i := range stars {
stars[i].Mag = starCatalogMag(r.uvarint())
}
for i := range stars {
stars[i].PmRA = starCatalogScaled(r.uvarint(), 1000)
}
for i := range stars {
stars[i].PmDec = starCatalogScaled(r.uvarint(), 1000)
}
for i := range stars {
stars[i].RadVel = starCatalogScaled(r.uvarint(), 1)
}
for i := range stars {
stars[i].RotVel = starCatalogScaled(r.uvarint(), 1)
}
for i := range stars {
stars[i].Pc = starCatalogPc(r.uvarint())
}
var stringCount uint16
if err := binary.Read(reader, binary.LittleEndian, &stringCount); err != nil {
return nil, nil, nil, fmt.Errorf("read star catalog string count: %w", err)
stringCount := r.uvarint()
if r.err == nil && stringCount > uint64(len(payload)) {
return nil, nil, nil, fmt.Errorf("star catalog string count %d out of range", stringCount)
}
stringsTable := make([]string, int(stringCount)+1)
stringsTable := make([]string, stringCount+1)
for i := 1; i < len(stringsTable); i++ {
length, err := binary.ReadUvarint(reader)
if err != nil {
return nil, nil, nil, fmt.Errorf("read star catalog string length: %w", err)
}
value := make([]byte, length)
if _, err := io.ReadFull(reader, value); err != nil {
return nil, nil, nil, fmt.Errorf("read star catalog string: %w", err)
}
stringsTable[i] = string(value)
stringsTable[i] = string(r.bytes(r.uvarint()))
}
var maxHR uint16
if err := binary.Read(reader, binary.LittleEndian, &maxHR); err != nil {
return nil, nil, nil, fmt.Errorf("read star catalog max HR: %w", err)
indexes := make([][starCatalogDetailFields]uint64, count)
for k := 0; k < starCatalogDetailFields; k++ {
for i := range indexes {
indexes[i][k] = r.uvarint()
}
}
detail := make(map[uint16][]string)
for hr := uint16(1); hr <= maxHR; hr++ {
record := make([]string, 6)
for i, record := range indexes {
hasValue := false
for i := range record {
var index uint16
if err := binary.Read(reader, binary.LittleEndian, &index); err != nil {
return nil, nil, nil, fmt.Errorf("read star catalog detail index: %w", err)
values := make([]string, starCatalogDetailFields)
for k, index := range record {
if index >= uint64(len(stringsTable)) {
return nil, nil, nil, fmt.Errorf("detail string index out of range: hr=%d index=%d", i+1, index)
}
if int(index) >= len(stringsTable) {
return nil, nil, nil, fmt.Errorf("detail string index out of range: hr=%d index=%d", hr, index)
}
record[i] = stringsTable[index]
values[k] = stringsTable[index]
hasValue = hasValue || index != 0
}
if hasValue {
detail[hr] = record
detail[uint16(i+1)] = values
}
}
hip := make(map[uint16]uint32)
for hr := uint16(1); hr <= maxHR; hr++ {
var value uint32
if err := binary.Read(reader, binary.LittleEndian, &value); err != nil {
return nil, nil, nil, fmt.Errorf("read star catalog HIP: %w", err)
}
if value != 0 {
hip[hr] = value
prevHIP := int64(0)
for i := range stars {
value := r.uvarint()
if value == 0 {
continue
}
prevHIP += starCatalogZigzag(value - 1)
hip[uint16(i+1)] = uint32(prevHIP)
}
return data, detail, hip, nil
if r.err != nil {
return nil, nil, nil, r.err
}
if r.pos != len(payload) {
return nil, nil, nil, fmt.Errorf("%d trailing bytes in star catalog", len(payload)-r.pos)
}
return stars, detail, hip, nil
}