2026-05-01 22:38:44 +08:00
package basic
import (
"errors"
2026-10-18 16:13:14 +08:00
"fmt"
2026-08-06 12:00:56 +08:00
"math"
2026-10-18 16:13:14 +08:00
"sort"
2026-05-01 22:38:44 +08:00
"strings"
"sync"
"time"
)
2026-08-06 12:00:56 +08:00
// 本文件包含约 9100 颗亮星 / this file contains bright 9100 stars
2026-05-01 22:38:44 +08:00
// 9100颗亮星列表
type InnerStarData struct {
HR uint16 //Bright Star Number;[1/9110]+ Harvard Revised Number;亮星编号
Name string //Name, generally Bayer(如天狼星:Alpha CMA) and/or Flamsteed(如天狼星:9 CMA) name
HD uint32 //Henry Draper Catalog Number;HD星表编号
Ra float64 //Ra J2000;J2000历元赤经
Dec float64 //De J2000;J2000历元赤纬
Mag float64 //视星等
2026-08-06 12:00:56 +08:00
PmRA float64 //赤经投影年自行 cos(赤纬)*d赤经/dt,单位角秒/年
PmDec float64 //赤纬年自行,单位角秒/年
2026-05-01 22:38:44 +08:00
RadVel float64 //径向速度 km/s
RotVel float64 //自行速度 km/s
Pc float64 //秒差距
HIP uint32 //HIP星表编号
}
type StarData struct {
InnerStarData
ChineseName string
ChineseAlias string
ChineseBayerName string
CommonName string
CommonAliasName string
Cst string
CstChinese string
2026-10-18 16:13:14 +08:00
ChineseRegion string // 星名所属的宿/垣限定,用于区分同名星官 / mansion or enclosure qualifier.
2026-05-01 22:38:44 +08:00
}
2026-10-18 16:13:14 +08:00
var hr2name map [ uint16 ] starNameRecord
2026-05-01 22:38:44 +08:00
var hr2hip map [ uint16 ] uint32
2026-10-18 16:13:14 +08:00
var chnidx map [ string ][] uint16
var engidx map [ string ][] uint16
2026-05-01 22:38:44 +08:00
var parsedStarData [] InnerStarData
var cachedStarData [] StarData
var starDataOnce sync . Once
var starDataErr error
2026-10-18 16:13:14 +08:00
// ErrStarNotFound 未找到匹配的恒星 / no matching star.
var ErrStarNotFound = errors . New ( "star not found" )
// ErrStarNameAmbiguous 星名对应多颗恒星 / the star name matches more than one star.
var ErrStarNameAmbiguous = errors . New ( "ambiguous star name" )
2026-05-01 22:38:44 +08:00
func LoadStarData () error {
starDataOnce . Do ( func () {
starDataErr = initStarData ()
})
return starDataErr
}
func initStarData () error {
2026-10-15 23:54:15 +08:00
parsedStarData = initStarCatalogData ()
cachedStarData = make ([] StarData , len ( parsedStarData ))
for i , parsed := range parsedStarData {
2026-05-01 22:38:44 +08:00
cachedStarData [ i ] = fullStarData ( parsed )
}
2026-10-18 16:13:14 +08:00
chnidx = make ( map [ string ][] uint16 , len ( hr2name ) * 2 )
engidx = make ( map [ string ][] uint16 , len ( hr2name ))
2026-05-01 22:38:44 +08:00
for hr := 1 ; hr <= len ( cachedStarData ); hr ++ {
2026-10-18 16:13:14 +08:00
rec , ok := hr2name [ uint16 ( hr )]
2026-05-01 22:38:44 +08:00
if ! ok {
continue
}
2026-10-18 16:13:14 +08:00
registerStarChineseIndex ( uint16 ( hr ), rec . chinese )
for _ , alias := range rec . aliases {
registerStarChineseIndex ( uint16 ( hr ), alias )
if rec . region != "" {
registerStarChineseIndex ( uint16 ( hr ), alias + "[" + rec . region + "]" )
}
}
if rec . chinese != "" && rec . region != "" {
registerStarChineseIndex ( uint16 ( hr ), rec . chinese + "[" + rec . region + "]" )
}
addStarEnglishIndex ( uint16 ( hr ), rec . common )
2026-05-01 22:38:44 +08:00
}
return nil
}
func registerStarChineseIndex ( hr uint16 , name string ) {
if name == "" {
return
}
2026-10-18 16:13:14 +08:00
addStarChineseIndex ( hr , name )
2026-05-01 22:38:44 +08:00
if strings . HasSuffix ( name , "星" ) {
trimmed := strings . TrimSuffix ( name , "星" )
if trimmed != "" {
2026-10-18 16:13:14 +08:00
addStarChineseIndex ( hr , trimmed )
}
}
}
func addStarChineseIndex ( hr uint16 , name string ) {
list := chnidx [ name ]
for _ , existing := range list {
if existing == hr {
return
}
}
chnidx [ name ] = append ( list , hr )
}
func normalizeStarChineseName ( name string ) string {
name = strings . TrimSpace ( name )
if strings . HasSuffix ( name , "星" ) {
name = strings . TrimSuffix ( name , "星" )
}
return name
}
func normalizeStarEnglishName ( name string ) string {
return strings . ToLower ( strings . TrimSpace ( name ))
}
func addStarEnglishIndex ( hr uint16 , name string ) {
name = normalizeStarEnglishName ( name )
if name == "" {
return
}
list := engidx [ name ]
for _ , existing := range list {
if existing == hr {
return
}
}
engidx [ name ] = append ( list , hr )
}
func starDataByEnglishAll ( name string ) ([] StarData , error ) {
if err := LoadStarData (); err != nil {
return nil , err
}
name = normalizeStarEnglishName ( name )
hrs := engidx [ name ]
if len ( hrs ) == 0 {
return nil , fmt . Errorf ( "%w: %q" , ErrStarNotFound , name )
}
out := make ([] StarData , 0 , len ( hrs ))
for _ , hr := range hrs {
if hr == 0 || int ( hr ) > len ( cachedStarData ) {
continue
2026-05-01 22:38:44 +08:00
}
2026-10-18 16:13:14 +08:00
out = append ( out , cachedStarData [ hr - 1 ])
2026-05-01 22:38:44 +08:00
}
2026-10-18 16:13:14 +08:00
if len ( out ) == 0 {
return nil , fmt . Errorf ( "%w: %q" , ErrStarNotFound , name )
}
return out , nil
2026-05-01 22:38:44 +08:00
}
func fullStarData ( star InnerStarData ) StarData {
star . HIP = hr2hip [ star . HR ]
2026-10-18 16:13:14 +08:00
rec , ok := hr2name [ star . HR ]
if ! ok {
return StarData { InnerStarData : star }
}
return StarData {
InnerStarData : star ,
ChineseName : rec . chinese ,
ChineseAlias : strings . Join ( rec . aliases , ";" ),
ChineseRegion : rec . region ,
ChineseBayerName : rec . bayerZH ,
CommonName : rec . common ,
Cst : rec . cst ,
CstChinese : rec . cstZH ,
}
}
// StarDataByChinese 按中文名或别名查唯一恒星;同名多星返回 ErrStarNameAmbiguous / returns the unique star, or ErrStarNameAmbiguous for duplicate names.
func StarDataByChinese ( name string ) ( StarData , error ) {
matches , err := StarDataByChineseAll ( name )
if err != nil {
return StarData {}, err
}
if len ( matches ) != 1 {
return StarData {}, fmt . Errorf ( "%w: %q has %d candidates" , ErrStarNameAmbiguous , name , len ( matches ))
}
return matches [ 0 ], nil
}
// StarDataByChineseAll 返回中文名或别名的全部候选,按 HR 升序 / all candidates by HR order.
func StarDataByChineseAll ( name string ) ([] StarData , error ) {
if err := LoadStarData (); err != nil {
return nil , err
}
name = normalizeStarChineseName ( name )
hrs := chnidx [ name ]
if len ( hrs ) == 0 {
return nil , fmt . Errorf ( "%w: %q" , ErrStarNotFound , name )
}
out := make ([] StarData , 0 , len ( hrs ))
for _ , hr := range hrs {
if hr == 0 || int ( hr ) > len ( cachedStarData ) {
continue
2026-05-01 22:38:44 +08:00
}
2026-10-18 16:13:14 +08:00
out = append ( out , cachedStarData [ hr - 1 ])
2026-05-01 22:38:44 +08:00
}
2026-10-18 16:13:14 +08:00
if len ( out ) == 0 {
return nil , fmt . Errorf ( "%w: %q" , ErrStarNotFound , name )
}
return out , nil
}
2026-05-01 22:38:44 +08:00
2026-10-18 16:13:14 +08:00
// StarDataByName 按中文名、中文别名或英文常用名查唯一恒星;同名多星返回 ErrStarNameAmbiguous / returns the unique star by Chinese name, alias or English common name, or ErrStarNameAmbiguous for duplicate names.
func StarDataByName ( name string ) ( StarData , error ) {
matches , err := StarDataByNameAll ( name )
if err != nil {
return StarData {}, err
}
if len ( matches ) != 1 {
return StarData {}, fmt . Errorf ( "%w: %q has %d candidates" , ErrStarNameAmbiguous , name , len ( matches ))
}
return matches [ 0 ], nil
2026-05-01 22:38:44 +08:00
}
2026-10-18 16:13:14 +08:00
// StarDataByNameAll 返回中文名、别名或英文常用名的全部候选,按 HR 升序 / all candidates by HR order.
func StarDataByNameAll ( name string ) ([] StarData , error ) {
2026-05-01 22:38:44 +08:00
if err := LoadStarData (); err != nil {
2026-10-18 16:13:14 +08:00
return nil , err
}
chinese , chineseErr := StarDataByChineseAll ( name )
english , englishErr := starDataByEnglishAll ( name )
if chineseErr != nil && ! errors . Is ( chineseErr , ErrStarNotFound ) {
return nil , chineseErr
}
if englishErr != nil && ! errors . Is ( englishErr , ErrStarNotFound ) {
return nil , englishErr
}
matches := append ( append ([] StarData {}, chinese ... ), english ... )
sort . Slice ( matches , func ( i , j int ) bool { return matches [ i ]. HR < matches [ j ]. HR })
out := matches [: 0 ]
for _ , star := range matches {
if len ( out ) == 0 || out [ len ( out ) - 1 ]. HR != star . HR {
out = append ( out , star )
}
}
if len ( out ) == 0 {
return nil , fmt . Errorf ( "%w: %q" , ErrStarNotFound , name )
}
return out , nil
}
// StarDataByNameIn 按中文名、别名或英文常用名与宿/垣限定查唯一恒星 / looks up the unique star within a mansion or enclosure.
func StarDataByNameIn ( name , region string ) ( StarData , error ) {
region = strings . TrimSpace ( region )
if region == "" {
return StarDataByName ( name )
}
matches , err := StarDataByNameAll ( name )
if err != nil {
2026-05-01 22:38:44 +08:00
return StarData {}, err
}
2026-10-18 16:13:14 +08:00
var found [] StarData
for _ , star := range matches {
if star . ChineseRegion == region {
found = append ( found , star )
}
2026-05-01 22:38:44 +08:00
}
2026-10-18 16:13:14 +08:00
switch len ( found ) {
case 0 :
return StarData {}, fmt . Errorf ( "%w: %q in %q" , ErrStarNotFound , name , region )
case 1 :
return found [ 0 ], nil
default :
return StarData {}, fmt . Errorf ( "%w: %q in %q has %d candidates" , ErrStarNameAmbiguous , name , region , len ( found ))
}
}
// StarDataByChineseIn 按中文名或别名与宿/垣限定查唯一恒星 / looks up the unique star within a mansion or enclosure.
func StarDataByChineseIn ( name , region string ) ( StarData , error ) {
region = strings . TrimSpace ( region )
if region == "" {
return StarDataByChinese ( name )
}
matches , err := StarDataByChineseAll ( name )
if err != nil {
return StarData {}, err
}
var found [] StarData
for _ , star := range matches {
if star . ChineseRegion == region {
found = append ( found , star )
}
}
switch len ( found ) {
case 0 :
return StarData {}, fmt . Errorf ( "%w: %q in %q" , ErrStarNotFound , name , region )
case 1 :
return found [ 0 ], nil
default :
return StarData {}, fmt . Errorf ( "%w: %q in %q has %d candidates" , ErrStarNameAmbiguous , name , region , len ( found ))
2026-05-01 22:38:44 +08:00
}
}
func StarDataByHR ( hr int ) ( StarData , error ) {
if err := LoadStarData (); err != nil {
return StarData {}, err
}
if hr <= 0 || hr > len ( cachedStarData ) {
2026-10-18 16:13:14 +08:00
return StarData {}, fmt . Errorf ( "%w: HR %d" , ErrStarNotFound , hr )
2026-05-01 22:38:44 +08:00
}
return cachedStarData [ hr - 1 ], nil
}
2026-09-23 18:55:12 +08:00
// RaDecByJde 按 TT 儒略日归算恒星位置,单位为度 / star position in degrees for a TT Julian date.
// Pc>0 时按三维空间运动推进(自行 + RadVel),否则退回只推进角分量的二维。
// With Pc>0 the position advances in three dimensions (proper motion plus radial velocity); otherwise only the angular components advance.
2026-05-01 22:38:44 +08:00
func ( s InnerStarData ) RaDecByJde ( jde float64 ) ( float64 , float64 ) {
2026-09-23 18:55:12 +08:00
if s . distanceAU () > 0 {
return s . raDecByJde3D ( jde )
}
return s . raDecByJde2D ( jde )
}
// RaDecByDate 按给定时刻归算恒星位置,单位为度 / star position in degrees at the given instant.
// 传入时刻按 UTC 解释并换算成 TT 儒略日,与 RaDecByJde 同口径。
// The instant is read as UTC and converted to a TT Julian date, the same convention as RaDecByJde.
func ( s StarData ) RaDecByDate ( date time . Time ) ( float64 , float64 ) {
// 星表历元与岁差都按 TT 度量,自行归算的历元差必须用 TT 儒略日。
jde := UTC2TT ( Date2JD ( date . UTC ()))
return s . RaDecByJde ( jde )
}
func ( s InnerStarData ) distanceAU () float64 {
if s . Pc <= 0 {
return 0
}
// 秒差距到天文单位:648000/π 为定义值。
return s . Pc * 648000 / math . Pi
}
// raDecByJde2D 只推进赤经赤纬两个角分量,等价于把恒星当作无穷远。
func ( s InnerStarData ) raDecByJde2D ( jde float64 ) ( float64 , float64 ) {
// 自行按儒略年计:J2000 是儒略历元,年长 365.25 日而非回归年。
// pmRA 是投影自行 cos(Dec)*dRA/dt,不是 dRA/dt。
year := ( jde - 2451545.0 ) / 365.25
2026-08-06 12:00:56 +08:00
dec := s . Dec + year * s . PmDec / 3600
cosDec := math . Cos ( s . Dec * math . Pi / 180 )
ra := s . Ra
if math . Abs ( cosDec ) > 1e-12 {
ra += year * s . PmRA / ( 3600 * cosDec )
}
return Precess ( ra , dec , 2451545.0 , jde )
2026-05-01 22:38:44 +08:00
}
2026-09-23 18:55:12 +08:00
// raDecByJde3D 按三维匀速直线运动推进位置矢量,赤经赤纬只是其方向。
func ( s InnerStarData ) raDecByJde3D ( jde float64 ) ( float64 , float64 ) {
year := ( jde - 2451545.0 ) / 365.25
raRad := s . Ra * math . Pi / 180
decRad := s . Dec * math . Pi / 180
sinDec , cosDec := math . Sin ( decRad ), math . Cos ( decRad )
distance := s . distanceAU ()
// 切向速度 AU/年 = 角速度(rad/年) × 距离;径向速度 km/s 换算为 AU/年。
pmRA := s . PmRA * math . Pi / ( 180 * 3600 ) * distance
pmDec := s . PmDec * math . Pi / ( 180 * 3600 ) * distance
radial := s . RadVel * 365.25 * 86400 / 149597870.7
cosRA , sinRA := math . Cos ( raRad ), math . Sin ( raRad )
position := [ 3 ] float64 {
distance * cosDec * cosRA + year * ( pmDec * ( - sinDec * cosRA ) - pmRA * sinRA + radial * cosDec * cosRA ),
distance * cosDec * sinRA + year * ( pmDec * ( - sinDec * sinRA ) + pmRA * cosRA + radial * cosDec * sinRA ),
distance * sinDec + year * ( pmDec * cosDec + radial * sinDec ),
}
norm := math . Sqrt ( position [ 0 ] * position [ 0 ] + position [ 1 ] * position [ 1 ] + position [ 2 ] * position [ 2 ])
ra := math . Atan2 ( position [ 1 ], position [ 0 ]) * 180 / math . Pi
if ra < 0 {
ra += 360
}
dec := math . Asin ( position [ 2 ] / norm ) * 180 / math . Pi
return Precess ( ra , dec , 2451545.0 , jde )
2026-05-01 22:38:44 +08:00
}