feat: 完善时标与天象几何计算并扩展输出接口

- 新增时标、ΔT 模型、质心时间与 UT1 支持
- 改进日月食、月掩、行星事件及路径边界计算
- 完善恒星三维自行与动态距离传播
- 扩展 SVG、GeoJSON、KML 输出与底层距离换算工具
- 整理中英文手册、示例资源及回归测试
This commit is contained in:
2026-09-23 18:55:12 +08:00
parent 1f31a9b5b5
commit 16c62a97d5
503 changed files with 33290 additions and 9471 deletions
+24 -29
View File
@@ -179,11 +179,11 @@ func ApparentTopocentricEquatorial(date time.Time, elements Elements, observerLo
// Altitude 视高度角 / apparent altitude.
//
// 返回目标在观测者所在地的视高度角,单位度;经度东正西负,纬度北正南负,海拔单位米。
// 返回目标在观测者所在地的视高度角,单位度;经度东正西负,纬度北正南负,椭球高单位米。
// Returns the apparent altitude of the target for the observing site, in degrees. Longitude is east-positive, latitude is north-positive, and height is in meters.
func Altitude(date time.Time, elements Elements, observerLon, observerLat, observerHeight float64) float64 {
jde := basic.Date2JDE(date)
return basic.OrbitHeight(jde, observerLon, observerLat, observationTimezone(date), observerHeight, toBasicElements(elements))
localJD := basic.Date2JD(date)
return basic.OrbitHeight(localJD, observerLon, observerLat, observationTimezone(date), observerHeight, toBasicElements(elements))
}
// Zenith 天顶距 / zenith distance.
@@ -199,8 +199,8 @@ func Zenith(date time.Time, elements Elements, observerLon, observerLat, observe
// 返回目标在观测者所在地的视方位角,按正北为 0°、向东增加。
// Returns the apparent azimuth of the target for the observing site, measured from north toward east.
func Azimuth(date time.Time, elements Elements, observerLon, observerLat, observerHeight float64) float64 {
jde := basic.Date2JDE(date)
return basic.OrbitAzimuth(jde, observerLon, observerLat, observationTimezone(date), observerHeight, toBasicElements(elements))
localJD := basic.Date2JD(date)
return basic.OrbitAzimuth(localJD, observerLon, observerLat, observationTimezone(date), observerHeight, toBasicElements(elements))
}
// HourAngle 站心视时角 / topocentric hour angle.
@@ -208,8 +208,8 @@ func Azimuth(date time.Time, elements Elements, observerLon, observerLat, observ
// 返回目标在观测者所在地的站心视时角,单位度。
// Returns the apparent topocentric hour angle of the target for the observing site, in degrees.
func HourAngle(date time.Time, elements Elements, observerLon, observerLat, observerHeight float64) float64 {
jde := basic.Date2JDE(date)
return basic.OrbitHourAngle(jde, observerLon, observerLat, observationTimezone(date), observerHeight, toBasicElements(elements))
localJD := basic.Date2JD(date)
return basic.OrbitHourAngle(localJD, observerLon, observerLat, observationTimezone(date), observerHeight, toBasicElements(elements))
}
// CulminationTime 中天时刻 / culmination time.
@@ -217,13 +217,11 @@ func HourAngle(date time.Time, elements Elements, observerLon, observerLat, obse
// 返回目标在给定当地日期内的中天时刻,结果保持输入 `date` 的时区。
// Returns the culmination time of the target on the supplied local civil day. The result keeps the timezone of `date`.
func CulminationTime(date time.Time, elements Elements, observerLon, observerLat, observerHeight float64) time.Time {
if date.Hour() > 12 {
date = date.Add(-12 * time.Hour)
}
date = time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
timezone := observationTimezone(date)
jde := basic.Date2JDE(date)
calcJde := basic.OrbitCulminationTime(jde, observerLon, observerLat, timezone, observerHeight, toBasicElements(elements)) - timezone/24.0
return basic.JDE2DateByZone(calcJde, date.Location(), false)
localJD := basic.Date2JD(date)
calcJD := basic.OrbitCulminationTime(localJD, observerLon, observerLat, timezone, observerHeight, toBasicElements(elements)) - timezone/24.0
return basic.JD2DateByZone(calcJD, date.Location(), false)
}
// RiseTime 升起时刻 / rise time.
@@ -235,13 +233,11 @@ func RiseTime(date time.Time, elements Elements, observerLon, observerLat, obser
if aero {
aeroFloat = 1
}
if date.Hour() > 12 {
date = date.Add(-12 * time.Hour)
}
date = time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
timezone := observationTimezone(date)
jde := basic.Date2JDE(date)
calcJde, err := basic.OrbitRiseTime(jde, observerLon, observerLat, timezone, aeroFloat, observerHeight, toBasicElements(elements))
return orbitRiseSetResult(date, calcJde, err)
localJD := basic.Date2JD(date)
calcJD, err := basic.OrbitRiseTime(localJD, observerLon, observerLat, timezone, aeroFloat, observerHeight, toBasicElements(elements))
return orbitRiseSetResult(date, calcJD, err)
}
// SetTime 落下时刻 / set time.
@@ -253,16 +249,14 @@ func SetTime(date time.Time, elements Elements, observerLon, observerLat, observ
if aero {
aeroFloat = 1
}
if date.Hour() > 12 {
date = date.Add(-12 * time.Hour)
}
date = time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
timezone := observationTimezone(date)
jde := basic.Date2JDE(date)
calcJde, err := basic.OrbitSetTime(jde, observerLon, observerLat, timezone, aeroFloat, observerHeight, toBasicElements(elements))
return orbitRiseSetResult(date, calcJde, err)
localJD := basic.Date2JD(date)
calcJD, err := basic.OrbitSetTime(localJD, observerLon, observerLat, timezone, aeroFloat, observerHeight, toBasicElements(elements))
return orbitRiseSetResult(date, calcJD, err)
}
func orbitRiseSetResult(date time.Time, jde float64, err error) (time.Time, error) {
func orbitRiseSetResult(date time.Time, jd float64, err error) (time.Time, error) {
if err != nil {
switch {
case errors.Is(err, basic.ErrNeverRise):
@@ -273,7 +267,8 @@ func orbitRiseSetResult(date time.Time, jde float64, err error) (time.Time, erro
return time.Time{}, err
}
}
return basic.JDE2DateByZone(jde, date.Location(), true), nil
_, offset := date.Zone()
return basic.JD2DateByZone(jd-float64(offset)/86400, date.Location(), false), nil
}
func observationTimezone(date time.Time) float64 {
@@ -282,8 +277,8 @@ func observationTimezone(date time.Time) float64 {
}
func ttJulianDay(date time.Time) float64 {
jdeUTC := basic.Date2JDE(date.UTC())
return basic.TD2UT(jdeUTC, true)
jdUTC := basic.Date2JD(date.UTC())
return basic.UTC2TT(jdUTC)
}
func toBasicElements(elements Elements) basic.OrbitElements {
+4 -4
View File
@@ -90,7 +90,7 @@ func TestGeometricOrbitMatchesJPLBaseline(t *testing.T) {
basicElements := toBasicElements(elements)
for _, sample := range object.Samples {
date := basic.JDE2DateByZone(basic.TD2UT(sample.JDTT, false), time.UTC, false)
date := basic.JD2DateByZone(basic.TT2UTC(sample.JDTT), time.UTC, false)
helVector := basic.OrbitHeliocentricXYZJ2000(sample.JDTT, basicElements)
helVectorDiff := vectorDiffAU(helVector, sample.Heliocentric.Vector)
@@ -213,7 +213,7 @@ func runObservationBaseline(
for _, object := range objects {
elements := elementsFromBaseline(object)
for _, sample := range object.Samples {
date := basic.JDE2DateByZone(basic.TD2UT(sample.JDTT, false), time.UTC, false)
date := basic.JD2DateByZone(basic.TT2UTC(sample.JDTT), time.UTC, false)
got := gotFn(date, elements)
want := wantFn(sample)
raDiff := angleDiffAbs(got.RA, want.RA)
@@ -366,10 +366,10 @@ func TestObservationHelpersMatchTopocentricCoordinates(t *testing.T) {
}
}
jde := basic.Date2JDE(date)
jde := basic.Date2JD(date)
_, offsetSeconds := date.Zone()
timezone := float64(offsetSeconds) / 3600.0
siderealLongitude := normalize360(basic.ApparentSiderealTime(jde-timezone/24.0)*15 + shanghaiLon)
siderealLongitude := normalize360(basic.ApparentSiderealTime(basic.UTC2UT1(jde-timezone/24.0))*15 + shanghaiLon)
wantHourAngle := normalize360(siderealLongitude - topocentric.RA)
if angleDiffAbs(hourAngle, wantHourAngle) > 1e-9 {
t.Fatalf("hour angle mismatch: got %.12f want %.12f", hourAngle, wantHourAngle)
+1 -1
View File
@@ -14,7 +14,7 @@ func ParallacticAngle(date time.Time, elements Elements, observerLon, observerLa
// 时角与赤纬须取自同一次站心求解:分属两条儒略日路径(相差 1 ULP ≈ 40 µs)会引入 ≤2e-9 度漂移,
// 因此统一到同一时刻后的亚纳度量级输出变化是有意为之,不是纯性能改动。
_, dec, hourAngle := basic.OrbitHourAngleWithTopocentric(
basic.Date2JDE(date),
basic.Date2JD(date),
observerLon,
observerLat,
observationTimezone(date),
+8 -4
View File
@@ -15,7 +15,7 @@ func TestParallacticAngleMatchesHourAngleForm(t *testing.T) {
date := time.Date(2025, 11, 21, 20, 0, 0, 0, time.FixedZone("CST", 8*3600))
_, dec, _ := basic.OrbitHourAngleWithTopocentric(
basic.Date2JDE(date),
basic.Date2JD(date),
shanghaiLon, shanghaiLat, observationTimezone(date), shanghaiHeightMeters,
toBasicElements(elements),
)
@@ -41,8 +41,8 @@ func duplicatedSolveParallacticAngle(date time.Time, elements Elements, observer
}
func parallacticJDPathsAgree(date time.Time) bool {
utcPath := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
hourAnglePath := basic.TD2UT(basic.Date2JDE(date)-observationTimezone(date)/24.0, true)
utcPath := basic.UTC2TT(basic.Date2JD(date.UTC()))
hourAnglePath := basic.UTC2TT(basic.Date2JD(date) - observationTimezone(date)/24.0)
return utcPath == hourAnglePath
}
@@ -137,9 +137,13 @@ func parallacticRandomCases(count int) []parallacticCase {
return cases
}
// 固定民用时标换算政策,避免比较样本随默认值变化。
func TestParallacticAngleDifferentialAgainstDuplicatedSolve(t *testing.T) {
previous := basic.GetTimeScaleFuturePolicy()
basic.SetTimeScaleFuturePolicy(basic.TimeScaleLeapSecond)
t.Cleanup(func() { basic.SetTimeScaleFuturePolicy(previous) })
const (
maxAbsoluteTolerance = 2e-9
maxAbsoluteTolerance = 3e-9
maxRelativeTolerance = 5e-11
)
cases := append(parallacticFixedCases(), parallacticRandomCases(240)...)