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
+244
View File
@@ -0,0 +1,244 @@
package catalog
import (
"bufio"
"fmt"
"io"
"math"
"strings"
"time"
"b612.me/astro/basic"
)
// ReadHipparcos2007 读取 Hipparcos 定宽星表 / read the Hipparcos fixed-width catalog.
func ReadHipparcos2007(r io.Reader, options ReadOptions) (*Catalog, error) {
catalog := newCatalog()
scanner := bufio.NewScanner(r)
scanner.Buffer(make([]byte, 64*1024), 2*1024*1024)
lineNumber := 0
for scanner.Scan() {
lineNumber++
line := scanner.Bytes()
if isIgnorableFixedLine(line) {
continue
}
entry, err := hipparcosEntry(line, lineNumber)
if err != nil {
if options.Strict {
return nil, err
}
catalog.skipped++
continue
}
if err := addEntry(catalog, entry, lineNumber, options); err != nil {
return nil, err
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
catalog.finalize()
return catalog, nil
}
func hipparcosEntry(line []byte, lineNumber int) (Entry, error) {
hip, present, err := fixedInt(line, 0, 6)
if err != nil || !present || hip <= 0 {
return Entry{}, fixedError(lineNumber, "invalid HIP")
}
ra, present, err := fixedFloat(line, 15, 28)
if err != nil || !present {
return Entry{}, fixedError(lineNumber, "invalid RArad")
}
dec, present, err := fixedFloat(line, 29, 42)
if err != nil || !present {
return Entry{}, fixedError(lineNumber, "invalid DErad")
}
parallax, _, err := fixedFloat(line, 43, 50)
if err != nil {
return Entry{}, fixedError(lineNumber, "invalid Plx")
}
pmRA, _, err := fixedFloat(line, 51, 59)
if err != nil {
return Entry{}, fixedError(lineNumber, "invalid pmRA")
}
pmDec, _, err := fixedFloat(line, 60, 68)
if err != nil {
return Entry{}, fixedError(lineNumber, "invalid pmDE")
}
magnitude, magnitudePresent, err := fixedFloat(line, 129, 136)
if err != nil {
return Entry{}, fixedError(lineNumber, "invalid Hpmag")
}
bv, bvPresent, err := fixedFloat(line, 152, 158)
if err != nil {
return Entry{}, fixedError(lineNumber, "invalid B-V")
}
id := fmt.Sprintf("HIP %d", hip)
// 非正视差按无视差处理;非有限值原样保留,交给 Validate 拒收。
if finite(parallax) && parallax <= 0 {
parallax = 0
}
if !magnitudePresent {
magnitude = math.NaN()
}
if !bvPresent {
bv = math.NaN()
}
return Entry{
ID: id,
Coordinate: parseCoordinate(id, angularFromRadians(ra), angularFromRadians(dec), pmRA, pmDec, parallax, 0,
julianYearTime(1991.25), basic.CoordinateFrameICRS),
Magnitude: magnitude,
Band: BandHp,
BPRP: math.NaN(),
BV: bv,
}, nil
}
// ReadBSC5 读取 BSC5 定宽星表 / read the BSC5 fixed-width catalog.
func ReadBSC5(r io.Reader, options ReadOptions) (*Catalog, error) {
catalog := newCatalog()
scanner := bufio.NewScanner(r)
scanner.Buffer(make([]byte, 64*1024), 2*1024*1024)
lineNumber := 0
for scanner.Scan() {
lineNumber++
line := scanner.Bytes()
if isIgnorableFixedLine(line) {
continue
}
entry, present, err := bsc5Entry(line, lineNumber)
if err != nil {
if options.Strict {
return nil, err
}
catalog.skipped++
continue
}
if !present {
catalog.skipped++
continue
}
if err := addEntry(catalog, entry, lineNumber, options); err != nil {
return nil, err
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
catalog.finalize()
return catalog, nil
}
func bsc5Entry(line []byte, lineNumber int) (Entry, bool, error) {
hr, present, err := fixedInt(line, 0, 4)
if err != nil {
return Entry{}, false, fixedError(lineNumber, "invalid HR")
}
if !present || hr <= 0 {
return Entry{}, false, nil
}
name, _ := fixedField(line, 4, 14)
hd, _, err := fixedInt(line, 25, 31)
if err != nil {
return Entry{}, false, fixedError(lineNumber, "invalid HD")
}
sao, _, err := fixedInt(line, 31, 37)
if err != nil {
return Entry{}, false, fixedError(lineNumber, "invalid SAO")
}
raHour, raHourPresent, err := fixedInt(line, 75, 77)
if err != nil {
return Entry{}, false, fixedError(lineNumber, "invalid RA hour")
}
raMinute, raMinutePresent, err := fixedInt(line, 77, 79)
if err != nil {
return Entry{}, false, fixedError(lineNumber, "invalid RA minute")
}
raSecond, raSecondPresent, err := fixedFloat(line, 79, 83)
if err != nil {
return Entry{}, false, fixedError(lineNumber, "invalid RA second")
}
decSign, decSignPresent := fixedField(line, 83, 84)
decDegree, decDegreePresent, err := fixedInt(line, 84, 86)
if err != nil {
return Entry{}, false, fixedError(lineNumber, "invalid Dec degree")
}
decMinute, decMinutePresent, err := fixedInt(line, 86, 88)
if err != nil {
return Entry{}, false, fixedError(lineNumber, "invalid Dec minute")
}
decSecond, decSecondPresent, err := fixedInt(line, 88, 90)
if err != nil {
return Entry{}, false, fixedError(lineNumber, "invalid Dec second")
}
if !raHourPresent || !raMinutePresent || !raSecondPresent || !decSignPresent || !decDegreePresent || !decMinutePresent || !decSecondPresent {
return Entry{}, false, nil
}
ra := (float64(raHour) + float64(raMinute)/60 + raSecond/3600) * 15
dec := float64(decDegree) + float64(decMinute)/60 + float64(decSecond)/3600
if strings.HasPrefix(decSign, "-") {
dec = -dec
}
vmag, vmPresent, err := fixedFloat(line, 102, 107)
if err != nil {
return Entry{}, false, fixedError(lineNumber, "invalid Vmag")
}
bv, bvPresent, err := fixedFloat(line, 109, 114)
if err != nil {
return Entry{}, false, fixedError(lineNumber, "invalid B-V")
}
pmRA, _, err := fixedFloat(line, 148, 154)
if err != nil {
return Entry{}, false, fixedError(lineNumber, "invalid pmRA")
}
pmDec, _, err := fixedFloat(line, 154, 160)
if err != nil {
return Entry{}, false, fixedError(lineNumber, "invalid pmDE")
}
parallax, _, err := fixedFloat(line, 161, 166)
if err != nil {
return Entry{}, false, fixedError(lineNumber, "invalid parallax")
}
radial, _, err := fixedInt(line, 166, 170)
if err != nil {
return Entry{}, false, fixedError(lineNumber, "invalid radial velocity")
}
id := fmt.Sprintf("HR %d", hr)
aliases := make([]string, 0, 3)
if name != "" {
aliases = append(aliases, strings.Join(strings.Fields(name), " "))
}
if hd > 0 {
aliases = append(aliases, fmt.Sprintf("HD %d", hd))
}
if sao > 0 {
aliases = append(aliases, fmt.Sprintf("SAO %d", sao))
}
if !vmPresent {
vmag = math.NaN()
}
if !bvPresent {
bv = math.NaN()
}
// 非正视差按无视差处理;非有限值原样保留,交给 Validate 拒收。
if finite(parallax) && parallax <= 0 {
parallax = 0
}
return Entry{
ID: id, Aliases: aliases,
Coordinate: parseCoordinate(id, ra, dec, pmRA*1000, pmDec*1000, parallax*1000, float64(radial), time.Date(2000, 1, 1, 12, 0, 0, 0, time.UTC), basic.CoordinateFrameJ2000),
Magnitude: vmag, Band: BandV, BPRP: math.NaN(), BV: bv,
}, true, nil
}
func isIgnorableFixedLine(line []byte) bool {
trimmed := strings.TrimSpace(string(line))
return trimmed == "" || strings.HasPrefix(trimmed, "#") || strings.HasPrefix(trimmed, "//")
}
func fixedError(line int, reason string) error {
return fmt.Errorf("record %d: %s", line, reason)
}