feat: 扩展天文计算能力
- 新增日食、月食、本地可见性、中心线、半影区域、SVG 图示与沙罗周期信息 - 新增行星冲合、留、方照、物理星历、视直径、相位、亮肢角、轨道节点等计算 - 新增木星伽利略卫星位置、现象与接触事件计算 - 新增恒星星表、星座判定、自行修正与观测辅助能力 - 新增 coord、formula、orbit、sundial、lite/sun、lite/moon 等扩展包 - 完善农历年号、月相英文别名、视差角、大气质量、折射、日晷与双星计算 - 增加 NASA、JPL Horizons、IMCCE 等回归测试数据与基线测试 - 重构基础算法文件组织,补充大量公开 API 注释和语义回归测试 - 更新中文和英文 README,补充示例、精度说明、SVG 配图
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
package jupiter
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"b612.me/astro/basic"
|
||||
"b612.me/astro/calendar"
|
||||
)
|
||||
|
||||
// Semidiameter 木星视半径,单位角秒 / apparent Jupiter semidiameter in arcseconds.
|
||||
func Semidiameter(date time.Time) float64 {
|
||||
return SemidiameterN(date, -1)
|
||||
}
|
||||
|
||||
// SemidiameterN 木星视半径(截断版),单位角秒 / truncated apparent Jupiter semidiameter in arcseconds.
|
||||
func SemidiameterN(date time.Time, n int) float64 {
|
||||
jde := calendar.Date2JDE(date.UTC())
|
||||
return basic.JupiterSemidiameterN(basic.TD2UT(jde, true), n)
|
||||
}
|
||||
|
||||
// Diameter 木星视直径,单位角秒 / apparent Jupiter diameter in arcseconds.
|
||||
func Diameter(date time.Time) float64 {
|
||||
return DiameterN(date, -1)
|
||||
}
|
||||
|
||||
// DiameterN 木星视直径(截断版),单位角秒 / truncated apparent Jupiter diameter in arcseconds.
|
||||
func DiameterN(date time.Time, n int) float64 {
|
||||
jde := calendar.Date2JDE(date.UTC())
|
||||
return basic.JupiterDiameterN(basic.TD2UT(jde, true), n)
|
||||
}
|
||||
+145
-84
@@ -15,63 +15,115 @@ var (
|
||||
ERR_JUPITER_NEVER_DOWN = ERR_JUPITER_NEVER_SET
|
||||
)
|
||||
|
||||
// ApparentLo 视黄经
|
||||
func riseSetResult(date time.Time, jde float64, err error) (time.Time, error) {
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, basic.ErrNeverRise):
|
||||
return time.Time{}, ERR_JUPITER_NEVER_RISE
|
||||
case errors.Is(err, basic.ErrNeverSet):
|
||||
return time.Time{}, ERR_JUPITER_NEVER_SET
|
||||
default:
|
||||
return time.Time{}, err
|
||||
}
|
||||
}
|
||||
return basic.JDE2DateByZone(jde, date.Location(), true), nil
|
||||
}
|
||||
|
||||
// ApparentLo 视黄经 / apparent ecliptic longitude.
|
||||
//
|
||||
// 返回木星在 date 对应绝对时刻的瞬时视黄经,单位度。
|
||||
// Returns the apparent ecliptic longitude of Jupiter at the instant represented by date, in degrees.
|
||||
func ApparentLo(date time.Time) float64 {
|
||||
jde := calendar.Date2JDE(date)
|
||||
jde := calendar.Date2JDE(date.UTC())
|
||||
return basic.JupiterApparentLo(basic.TD2UT(jde, true))
|
||||
}
|
||||
|
||||
// ApparentBo 视黄纬
|
||||
// ApparentBo 视黄纬 / apparent ecliptic latitude.
|
||||
//
|
||||
// 返回木星在 date 对应绝对时刻的瞬时视黄纬,单位度。
|
||||
// Returns the apparent ecliptic latitude of Jupiter at the instant represented by date, in degrees.
|
||||
func ApparentBo(date time.Time) float64 {
|
||||
jde := calendar.Date2JDE(date)
|
||||
jde := calendar.Date2JDE(date.UTC())
|
||||
return basic.JupiterApparentBo(basic.TD2UT(jde, true))
|
||||
}
|
||||
|
||||
// ApparentRa 视赤经
|
||||
// ApparentRa 视赤经 / apparent right ascension.
|
||||
//
|
||||
// 返回木星在 date 对应绝对时刻的瞬时视赤经,单位度。
|
||||
// Returns the apparent right ascension of Jupiter at the instant represented by date, in degrees.
|
||||
func ApparentRa(date time.Time) float64 {
|
||||
jde := calendar.Date2JDE(date)
|
||||
jde := calendar.Date2JDE(date.UTC())
|
||||
return basic.JupiterApparentRa(basic.TD2UT(jde, true))
|
||||
}
|
||||
|
||||
// ApparentDec 视赤纬
|
||||
// ApparentDec 视赤纬 / apparent declination.
|
||||
//
|
||||
// 返回木星在 date 对应绝对时刻的瞬时视赤纬,单位度。
|
||||
// Returns the apparent declination of Jupiter at the instant represented by date, in degrees.
|
||||
func ApparentDec(date time.Time) float64 {
|
||||
jde := calendar.Date2JDE(date)
|
||||
jde := calendar.Date2JDE(date.UTC())
|
||||
return basic.JupiterApparentDec(basic.TD2UT(jde, true))
|
||||
}
|
||||
|
||||
// ApparentRaDec 视赤经赤纬
|
||||
// ApparentRaDec 视赤经、视赤纬 / apparent right ascension and declination.
|
||||
//
|
||||
// 返回木星在 date 对应绝对时刻的瞬时视赤经与视赤纬,单位度。
|
||||
// Returns the apparent right ascension and declination of Jupiter at the instant represented by date, in degrees.
|
||||
func ApparentRaDec(date time.Time) (float64, float64) {
|
||||
jde := calendar.Date2JDE(date)
|
||||
jde := calendar.Date2JDE(date.UTC())
|
||||
return basic.JupiterApparentRaDec(basic.TD2UT(jde, true))
|
||||
}
|
||||
|
||||
// ApparentMagnitude 视星等
|
||||
// ApparentMagnitude 视星等 / apparent magnitude.
|
||||
//
|
||||
// 返回木星在 date 对应绝对时刻的视星等。
|
||||
// Returns the apparent visual magnitude of Jupiter at the instant represented by date.
|
||||
func ApparentMagnitude(date time.Time) float64 {
|
||||
jde := calendar.Date2JDE(date)
|
||||
jde := calendar.Date2JDE(date.UTC())
|
||||
return basic.JupiterMag(basic.TD2UT(jde, true))
|
||||
}
|
||||
|
||||
// EarthDistance 与地球距离(天文单位)
|
||||
// EarthDistance 地心距离 / Earth distance.
|
||||
//
|
||||
// 返回木星在 date 对应绝对时刻到地球的距离,单位 AU。
|
||||
// Returns the distance from Jupiter to Earth at the instant represented by date, in astronomical units.
|
||||
func EarthDistance(date time.Time) float64 {
|
||||
jde := calendar.Date2JDE(date)
|
||||
jde := calendar.Date2JDE(date.UTC())
|
||||
return basic.EarthJupiterAway(basic.TD2UT(jde, true))
|
||||
}
|
||||
|
||||
// EarthDistance 与太阳距离(天文单位)
|
||||
// SunDistance 日心距离 / Sun distance.
|
||||
//
|
||||
// 返回木星在 date 对应绝对时刻到太阳的距离,单位 AU。
|
||||
// Returns the distance from Jupiter to the Sun at the instant represented by date, in astronomical units.
|
||||
func SunDistance(date time.Time) float64 {
|
||||
jde := calendar.Date2JDE(date)
|
||||
jde := calendar.Date2JDE(date.UTC())
|
||||
return planet.WherePlanet(4, 2, basic.TD2UT(jde, true))
|
||||
}
|
||||
|
||||
// Zenith 高度角
|
||||
func Zenith(date time.Time, lon, lat float64) float64 {
|
||||
// Altitude 高度角 / altitude.
|
||||
//
|
||||
// date 表示观测时刻,会读取其时区参与地方时计算;lon 为观测者经度,东正西负;lat 为观测者纬度,北正南负。返回值单位度。
|
||||
// date is the observing instant and its zone offset participates in local-time calculations. lon is east-positive longitude, lat is north-positive latitude, and the result is in degrees.
|
||||
func Altitude(date time.Time, lon, lat float64) float64 {
|
||||
jde := basic.Date2JDE(date)
|
||||
_, loc := date.Zone()
|
||||
timezone := float64(loc) / 3600.0
|
||||
return basic.JupiterHeight(jde, lon, lat, timezone)
|
||||
}
|
||||
|
||||
// Azimuth 方位角
|
||||
// Zenith 天顶距 / zenith distance.
|
||||
//
|
||||
// 参数与 Altitude 相同,返回值为对应时刻的天顶距,单位度。
|
||||
// Uses the same inputs as Altitude and returns the zenith distance in degrees.
|
||||
func Zenith(date time.Time, lon, lat float64) float64 {
|
||||
return 90 - Altitude(date, lon, lat)
|
||||
}
|
||||
|
||||
// Azimuth 方位角 / azimuth.
|
||||
//
|
||||
// date 表示观测时刻,会读取其时区参与地方时计算;lon 为观测者经度,东正西负;lat 为观测者纬度,北正南负。返回值按正北为 0°、向东增加。
|
||||
// date is the observing instant and its zone offset participates in local-time calculations. lon is east-positive longitude, lat is north-positive latitude, and azimuth is measured from north toward east.
|
||||
func Azimuth(date time.Time, lon, lat float64) float64 {
|
||||
jde := basic.Date2JDE(date)
|
||||
_, loc := date.Zone()
|
||||
@@ -79,8 +131,10 @@ func Azimuth(date time.Time, lon, lat float64) float64 {
|
||||
return basic.JupiterAzimuth(jde, lon, lat, timezone)
|
||||
}
|
||||
|
||||
// HourAngle 时角
|
||||
// 返回给定经纬度、对应date时区date时刻的时角(
|
||||
// HourAngle 时角 / hour angle.
|
||||
//
|
||||
// date 表示观测时刻,会读取其时区参与地方时计算;lon 为观测者经度,东正西负。返回值单位度。
|
||||
// date is the observing instant and its zone offset participates in local-time calculations. lon is east-positive longitude and the returned hour angle is in degrees.
|
||||
func HourAngle(date time.Time, lon float64) float64 {
|
||||
jde := basic.Date2JDE(date)
|
||||
_, loc := date.Zone()
|
||||
@@ -88,8 +142,10 @@ func HourAngle(date time.Time, lon float64) float64 {
|
||||
return basic.JupiterHourAngle(jde, lon, timezone)
|
||||
}
|
||||
|
||||
// CulminationTime 中天时间
|
||||
// 返回给定经纬度、对应date时区date时刻的中天日期
|
||||
// CulminationTime 中天时刻 / culmination time.
|
||||
//
|
||||
// date 取其所在时区的当地日期,返回值保持相同时区;lon 为观测者经度,东正西负。
|
||||
// date is interpreted on its local civil day and the result keeps the same time zone. lon is east-positive longitude.
|
||||
func CulminationTime(date time.Time, lon float64) time.Time {
|
||||
if date.Hour() > 12 {
|
||||
date = date.Add(time.Hour * -12)
|
||||
@@ -101,14 +157,11 @@ func CulminationTime(date time.Time, lon float64) time.Time {
|
||||
return basic.JDE2DateByZone(calcJde, date.Location(), false)
|
||||
}
|
||||
|
||||
// RiseTime 升起时间
|
||||
// date,取日期,时区忽略
|
||||
// lon,经度,东正西负
|
||||
// lat,纬度,北正南负
|
||||
// height,高度
|
||||
// aero,true时进行大气修正
|
||||
// RiseTime 升起时间 / rise time.
|
||||
//
|
||||
// date 取其所在时区的当地日期,返回值保持相同时区;lon 为东正西负经度,lat 为北正南负纬度;height 为观测点海拔高度(米);aero 为 true 时加入标准大气折射。
|
||||
// date is interpreted on its local civil day and the result keeps the same time zone. lon is east-positive longitude, lat is north-positive latitude, height is observer elevation in meters, and aero enables standard atmospheric refraction.
|
||||
func RiseTime(date time.Time, lon, lat, height float64, aero bool) (time.Time, error) {
|
||||
var err error
|
||||
var aeroFloat float64
|
||||
if aero {
|
||||
aeroFloat = 1
|
||||
@@ -119,35 +172,25 @@ func RiseTime(date time.Time, lon, lat, height float64, aero bool) (time.Time, e
|
||||
jde := basic.Date2JDE(date)
|
||||
_, loc := date.Zone()
|
||||
timezone := float64(loc) / 3600.0
|
||||
riseJde := basic.JupiterRiseTime(jde, lon, lat, timezone, aeroFloat, height)
|
||||
if riseJde == -2 {
|
||||
err = ERR_JUPITER_NEVER_RISE
|
||||
}
|
||||
if riseJde == -1 {
|
||||
err = ERR_JUPITER_NEVER_SET
|
||||
}
|
||||
return basic.JDE2DateByZone(riseJde, date.Location(), true), err
|
||||
riseJde, err := basic.JupiterRiseTime(jde, lon, lat, timezone, aeroFloat, height)
|
||||
return riseSetResult(date, riseJde, err)
|
||||
}
|
||||
|
||||
// deprecated: -- use SetTime instead
|
||||
// DownTime 落下时间
|
||||
// date,取日期,时区忽略
|
||||
// lon,经度,东正西负
|
||||
// lat,纬度,北正南负
|
||||
// height,高度
|
||||
// aero,true时进行大气修正
|
||||
// DownTime 落下时间别名 / deprecated set-time alias.
|
||||
//
|
||||
// Deprecated: use SetTime instead.
|
||||
//
|
||||
// 参数与 SetTime 相同,仅为兼容旧接口保留。
|
||||
// Same as SetTime and kept only for backward compatibility.
|
||||
func DownTime(date time.Time, lon, lat, height float64, aero bool) (time.Time, error) {
|
||||
return SetTime(date, lon, lat, height, aero)
|
||||
}
|
||||
|
||||
// SetTime 落下时间
|
||||
// date,取日期,时区忽略
|
||||
// lon,经度,东正西负
|
||||
// lat,纬度,北正南负
|
||||
// height,高度
|
||||
// aero,true时进行大气修正
|
||||
// SetTime 落下时间 / set time.
|
||||
//
|
||||
// 参数与 RiseTime 相同,返回给定当地日期内的落下时刻。
|
||||
// Uses the same inputs as RiseTime and returns the set time on the corresponding local civil day.
|
||||
func SetTime(date time.Time, lon, lat, height float64, aero bool) (time.Time, error) {
|
||||
var err error
|
||||
var aeroFloat float64
|
||||
if aero {
|
||||
aeroFloat = 1
|
||||
@@ -158,95 +201,113 @@ func SetTime(date time.Time, lon, lat, height float64, aero bool) (time.Time, er
|
||||
jde := basic.Date2JDE(date)
|
||||
_, loc := date.Zone()
|
||||
timezone := float64(loc) / 3600.0
|
||||
riseJde := basic.JupiterDownTime(jde, lon, lat, timezone, aeroFloat, height)
|
||||
if riseJde == -2 {
|
||||
err = ERR_JUPITER_NEVER_RISE
|
||||
}
|
||||
if riseJde == -1 {
|
||||
err = ERR_JUPITER_NEVER_SET
|
||||
}
|
||||
return basic.JDE2DateByZone(riseJde, date.Location(), true), err
|
||||
riseJde, err := basic.JupiterSetTime(jde, lon, lat, timezone, aeroFloat, height)
|
||||
return riseSetResult(date, riseJde, err)
|
||||
}
|
||||
|
||||
// LastConjunction 上次合日时间
|
||||
// 返回上次合日时间
|
||||
// LastConjunction 上一次合日 / previous conjunction with the Sun.
|
||||
//
|
||||
// 返回 date 之前最近一次与太阳的合日时刻,结果保持 date 的时区。
|
||||
// Returns the most recent conjunction with the Sun relative to date, keeping date's time zone.
|
||||
func LastConjunction(date time.Time) time.Time {
|
||||
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
|
||||
return basic.JDE2DateByZone(basic.LastJupiterConjunction(jde), date.Location(), false)
|
||||
}
|
||||
|
||||
// NextConjunction 下次合日时间
|
||||
// 返回下次合日时间
|
||||
// NextConjunction 下一次合日 / next conjunction with the Sun.
|
||||
//
|
||||
// 返回 date 之后最近一次与太阳的合日时刻,结果保持 date 的时区。
|
||||
// Returns the next conjunction with the Sun relative to date, keeping date's time zone.
|
||||
func NextConjunction(date time.Time) time.Time {
|
||||
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
|
||||
return basic.JDE2DateByZone(basic.NextJupiterConjunction(jde), date.Location(), false)
|
||||
}
|
||||
|
||||
// LastOpposition 上次冲日时间
|
||||
// 返回上次冲日时间
|
||||
// LastOpposition 上一次冲日 / previous opposition.
|
||||
//
|
||||
// 返回 date 之前最近一次冲日时刻,结果保持 date 的时区。
|
||||
// Returns the most recent opposition relative to date, keeping date's time zone.
|
||||
func LastOpposition(date time.Time) time.Time {
|
||||
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
|
||||
return basic.JDE2DateByZone(basic.LastJupiterOpposition(jde), date.Location(), false)
|
||||
}
|
||||
|
||||
// NextOpposition 下次冲日时间
|
||||
// 返回下次冲日时间
|
||||
// NextOpposition 下一次冲日 / next opposition.
|
||||
//
|
||||
// 返回 date 之后最近一次冲日时刻,结果保持 date 的时区。
|
||||
// Returns the next opposition relative to date, keeping date's time zone.
|
||||
func NextOpposition(date time.Time) time.Time {
|
||||
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
|
||||
return basic.JDE2DateByZone(basic.NextJupiterOpposition(jde), date.Location(), false)
|
||||
}
|
||||
|
||||
// LastProgradeToRetrograde 上次留(顺转逆)
|
||||
// 返回上次顺转逆留的时间
|
||||
// LastProgradeToRetrograde 上一次顺行转逆行留 / previous station from prograde to retrograde.
|
||||
//
|
||||
// 返回 date 之前最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
|
||||
// Returns the most recent stationary point where motion changes from prograde to retrograde relative to date, keeping date's time zone.
|
||||
func LastProgradeToRetrograde(date time.Time) time.Time {
|
||||
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
|
||||
return basic.JDE2DateByZone(basic.LastJupiterProgradeToRetrograde(jde), date.Location(), false)
|
||||
}
|
||||
|
||||
// NextProgradeToRetrograde 下次留(顺转逆)
|
||||
// 返回下次顺转逆留的时间
|
||||
// NextProgradeToRetrograde 下一次顺行转逆行留 / next station from prograde to retrograde.
|
||||
//
|
||||
// 返回 date 之后最近一次由顺行转为逆行的留时刻,结果保持 date 的时区。
|
||||
// Returns the next stationary point where motion changes from prograde to retrograde relative to date, keeping date's time zone.
|
||||
func NextProgradeToRetrograde(date time.Time) time.Time {
|
||||
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
|
||||
return basic.JDE2DateByZone(basic.NextJupiterProgradeToRetrograde(jde), date.Location(), false)
|
||||
}
|
||||
|
||||
// LastRetrogradeToPrograde 上次留(逆转瞬)
|
||||
// 返回上次逆转瞬留的时间
|
||||
// LastRetrogradeToPrograde 上一次逆行转顺行留 / previous station from retrograde to prograde.
|
||||
//
|
||||
// 返回 date 之前最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
|
||||
// Returns the most recent stationary point where motion changes from retrograde to prograde relative to date, keeping date's time zone.
|
||||
func LastRetrogradeToPrograde(date time.Time) time.Time {
|
||||
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
|
||||
return basic.JDE2DateByZone(basic.LastJupiterRetrogradeToPrograde(jde), date.Location(), false)
|
||||
}
|
||||
|
||||
// NextRetrogradeToPrograde 上次留(逆转瞬)
|
||||
// // 返回上次逆转瞬留的时间
|
||||
// NextRetrogradeToPrograde 下一次逆行转顺行留 / next station from retrograde to prograde.
|
||||
//
|
||||
// 返回 date 之后最近一次由逆行转为顺行的留时刻,结果保持 date 的时区。
|
||||
// Returns the next stationary point where motion changes from retrograde to prograde relative to date, keeping date's time zone.
|
||||
func NextRetrogradeToPrograde(date time.Time) time.Time {
|
||||
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
|
||||
return basic.JDE2DateByZone(basic.NextJupiterRetrogradeToPrograde(jde), date.Location(), false)
|
||||
}
|
||||
|
||||
// LastEasternQuadrature 上次东方照时间
|
||||
// 返回上次东方照时间
|
||||
// LastEasternQuadrature 上一次东方照 / previous eastern quadrature.
|
||||
//
|
||||
// 返回 date 之前最近一次东方照时刻,结果保持 date 的时区。
|
||||
// Returns the most recent eastern quadrature relative to date, keeping date's time zone.
|
||||
func LastEasternQuadrature(date time.Time) time.Time {
|
||||
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
|
||||
return basic.JDE2DateByZone(basic.LastJupiterEasternQuadrature(jde), date.Location(), false)
|
||||
}
|
||||
|
||||
// NextEasternQuadrature 下次东方照时间
|
||||
// 返回下次东方照时间
|
||||
// NextEasternQuadrature 下一次东方照 / next eastern quadrature.
|
||||
//
|
||||
// 返回 date 之后最近一次东方照时刻,结果保持 date 的时区。
|
||||
// Returns the next eastern quadrature relative to date, keeping date's time zone.
|
||||
func NextEasternQuadrature(date time.Time) time.Time {
|
||||
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
|
||||
return basic.JDE2DateByZone(basic.NextJupiterEasternQuadrature(jde), date.Location(), false)
|
||||
}
|
||||
|
||||
// LastWesternQuadrature 上次西方照时间
|
||||
// 返回上次西方照时间
|
||||
// LastWesternQuadrature 上一次西方照 / previous western quadrature.
|
||||
//
|
||||
// 返回 date 之前最近一次西方照时刻,结果保持 date 的时区。
|
||||
// Returns the most recent western quadrature relative to date, keeping date's time zone.
|
||||
func LastWesternQuadrature(date time.Time) time.Time {
|
||||
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
|
||||
return basic.JDE2DateByZone(basic.LastJupiterWesternQuadrature(jde), date.Location(), false)
|
||||
}
|
||||
|
||||
// NextWesternQuadrature 下次西方照时间
|
||||
// 返回下次西方照时间
|
||||
// NextWesternQuadrature 下一次西方照 / next western quadrature.
|
||||
//
|
||||
// 返回 date 之后最近一次西方照时刻,结果保持 date 的时区。
|
||||
// Returns the next western quadrature relative to date, keeping date's time zone.
|
||||
func NextWesternQuadrature(date time.Time) time.Time {
|
||||
jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true)
|
||||
return basic.JDE2DateByZone(basic.NextJupiterWesternQuadrature(jde), date.Location(), false)
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package jupiter
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"b612.me/astro/basic"
|
||||
"b612.me/astro/calendar"
|
||||
)
|
||||
|
||||
// AscendingNode 木星升交点黄经 / ascending node longitude of Jupiter.
|
||||
func AscendingNode(date time.Time) float64 {
|
||||
return AscendingNodeN(date, -1)
|
||||
}
|
||||
|
||||
// AscendingNodeN 木星升交点黄经(截断版) / truncated ascending node longitude of Jupiter.
|
||||
func AscendingNodeN(date time.Time, n int) float64 {
|
||||
jde := calendar.Date2JDE(date.UTC())
|
||||
return basic.JupiterAscendingNodeN(basic.TD2UT(jde, true), n)
|
||||
}
|
||||
|
||||
// DescendingNode 木星降交点黄经 / descending node longitude of Jupiter.
|
||||
func DescendingNode(date time.Time) float64 {
|
||||
return DescendingNodeN(date, -1)
|
||||
}
|
||||
|
||||
// DescendingNodeN 木星降交点黄经(截断版) / truncated descending node longitude of Jupiter.
|
||||
func DescendingNodeN(date time.Time, n int) float64 {
|
||||
jde := calendar.Date2JDE(date.UTC())
|
||||
return basic.JupiterDescendingNodeN(basic.TD2UT(jde, true), n)
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package jupiter
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestObservationNFullMatchesDefault(t *testing.T) {
|
||||
date := time.Date(2026, 4, 26, 9, 30, 45, 123456789, time.FixedZone("CST", 8*3600))
|
||||
lon := 116.391
|
||||
lat := 39.907
|
||||
height := 45.0
|
||||
|
||||
assertSame := func(name string, got, want float64) {
|
||||
t.Helper()
|
||||
if math.Float64bits(got) != math.Float64bits(want) {
|
||||
t.Fatalf("%s full-n mismatch", name)
|
||||
}
|
||||
}
|
||||
assertSamePair := func(name string, got1, got2, want1, want2 float64) {
|
||||
t.Helper()
|
||||
assertSame(name+".1", got1, want1)
|
||||
assertSame(name+".2", got2, want2)
|
||||
}
|
||||
assertTimeSame := func(name string, got, want time.Time) {
|
||||
t.Helper()
|
||||
if got.UnixNano() != want.UnixNano() || got.Location().String() != want.Location().String() {
|
||||
t.Fatalf("%s full-n mismatch", name)
|
||||
}
|
||||
}
|
||||
assertErrSame := func(name string, got, want error) {
|
||||
t.Helper()
|
||||
switch {
|
||||
case got == nil && want == nil:
|
||||
return
|
||||
case got == nil || want == nil:
|
||||
t.Fatalf("%s full-n mismatch", name)
|
||||
case got.Error() != want.Error():
|
||||
t.Fatalf("%s full-n mismatch", name)
|
||||
}
|
||||
}
|
||||
|
||||
floatChecks := []struct {
|
||||
name string
|
||||
got func() float64
|
||||
want func() float64
|
||||
}{
|
||||
{"ApparentLo", func() float64 { return ApparentLo(date) }, func() float64 { return ApparentLoN(date, -1) }},
|
||||
{"ApparentBo", func() float64 { return ApparentBo(date) }, func() float64 { return ApparentBoN(date, -1) }},
|
||||
{"ApparentRa", func() float64 { return ApparentRa(date) }, func() float64 { return ApparentRaN(date, -1) }},
|
||||
{"ApparentDec", func() float64 { return ApparentDec(date) }, func() float64 { return ApparentDecN(date, -1) }},
|
||||
{"ApparentMagnitude", func() float64 { return ApparentMagnitude(date) }, func() float64 { return ApparentMagnitudeN(date, -1) }},
|
||||
{"PhaseAngle", func() float64 { return PhaseAngle(date) }, func() float64 { return PhaseAngleN(date, -1) }},
|
||||
{"IlluminatedFraction", func() float64 { return IlluminatedFraction(date) }, func() float64 { return IlluminatedFractionN(date, -1) }},
|
||||
{"Phase", func() float64 { return Phase(date) }, func() float64 { return PhaseN(date, -1) }},
|
||||
{"BrightLimbPositionAngle", func() float64 { return BrightLimbPositionAngle(date) }, func() float64 { return BrightLimbPositionAngleN(date, -1) }},
|
||||
{"EarthDistance", func() float64 { return EarthDistance(date) }, func() float64 { return EarthDistanceN(date, -1) }},
|
||||
{"SunDistance", func() float64 { return SunDistance(date) }, func() float64 { return SunDistanceN(date, -1) }},
|
||||
{"Altitude", func() float64 { return Altitude(date, lon, lat) }, func() float64 { return AltitudeN(date, lon, lat, -1) }},
|
||||
{"Zenith", func() float64 { return Zenith(date, lon, lat) }, func() float64 { return ZenithN(date, lon, lat, -1) }},
|
||||
{"Azimuth", func() float64 { return Azimuth(date, lon, lat) }, func() float64 { return AzimuthN(date, lon, lat, -1) }},
|
||||
{"HourAngle", func() float64 { return HourAngle(date, lon) }, func() float64 { return HourAngleN(date, lon, -1) }},
|
||||
{"ParallacticAngle", func() float64 { return ParallacticAngle(date, lon, lat) }, func() float64 { return ParallacticAngleN(date, lon, lat, -1) }},
|
||||
}
|
||||
for _, tc := range floatChecks {
|
||||
assertSame(tc.name, tc.got(), tc.want())
|
||||
}
|
||||
|
||||
if math.Abs((Altitude(date, lon, lat)+Zenith(date, lon, lat))-90) > 1e-12 {
|
||||
t.Fatal("altitude + zenith should equal 90 degrees")
|
||||
}
|
||||
|
||||
gotRa, gotDec := ApparentRaDec(date)
|
||||
wantRa, wantDec := ApparentRaDecN(date, -1)
|
||||
assertSamePair("ApparentRaDec", gotRa, gotDec, wantRa, wantDec)
|
||||
|
||||
assertTimeSame("CulminationTime", CulminationTime(date, lon), CulminationTimeN(date, lon, -1))
|
||||
|
||||
rise1, err1 := RiseTime(date, lon, lat, height, true)
|
||||
rise2, err2 := RiseTimeN(date, lon, lat, height, true, -1)
|
||||
assertTimeSame("RiseTime", rise1, rise2)
|
||||
assertErrSame("RiseTime.err", err1, err2)
|
||||
|
||||
set1, err1 := SetTime(date, lon, lat, height, true)
|
||||
set2, err2 := SetTimeN(date, lon, lat, height, true, -1)
|
||||
assertTimeSame("SetTime", set1, set2)
|
||||
assertErrSame("SetTime.err", err1, err2)
|
||||
|
||||
down1, err1 := DownTime(date, lon, lat, height, true)
|
||||
down2, err2 := DownTimeN(date, lon, lat, height, true, -1)
|
||||
assertTimeSame("DownTime", down1, down2)
|
||||
assertErrSame("DownTime.err", err1, err2)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package jupiter
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"b612.me/astro/basic"
|
||||
)
|
||||
|
||||
// ParallacticAngle 木星视差角(天顶方向角) / Jupiter parallactic angle.
|
||||
func ParallacticAngle(date time.Time, lon, lat float64) float64 {
|
||||
return basic.ParallacticAngleByHourAngle(HourAngle(date, lon), ApparentDec(date), lat)
|
||||
}
|
||||
|
||||
// ParallacticAngleN 截断项木星视差角(天顶方向角) / truncated Jupiter parallactic angle.
|
||||
func ParallacticAngleN(date time.Time, lon, lat float64, n int) float64 {
|
||||
return basic.ParallacticAngleByHourAngle(HourAngleN(date, lon, n), ApparentDecN(date, n), lat)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package jupiter
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"b612.me/astro/basic"
|
||||
"b612.me/astro/calendar"
|
||||
)
|
||||
|
||||
// PhaseAngle 相位角,单位度 / phase angle in degrees.
|
||||
func PhaseAngle(date time.Time) float64 {
|
||||
return PhaseAngleN(date, -1)
|
||||
}
|
||||
|
||||
// PhaseAngleN 相位角(截断版),单位度 / truncated phase angle in degrees.
|
||||
func PhaseAngleN(date time.Time, n int) float64 {
|
||||
return basic.JupiterPhaseAngleN(phaseJD(date), n)
|
||||
}
|
||||
|
||||
// IlluminatedFraction 被照亮比例 / illuminated fraction.
|
||||
func IlluminatedFraction(date time.Time) float64 {
|
||||
return IlluminatedFractionN(date, -1)
|
||||
}
|
||||
|
||||
// IlluminatedFractionN 被照亮比例(截断版) / truncated illuminated fraction.
|
||||
func IlluminatedFractionN(date time.Time, n int) float64 {
|
||||
return basic.JupiterIlluminatedFractionN(phaseJD(date), n)
|
||||
}
|
||||
|
||||
// Phase 相位,被照亮比例 / phase, illuminated fraction.
|
||||
func Phase(date time.Time) float64 {
|
||||
return IlluminatedFraction(date)
|
||||
}
|
||||
|
||||
// PhaseN 相位(截断版),被照亮比例 / truncated phase, illuminated fraction.
|
||||
func PhaseN(date time.Time, n int) float64 {
|
||||
return IlluminatedFractionN(date, n)
|
||||
}
|
||||
|
||||
// BrightLimbPositionAngle 亮面中心位置角,单位度 / bright limb position angle in degrees.
|
||||
func BrightLimbPositionAngle(date time.Time) float64 {
|
||||
return BrightLimbPositionAngleN(date, -1)
|
||||
}
|
||||
|
||||
// BrightLimbPositionAngleN 亮面中心位置角(截断版),单位度 / truncated bright limb position angle in degrees.
|
||||
func BrightLimbPositionAngleN(date time.Time, n int) float64 {
|
||||
return basic.JupiterBrightLimbPositionAngleN(phaseJD(date), n)
|
||||
}
|
||||
|
||||
func phaseJD(date time.Time) float64 {
|
||||
return basic.TD2UT(calendar.Date2JDE(date.UTC()), true)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package jupiter
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"b612.me/astro/basic"
|
||||
)
|
||||
|
||||
// GalileanSatellitePhenomenon 木星伽利略卫星瞬时现象 / instantaneous Galilean-satellite phenomena.
|
||||
//
|
||||
// Transit 为凌日,Occultation 为掩蔽,Eclipse 为进入木星本影,ShadowTransit 为卫星影子落在可见木星盘面上。
|
||||
// ShadowOffset* 仅在 ShadowTransit 为 true 时有意义。
|
||||
// Transit means a transit across Jupiter's disk, Occultation means hidden behind Jupiter, Eclipse means inside Jupiter's umbra, and ShadowTransit means the shadow falls on the visible Jovian disk.
|
||||
// ShadowOffset* are meaningful only when ShadowTransit is true.
|
||||
type GalileanSatellitePhenomenon struct {
|
||||
Transit bool
|
||||
Occultation bool
|
||||
Eclipse bool
|
||||
ShadowTransit bool
|
||||
|
||||
ShadowOffsetXArcsec float64
|
||||
ShadowOffsetYArcsec float64
|
||||
|
||||
ShadowOffsetXJupiterR float64
|
||||
ShadowOffsetYJupiterR float64
|
||||
}
|
||||
|
||||
// GalileanPhenomenaInfo 四颗伽利略卫星瞬时现象 / instantaneous phenomena of the four Galilean satellites.
|
||||
type GalileanPhenomenaInfo struct {
|
||||
Io GalileanSatellitePhenomenon
|
||||
Europa GalileanSatellitePhenomenon
|
||||
Ganymede GalileanSatellitePhenomenon
|
||||
Callisto GalileanSatellitePhenomenon
|
||||
}
|
||||
|
||||
// SatellitePhenomena 木星四颗伽利略卫星瞬时现象 / instantaneous phenomena of Jupiter's four Galilean satellites.
|
||||
//
|
||||
// date 表示观测绝对时刻;内部使用该时刻对应的 TT/TDB 历元求值。
|
||||
// date is the observing instant; internally the corresponding TT/TDB epoch is used.
|
||||
func SatellitePhenomena(date time.Time) GalileanPhenomenaInfo {
|
||||
jde := basic.Date2JDE(date.UTC())
|
||||
phenomena := basic.JupiterGalileanSatellitePhenomena(jde)
|
||||
return GalileanPhenomenaInfo{
|
||||
Io: galileanPhenomenonFromBasic(phenomena[0]),
|
||||
Europa: galileanPhenomenonFromBasic(phenomena[1]),
|
||||
Ganymede: galileanPhenomenonFromBasic(phenomena[2]),
|
||||
Callisto: galileanPhenomenonFromBasic(phenomena[3]),
|
||||
}
|
||||
}
|
||||
|
||||
func galileanPhenomenonFromBasic(phenomenon basic.JupiterGalileanPhenomenon) GalileanSatellitePhenomenon {
|
||||
return GalileanSatellitePhenomenon{
|
||||
Transit: phenomenon.Transit,
|
||||
Occultation: phenomenon.Occultation,
|
||||
Eclipse: phenomenon.Eclipse,
|
||||
ShadowTransit: phenomenon.ShadowTransit,
|
||||
ShadowOffsetXArcsec: phenomenon.ShadowOffsetXArcsec,
|
||||
ShadowOffsetYArcsec: phenomenon.ShadowOffsetYArcsec,
|
||||
ShadowOffsetXJupiterR: phenomenon.ShadowOffsetXJupiterRadii,
|
||||
ShadowOffsetYJupiterR: phenomenon.ShadowOffsetYJupiterRadii,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package jupiter
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"b612.me/astro/basic"
|
||||
)
|
||||
|
||||
// GalileanPhenomenonContactPhase 接触阶段 / contact phase.
|
||||
type GalileanPhenomenonContactPhase string
|
||||
|
||||
const (
|
||||
// GalileanPhenomenonContactDisappearance 初亏/初入接触阶段 / disappearance ingress contact.
|
||||
GalileanPhenomenonContactDisappearance GalileanPhenomenonContactPhase = "disappearance"
|
||||
// GalileanPhenomenonContactReappearance 复圆/复出接触阶段 / reappearance egress contact.
|
||||
GalileanPhenomenonContactReappearance GalileanPhenomenonContactPhase = "reappearance"
|
||||
)
|
||||
|
||||
// GalileanPhenomenonContact 伽利略卫星接触窗口 / Galilean-satellite contact window.
|
||||
//
|
||||
// Start/End 是有限圆盘或有限影斑开始/结束接触的时刻;ModelCrossing 是这套连续接触模型下,
|
||||
// 零半径参考点穿越边界的时刻。
|
||||
// Start/End mark the beginning/end of the finite-disk or finite-shadow contact interval.
|
||||
// ModelCrossing is the zero-radius boundary crossing in this continuous contact model.
|
||||
type GalileanPhenomenonContact struct {
|
||||
Valid bool
|
||||
Phase GalileanPhenomenonContactPhase
|
||||
|
||||
Start time.Time
|
||||
ModelCrossing time.Time
|
||||
End time.Time
|
||||
|
||||
Duration time.Duration
|
||||
}
|
||||
|
||||
// GalileanPhenomenonContactEvent IMCCE 风格的 D/F 接触事件 / IMCCE-style D/F contact event.
|
||||
//
|
||||
// 这个 API 返回有限圆盘/有限影斑的接触窗口,适合和 IMCCE 的 `TR.D/TR.F/OC.D/OC.F/EC.D/EC.F/SH.D/SH.F` 对齐;
|
||||
// 现有 `GalileanPhenomenonEvent` 返回的是零半径几何模型保持 active 的整段区间,两者语义不同。
|
||||
// 其中 `shadow_transit` 先用半影/本影边界求部分相持续时间,再把这段持续时间中心放在旧影轴过盘时刻上。
|
||||
// This API returns finite-disk / finite-shadow contact windows and is intended to align with IMCCE
|
||||
// `TR.D/TR.F/OC.D/OC.F/EC.D/EC.F/SH.D/SH.F` rows. The existing `GalileanPhenomenonEvent` returns the
|
||||
// whole active interval of the zero-radius geometric model, so the semantics are different.
|
||||
// For `shadow_transit`, the partial-phase duration comes from penumbra/umbra boundaries,
|
||||
// while the reported D/F time is centered on the shadow-axis limb crossing from the existing full-event model.
|
||||
type GalileanPhenomenonContactEvent struct {
|
||||
Valid bool
|
||||
Satellite int
|
||||
Type GalileanPhenomenonType
|
||||
|
||||
Disappearance GalileanPhenomenonContact
|
||||
Greatest time.Time
|
||||
Reappearance GalileanPhenomenonContact
|
||||
|
||||
GreatestState GalileanSatellitePhenomenon
|
||||
}
|
||||
|
||||
// LastGalileanPhenomenonContactEvent 上一次 IMCCE 风格接触事件 / previous IMCCE-style contact event.
|
||||
func LastGalileanPhenomenonContactEvent(date time.Time, satellite int, phenomenonType GalileanPhenomenonType) GalileanPhenomenonContactEvent {
|
||||
return galileanPhenomenonContactEventFromBasic(
|
||||
basic.LastJupiterGalileanPhenomenonContactEvent(
|
||||
basic.Date2JDE(date.UTC()),
|
||||
satellite,
|
||||
basic.JupiterGalileanPhenomenonType(phenomenonType),
|
||||
),
|
||||
date.Location(),
|
||||
)
|
||||
}
|
||||
|
||||
// NextGalileanPhenomenonContactEvent 下一次 IMCCE 风格接触事件 / next IMCCE-style contact event.
|
||||
func NextGalileanPhenomenonContactEvent(date time.Time, satellite int, phenomenonType GalileanPhenomenonType) GalileanPhenomenonContactEvent {
|
||||
return galileanPhenomenonContactEventFromBasic(
|
||||
basic.NextJupiterGalileanPhenomenonContactEvent(
|
||||
basic.Date2JDE(date.UTC()),
|
||||
satellite,
|
||||
basic.JupiterGalileanPhenomenonType(phenomenonType),
|
||||
),
|
||||
date.Location(),
|
||||
)
|
||||
}
|
||||
|
||||
// ClosestGalileanPhenomenonContactEvent 最近一次 IMCCE 风格接触事件 / closest IMCCE-style contact event.
|
||||
func ClosestGalileanPhenomenonContactEvent(date time.Time, satellite int, phenomenonType GalileanPhenomenonType) GalileanPhenomenonContactEvent {
|
||||
return galileanPhenomenonContactEventFromBasic(
|
||||
basic.ClosestJupiterGalileanPhenomenonContactEvent(
|
||||
basic.Date2JDE(date.UTC()),
|
||||
satellite,
|
||||
basic.JupiterGalileanPhenomenonType(phenomenonType),
|
||||
),
|
||||
date.Location(),
|
||||
)
|
||||
}
|
||||
|
||||
func galileanPhenomenonContactEventFromBasic(event basic.JupiterGalileanPhenomenonContactEvent, loc *time.Location) GalileanPhenomenonContactEvent {
|
||||
if !event.Valid {
|
||||
return GalileanPhenomenonContactEvent{}
|
||||
}
|
||||
greatest := basic.JDE2DateByZone(event.Greatest, loc, false)
|
||||
return GalileanPhenomenonContactEvent{
|
||||
Valid: true,
|
||||
Satellite: event.Satellite,
|
||||
Type: GalileanPhenomenonType(event.Type),
|
||||
Disappearance: galileanPhenomenonContactFromBasic(event.Disappearance, loc),
|
||||
Greatest: greatest,
|
||||
Reappearance: galileanPhenomenonContactFromBasic(event.Reappearance, loc),
|
||||
GreatestState: galileanPhenomenonFromBasic(event.GreatestPhenomenon),
|
||||
}
|
||||
}
|
||||
|
||||
func galileanPhenomenonContactFromBasic(contact basic.JupiterGalileanPhenomenonContact, loc *time.Location) GalileanPhenomenonContact {
|
||||
if !contact.Valid {
|
||||
return GalileanPhenomenonContact{}
|
||||
}
|
||||
start := basic.JDE2DateByZone(contact.Start, loc, false)
|
||||
modelCrossing := basic.JDE2DateByZone(contact.ModelCrossing, loc, false)
|
||||
end := basic.JDE2DateByZone(contact.End, loc, false)
|
||||
return GalileanPhenomenonContact{
|
||||
Valid: true,
|
||||
Phase: GalileanPhenomenonContactPhase(contact.Phase),
|
||||
Start: start,
|
||||
ModelCrossing: modelCrossing,
|
||||
End: end,
|
||||
Duration: end.Sub(start),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package jupiter
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"b612.me/astro/basic"
|
||||
)
|
||||
|
||||
func TestGalileanPhenomenonContactEventWrappersMatchBasic(t *testing.T) {
|
||||
records := loadGalileanPublicEventBaseline(t)
|
||||
loc := time.FixedZone("UTC+8", 8*3600)
|
||||
for _, record := range records {
|
||||
startUTC := mustParseGalileanEventTime(t, record.StartUTC)
|
||||
endUTC := mustParseGalileanEventTime(t, record.EndUTC)
|
||||
queryMid := startUTC.Add(endUTC.Sub(startUTC) / 2).In(loc)
|
||||
phenomenonType := GalileanPhenomenonType(record.Type)
|
||||
|
||||
got := ClosestGalileanPhenomenonContactEvent(queryMid, record.Satellite, phenomenonType)
|
||||
want := basic.ClosestJupiterGalileanPhenomenonContactEvent(
|
||||
basic.Date2JDE(queryMid.UTC()),
|
||||
record.Satellite,
|
||||
basic.JupiterGalileanPhenomenonType(phenomenonType),
|
||||
)
|
||||
assertGalileanContactWrapperMatchesBasic(t, record.Label, got, want, loc)
|
||||
}
|
||||
}
|
||||
|
||||
func assertGalileanContactWrapperMatchesBasic(
|
||||
t *testing.T,
|
||||
name string,
|
||||
got GalileanPhenomenonContactEvent,
|
||||
want basic.JupiterGalileanPhenomenonContactEvent,
|
||||
loc *time.Location,
|
||||
) {
|
||||
t.Helper()
|
||||
if got.Valid != want.Valid {
|
||||
t.Fatalf("%s valid mismatch: got %v want %v", name, got.Valid, want.Valid)
|
||||
}
|
||||
if !got.Valid {
|
||||
return
|
||||
}
|
||||
if got.Greatest.Location() != loc {
|
||||
t.Fatalf("%s greatest timezone mismatch", name)
|
||||
}
|
||||
wantGreatest := basic.JDE2DateByZone(want.Greatest, loc, false)
|
||||
if !got.Greatest.Equal(wantGreatest) {
|
||||
t.Fatalf("%s greatest mismatch: got %s want %s", name, got.Greatest.Format(time.RFC3339Nano), wantGreatest.Format(time.RFC3339Nano))
|
||||
}
|
||||
if got.Satellite != want.Satellite || string(got.Type) != string(want.Type) {
|
||||
t.Fatalf("%s id/type mismatch", name)
|
||||
}
|
||||
assertGalileanContactMatchesBasic(t, name+" disappearance", got.Disappearance, want.Disappearance, loc)
|
||||
assertGalileanContactMatchesBasic(t, name+" reappearance", got.Reappearance, want.Reappearance, loc)
|
||||
assertSameGalileanPhenomenon(t, name+" greatest", got.GreatestState, want.GreatestPhenomenon)
|
||||
}
|
||||
|
||||
func assertGalileanContactMatchesBasic(
|
||||
t *testing.T,
|
||||
name string,
|
||||
got GalileanPhenomenonContact,
|
||||
want basic.JupiterGalileanPhenomenonContact,
|
||||
loc *time.Location,
|
||||
) {
|
||||
t.Helper()
|
||||
if got.Valid != want.Valid {
|
||||
t.Fatalf("%s valid mismatch", name)
|
||||
}
|
||||
if !got.Valid {
|
||||
return
|
||||
}
|
||||
wantStart := basic.JDE2DateByZone(want.Start, loc, false)
|
||||
wantModel := basic.JDE2DateByZone(want.ModelCrossing, loc, false)
|
||||
wantEnd := basic.JDE2DateByZone(want.End, loc, false)
|
||||
if got.Start.Location() != loc || got.ModelCrossing.Location() != loc || got.End.Location() != loc {
|
||||
t.Fatalf("%s timezone mismatch", name)
|
||||
}
|
||||
if !got.Start.Equal(wantStart) || !got.ModelCrossing.Equal(wantModel) || !got.End.Equal(wantEnd) {
|
||||
t.Fatalf(
|
||||
"%s time mismatch: got [%s %s %s] want [%s %s %s]",
|
||||
name,
|
||||
got.Start.Format(time.RFC3339Nano),
|
||||
got.ModelCrossing.Format(time.RFC3339Nano),
|
||||
got.End.Format(time.RFC3339Nano),
|
||||
wantStart.Format(time.RFC3339Nano),
|
||||
wantModel.Format(time.RFC3339Nano),
|
||||
wantEnd.Format(time.RFC3339Nano),
|
||||
)
|
||||
}
|
||||
if got.Duration != got.End.Sub(got.Start) {
|
||||
t.Fatalf("%s duration mismatch", name)
|
||||
}
|
||||
if string(got.Phase) != string(want.Phase) {
|
||||
t.Fatalf("%s phase mismatch: got %q want %q", name, got.Phase, want.Phase)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package jupiter
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"b612.me/astro/basic"
|
||||
)
|
||||
|
||||
// GalileanPhenomenonType 伽利略卫星现象类型 / Galilean-satellite phenomenon type.
|
||||
type GalileanPhenomenonType string
|
||||
|
||||
const (
|
||||
// GalileanPhenomenonTransit 凌日 / satellite transit across Jupiter.
|
||||
GalileanPhenomenonTransit GalileanPhenomenonType = "transit"
|
||||
// GalileanPhenomenonOccultation 掩蔽 / occultation behind Jupiter.
|
||||
GalileanPhenomenonOccultation GalileanPhenomenonType = "occultation"
|
||||
// GalileanPhenomenonEclipse 食 / eclipse in Jupiter's shadow.
|
||||
GalileanPhenomenonEclipse GalileanPhenomenonType = "eclipse"
|
||||
// GalileanPhenomenonShadowTransit 影凌 / shadow transit across Jupiter.
|
||||
GalileanPhenomenonShadowTransit GalileanPhenomenonType = "shadow_transit"
|
||||
)
|
||||
|
||||
const (
|
||||
// GalileanSatelliteIo 木卫一 / Io.
|
||||
GalileanSatelliteIo = 1
|
||||
// GalileanSatelliteEuropa 木卫二 / Europa.
|
||||
GalileanSatelliteEuropa = 2
|
||||
// GalileanSatelliteGanymede 木卫三 / Ganymede.
|
||||
GalileanSatelliteGanymede = 3
|
||||
// GalileanSatelliteCallisto 木卫四 / Callisto.
|
||||
GalileanSatelliteCallisto = 4
|
||||
)
|
||||
|
||||
// GalileanPhenomenonEvent 伽利略卫星整场现象 / full Galilean-satellite event.
|
||||
//
|
||||
// Start、Greatest、End 都保持调用者输入的时区。
|
||||
// GreatestState 是食甚/现象最深时刻对应的瞬时现象标志与影心偏移。
|
||||
// 这里的 Start/End 基于零半径几何模型是否 active,不是 IMCCE 年表中的 D/F 接触时刻;
|
||||
// 如果需要有限圆盘/有限影斑的接触窗口,请改用 `GalileanPhenomenonContactEvent`。
|
||||
// Start, Greatest, and End preserve the caller's timezone.
|
||||
// GreatestState contains the instantaneous phenomenon flags and shadow offsets at greatest event depth.
|
||||
// Start/End here are based on whether the zero-radius geometric model is active, not the IMCCE D/F contact times.
|
||||
// Use `GalileanPhenomenonContactEvent` when you need finite-disk or finite-shadow contact windows.
|
||||
type GalileanPhenomenonEvent struct {
|
||||
Valid bool
|
||||
Satellite int
|
||||
Type GalileanPhenomenonType
|
||||
|
||||
Start time.Time
|
||||
Greatest time.Time
|
||||
End time.Time
|
||||
|
||||
Duration time.Duration
|
||||
GreatestState GalileanSatellitePhenomenon
|
||||
}
|
||||
|
||||
// LastGalileanPhenomenonEvent 上一次伽利略卫星现象 / previous Galilean-satellite event.
|
||||
//
|
||||
// date 表示查询绝对时刻;satellite 取 `1=Io, 2=Europa, 3=Ganymede, 4=Callisto`;
|
||||
// phenomenonType 取 `transit/occultation/eclipse/shadow_transit` 中之一。
|
||||
// date is the query instant; satellite is `1=Io, 2=Europa, 3=Ganymede, 4=Callisto`;
|
||||
// phenomenonType is one of `transit/occultation/eclipse/shadow_transit`.
|
||||
func LastGalileanPhenomenonEvent(date time.Time, satellite int, phenomenonType GalileanPhenomenonType) GalileanPhenomenonEvent {
|
||||
return galileanPhenomenonEventFromBasic(
|
||||
basic.LastJupiterGalileanPhenomenonEvent(
|
||||
basic.Date2JDE(date.UTC()),
|
||||
satellite,
|
||||
basic.JupiterGalileanPhenomenonType(phenomenonType),
|
||||
),
|
||||
date.Location(),
|
||||
)
|
||||
}
|
||||
|
||||
// NextGalileanPhenomenonEvent 下一次伽利略卫星现象 / next Galilean-satellite event.
|
||||
func NextGalileanPhenomenonEvent(date time.Time, satellite int, phenomenonType GalileanPhenomenonType) GalileanPhenomenonEvent {
|
||||
return galileanPhenomenonEventFromBasic(
|
||||
basic.NextJupiterGalileanPhenomenonEvent(
|
||||
basic.Date2JDE(date.UTC()),
|
||||
satellite,
|
||||
basic.JupiterGalileanPhenomenonType(phenomenonType),
|
||||
),
|
||||
date.Location(),
|
||||
)
|
||||
}
|
||||
|
||||
// ClosestGalileanPhenomenonEvent 最近一次伽利略卫星现象 / closest Galilean-satellite event.
|
||||
func ClosestGalileanPhenomenonEvent(date time.Time, satellite int, phenomenonType GalileanPhenomenonType) GalileanPhenomenonEvent {
|
||||
return galileanPhenomenonEventFromBasic(
|
||||
basic.ClosestJupiterGalileanPhenomenonEvent(
|
||||
basic.Date2JDE(date.UTC()),
|
||||
satellite,
|
||||
basic.JupiterGalileanPhenomenonType(phenomenonType),
|
||||
),
|
||||
date.Location(),
|
||||
)
|
||||
}
|
||||
|
||||
func galileanPhenomenonEventFromBasic(event basic.JupiterGalileanPhenomenonEvent, loc *time.Location) GalileanPhenomenonEvent {
|
||||
if !event.Valid {
|
||||
return GalileanPhenomenonEvent{}
|
||||
}
|
||||
start := basic.JDE2DateByZone(event.Start, loc, false)
|
||||
greatest := basic.JDE2DateByZone(event.Greatest, loc, false)
|
||||
end := basic.JDE2DateByZone(event.End, loc, false)
|
||||
return GalileanPhenomenonEvent{
|
||||
Valid: true,
|
||||
Satellite: event.Satellite,
|
||||
Type: GalileanPhenomenonType(event.Type),
|
||||
Start: start,
|
||||
Greatest: greatest,
|
||||
End: end,
|
||||
Duration: end.Sub(start),
|
||||
GreatestState: galileanPhenomenonFromBasic(event.GreatestPhenomenon),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package jupiter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"b612.me/astro/basic"
|
||||
)
|
||||
|
||||
type galileanPublicEventBaselineRecord struct {
|
||||
Label string `json:"label"`
|
||||
Satellite int `json:"satellite"`
|
||||
Type string `json:"type"`
|
||||
StartUTC string `json:"start_utc"`
|
||||
StartDurationMinutes float64 `json:"start_duration_minutes"`
|
||||
EndUTC string `json:"end_utc"`
|
||||
EndDurationMinutes float64 `json:"end_duration_minutes"`
|
||||
}
|
||||
|
||||
func TestGalileanPhenomenonEventWrappersMatchBasic(t *testing.T) {
|
||||
records := loadGalileanPublicEventBaseline(t)
|
||||
loc := time.FixedZone("UTC+8", 8*3600)
|
||||
for _, record := range records {
|
||||
startUTC := mustParseGalileanEventTime(t, record.StartUTC)
|
||||
endUTC := mustParseGalileanEventTime(t, record.EndUTC)
|
||||
queryBefore := startUTC.Add(-12 * time.Hour).In(loc)
|
||||
queryAfter := endUTC.Add(12 * time.Hour).In(loc)
|
||||
queryMid := startUTC.Add(endUTC.Sub(startUTC) / 2).In(loc)
|
||||
phenomenonType := GalileanPhenomenonType(record.Type)
|
||||
|
||||
assertGalileanWrapperMatchesBasic(
|
||||
t,
|
||||
record.Label+" next",
|
||||
NextGalileanPhenomenonEvent(queryBefore, record.Satellite, phenomenonType),
|
||||
basic.NextJupiterGalileanPhenomenonEvent(basic.Date2JDE(queryBefore.UTC()), record.Satellite, basic.JupiterGalileanPhenomenonType(phenomenonType)),
|
||||
loc,
|
||||
)
|
||||
assertGalileanWrapperMatchesBasic(
|
||||
t,
|
||||
record.Label+" last",
|
||||
LastGalileanPhenomenonEvent(queryAfter, record.Satellite, phenomenonType),
|
||||
basic.LastJupiterGalileanPhenomenonEvent(basic.Date2JDE(queryAfter.UTC()), record.Satellite, basic.JupiterGalileanPhenomenonType(phenomenonType)),
|
||||
loc,
|
||||
)
|
||||
assertGalileanWrapperMatchesBasic(
|
||||
t,
|
||||
record.Label+" closest",
|
||||
ClosestGalileanPhenomenonEvent(queryMid, record.Satellite, phenomenonType),
|
||||
basic.ClosestJupiterGalileanPhenomenonEvent(basic.Date2JDE(queryMid.UTC()), record.Satellite, basic.JupiterGalileanPhenomenonType(phenomenonType)),
|
||||
loc,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func assertGalileanWrapperMatchesBasic(
|
||||
t *testing.T,
|
||||
name string,
|
||||
got GalileanPhenomenonEvent,
|
||||
want basic.JupiterGalileanPhenomenonEvent,
|
||||
loc *time.Location,
|
||||
) {
|
||||
t.Helper()
|
||||
if got.Valid != want.Valid {
|
||||
t.Fatalf("%s valid mismatch: got %v want %v", name, got.Valid, want.Valid)
|
||||
}
|
||||
if !got.Valid {
|
||||
return
|
||||
}
|
||||
if got.Start.Location() != loc || got.Greatest.Location() != loc || got.End.Location() != loc {
|
||||
t.Fatalf("%s timezone mismatch", name)
|
||||
}
|
||||
wantStart := basic.JDE2DateByZone(want.Start, loc, false)
|
||||
wantGreatest := basic.JDE2DateByZone(want.Greatest, loc, false)
|
||||
wantEnd := basic.JDE2DateByZone(want.End, loc, false)
|
||||
if !got.Start.Equal(wantStart) || !got.Greatest.Equal(wantGreatest) || !got.End.Equal(wantEnd) {
|
||||
t.Fatalf(
|
||||
"%s time mismatch: got [%s %s %s] want [%s %s %s]",
|
||||
name,
|
||||
got.Start.Format(time.RFC3339Nano),
|
||||
got.Greatest.Format(time.RFC3339Nano),
|
||||
got.End.Format(time.RFC3339Nano),
|
||||
wantStart.Format(time.RFC3339Nano),
|
||||
wantGreatest.Format(time.RFC3339Nano),
|
||||
wantEnd.Format(time.RFC3339Nano),
|
||||
)
|
||||
}
|
||||
if got.Duration != got.End.Sub(got.Start) {
|
||||
t.Fatalf("%s duration mismatch: got %s want %s", name, got.Duration, got.End.Sub(got.Start))
|
||||
}
|
||||
if got.Satellite != want.Satellite || string(got.Type) != string(want.Type) {
|
||||
t.Fatalf("%s id/type mismatch", name)
|
||||
}
|
||||
assertSameGalileanPhenomenon(t, name+" greatest", got.GreatestState, want.GreatestPhenomenon)
|
||||
}
|
||||
|
||||
func assertSameGalileanPhenomenon(t *testing.T, name string, got GalileanSatellitePhenomenon, want basic.JupiterGalileanPhenomenon) {
|
||||
t.Helper()
|
||||
if got.Transit != want.Transit || got.Occultation != want.Occultation || got.Eclipse != want.Eclipse || got.ShadowTransit != want.ShadowTransit {
|
||||
t.Fatalf("%s flag mismatch", name)
|
||||
}
|
||||
gotFloats := []float64{got.ShadowOffsetXArcsec, got.ShadowOffsetYArcsec, got.ShadowOffsetXJupiterR, got.ShadowOffsetYJupiterR}
|
||||
wantFloats := []float64{want.ShadowOffsetXArcsec, want.ShadowOffsetYArcsec, want.ShadowOffsetXJupiterRadii, want.ShadowOffsetYJupiterRadii}
|
||||
for i := range gotFloats {
|
||||
if math.Float64bits(gotFloats[i]) != math.Float64bits(wantFloats[i]) {
|
||||
t.Fatalf("%s shadow field %d mismatch: got %.18f want %.18f", name, i, gotFloats[i], wantFloats[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadGalileanPublicEventBaseline(t *testing.T) []galileanPublicEventBaselineRecord {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile("testdata/galilean_events_imcce_2026.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var records []galileanPublicEventBaselineRecord
|
||||
if err := json.Unmarshal(data, &records); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(records) == 0 {
|
||||
t.Fatal("empty galilean public event baseline")
|
||||
}
|
||||
return records
|
||||
}
|
||||
|
||||
func mustParseGalileanEventTime(t *testing.T, value string) time.Time {
|
||||
t.Helper()
|
||||
date, err := time.Parse(time.RFC3339Nano, value)
|
||||
if err != nil {
|
||||
t.Fatalf("parse %q: %v", value, err)
|
||||
}
|
||||
return date
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package jupiter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const galileanShadowToleranceArcsec = 0.2
|
||||
|
||||
type galileanPhenomenaSample struct {
|
||||
UTC string `json:"utc"`
|
||||
Phenomena map[string]galileanPhenomenonExpectation `json:"phenomena"`
|
||||
}
|
||||
|
||||
type galileanPhenomenonExpectation struct {
|
||||
Transit bool `json:"transit"`
|
||||
Occultation bool `json:"occultation"`
|
||||
Eclipse bool `json:"eclipse"`
|
||||
ShadowTransit bool `json:"shadow_transit"`
|
||||
ShadowXArcsec *float64 `json:"shadow_x_arcsec,omitempty"`
|
||||
ShadowYArcsec *float64 `json:"shadow_y_arcsec,omitempty"`
|
||||
}
|
||||
|
||||
func TestGalileanPhenomenaAgainstHorizonsBaseline(t *testing.T) {
|
||||
samples := loadGalileanPhenomenaBaseline(t)
|
||||
maxShadowX := 0.0
|
||||
maxShadowY := 0.0
|
||||
for _, sample := range samples {
|
||||
date, err := time.Parse(time.RFC3339, sample.UTC)
|
||||
if err != nil {
|
||||
t.Fatalf("parse %s: %v", sample.UTC, err)
|
||||
}
|
||||
got := SatellitePhenomena(date)
|
||||
for name, want := range sample.Phenomena {
|
||||
phenomenon := selectGalileanPhenomenon(got, name)
|
||||
if phenomenon.Transit != want.Transit {
|
||||
t.Fatalf("%s transit mismatch at %s: got %v want %v", name, sample.UTC, phenomenon.Transit, want.Transit)
|
||||
}
|
||||
if phenomenon.Occultation != want.Occultation {
|
||||
t.Fatalf("%s occultation mismatch at %s: got %v want %v", name, sample.UTC, phenomenon.Occultation, want.Occultation)
|
||||
}
|
||||
if phenomenon.Eclipse != want.Eclipse {
|
||||
t.Fatalf("%s eclipse mismatch at %s: got %v want %v", name, sample.UTC, phenomenon.Eclipse, want.Eclipse)
|
||||
}
|
||||
if phenomenon.ShadowTransit != want.ShadowTransit {
|
||||
t.Fatalf("%s shadow-transit mismatch at %s: got %v want %v", name, sample.UTC, phenomenon.ShadowTransit, want.ShadowTransit)
|
||||
}
|
||||
if !want.ShadowTransit {
|
||||
continue
|
||||
}
|
||||
if want.ShadowXArcsec == nil || want.ShadowYArcsec == nil {
|
||||
t.Fatalf("%s shadow baseline incomplete at %s", name, sample.UTC)
|
||||
}
|
||||
xDiff := math.Abs(phenomenon.ShadowOffsetXArcsec - *want.ShadowXArcsec)
|
||||
yDiff := math.Abs(phenomenon.ShadowOffsetYArcsec - *want.ShadowYArcsec)
|
||||
if xDiff > maxShadowX {
|
||||
maxShadowX = xDiff
|
||||
}
|
||||
if yDiff > maxShadowY {
|
||||
maxShadowY = yDiff
|
||||
}
|
||||
if xDiff > galileanShadowToleranceArcsec {
|
||||
t.Fatalf("%s shadow X mismatch at %s: got %.6f want %.6f", name, sample.UTC, phenomenon.ShadowOffsetXArcsec, *want.ShadowXArcsec)
|
||||
}
|
||||
if yDiff > galileanShadowToleranceArcsec {
|
||||
t.Fatalf("%s shadow Y mismatch at %s: got %.6f want %.6f", name, sample.UTC, phenomenon.ShadowOffsetYArcsec, *want.ShadowYArcsec)
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Logf("galilean phenomena shadow max diff: X=%.3f arcsec Y=%.3f arcsec", maxShadowX, maxShadowY)
|
||||
}
|
||||
|
||||
func loadGalileanPhenomenaBaseline(t *testing.T) []galileanPhenomenaSample {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile("testdata/galilean_phenomena_horizons.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var samples []galileanPhenomenaSample
|
||||
if err := json.Unmarshal(data, &samples); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(samples) == 0 {
|
||||
t.Fatal("empty phenomena baseline")
|
||||
}
|
||||
return samples
|
||||
}
|
||||
|
||||
func selectGalileanPhenomenon(info GalileanPhenomenaInfo, name string) GalileanSatellitePhenomenon {
|
||||
switch name {
|
||||
case "io":
|
||||
return info.Io
|
||||
case "europa":
|
||||
return info.Europa
|
||||
case "ganymede":
|
||||
return info.Ganymede
|
||||
case "callisto":
|
||||
return info.Callisto
|
||||
default:
|
||||
panic("unknown satellite: " + name)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package jupiter
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"b612.me/astro/basic"
|
||||
)
|
||||
|
||||
// PhysicalInfo 木星物理观测参数 / physical observing parameters of Jupiter.
|
||||
type PhysicalInfo struct {
|
||||
// SubEarthLongitude 子地经度,单位度;采用 Jupiter 当前 IAU/Horizons 西经为正约定。
|
||||
SubEarthLongitude float64
|
||||
// SubEarthLatitude 子地纬度,单位度。
|
||||
SubEarthLatitude float64
|
||||
// SubSolarLongitude 子日经度,单位度;采用 Jupiter 当前 IAU/Horizons 西经为正约定。
|
||||
SubSolarLongitude float64
|
||||
// SubSolarLatitude 子日纬度,单位度。
|
||||
SubSolarLatitude float64
|
||||
// NorthPolePositionAngle 木星北极位置角,单位度。
|
||||
NorthPolePositionAngle float64
|
||||
// DS 太阳相对木星赤道的行星中心赤纬,单位度。
|
||||
DS float64
|
||||
// DE 地球相对木星赤道的行星中心赤纬,单位度。
|
||||
DE float64
|
||||
// CentralMeridianSystemI 木星 System I 照亮盘中央经线,单位度,西经为正。
|
||||
CentralMeridianSystemI float64
|
||||
// CentralMeridianSystemII 木星 System II 照亮盘中央经线,单位度,西经为正。
|
||||
CentralMeridianSystemII float64
|
||||
// CentralMeridianSystemIII 木星 System III 盘面中央经线,单位度,西经为正。
|
||||
CentralMeridianSystemIII float64
|
||||
}
|
||||
|
||||
// CentralMeridianInfo 木星 System I/II/III 中央经线 / Jupiter System I/II/III central meridians.
|
||||
type CentralMeridianInfo struct {
|
||||
// SystemI 木星 System I 照亮盘中央经线,单位度,西经为正。
|
||||
SystemI float64
|
||||
// SystemII 木星 System II 照亮盘中央经线,单位度,西经为正。
|
||||
SystemII float64
|
||||
// SystemIII 木星 System III 盘面中央经线,单位度,西经为正。
|
||||
SystemIII float64
|
||||
}
|
||||
|
||||
// Physical 木星物理观测参数 / physical observing parameters of Jupiter.
|
||||
func Physical(date time.Time) PhysicalInfo {
|
||||
return PhysicalN(date, -1)
|
||||
}
|
||||
|
||||
// PhysicalN 木星物理观测参数(截断版) / truncated physical observing parameters of Jupiter.
|
||||
func PhysicalN(date time.Time, n int) PhysicalInfo {
|
||||
jde := basic.Date2JDE(date.UTC())
|
||||
jd := basic.TD2UT(jde, true)
|
||||
info := basic.JupiterPhysicalN(jd, n)
|
||||
meridians := basic.JupiterCentralMeridiansN(jd, n)
|
||||
ds, de := basic.JupiterDSDEN(jd, n)
|
||||
return PhysicalInfo{
|
||||
SubEarthLongitude: info.SubEarthLongitude,
|
||||
SubEarthLatitude: info.SubEarthLatitude,
|
||||
SubSolarLongitude: info.SubSolarLongitude,
|
||||
SubSolarLatitude: info.SubSolarLatitude,
|
||||
NorthPolePositionAngle: info.NorthPolePositionAngle,
|
||||
DS: ds,
|
||||
DE: de,
|
||||
CentralMeridianSystemI: meridians.SystemI,
|
||||
CentralMeridianSystemII: meridians.SystemII,
|
||||
CentralMeridianSystemIII: meridians.SystemIII,
|
||||
}
|
||||
}
|
||||
|
||||
// CentralMeridians 木星 System I/II/III 中央经线 / Jupiter System I/II/III central meridians.
|
||||
func CentralMeridians(date time.Time) CentralMeridianInfo {
|
||||
return CentralMeridiansN(date, -1)
|
||||
}
|
||||
|
||||
// CentralMeridiansN 木星 System I/II/III 中央经线(截断版) / truncated Jupiter System I/II/III central meridians.
|
||||
func CentralMeridiansN(date time.Time, n int) CentralMeridianInfo {
|
||||
jde := basic.Date2JDE(date.UTC())
|
||||
info := basic.JupiterCentralMeridiansN(basic.TD2UT(jde, true), n)
|
||||
return CentralMeridianInfo{
|
||||
SystemI: info.SystemI,
|
||||
SystemII: info.SystemII,
|
||||
SystemIII: info.SystemIII,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package jupiter
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"b612.me/astro/basic"
|
||||
)
|
||||
|
||||
func TestPhysicalWrapperMatchesBasic(t *testing.T) {
|
||||
date := time.Date(2026, 4, 28, 9, 30, 45, 0, time.UTC)
|
||||
jde := basic.Date2JDE(date.UTC())
|
||||
|
||||
got := Physical(date)
|
||||
gotN := PhysicalN(date, -1)
|
||||
want := basic.JupiterPhysicalN(basic.TD2UT(jde, true), -1)
|
||||
wantMeridians := basic.JupiterCentralMeridiansN(basic.TD2UT(jde, true), -1)
|
||||
gotMeridians := CentralMeridians(date)
|
||||
gotMeridiansN := CentralMeridiansN(date, -1)
|
||||
|
||||
assertSamePhysicalFloat(t, "SubEarthLongitude", got.SubEarthLongitude, want.SubEarthLongitude)
|
||||
assertSamePhysicalFloat(t, "SubEarthLatitude", got.SubEarthLatitude, want.SubEarthLatitude)
|
||||
assertSamePhysicalFloat(t, "SubSolarLongitude", got.SubSolarLongitude, want.SubSolarLongitude)
|
||||
assertSamePhysicalFloat(t, "SubSolarLatitude", got.SubSolarLatitude, want.SubSolarLatitude)
|
||||
assertSamePhysicalFloat(t, "NorthPolePositionAngle", got.NorthPolePositionAngle, want.NorthPolePositionAngle)
|
||||
wantDS, wantDE := basic.JupiterDSDEN(basic.TD2UT(jde, true), -1)
|
||||
assertSamePhysicalFloat(t, "DS", got.DS, wantDS)
|
||||
assertSamePhysicalFloat(t, "DE", got.DE, wantDE)
|
||||
assertSamePhysicalFloat(t, "CentralMeridianSystemI", got.CentralMeridianSystemI, wantMeridians.SystemI)
|
||||
assertSamePhysicalFloat(t, "CentralMeridianSystemII", got.CentralMeridianSystemII, wantMeridians.SystemII)
|
||||
assertSamePhysicalFloat(t, "CentralMeridianSystemIII", got.CentralMeridianSystemIII, wantMeridians.SystemIII)
|
||||
|
||||
assertSamePhysicalFloat(t, "PhysicalN.SubEarthLongitude", got.SubEarthLongitude, gotN.SubEarthLongitude)
|
||||
assertSamePhysicalFloat(t, "PhysicalN.SubEarthLatitude", got.SubEarthLatitude, gotN.SubEarthLatitude)
|
||||
assertSamePhysicalFloat(t, "PhysicalN.SubSolarLongitude", got.SubSolarLongitude, gotN.SubSolarLongitude)
|
||||
assertSamePhysicalFloat(t, "PhysicalN.SubSolarLatitude", got.SubSolarLatitude, gotN.SubSolarLatitude)
|
||||
assertSamePhysicalFloat(t, "PhysicalN.NorthPolePositionAngle", got.NorthPolePositionAngle, gotN.NorthPolePositionAngle)
|
||||
assertSamePhysicalFloat(t, "PhysicalN.DS", got.DS, gotN.DS)
|
||||
assertSamePhysicalFloat(t, "PhysicalN.DE", got.DE, gotN.DE)
|
||||
assertSamePhysicalFloat(t, "PhysicalN.CentralMeridianSystemI", got.CentralMeridianSystemI, gotN.CentralMeridianSystemI)
|
||||
assertSamePhysicalFloat(t, "PhysicalN.CentralMeridianSystemII", got.CentralMeridianSystemII, gotN.CentralMeridianSystemII)
|
||||
assertSamePhysicalFloat(t, "PhysicalN.CentralMeridianSystemIII", got.CentralMeridianSystemIII, gotN.CentralMeridianSystemIII)
|
||||
assertSamePhysicalFloat(t, "CentralMeridians.SystemI", gotMeridians.SystemI, wantMeridians.SystemI)
|
||||
assertSamePhysicalFloat(t, "CentralMeridians.SystemII", gotMeridians.SystemII, wantMeridians.SystemII)
|
||||
assertSamePhysicalFloat(t, "CentralMeridians.SystemIII", gotMeridians.SystemIII, wantMeridians.SystemIII)
|
||||
assertSamePhysicalFloat(t, "CentralMeridiansN.SystemI", gotMeridians.SystemI, gotMeridiansN.SystemI)
|
||||
assertSamePhysicalFloat(t, "CentralMeridiansN.SystemII", gotMeridians.SystemII, gotMeridiansN.SystemII)
|
||||
assertSamePhysicalFloat(t, "CentralMeridiansN.SystemIII", gotMeridians.SystemIII, gotMeridiansN.SystemIII)
|
||||
}
|
||||
|
||||
func assertSamePhysicalFloat(t *testing.T, name string, got, want float64) {
|
||||
t.Helper()
|
||||
if math.Float64bits(got) != math.Float64bits(want) {
|
||||
t.Fatalf("%s mismatch: got %.18f want %.18f", name, got, want)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package jupiter
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPhysicalPreservesInstantAcrossTimezones(t *testing.T) {
|
||||
utc := time.Date(2026, 4, 28, 9, 30, 45, 123000000, time.UTC)
|
||||
shanghai := utc.In(time.FixedZone("UTC+8", 8*3600))
|
||||
got := Physical(shanghai)
|
||||
want := Physical(utc)
|
||||
valuesGot := []float64{got.SubEarthLongitude, got.SubEarthLatitude, got.SubSolarLongitude, got.SubSolarLatitude, got.NorthPolePositionAngle, got.DS, got.DE, got.CentralMeridianSystemI, got.CentralMeridianSystemII, got.CentralMeridianSystemIII}
|
||||
valuesWant := []float64{want.SubEarthLongitude, want.SubEarthLatitude, want.SubSolarLongitude, want.SubSolarLatitude, want.NorthPolePositionAngle, want.DS, want.DE, want.CentralMeridianSystemI, want.CentralMeridianSystemII, want.CentralMeridianSystemIII}
|
||||
for i := range valuesGot {
|
||||
if math.Float64bits(valuesGot[i]) != math.Float64bits(valuesWant[i]) {
|
||||
t.Fatalf("timezone instant mismatch at index %d: got %.18f want %.18f", i, valuesGot[i], valuesWant[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package jupiter
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"b612.me/astro/basic"
|
||||
)
|
||||
|
||||
// GalileanSatellitePosition 木星伽利略卫星视位置 / apparent position of a Galilean satellite.
|
||||
//
|
||||
// Jovicentric* 为木心 J2000 平赤道直角坐标与速度,单位 AU / AU-day。
|
||||
// ApparentRA/Dec 为该卫星的地心天体测量赤经赤纬,单位度。
|
||||
// OffsetX/OffsetY 以木星中心为原点,分别沿天球东、北方向为正。
|
||||
// Jovicentric* are Jovicentric J2000 mean-equatorial coordinates and velocities in AU / AU-day.
|
||||
// ApparentRA/Dec are geocentric astrometric right ascension and declination in degrees.
|
||||
// OffsetX/OffsetY are measured from Jupiter's center, positive to celestial east and north.
|
||||
type GalileanSatellitePosition struct {
|
||||
JovicentricX float64
|
||||
JovicentricY float64
|
||||
JovicentricZ float64
|
||||
JovicentricVX float64
|
||||
JovicentricVY float64
|
||||
JovicentricVZ float64
|
||||
|
||||
ApparentRA float64
|
||||
ApparentDec float64
|
||||
EarthDistance float64
|
||||
OffsetXArcsec float64
|
||||
OffsetYArcsec float64
|
||||
OffsetXJupiterR float64
|
||||
OffsetYJupiterR float64
|
||||
OffsetZJupiterR float64
|
||||
InFrontOfJupiter bool
|
||||
}
|
||||
|
||||
// GalileanSatellitesInfo 四颗伽利略卫星视位置 / apparent positions of the four Galilean satellites.
|
||||
type GalileanSatellitesInfo struct {
|
||||
Io GalileanSatellitePosition
|
||||
Europa GalileanSatellitePosition
|
||||
Ganymede GalileanSatellitePosition
|
||||
Callisto GalileanSatellitePosition
|
||||
}
|
||||
|
||||
// Satellites 木星四颗伽利略卫星视位置 / apparent positions of Jupiter's four Galilean satellites.
|
||||
//
|
||||
// date 表示观测绝对时刻;内部使用该时刻对应的 TT/TDB 历元做 L1 星历求值。
|
||||
// date is the observing instant; internally the corresponding TT/TDB epoch is used for the L1 ephemeris evaluation.
|
||||
func Satellites(date time.Time) GalileanSatellitesInfo {
|
||||
jde := basic.Date2JDE(date.UTC())
|
||||
observations := basic.JupiterGalileanSatelliteObservations(jde)
|
||||
return GalileanSatellitesInfo{
|
||||
Io: galileanSatellitePositionFromBasic(observations[0]),
|
||||
Europa: galileanSatellitePositionFromBasic(observations[1]),
|
||||
Ganymede: galileanSatellitePositionFromBasic(observations[2]),
|
||||
Callisto: galileanSatellitePositionFromBasic(observations[3]),
|
||||
}
|
||||
}
|
||||
|
||||
func galileanSatellitePositionFromBasic(observation basic.JupiterGalileanObservation) GalileanSatellitePosition {
|
||||
return GalileanSatellitePosition{
|
||||
JovicentricX: observation.State.X,
|
||||
JovicentricY: observation.State.Y,
|
||||
JovicentricZ: observation.State.Z,
|
||||
JovicentricVX: observation.State.VX,
|
||||
JovicentricVY: observation.State.VY,
|
||||
JovicentricVZ: observation.State.VZ,
|
||||
|
||||
ApparentRA: observation.RA,
|
||||
ApparentDec: observation.Dec,
|
||||
EarthDistance: observation.Distance,
|
||||
OffsetXArcsec: observation.OffsetXArcsec,
|
||||
OffsetYArcsec: observation.OffsetYArcsec,
|
||||
OffsetXJupiterR: observation.OffsetXJupiterRadii,
|
||||
OffsetYJupiterR: observation.OffsetYJupiterRadii,
|
||||
OffsetZJupiterR: observation.OffsetZJupiterRadii,
|
||||
InFrontOfJupiter: observation.InFrontOfJupiter,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package jupiter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const galileanHorizonsToleranceArcsec = 1.0
|
||||
|
||||
type galileanHorizonsEquatorialRecord struct {
|
||||
RA float64 `json:"ra_deg"`
|
||||
Dec float64 `json:"dec_deg"`
|
||||
Delta float64 `json:"delta_au"`
|
||||
}
|
||||
|
||||
type galileanHorizonsOffsetRecord struct {
|
||||
RA float64 `json:"ra_deg"`
|
||||
Dec float64 `json:"dec_deg"`
|
||||
Delta float64 `json:"delta_au"`
|
||||
OffsetXArcsec float64 `json:"offset_x_arcsec"`
|
||||
OffsetYArcsec float64 `json:"offset_y_arcsec"`
|
||||
}
|
||||
|
||||
type galileanHorizonsSample struct {
|
||||
UTC string `json:"utc"`
|
||||
Jupiter galileanHorizonsEquatorialRecord `json:"jupiter"`
|
||||
Satellites map[string]galileanHorizonsOffsetRecord `json:"satellites"`
|
||||
}
|
||||
|
||||
func TestGalileanSatellitesAgainstHorizonsRelativeOffsets(t *testing.T) {
|
||||
samples := loadGalileanHorizonsBaseline(t)
|
||||
maxXDiff := 0.0
|
||||
maxYDiff := 0.0
|
||||
for _, sample := range samples {
|
||||
date, err := time.Parse(time.RFC3339, sample.UTC)
|
||||
if err != nil {
|
||||
t.Fatalf("parse %s: %v", sample.UTC, err)
|
||||
}
|
||||
got := Satellites(date)
|
||||
for name, want := range sample.Satellites {
|
||||
position := selectGalileanSatellite(got, name)
|
||||
xDiff := math.Abs(position.OffsetXArcsec - want.OffsetXArcsec)
|
||||
yDiff := math.Abs(position.OffsetYArcsec - want.OffsetYArcsec)
|
||||
if xDiff > maxXDiff {
|
||||
maxXDiff = xDiff
|
||||
}
|
||||
if yDiff > maxYDiff {
|
||||
maxYDiff = yDiff
|
||||
}
|
||||
if xDiff > galileanHorizonsToleranceArcsec {
|
||||
t.Fatalf("%s X mismatch at %s: got %.6f want %.6f", name, sample.UTC, position.OffsetXArcsec, want.OffsetXArcsec)
|
||||
}
|
||||
if yDiff > galileanHorizonsToleranceArcsec {
|
||||
t.Fatalf("%s Y mismatch at %s: got %.6f want %.6f", name, sample.UTC, position.OffsetYArcsec, want.OffsetYArcsec)
|
||||
}
|
||||
wantFront := want.Delta < sample.Jupiter.Delta
|
||||
if position.InFrontOfJupiter != wantFront {
|
||||
t.Fatalf("%s front/back mismatch at %s: got %v want %v", name, sample.UTC, position.InFrontOfJupiter, wantFront)
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Logf("galilean Horizons max diff: X=%.3f arcsec Y=%.3f arcsec", maxXDiff, maxYDiff)
|
||||
}
|
||||
|
||||
func loadGalileanHorizonsBaseline(t *testing.T) []galileanHorizonsSample {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile("testdata/galilean_satellites_horizons.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var samples []galileanHorizonsSample
|
||||
if err := json.Unmarshal(data, &samples); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(samples) == 0 {
|
||||
t.Fatal("empty Galilean baseline")
|
||||
}
|
||||
return samples
|
||||
}
|
||||
|
||||
func selectGalileanSatellite(info GalileanSatellitesInfo, name string) GalileanSatellitePosition {
|
||||
switch name {
|
||||
case "io":
|
||||
return info.Io
|
||||
case "europa":
|
||||
return info.Europa
|
||||
case "ganymede":
|
||||
return info.Ganymede
|
||||
case "callisto":
|
||||
return info.Callisto
|
||||
default:
|
||||
panic("unknown satellite: " + name)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
[
|
||||
{
|
||||
"label": "io_transit_20260402",
|
||||
"satellite": 1,
|
||||
"type": "transit",
|
||||
"start_utc": "2026-04-02T18:00:36.716Z",
|
||||
"start_duration_minutes": 3.62,
|
||||
"end_utc": "2026-04-02T20:16:11.316Z",
|
||||
"end_duration_minutes": 3.62,
|
||||
"source": "IMCCE phenomenes des satellites de Jupiter 2026 (English PDF, TT times)",
|
||||
"source_lines": "PDF lines 660,662"
|
||||
},
|
||||
{
|
||||
"label": "io_shadow_20260402",
|
||||
"satellite": 1,
|
||||
"type": "shadow_transit",
|
||||
"start_utc": "2026-04-02T19:17:10.316Z",
|
||||
"start_duration_minutes": 3.61,
|
||||
"end_utc": "2026-04-02T21:33:23.716Z",
|
||||
"end_duration_minutes": 3.61,
|
||||
"source": "IMCCE phenomenes des satellites de Jupiter 2026 (English PDF, TT times)",
|
||||
"source_lines": "PDF lines 661,664"
|
||||
},
|
||||
{
|
||||
"label": "ganymede_occultation_20260406",
|
||||
"satellite": 3,
|
||||
"type": "occultation",
|
||||
"start_utc": "2026-04-06T10:35:18.616Z",
|
||||
"start_duration_minutes": 8.93,
|
||||
"end_utc": "2026-04-06T13:54:58.216Z",
|
||||
"end_duration_minutes": 8.93,
|
||||
"source": "IMCCE phenomenes des satellites de Jupiter 2026 (English PDF, TT times)",
|
||||
"source_lines": "PDF lines 690,691"
|
||||
},
|
||||
{
|
||||
"label": "ganymede_eclipse_20260406",
|
||||
"satellite": 3,
|
||||
"type": "eclipse",
|
||||
"start_utc": "2026-04-06T15:47:24.216Z",
|
||||
"start_duration_minutes": 8.61,
|
||||
"end_utc": "2026-04-06T19:14:32.116Z",
|
||||
"end_duration_minutes": 8.61,
|
||||
"source": "IMCCE phenomenes des satellites de Jupiter 2026 (English PDF, TT times)",
|
||||
"source_lines": "PDF lines 692,693"
|
||||
},
|
||||
{
|
||||
"label": "europa_transit_20260406",
|
||||
"satellite": 2,
|
||||
"type": "transit",
|
||||
"start_utc": "2026-04-06T22:32:36.216Z",
|
||||
"start_duration_minutes": 3.85,
|
||||
"end_utc": "2026-04-07T01:21:05.516Z",
|
||||
"end_duration_minutes": 3.85,
|
||||
"source": "IMCCE phenomenes des satellites de Jupiter 2026 (English PDF, TT times)",
|
||||
"source_lines": "PDF lines 694,696"
|
||||
},
|
||||
{
|
||||
"label": "europa_shadow_20260407",
|
||||
"satellite": 2,
|
||||
"type": "shadow_transit",
|
||||
"start_utc": "2026-04-07T01:04:31.716Z",
|
||||
"start_duration_minutes": 3.81,
|
||||
"end_utc": "2026-04-07T03:54:13.416Z",
|
||||
"end_duration_minutes": 3.81,
|
||||
"source": "IMCCE phenomenes des satellites de Jupiter 2026 (English PDF, TT times)",
|
||||
"source_lines": "PDF lines 695,697"
|
||||
},
|
||||
{
|
||||
"label": "callisto_transit_20260403",
|
||||
"satellite": 4,
|
||||
"type": "transit",
|
||||
"start_utc": "2026-04-03T13:07:09.016Z",
|
||||
"start_duration_minutes": 11.99,
|
||||
"end_utc": "2026-04-03T17:04:35.916Z",
|
||||
"end_duration_minutes": 11.92,
|
||||
"source": "IMCCE phenomenes des satellites de Jupiter 2026 (English PDF, TT times)",
|
||||
"source_lines": "PDF lines 671,674"
|
||||
},
|
||||
{
|
||||
"label": "callisto_shadow_20260404",
|
||||
"satellite": 4,
|
||||
"type": "shadow_transit",
|
||||
"start_utc": "2026-04-04T01:08:40.516Z",
|
||||
"start_duration_minutes": 10.94,
|
||||
"end_utc": "2026-04-04T05:26:50.116Z",
|
||||
"end_duration_minutes": 10.98,
|
||||
"source": "IMCCE phenomenes des satellites de Jupiter 2026 (English PDF, TT times)",
|
||||
"source_lines": "PDF lines 676,677"
|
||||
}
|
||||
]
|
||||
+327
@@ -0,0 +1,327 @@
|
||||
[
|
||||
{
|
||||
"utc": "1998-01-08T12:00:00Z",
|
||||
"phenomena": {
|
||||
"callisto": {
|
||||
"transit": false,
|
||||
"occultation": true,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
},
|
||||
"europa": {
|
||||
"transit": false,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
},
|
||||
"ganymede": {
|
||||
"transit": false,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
},
|
||||
"io": {
|
||||
"transit": false,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"utc": "1999-01-01T02:00:00Z",
|
||||
"phenomena": {
|
||||
"callisto": {
|
||||
"transit": false,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
},
|
||||
"europa": {
|
||||
"transit": false,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
},
|
||||
"ganymede": {
|
||||
"transit": false,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
},
|
||||
"io": {
|
||||
"transit": true,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": true,
|
||||
"shadow_x_arcsec": 17.228193145273753,
|
||||
"shadow_y_arcsec": 3.256241312014926
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"utc": "2000-01-01T06:00:00Z",
|
||||
"phenomena": {
|
||||
"callisto": {
|
||||
"transit": false,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
},
|
||||
"europa": {
|
||||
"transit": false,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
},
|
||||
"ganymede": {
|
||||
"transit": true,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
},
|
||||
"io": {
|
||||
"transit": false,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"utc": "2000-01-01T11:00:00Z",
|
||||
"phenomena": {
|
||||
"callisto": {
|
||||
"transit": false,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
},
|
||||
"europa": {
|
||||
"transit": true,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
},
|
||||
"ganymede": {
|
||||
"transit": false,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": true,
|
||||
"shadow_x_arcsec": 3.158460096080454,
|
||||
"shadow_y_arcsec": -16.417707231374926
|
||||
},
|
||||
"io": {
|
||||
"transit": false,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"utc": "2000-01-01T14:00:00Z",
|
||||
"phenomena": {
|
||||
"callisto": {
|
||||
"transit": false,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
},
|
||||
"europa": {
|
||||
"transit": false,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": true,
|
||||
"shadow_x_arcsec": -5.755686876706326,
|
||||
"shadow_y_arcsec": -12.656123592995895
|
||||
},
|
||||
"ganymede": {
|
||||
"transit": false,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
},
|
||||
"io": {
|
||||
"transit": false,
|
||||
"occultation": true,
|
||||
"eclipse": true,
|
||||
"shadow_transit": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"utc": "2000-01-01T15:00:00Z",
|
||||
"phenomena": {
|
||||
"callisto": {
|
||||
"transit": false,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
},
|
||||
"europa": {
|
||||
"transit": false,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
},
|
||||
"ganymede": {
|
||||
"transit": false,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
},
|
||||
"io": {
|
||||
"transit": false,
|
||||
"occultation": false,
|
||||
"eclipse": true,
|
||||
"shadow_transit": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"utc": "2000-01-02T00:00:00Z",
|
||||
"phenomena": {
|
||||
"callisto": {
|
||||
"transit": false,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
},
|
||||
"europa": {
|
||||
"transit": false,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
},
|
||||
"ganymede": {
|
||||
"transit": false,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
},
|
||||
"io": {
|
||||
"transit": false,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"utc": "2000-01-03T05:00:00Z",
|
||||
"phenomena": {
|
||||
"callisto": {
|
||||
"transit": false,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
},
|
||||
"europa": {
|
||||
"transit": false,
|
||||
"occultation": true,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
},
|
||||
"ganymede": {
|
||||
"transit": false,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
},
|
||||
"io": {
|
||||
"transit": false,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"utc": "2000-01-03T08:00:00Z",
|
||||
"phenomena": {
|
||||
"callisto": {
|
||||
"transit": false,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
},
|
||||
"europa": {
|
||||
"transit": false,
|
||||
"occultation": false,
|
||||
"eclipse": true,
|
||||
"shadow_transit": false
|
||||
},
|
||||
"ganymede": {
|
||||
"transit": false,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
},
|
||||
"io": {
|
||||
"transit": false,
|
||||
"occultation": true,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"utc": "2000-01-04T20:00:00Z",
|
||||
"phenomena": {
|
||||
"callisto": {
|
||||
"transit": false,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
},
|
||||
"europa": {
|
||||
"transit": false,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
},
|
||||
"ganymede": {
|
||||
"transit": false,
|
||||
"occultation": true,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
},
|
||||
"io": {
|
||||
"transit": false,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"utc": "2000-01-05T01:00:00Z",
|
||||
"phenomena": {
|
||||
"callisto": {
|
||||
"transit": false,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
},
|
||||
"europa": {
|
||||
"transit": true,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
},
|
||||
"ganymede": {
|
||||
"transit": false,
|
||||
"occultation": false,
|
||||
"eclipse": true,
|
||||
"shadow_transit": false
|
||||
},
|
||||
"io": {
|
||||
"transit": false,
|
||||
"occultation": false,
|
||||
"eclipse": false,
|
||||
"shadow_transit": false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
+344
@@ -0,0 +1,344 @@
|
||||
[
|
||||
{
|
||||
"utc": "1973-04-15T00:00:00Z",
|
||||
"jupiter": {
|
||||
"ra_deg": 311.88806,
|
||||
"dec_deg": -18.29401,
|
||||
"delta_au": 5.28137291912345
|
||||
},
|
||||
"satellites": {
|
||||
"callisto": {
|
||||
"ra_deg": 311.7589,
|
||||
"dec_deg": -18.33129,
|
||||
"delta_au": 5.2768299268895,
|
||||
"offset_x_arcsec": -441.3803197314987,
|
||||
"offset_y_arcsec": -134.36446774862958
|
||||
},
|
||||
"europa": {
|
||||
"ra_deg": 311.90297,
|
||||
"dec_deg": -18.2902,
|
||||
"delta_au": 5.28564623493191,
|
||||
"offset_x_arcsec": 50.96424397059699,
|
||||
"offset_y_arcsec": 13.713918937389815
|
||||
},
|
||||
"ganymede": {
|
||||
"ra_deg": 311.85552,
|
||||
"dec_deg": -18.30439,
|
||||
"delta_au": 5.2878698628188,
|
||||
"offset_x_arcsec": -111.21668080202586,
|
||||
"offset_y_arcsec": -37.37791869906149
|
||||
},
|
||||
"io": {
|
||||
"ra_deg": 311.85761,
|
||||
"dec_deg": -18.30307,
|
||||
"delta_au": 5.2818325588453,
|
||||
"offset_x_arcsec": -104.07417725728882,
|
||||
"offset_y_arcsec": -32.62468494888245
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"utc": "1979-07-09T12:00:00Z",
|
||||
"jupiter": {
|
||||
"ra_deg": 135.47291,
|
||||
"dec_deg": 17.61495,
|
||||
"delta_au": 6.22692079240897
|
||||
},
|
||||
"satellites": {
|
||||
"callisto": {
|
||||
"ra_deg": 135.41111,
|
||||
"dec_deg": 17.634,
|
||||
"delta_au": 6.21639039566952,
|
||||
"offset_x_arcsec": -212.02591082772028,
|
||||
"offset_y_arcsec": 68.61463977683415
|
||||
},
|
||||
"europa": {
|
||||
"ra_deg": 135.45128,
|
||||
"dec_deg": 17.62131,
|
||||
"delta_au": 6.22314306640108,
|
||||
"offset_x_arcsec": -74.21428902094401,
|
||||
"offset_y_arcsec": 22.900240712738924
|
||||
},
|
||||
"ganymede": {
|
||||
"ra_deg": 135.431,
|
||||
"dec_deg": 17.62783,
|
||||
"delta_au": 6.22139657545851,
|
||||
"offset_x_arcsec": -143.79142173449782,
|
||||
"offset_y_arcsec": 46.383925792891034
|
||||
},
|
||||
"io": {
|
||||
"ra_deg": 135.48048,
|
||||
"dec_deg": 17.61266,
|
||||
"delta_au": 6.22960675673866,
|
||||
"offset_x_arcsec": 25.974530718161613,
|
||||
"offset_y_arcsec": -8.243480803327909
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"utc": "1986-02-28T06:00:00Z",
|
||||
"jupiter": {
|
||||
"ra_deg": 334.31927,
|
||||
"dec_deg": -11.53761,
|
||||
"delta_au": 6.00043827450847
|
||||
},
|
||||
"satellites": {
|
||||
"callisto": {
|
||||
"ra_deg": 334.39264,
|
||||
"dec_deg": -11.50804,
|
||||
"delta_au": 5.99075804173071,
|
||||
"offset_x_arcsec": 258.8221117548082,
|
||||
"offset_y_arcsec": 106.4189385239666
|
||||
},
|
||||
"europa": {
|
||||
"ra_deg": 334.34619,
|
||||
"dec_deg": -11.5258,
|
||||
"delta_au": 6.00369206259064,
|
||||
"offset_x_arcsec": 94.95775500954227,
|
||||
"offset_y_arcsec": 42.51154273649485
|
||||
},
|
||||
"ganymede": {
|
||||
"ra_deg": 334.35893,
|
||||
"dec_deg": -11.52063,
|
||||
"delta_au": 6.00605898085651,
|
||||
"offset_x_arcsec": 139.89947712091362,
|
||||
"offset_y_arcsec": 61.11832971139567
|
||||
},
|
||||
"io": {
|
||||
"ra_deg": 334.29956,
|
||||
"dec_deg": -11.54609,
|
||||
"delta_au": 5.9987041475836,
|
||||
"offset_x_arcsec": -69.52013591273698,
|
||||
"offset_y_arcsec": -30.530393390408392
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"utc": "1990-01-01T00:00:00Z",
|
||||
"jupiter": {
|
||||
"ra_deg": 95.81931,
|
||||
"dec_deg": 23.21442,
|
||||
"delta_au": 4.17057854607155
|
||||
},
|
||||
"satellites": {
|
||||
"callisto": {
|
||||
"ra_deg": 95.92891,
|
||||
"dec_deg": 23.21348,
|
||||
"delta_au": 4.18089325952361,
|
||||
"offset_x_arcsec": 362.6174261571469,
|
||||
"offset_y_arcsec": -3.247297114635362
|
||||
},
|
||||
"europa": {
|
||||
"ra_deg": 95.75478,
|
||||
"dec_deg": 23.21883,
|
||||
"delta_au": 4.17160377436699,
|
||||
"offset_x_arcsec": -213.4923982076987,
|
||||
"offset_y_arcsec": 15.923397637886637
|
||||
},
|
||||
"ganymede": {
|
||||
"ra_deg": 95.72549,
|
||||
"dec_deg": 23.22076,
|
||||
"delta_au": 4.17400474524911,
|
||||
"offset_x_arcsec": -310.3915493369612,
|
||||
"offset_y_arcsec": 22.924196432221798
|
||||
},
|
||||
"io": {
|
||||
"ra_deg": 95.85972,
|
||||
"dec_deg": 23.21264,
|
||||
"delta_au": 4.17134483600364,
|
||||
"offset_x_arcsec": 133.69948476482548,
|
||||
"offset_y_arcsec": -6.3894167489867995
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"utc": "1994-07-16T20:00:00Z",
|
||||
"jupiter": {
|
||||
"ra_deg": 213.25869,
|
||||
"dec_deg": -12.18243,
|
||||
"delta_au": 5.13006301194003
|
||||
},
|
||||
"satellites": {
|
||||
"callisto": {
|
||||
"ra_deg": 213.33486,
|
||||
"dec_deg": -12.21633,
|
||||
"delta_au": 5.14030952670628,
|
||||
"offset_x_arcsec": 268.002683889942,
|
||||
"offset_y_arcsec": -122.07769582646469
|
||||
},
|
||||
"europa": {
|
||||
"ra_deg": 213.3048,
|
||||
"dec_deg": -12.19819,
|
||||
"delta_au": 5.1287760793537,
|
||||
"offset_x_arcsec": 162.24824222991055,
|
||||
"offset_y_arcsec": -56.74979461519546
|
||||
},
|
||||
"ganymede": {
|
||||
"ra_deg": 213.33267,
|
||||
"dec_deg": -12.21081,
|
||||
"delta_au": 5.13171837461076,
|
||||
"offset_x_arcsec": 260.3026257035611,
|
||||
"offset_y_arcsec": -102.20354428249364
|
||||
},
|
||||
"io": {
|
||||
"ra_deg": 213.25028,
|
||||
"dec_deg": -12.18102,
|
||||
"delta_au": 5.13278719330082,
|
||||
"offset_x_arcsec": -29.594361442763407,
|
||||
"offset_y_arcsec": 5.075541713701257
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"utc": "2000-01-01T00:00:00Z",
|
||||
"jupiter": {
|
||||
"ra_deg": 23.85196,
|
||||
"dec_deg": 8.58622,
|
||||
"delta_au": 4.61342273127327
|
||||
},
|
||||
"satellites": {
|
||||
"callisto": {
|
||||
"ra_deg": 23.94979,
|
||||
"dec_deg": 8.63255,
|
||||
"delta_au": 4.62253449896296,
|
||||
"offset_x_arcsec": 348.19826724085266,
|
||||
"offset_y_arcsec": 166.83261886559407
|
||||
},
|
||||
"europa": {
|
||||
"ra_deg": 23.89053,
|
||||
"dec_deg": 8.60003,
|
||||
"delta_au": 4.61036909784164,
|
||||
"offset_x_arcsec": 137.29079390672234,
|
||||
"offset_y_arcsec": 49.72291009234657
|
||||
},
|
||||
"ganymede": {
|
||||
"ra_deg": 23.87051,
|
||||
"dec_deg": 8.58946,
|
||||
"delta_au": 4.60642203313293,
|
||||
"offset_x_arcsec": 66.030987085105,
|
||||
"offset_y_arcsec": 11.665596446458599
|
||||
},
|
||||
"io": {
|
||||
"ra_deg": 23.82242,
|
||||
"dec_deg": 8.57313,
|
||||
"delta_au": 4.61225879634782,
|
||||
"offset_x_arcsec": -105.1557573112971,
|
||||
"offset_y_arcsec": -47.119959026939206
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"utc": "2003-11-01T03:00:00Z",
|
||||
"jupiter": {
|
||||
"ra_deg": 164.75393,
|
||||
"dec_deg": 7.54667,
|
||||
"delta_au": 5.89519011181137
|
||||
},
|
||||
"satellites": {
|
||||
"callisto": {
|
||||
"ra_deg": 164.67248,
|
||||
"dec_deg": 7.58541,
|
||||
"delta_au": 5.88672225624688,
|
||||
"offset_x_arcsec": -290.6541511800618,
|
||||
"offset_y_arcsec": 139.49127107610735
|
||||
},
|
||||
"europa": {
|
||||
"ra_deg": 164.79359,
|
||||
"dec_deg": 7.5289,
|
||||
"delta_au": 5.89499084824017,
|
||||
"offset_x_arcsec": 141.5451201277793,
|
||||
"offset_y_arcsec": -63.9655812066536
|
||||
},
|
||||
"ganymede": {
|
||||
"ra_deg": 164.69132,
|
||||
"dec_deg": 7.57511,
|
||||
"delta_au": 5.89658460351966,
|
||||
"offset_x_arcsec": -223.42897345174333,
|
||||
"offset_y_arcsec": 102.40009278019474
|
||||
},
|
||||
"io": {
|
||||
"ra_deg": 164.72967,
|
||||
"dec_deg": 7.5579,
|
||||
"delta_au": 5.89452838126592,
|
||||
"offset_x_arcsec": -86.5772659690193,
|
||||
"offset_y_arcsec": 40.43041079542706
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"utc": "2007-05-10T18:30:00Z",
|
||||
"jupiter": {
|
||||
"ra_deg": 256.8904,
|
||||
"dec_deg": -22.15607,
|
||||
"delta_au": 4.41668013865791
|
||||
},
|
||||
"satellites": {
|
||||
"callisto": {
|
||||
"ra_deg": 256.92962,
|
||||
"dec_deg": -22.15161,
|
||||
"delta_au": 4.40445022555321,
|
||||
"offset_x_arcsec": 130.77052785171492,
|
||||
"offset_y_arcsec": 16.03912383080068
|
||||
},
|
||||
"europa": {
|
||||
"ra_deg": 256.94812,
|
||||
"dec_deg": -22.15993,
|
||||
"delta_au": 4.41502519173859,
|
||||
"offset_x_arcsec": 192.4433571554188,
|
||||
"offset_y_arcsec": -13.932562898451753
|
||||
},
|
||||
"ganymede": {
|
||||
"ra_deg": 256.96968,
|
||||
"dec_deg": -22.15942,
|
||||
"delta_au": 4.41230056589285,
|
||||
"offset_x_arcsec": 264.32717135812686,
|
||||
"offset_y_arcsec": -12.128977503550622
|
||||
},
|
||||
"io": {
|
||||
"ra_deg": 256.8516,
|
||||
"dec_deg": -22.15279,
|
||||
"delta_au": 4.41625480314586,
|
||||
"offset_x_arcsec": -129.36904468312147,
|
||||
"offset_y_arcsec": 11.791482646233307
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"utc": "2010-09-21T00:00:00Z",
|
||||
"jupiter": {
|
||||
"ra_deg": 359.06914,
|
||||
"dec_deg": -2.14436,
|
||||
"delta_au": 3.95393079286274
|
||||
},
|
||||
"satellites": {
|
||||
"callisto": {
|
||||
"ra_deg": 358.92727,
|
||||
"dec_deg": -2.20719,
|
||||
"delta_au": 3.96045524535587,
|
||||
"offset_x_arcsec": -510.35338962334276,
|
||||
"offset_y_arcsec": -226.2123343336146
|
||||
},
|
||||
"europa": {
|
||||
"ra_deg": 359.12197,
|
||||
"dec_deg": -2.11777,
|
||||
"delta_au": 3.95588150012068,
|
||||
"offset_x_arcsec": 190.05811838293806,
|
||||
"offset_y_arcsec": 95.72076203774765
|
||||
},
|
||||
"ganymede": {
|
||||
"ra_deg": 359.00696,
|
||||
"dec_deg": -2.17074,
|
||||
"delta_au": 3.95934036114568,
|
||||
"offset_x_arcsec": -223.687388112253,
|
||||
"offset_y_arcsec": -94.97259749250195
|
||||
},
|
||||
"io": {
|
||||
"ra_deg": 359.10579,
|
||||
"dec_deg": -2.12709,
|
||||
"delta_au": 3.95359305390024,
|
||||
"offset_x_arcsec": 131.84909358793013,
|
||||
"offset_y_arcsec": 62.17043482817281
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,137 @@
|
||||
package jupiter
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"b612.me/astro/basic"
|
||||
"b612.me/astro/calendar"
|
||||
"b612.me/astro/planet"
|
||||
)
|
||||
|
||||
// N variants keep the same semantics as the non-N APIs; n < 0 means full series.
|
||||
|
||||
// ApparentLoN 视黄经(截断版) / truncated apparent ecliptic longitude.
|
||||
func ApparentLoN(date time.Time, n int) float64 {
|
||||
jde := calendar.Date2JDE(date.UTC())
|
||||
return basic.JupiterApparentLoN(basic.TD2UT(jde, true), n)
|
||||
}
|
||||
|
||||
// ApparentBoN 视黄纬(截断版) / truncated apparent ecliptic latitude.
|
||||
func ApparentBoN(date time.Time, n int) float64 {
|
||||
jde := calendar.Date2JDE(date.UTC())
|
||||
return basic.JupiterApparentBoN(basic.TD2UT(jde, true), n)
|
||||
}
|
||||
|
||||
// ApparentRaN 视赤经(截断版) / truncated apparent right ascension.
|
||||
func ApparentRaN(date time.Time, n int) float64 {
|
||||
jde := calendar.Date2JDE(date.UTC())
|
||||
return basic.JupiterApparentRaN(basic.TD2UT(jde, true), n)
|
||||
}
|
||||
|
||||
// ApparentDecN 视赤纬(截断版) / truncated apparent declination.
|
||||
func ApparentDecN(date time.Time, n int) float64 {
|
||||
jde := calendar.Date2JDE(date.UTC())
|
||||
return basic.JupiterApparentDecN(basic.TD2UT(jde, true), n)
|
||||
}
|
||||
|
||||
// ApparentRaDecN 视赤经赤纬(截断版) / truncated apparent right ascension and declination.
|
||||
func ApparentRaDecN(date time.Time, n int) (float64, float64) {
|
||||
jde := calendar.Date2JDE(date.UTC())
|
||||
return basic.JupiterApparentRaDecN(basic.TD2UT(jde, true), n)
|
||||
}
|
||||
|
||||
// ApparentMagnitudeN 视星等(截断版) / truncated apparent magnitude.
|
||||
func ApparentMagnitudeN(date time.Time, n int) float64 {
|
||||
jde := calendar.Date2JDE(date.UTC())
|
||||
return basic.JupiterMagN(basic.TD2UT(jde, true), n)
|
||||
}
|
||||
|
||||
// EarthDistanceN 地球距离(截断版) / truncated Earth distance.
|
||||
func EarthDistanceN(date time.Time, n int) float64 {
|
||||
jde := calendar.Date2JDE(date.UTC())
|
||||
return basic.EarthJupiterAwayN(basic.TD2UT(jde, true), n)
|
||||
}
|
||||
|
||||
// SunDistanceN 太阳距离(截断版) / truncated Sun distance.
|
||||
func SunDistanceN(date time.Time, n int) float64 {
|
||||
jde := calendar.Date2JDE(date.UTC())
|
||||
return planet.WherePlanetN(4, 2, basic.TD2UT(jde, true), n)
|
||||
}
|
||||
|
||||
// AltitudeN 高度角(截断版) / truncated altitude angle.
|
||||
func AltitudeN(date time.Time, lon, lat float64, n int) float64 {
|
||||
jde := basic.Date2JDE(date)
|
||||
_, loc := date.Zone()
|
||||
timezone := float64(loc) / 3600.0
|
||||
return basic.JupiterHeightN(jde, lon, lat, timezone, n)
|
||||
}
|
||||
|
||||
// ZenithN 天顶距(截断版) / truncated zenith distance.
|
||||
func ZenithN(date time.Time, lon, lat float64, n int) float64 {
|
||||
return 90 - AltitudeN(date, lon, lat, n)
|
||||
}
|
||||
|
||||
// AzimuthN 方位角(截断版) / truncated azimuth angle.
|
||||
func AzimuthN(date time.Time, lon, lat float64, n int) float64 {
|
||||
jde := basic.Date2JDE(date)
|
||||
_, loc := date.Zone()
|
||||
timezone := float64(loc) / 3600.0
|
||||
return basic.JupiterAzimuthN(jde, lon, lat, timezone, n)
|
||||
}
|
||||
|
||||
// HourAngleN 时角(截断版) / truncated hour angle.
|
||||
func HourAngleN(date time.Time, lon float64, n int) float64 {
|
||||
jde := basic.Date2JDE(date)
|
||||
_, loc := date.Zone()
|
||||
timezone := float64(loc) / 3600.0
|
||||
return basic.JupiterHourAngleN(jde, lon, timezone, n)
|
||||
}
|
||||
|
||||
// CulminationTimeN 中天时间(截断版) / truncated culmination time.
|
||||
func CulminationTimeN(date time.Time, lon float64, n int) time.Time {
|
||||
if date.Hour() > 12 {
|
||||
date = date.Add(time.Hour * -12)
|
||||
}
|
||||
jde := basic.Date2JDE(date)
|
||||
_, loc := date.Zone()
|
||||
timezone := float64(loc) / 3600.0
|
||||
calcJde := basic.JupiterCulminationTimeN(jde, lon, timezone, n) - timezone/24.0
|
||||
return basic.JDE2DateByZone(calcJde, date.Location(), false)
|
||||
}
|
||||
|
||||
// RiseTimeN 升起时间(截断版) / truncated rise time.
|
||||
func RiseTimeN(date time.Time, lon, lat, height float64, aero bool, n int) (time.Time, error) {
|
||||
var aeroFloat float64
|
||||
if aero {
|
||||
aeroFloat = 1
|
||||
}
|
||||
if date.Hour() > 12 {
|
||||
date = date.Add(time.Hour * -12)
|
||||
}
|
||||
jde := basic.Date2JDE(date)
|
||||
_, loc := date.Zone()
|
||||
timezone := float64(loc) / 3600.0
|
||||
riseJde, err := basic.JupiterRiseTimeN(jde, lon, lat, timezone, aeroFloat, height, n)
|
||||
return riseSetResult(date, riseJde, err)
|
||||
}
|
||||
|
||||
// DownTimeN 落下时间别名(截断版) / truncated down-time alias.
|
||||
func DownTimeN(date time.Time, lon, lat, height float64, aero bool, n int) (time.Time, error) {
|
||||
return SetTimeN(date, lon, lat, height, aero, n)
|
||||
}
|
||||
|
||||
// SetTimeN 落下时间(截断版) / truncated set time.
|
||||
func SetTimeN(date time.Time, lon, lat, height float64, aero bool, n int) (time.Time, error) {
|
||||
var aeroFloat float64
|
||||
if aero {
|
||||
aeroFloat = 1
|
||||
}
|
||||
if date.Hour() > 12 {
|
||||
date = date.Add(time.Hour * -12)
|
||||
}
|
||||
jde := basic.Date2JDE(date)
|
||||
_, loc := date.Zone()
|
||||
timezone := float64(loc) / 3600.0
|
||||
riseJde, err := basic.JupiterSetTimeN(jde, lon, lat, timezone, aeroFloat, height, n)
|
||||
return riseSetResult(date, riseJde, err)
|
||||
}
|
||||
Reference in New Issue
Block a user