Files
astro/basic/moon_observation.go
T
b612 16c62a97d5 feat: 完善时标与天象几何计算并扩展输出接口
- 新增时标、ΔT 模型、质心时间与 UT1 支持
- 改进日月食、月掩、行星事件及路径边界计算
- 完善恒星三维自行与动态距离传播
- 扩展 SVG、GeoJSON、KML 输出与底层距离换算工具
- 整理中英文手册、示例资源及回归测试
2026-09-23 18:55:12 +08:00

486 lines
19 KiB
Go

package basic
import (
"math"
. "b612.me/astro/tools"
)
/*
* 月球方位角
*/
func MoonAzimuth(jd, lon, lat, tz float64) float64 {
//tmp := (tz*15 - lon) * 4 / 60
jde := UTC2TT(jd - tz/24)
ra := MoonTrueRa(jde)
dec := MoonTrueDec(jde)
away := MoonAway(jde) / 149597870.7
ndec := TopocentricDec(ra, dec, lat, lon, jd-tz/24, away, 0)
nra := TopocentricRa(ra, dec, lat, lon, jd-tz/24, away, 0)
jdUT := jd - tz/24
st := Limit360(ApparentSiderealTime(UTC2UT1(jdUT))*15 + lon)
hourAngle := Limit360(st - nra)
tmp2 := Sin(hourAngle) / (Cos(hourAngle)*Sin(lat) - Tan(ndec)*Cos(lat))
azimuth := ArcTan(tmp2)
if azimuth < 0 {
if hourAngle/15 < 12 {
return azimuth + 360
} else {
return azimuth + 180
}
} else {
if hourAngle/15 < 12 {
return azimuth + 180
} else {
return azimuth
}
}
}
func MoonHeight(jd, lon, lat, tz float64) float64 {
// tmp := (tz*15 - lon) * 4 / 60
//truejd=jd-tmp/24;
jde := UTC2TT(jd - tz/24)
ra := MoonTrueRa(jde)
dec := MoonTrueDec(jde)
away := MoonAway(jde) / 149597870.7
ndec := TopocentricDec(ra, dec, lat, lon, jd-tz/24, away, 0)
nra := TopocentricRa(ra, dec, lat, lon, jd-tz/24, away, 0)
jdUT := jd - tz/24
st := Limit360(ApparentSiderealTime(UTC2UT1(jdUT))*15 + lon)
hourAngle := Limit360(st - nra)
tmp2 := Sin(lat)*Sin(ndec) + Cos(ndec)*Cos(lat)*Cos(hourAngle)
return ArcSin(tmp2)
}
func HMoonAzimuth(jd, lon, lat, tz float64) float64 {
return HMoonAzimuthN(jd, lon, lat, tz, -1)
}
func HMoonAzimuthN(jd, lon, lat, tz float64, n int) float64 {
jde := UTC2TT(jd - tz/24)
ra := HMoonTrueRaN(jde, n)
dec := HMoonTrueDecN(jde, n)
away := HMoonAwayN(jde, n) / 149597870.7
ndec := TopocentricDec(ra, dec, lat, lon, jd-tz/24, away, 0)
nra := TopocentricRa(ra, dec, lat, lon, jd-tz/24, away, 0)
jdUT := jd - tz/24
st := Limit360(ApparentSiderealTime(UTC2UT1(jdUT))*15 + lon)
hourAngle := Limit360(st - nra)
tmp2 := Sin(hourAngle) / (Cos(hourAngle)*Sin(lat) - Tan(ndec)*Cos(lat))
azimuth := ArcTan(tmp2)
if azimuth < 0 {
if hourAngle/15 < 12 {
return azimuth + 360
} else {
return azimuth + 180
}
} else {
if hourAngle/15 < 12 {
return azimuth + 180
} else {
return azimuth
}
}
}
// HMoonHeight 当地民用时儒略日下的月心几何高度角(度,不含折射)/ geometric Moon-centre altitude in degrees for a local civil Julian day.
//
// jd 是该时区的当地民用时(墙上时刻)儒略日,tz 是时区偏移小时数,库内按 jd−tz/24 换成 UTC。
// 只有 tz 给 0 时 jd 才是 UTC 儒略日;不要拿 UTC 数值再配非零 tz,那会多减一次时区。
// jd is that zone's local civil (wall-clock) Julian day and tz is the zone offset in hours,
// converted internally as jd-tz/24. Only tz 0 makes jd a UTC Julian day: pairing a UTC value with a
// non-zero tz subtracts the offset twice.
func HMoonHeight(jd, lon, lat, tz float64) float64 {
return HMoonHeightN(jd, lon, lat, tz, -1)
}
type moonObservationState struct {
altitude float64
distanceKM float64
}
func hMoonObservationStateN(jd, lon, lat, tz, height float64, n int) moonObservationState {
calculationJDE := UTC2TT(jd - tz/24)
ra, dec := HMoonTrueRaDecN(calculationJDE, n)
distanceKM := HMoonAwayN(calculationJDE, n)
distanceAU := distanceKM / angularDiameterAstronomicalUnitKM
topocentricRA, topocentricDec := TopocentricRaDec(ra, dec, lat, lon, jd-tz/24, distanceAU, height)
siderealTime := Limit360(ApparentSiderealTime(UTC2UT1(jd-tz/24))*15 + lon)
hourAngle := Limit360(siderealTime - topocentricRA)
altitudeSine := Sin(lat)*Sin(topocentricDec) + Cos(topocentricDec)*Cos(lat)*Cos(hourAngle)
return moonObservationState{
altitude: ArcSin(altitudeSine),
distanceKM: distanceKM,
}
}
func HMoonHeightN(jd, lon, lat, tz float64, n int) float64 {
return hMoonObservationStateN(jd, lon, lat, tz, 0, n).altitude
}
// MoonState 同一瞬间可对任意观测点复用的月球位置与恒星时 / one instant's lunar position and sidereal time, reusable across observers.
type MoonState struct {
rightAscension float64
declination float64
distanceAU float64
siderealTime float64
}
// MoonStateAt 由 UTC 儒略日构造该瞬间的可复用月球状态 / builds the reusable state for one UTC Julian day.
func MoonStateAt(utcJD float64) MoonState {
jde := UTC2TT(utcJD)
rightAscension, declination := HMoonTrueRaDec(jde)
return MoonState{
rightAscension: rightAscension,
declination: declination,
distanceAU: HMoonAway(jde) / angularDiameterAstronomicalUnitKM,
siderealTime: ApparentSiderealTime(UTC2UT1(utcJD)) * 15,
}
}
func (state MoonState) finite() bool {
return finite(state.rightAscension) && finite(state.declination) &&
finite(state.distanceAU) && finite(state.siderealTime)
}
// HMoonHeight 给定观测者经度、纬度(度,椭球高 0)的月心几何高度角,等于 HMoonHeight(构造本状态时的 UTC 儒略日, 经, 纬, 0)。
// HMoonHeight returns the geometric Moon-centre altitude for one observer, equal to HMoonHeight(the UTC Julian day given to MoonStateAt, lon, lat, 0).
func (state MoonState) HMoonHeight(longitude, latitude float64) float64 {
// 本状态固定是 UTC 瞬间、椭球高 0,因此只对应包级 tz=0、height=0 的用法。
// 恒星时已在状态里算好,这里不再走会重算恒星时与时标换算的 TopocentricRaDec。
topocentricRA, topocentricDec := topocentricRaDecWithSidereal(
state.rightAscension, state.declination, latitude, longitude, state.siderealTime, state.distanceAU, 0,
)
hourAngle := Limit360(Limit360(state.siderealTime+longitude) - topocentricRA)
return ArcSin(Sin(latitude)*Sin(topocentricDec) + Cos(topocentricDec)*Cos(latitude)*Cos(hourAngle))
}
// MoonHorizon 用本状态生成海平面几何月心地平圈,口径同包级 MoonHorizon / sea-level geometric Moon-centre horizon ring from this state.
func (state MoonState) MoonHorizon(samples int) [][2]float64 {
if !state.finite() {
return nil
}
if samples <= 0 {
samples = 360
}
if samples < 12 {
samples = 12
} else if samples > 1440 {
samples = 1440
}
parallax := math.Sin(0.0024427777777*rad) / state.distanceAU
longitude := (state.rightAscension - state.siderealTime) * rad
latitude := state.declination * rad
if !finite(parallax) || parallax <= 0 || parallax >= 1 || !finite(longitude) || !finite(latitude) {
return nil
}
center := [3]float64{math.Cos(latitude) * math.Cos(longitude), math.Cos(latitude) * math.Sin(longitude), math.Sin(latitude)}
north := [3]float64{-math.Sin(latitude) * math.Cos(longitude), -math.Sin(latitude) * math.Sin(longitude), math.Cos(latitude)}
east := [3]float64{-math.Sin(longitude), math.Cos(longitude), 0}
points := make([][2]float64, samples)
for index := range points {
bearing := 2 * math.Pi * float64(index) / float64(samples)
radius := math.Acos(parallax)
var point [3]float64
for iteration := 0; iteration < 8; iteration++ {
for axis := range point {
point[axis] = center[axis]*math.Cos(radius) +
(north[axis]*math.Cos(bearing)+east[axis]*math.Sin(bearing))*math.Sin(radius)
}
lat := math.Asin(math.Max(-1, math.Min(1, point[2]))) / rad
// The topocentric direction is horizontal when its dot product
// with the geodetic zenith vanishes: cos(radius)=observer/range.
next := math.Acos(parallax * (pcosi(lat, 0)*math.Cos(lat*rad) + psini(lat, 0)*math.Sin(lat*rad)))
if math.Abs(next-radius) < 1e-14 {
break
}
radius = next
}
points[index] = [2]float64{math.Atan2(point[1], point[0]) / rad, math.Asin(math.Max(-1, math.Min(1, point[2]))) / rad}
}
return points
}
func moonRiseSetResidual(jd, longitude, latitude, timeZone, zenithShift, height float64, n int) float64 {
state := hMoonObservationStateN(jd, longitude, latitude, timeZone, height, n)
// 相对观测者下沉地平线的视上缘高度角 / Apparent upper-limb altitude relative to the observer's depressed horizon.
residual := state.altitude + HeightDegreeByLat(height, latitude)
if zenithShift != 0 {
residual += RefractionFromTrueAltitude(state.altitude, refractionStandardPressureHPa, refractionStandardTemperatureC)
residual += angularSemidiameterArcsec(moonEquatorialRadiusKM, state.distanceKM) / 3600
}
return residual
}
// moonRiseSetOnCivilDay 在民用日内求升/落时刻;找不到过零时的错误口径与 rise_set.go 的 ErrNeverRise/ErrNeverSet 一致,
// fallbackErr 是调用方用中天/下中天残差预判的同一几何结论,命中时优先于扫描结果。
func moonRiseSetOnCivilDay(candidate, slope, civilDayStart, longitude, latitude, originalTimeZone,
localTimeZone, zenithShift, height float64, isRise bool, fallbackErr error) (float64, error) {
if eventRiseSetCandidateValid(candidate, civilDayStart, slope, isRise) {
return candidate, nil
}
return eventDirectionalRiseSetSearch(civilDayStart, isRise, fallbackErr, func(outputJD float64) float64 {
localJD := outputJD + localTimeZone/24 - originalTimeZone/24
return moonRiseSetResidual(localJD, longitude, latitude, localTimeZone, zenithShift, height, -1)
})
}
// 废弃
func GetMoonTZTime(jd, lon, lat, tz float64) float64 { //实际中天时间{
jd = math.Floor(jd) + 0.5
ttm := MoonTimeAngle(jd, lon, lat, tz)
if ttm > 0 && ttm < 180 {
jd += 0.5
}
estimateJD := jd
var ok bool
estimateJD, ok = eventNewtonRefine(estimateJD, 0.00001, func(prevJD float64) float64 {
stDegree := MoonTimeAngle(prevJD, lon, lat, tz) - 359.599
stDegreep := (MoonTimeAngle(prevJD+0.000005, lon, lat, tz) - MoonTimeAngle(prevJD-0.000005, lon, lat, tz)) / 0.00001
return stDegree / stDegreep
})
if !ok {
return math.NaN()
}
return estimateJD
}
func MoonCulminationTime(localJD, lon, lat, timezone float64) float64 {
// localJD 是本地民用日锚点(当地 0 时),不是力学时;ra/dec 为瞬时天球坐标,非 J2000 等固定历元。
localJD = math.Floor(localJD) + 0.5
estimateJD := localJD + Limit360(360-MoonTimeAngle(localJD, lon, lat, timezone))/15.0/24.0/0.9
limitHA := func(localJD, lon, timezone float64) float64 {
ha := MoonTimeAngle(localJD, lon, lat, timezone)
if ha < 180 {
ha += 360
}
return ha
}
var ok bool
estimateJD, ok = eventNewtonRefine(estimateJD, 0.00001, func(prevJD float64) float64 {
stDegree := limitHA(prevJD, lon, timezone) - 360
stDegreep := (limitHA(prevJD+0.000005, lon, timezone) - limitHA(prevJD-0.000005, lon, timezone)) / 0.00001
return stDegree / stDegreep
})
if !ok {
return math.NaN()
}
return estimateJD
}
func MoonTimeAngle(jd, lon, lat, tz float64) float64 {
startime := Limit360(ApparentSiderealTime(UTC2UT1(jd-tz/24))*15 + lon)
timeangle := startime - HMoonApparentRa(jd, lon, lat, tz)
if timeangle < 0 {
timeangle += 360
}
return timeangle
}
func GetMoonRiseTime(julianDay, longitude, latitude, timeZone, zenithShift, height float64) (float64, error) {
if !isFiniteFloat(julianDay) || !isFiniteFloat(longitude) || !isFiniteFloat(latitude) || !isFiniteFloat(timeZone) || !isFiniteFloat(zenithShift) || !isFiniteFloat(height) {
return 0, ErrInvalidObservationInput
}
originalTimeZone := timeZone
timeZone = longitude / 15
var timeToMeridian float64
civilDayStart := math.Floor(julianDay) + 0.5
// 时间分界线以传入的时区为准,不用当地时区,否则 0 时的判断会出错。
julianDay = math.Floor(julianDay) + 0.5
estimatedTime := julianDay
moonResidual := moonRiseSetResidual(julianDay, longitude, latitude, originalTimeZone, zenithShift, height, -1)
moonAngle := StandardAltitudeMoon(zenithShift, height, latitude)
moonAngleTime := MoonTimeAngle(julianDay, longitude, latitude, originalTimeZone)
if moonResidual > 0 { // 月亮在地平线上或在落下与下中天之间
if moonAngleTime > 180 {
timeToMeridian = (180 + 360 - moonAngleTime) / 15
} else {
timeToMeridian = (180 - moonAngleTime) / 15
}
estimatedTime += (timeToMeridian/24 + (timeToMeridian/24*12.0)/15.0/24.0)
}
if moonResidual < 0 && moonAngleTime > 180 {
timeToMeridian = (180 - moonAngleTime) / 15
estimatedTime += (timeToMeridian/24 + (timeToMeridian/24*12.0)/15.0/24.0)
} else if moonResidual < 0 && moonAngleTime < 180 {
timeToMeridian = (180 - moonAngleTime) / 15
estimatedTime += (timeToMeridian/24 + (timeToMeridian/24*12.0)/15.0/24.0)
}
currentAngle := MoonTimeAngle(estimatedTime, longitude, latitude, timeZone)
if math.Abs(currentAngle-180) > 0.5 {
estimatedTime += (180 - currentAngle) * 4.0 / 60.0 / 24.0
}
currentResidual := moonRiseSetResidual(estimatedTime, longitude, latitude, timeZone, zenithShift, height, -1)
if !(currentResidual < -10 && math.Abs(latitude) < 60) {
if currentResidual > 0 {
// 下中天仍在地平线上:当日无落下(也无可升起),口径见 moonRiseSetOnCivilDay。
return moonRiseSetOnCivilDay(math.NaN(), math.NaN(), civilDayStart, longitude, latitude,
originalTimeZone, timeZone, zenithShift, height, true, ErrNeverSet)
}
checkTime := estimatedTime + 12.0/24.0 + 6.0/15.0/24.0
checkAngle := MoonTimeAngle(checkTime, longitude, latitude, timeZone)
if checkAngle < 90 {
checkAngle += 360
}
checkTime += (360 - checkAngle) * 4.0 / 60.0 / 24.0
if moonRiseSetResidual(checkTime, longitude, latitude, timeZone, zenithShift, height, -1) < 0 {
// 上中天仍在地平线下:当日无升起。
return moonRiseSetOnCivilDay(math.NaN(), math.NaN(), civilDayStart, longitude, latitude,
originalTimeZone, timeZone, zenithShift, height, true, ErrNeverRise)
}
}
moonDeclination := MoonApparentDec(estimatedTime, longitude, latitude, timeZone)
tmp := (Sin(moonAngle) - Sin(moonDeclination)*Sin(latitude)) / (Cos(moonDeclination) * Cos(latitude))
if math.Abs(tmp) <= 1 && latitude < 85 {
hourAngle := (180 - ArcCos(tmp)) / 15
estimatedTime += hourAngle/24.00 + hourAngle/33.00/15.00
} else {
i := 0
for moonRiseSetResidual(estimatedTime, longitude, latitude, timeZone, zenithShift, height, -1) < 0 {
i++
estimatedTime += 15.0 / 60.0 / 24.0
if i > 48 {
break
}
}
}
// 使用牛顿迭代法求精确解
estimatedTime, slope := moonRiseSetResidualIteration(estimatedTime, longitude, latitude, timeZone, zenithShift, height, 0.00002)
estimatedTime = estimatedTime - timeZone/24 + originalTimeZone/24
return moonRiseSetOnCivilDay(estimatedTime, slope, civilDayStart, longitude, latitude,
originalTimeZone, timeZone, zenithShift, height, true, nil)
}
func GetMoonSetTime(julianDay, longitude, latitude, timeZone, zenithShift, height float64) (float64, error) {
if !isFiniteFloat(julianDay) || !isFiniteFloat(longitude) || !isFiniteFloat(latitude) || !isFiniteFloat(timeZone) || !isFiniteFloat(zenithShift) || !isFiniteFloat(height) {
return 0, ErrInvalidObservationInput
}
originalTimeZone := timeZone
timeZone = longitude / 15
var timeToMeridian float64
civilDayStart := math.Floor(julianDay) + 0.5
// 时间分界线以传入的时区为准,不用当地时区,否则 0 时的判断会出错。
julianDay = math.Floor(julianDay) + 0.5
estimatedTime := julianDay
moonResidual := moonRiseSetResidual(julianDay, longitude, latitude, originalTimeZone, zenithShift, height, -1)
moonAngle := StandardAltitudeMoon(zenithShift, height, latitude)
moonAngleTime := MoonTimeAngle(julianDay, longitude, latitude, originalTimeZone)
if moonResidual < 0 {
timeToMeridian = (360 - moonAngleTime) / 15
estimatedTime += (timeToMeridian/24 + (timeToMeridian/24.0*12.0)/15.0/24.0)
}
// 月亮在地平线上或在落下与下中天之间
if moonResidual > 0 && moonAngleTime < 180 {
timeToMeridian = (-moonAngleTime) / 15
estimatedTime += (timeToMeridian/24.0 + (timeToMeridian/24.0*12.0)/15.0/24.0)
} else if moonResidual > 0 {
timeToMeridian = (360 - moonAngleTime) / 15
estimatedTime += (timeToMeridian/24.0 + (timeToMeridian/24.0*12.0)/15.0/24.0)
}
currentAngle := MoonTimeAngle(estimatedTime, longitude, latitude, timeZone)
if currentAngle < 180 {
currentAngle += 360
}
if math.Abs(currentAngle-360) > 0.5 {
estimatedTime += (360 - currentAngle) * 4.0 / 60.0 / 24.0
}
// estimatedTime = 月球中天时间
currentResidual := moonRiseSetResidual(estimatedTime, longitude, latitude, timeZone, zenithShift, height, -1)
if !(currentResidual > 10 && math.Abs(latitude) < 60) {
if currentResidual < 0 {
// 上中天仍在地平线下:当日无升起,也就无落下。
return moonRiseSetOnCivilDay(math.NaN(), math.NaN(), civilDayStart, longitude, latitude,
originalTimeZone, timeZone, zenithShift, height, false, ErrNeverRise)
}
checkTime := estimatedTime + 12.0/24.0 + 6.0/15.0/24.0
angleSubtraction := 180 - MoonTimeAngle(checkTime, longitude, latitude, timeZone)
checkTime += angleSubtraction * 4.0 / 60.0 / 24.0
if moonRiseSetResidual(checkTime, longitude, latitude, timeZone, zenithShift, height, -1) > 0 {
// 下中天仍在地平线上:当日无落下。
return moonRiseSetOnCivilDay(math.NaN(), math.NaN(), civilDayStart, longitude, latitude,
originalTimeZone, timeZone, zenithShift, height, false, ErrNeverSet)
}
}
moonDeclination := MoonApparentDec(estimatedTime, longitude, latitude, timeZone)
tmp := (Sin(moonAngle) - Sin(moonDeclination)*Sin(latitude)) / (Cos(moonDeclination) * Cos(latitude))
if math.Abs(tmp) <= 1 && latitude < 85 {
hourAngle := (ArcCos(tmp)) / 15.0
estimatedTime += hourAngle/24 + hourAngle/33.0/15.0
} else {
i := 0
for moonRiseSetResidual(estimatedTime, longitude, latitude, timeZone, zenithShift, height, -1) > 0 {
i++
estimatedTime += 15.0 / 60.0 / 24.0
if i > 48 {
break
}
}
}
// 使用牛顿迭代法求精确解
estimatedTime, slope := moonRiseSetResidualIteration(estimatedTime, longitude, latitude, timeZone, zenithShift, height, 0.00002)
estimatedTime = estimatedTime - timeZone/24 + originalTimeZone/24
return moonRiseSetOnCivilDay(estimatedTime, slope, civilDayStart, longitude, latitude,
originalTimeZone, timeZone, zenithShift, height, false, nil)
}
// heightFunction 高度函数类型定义,用于牛顿迭代法
type heightFunction func(time, longitude, latitude, timeZone float64) float64
// moonRiseSetNewtonRaphsonIteration 牛顿-拉夫逊迭代法求解天体高度方程
func moonRiseSetNewtonRaphsonIteration(initialTime, longitude, latitude, timeZone, targetAngle float64,
heightFunc heightFunction, tolerance float64) float64 {
const derivativeStep = 0.000005
currentTime := initialTime
var ok bool
currentTime, ok = eventNewtonRefine(currentTime, tolerance, func(previousTime float64) float64 {
functionValue := heightFunc(previousTime, longitude, latitude, timeZone) - targetAngle
derivative := (heightFunc(previousTime+derivativeStep, longitude, latitude, timeZone) -
heightFunc(previousTime-derivativeStep, longitude, latitude, timeZone)) / (2 * derivativeStep)
return functionValue / derivative
})
if !ok {
return math.NaN()
}
return currentTime
}
func moonRiseSetResidualIteration(initialTime, longitude, latitude, timeZone, zenithShift, height, tolerance float64) (float64, float64) {
const derivativeStep = 0.000005
slope := math.NaN()
currentTime, ok := eventNewtonRefine(initialTime, tolerance, func(previousTime float64) float64 {
functionValue := moonRiseSetResidual(previousTime, longitude, latitude, timeZone, zenithShift, height, -1)
slope = (moonRiseSetResidual(previousTime+derivativeStep, longitude, latitude, timeZone, zenithShift, height, -1) -
moonRiseSetResidual(previousTime-derivativeStep, longitude, latitude, timeZone, zenithShift, height, -1)) / (2 * derivativeStep)
return functionValue / slope
})
if !ok {
return math.NaN(), math.NaN()
}
return currentTime, slope
}