299 lines
12 KiB
Go
299 lines
12 KiB
Go
|
|
package basic
|
||
|
|
|
||
|
|
import (
|
||
|
|
"math"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
// AltitudeLimb 日月取高度的位置;行星与恒星忽略 / limb used for Sun and Moon altitude.
|
||
|
|
type AltitudeLimb int
|
||
|
|
|
||
|
|
const (
|
||
|
|
// AltitudeLimbUpper 上缘,与日出日落口径相同 / upper limb.
|
||
|
|
AltitudeLimbUpper AltitudeLimb = iota
|
||
|
|
// AltitudeLimbCenter 中心 / centre.
|
||
|
|
AltitudeLimbCenter
|
||
|
|
// AltitudeLimbLower 下缘 / lower limb.
|
||
|
|
AltitudeLimbLower
|
||
|
|
)
|
||
|
|
|
||
|
|
// AltitudeCrossingOptions 任意高度升落选项;零值为上缘、标准气象(1010 hPa、10 °C)、计入折射 / options for altitude crossings.
|
||
|
|
type AltitudeCrossingOptions struct {
|
||
|
|
Limb AltitudeLimb
|
||
|
|
// NoRefraction 为 true 时目标高度按几何高度比较 / compare geometric altitude when true.
|
||
|
|
NoRefraction bool
|
||
|
|
// PressureHPa 为 0 时用标准气压与气温 / zero selects the standard atmosphere.
|
||
|
|
PressureHPa float64
|
||
|
|
TemperatureC float64
|
||
|
|
}
|
||
|
|
|
||
|
|
// AltitudeCrossingState 一个民用日内的穿越状态 / crossing state over one civil day.
|
||
|
|
type AltitudeCrossingState int
|
||
|
|
|
||
|
|
const (
|
||
|
|
// AltitudeCrosses 至少有一次升或落 / at least one crossing.
|
||
|
|
AltitudeCrosses AltitudeCrossingState = iota
|
||
|
|
// AltitudeAlwaysAbove 全天高于目标高度 / always above the target altitude.
|
||
|
|
AltitudeAlwaysAbove
|
||
|
|
// AltitudeAlwaysBelow 全天低于目标高度 / always below the target altitude.
|
||
|
|
AltitudeAlwaysBelow
|
||
|
|
// AltitudeTangent 极值与目标高度相差不足 1″,视为相切 / extremum within 1″ of the target.
|
||
|
|
AltitudeTangent
|
||
|
|
)
|
||
|
|
|
||
|
|
// AltitudeCrossing 民用日内首次升、落(本地儒略日,无则为 NaN)与残差范围(度)/ first rise and set in local JD with residual range in degrees.
|
||
|
|
type AltitudeCrossing struct {
|
||
|
|
Rise, Set float64
|
||
|
|
State AltitudeCrossingState
|
||
|
|
MinResidual float64
|
||
|
|
MaxResidual float64
|
||
|
|
}
|
||
|
|
|
||
|
|
// AltitudeCrossingResult date 所在民用日内首次升、落(无则为零值,对应 HasRise/HasSet 为 false)与残差范围(度)/ first rise and set on the civil day.
|
||
|
|
type AltitudeCrossingResult struct {
|
||
|
|
Rise, Set time.Time
|
||
|
|
HasRise, HasSet bool
|
||
|
|
State AltitudeCrossingState
|
||
|
|
MinResidual float64
|
||
|
|
MaxResidual float64
|
||
|
|
}
|
||
|
|
|
||
|
|
const (
|
||
|
|
altitudeScanStepDays = 10.0 / 1440
|
||
|
|
altitudeTangentDegrees = 1.0 / 3600
|
||
|
|
)
|
||
|
|
|
||
|
|
func (options AltitudeCrossingOptions) refraction(trueAltitude float64) float64 {
|
||
|
|
if options.NoRefraction {
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
if options.PressureHPa == 0 {
|
||
|
|
return RefractionFromTrueAltitude(trueAltitude, refractionStandardPressureHPa, refractionStandardTemperatureC)
|
||
|
|
}
|
||
|
|
return RefractionFromTrueAltitude(trueAltitude, options.PressureHPa, options.TemperatureC)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (options AltitudeCrossingOptions) limbOffset(semidiameterDeg float64) float64 {
|
||
|
|
switch options.Limb {
|
||
|
|
case AltitudeLimbUpper:
|
||
|
|
return semidiameterDeg
|
||
|
|
case AltitudeLimbLower:
|
||
|
|
return -semidiameterDeg
|
||
|
|
}
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
|
||
|
|
func (options AltitudeCrossingOptions) valid() bool {
|
||
|
|
if options.Limb < AltitudeLimbUpper || options.Limb > AltitudeLimbLower {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
return options.NoRefraction || options.PressureHPa == 0 || !math.IsNaN(RefractionFromTrueAltitude(0, options.PressureHPa, options.TemperatureC))
|
||
|
|
}
|
||
|
|
|
||
|
|
// SkyAltitudeCrossingWithInstance 日、月或行星在本地民用日 julianDay 内穿过视高度 targetAltitude(度,相对天文地平)的时刻 /
|
||
|
|
// Sun, Moon or planet crossings of an apparent altitude on the local civil day.
|
||
|
|
//
|
||
|
|
// height 只影响月球站心视差;需要以下沉地平为基准时由调用方从 targetAltitude 中减去地平俯角。
|
||
|
|
//
|
||
|
|
// 这是按儒略日窗口求解的低级接口:julianDay 与 timeZone(小时)由调用方拼出本地民用日,
|
||
|
|
// 固定时区不处理夏令时切换。julianDay 须取本地零点(x.5),正午及之后的值会落到次日窗口。一般用途请用 SkyAltitudeCrossingOnDate,它从 time.Time 推出
|
||
|
|
// 当天的真实时区窗口 / low-level julian-day window API; prefer SkyAltitudeCrossingOnDate.
|
||
|
|
func SkyAltitudeCrossingWithInstance(body SkyBody, julianDay, longitude, latitude, timeZone, height, targetAltitude float64,
|
||
|
|
options AltitudeCrossingOptions, inst *Instance) (AltitudeCrossing, error) {
|
||
|
|
altitude, err := skyAltitudeFunc(body, longitude, latitude, timeZone, height, inst)
|
||
|
|
if err != nil {
|
||
|
|
return AltitudeCrossing{}, err
|
||
|
|
}
|
||
|
|
start := math.Floor(julianDay) + 0.5
|
||
|
|
return altitudeCrossing(start, start+1, latitude, height, targetAltitude, options, altitude)
|
||
|
|
}
|
||
|
|
|
||
|
|
// SkyAltitudeCrossingOnDate 日、月或行星在 date 所在民用日内穿过视高度 targetAltitude 的时刻;夏令时切换日按实际日长搜索 /
|
||
|
|
// Sun, Moon or planet crossings of an apparent altitude on the civil day of date.
|
||
|
|
func SkyAltitudeCrossingOnDate(body SkyBody, date time.Time, longitude, latitude, height, targetAltitude float64,
|
||
|
|
options AltitudeCrossingOptions, inst *Instance) (AltitudeCrossingResult, error) {
|
||
|
|
start, end, timeZone := civilDayWindow(date)
|
||
|
|
altitude, err := skyAltitudeFunc(body, longitude, latitude, timeZone, height, inst)
|
||
|
|
if err != nil {
|
||
|
|
return AltitudeCrossingResult{}, err
|
||
|
|
}
|
||
|
|
raw, err := altitudeCrossing(start, end, latitude, height, targetAltitude, options, altitude)
|
||
|
|
return altitudeCrossingOnDate(date, timeZone, raw, err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// StarAltitudeCrossing 恒星(视赤经、视赤纬,度)在本地民用日内穿过视高度 targetAltitude 的时刻 / star crossings of an apparent altitude.
|
||
|
|
//
|
||
|
|
// julianDay 须取本地零点(x.5),正午及之后的值会落到次日窗口;一般用途请用 StarAltitudeCrossingOnDate。
|
||
|
|
func StarAltitudeCrossing(julianDay, ra, dec, longitude, latitude, timeZone, targetAltitude float64, options AltitudeCrossingOptions) (AltitudeCrossing, error) {
|
||
|
|
if !isFiniteFloat(ra) || !isFiniteFloat(dec) {
|
||
|
|
return AltitudeCrossing{}, ErrInvalidObservationInput
|
||
|
|
}
|
||
|
|
start := math.Floor(julianDay) + 0.5
|
||
|
|
return altitudeCrossing(start, start+1, latitude, 0, targetAltitude, options, starAltitudeFunc(ra, dec, longitude, latitude, timeZone))
|
||
|
|
}
|
||
|
|
|
||
|
|
// StarAltitudeCrossingOnDate 恒星在 date 所在民用日内穿过视高度 targetAltitude 的时刻;夏令时切换日按实际日长搜索 /
|
||
|
|
// star crossings of an apparent altitude on the civil day of date.
|
||
|
|
func StarAltitudeCrossingOnDate(date time.Time, ra, dec, longitude, latitude, targetAltitude float64, options AltitudeCrossingOptions) (AltitudeCrossingResult, error) {
|
||
|
|
if !isFiniteFloat(ra) || !isFiniteFloat(dec) {
|
||
|
|
return AltitudeCrossingResult{}, ErrInvalidObservationInput
|
||
|
|
}
|
||
|
|
start, end, timeZone := civilDayWindow(date)
|
||
|
|
raw, err := altitudeCrossing(start, end, latitude, 0, targetAltitude, options, starAltitudeFunc(ra, dec, longitude, latitude, timeZone))
|
||
|
|
return altitudeCrossingOnDate(date, timeZone, raw, err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// civilDayWindow 以当天零点的偏移固定本地时标,窗口为真实日长(夏令时切换日 23 或 25 小时)。
|
||
|
|
func civilDayWindow(date time.Time) (start, end, timeZone float64) {
|
||
|
|
year, month, day := date.Date()
|
||
|
|
midnight := time.Date(year, month, day, 0, 0, 0, 0, date.Location())
|
||
|
|
next := time.Date(year, month, day+1, 0, 0, 0, 0, date.Location())
|
||
|
|
_, offset := midnight.Zone()
|
||
|
|
start = Date2JD(midnight)
|
||
|
|
return start, start + next.Sub(midnight).Hours()/24, float64(offset) / 3600
|
||
|
|
}
|
||
|
|
|
||
|
|
func altitudeCrossingOnDate(date time.Time, timeZone float64, raw AltitudeCrossing, err error) (AltitudeCrossingResult, error) {
|
||
|
|
if err != nil {
|
||
|
|
return AltitudeCrossingResult{}, err
|
||
|
|
}
|
||
|
|
convert := func(jd float64) (time.Time, bool) {
|
||
|
|
if math.IsNaN(jd) {
|
||
|
|
return time.Time{}, false
|
||
|
|
}
|
||
|
|
return JD2DateByZone(jd-timeZone/24, date.Location(), false), true
|
||
|
|
}
|
||
|
|
out := AltitudeCrossingResult{State: raw.State, MinResidual: raw.MinResidual, MaxResidual: raw.MaxResidual}
|
||
|
|
out.Rise, out.HasRise = convert(raw.Rise)
|
||
|
|
out.Set, out.HasSet = convert(raw.Set)
|
||
|
|
return out, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func skyAltitudeFunc(body SkyBody, longitude, latitude, timeZone, height float64, inst *Instance) (func(jd float64) (float64, float64), error) {
|
||
|
|
switch {
|
||
|
|
case body == SkySun:
|
||
|
|
return func(jd float64) (float64, float64) {
|
||
|
|
state := sunObservationStateN(jd, longitude, latitude, timeZone, -1, inst)
|
||
|
|
return state.altitude, angularSemidiameterFromAU(sunEquatorialRadiusKM, state.distanceAU) / 3600
|
||
|
|
}, nil
|
||
|
|
case body == SkyMoon:
|
||
|
|
return func(jd float64) (float64, float64) {
|
||
|
|
state := hMoonObservationStateN(jd, longitude, latitude, timeZone, height, -1, inst)
|
||
|
|
return state.altitude, angularSemidiameterArcsec(moonEquatorialRadiusKM, state.distanceKM) / 3600
|
||
|
|
}, nil
|
||
|
|
case body.valid():
|
||
|
|
heightFn := skyPlanetHeight(body)
|
||
|
|
return func(jd float64) (float64, float64) { return heightFn(jd, longitude, latitude, timeZone, inst), 0 }, nil
|
||
|
|
}
|
||
|
|
return nil, ErrInvalidObservationInput
|
||
|
|
}
|
||
|
|
|
||
|
|
func starAltitudeFunc(ra, dec, longitude, latitude, timeZone float64) func(jd float64) (float64, float64) {
|
||
|
|
return func(jd float64) (float64, float64) {
|
||
|
|
return StarHeight(jd, ra, dec, longitude, latitude, timeZone), 0
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func skyPlanetHeight(body SkyBody) func(localJD, lon, lat, timezone float64, inst *Instance) float64 {
|
||
|
|
switch body {
|
||
|
|
case SkyMercury:
|
||
|
|
return MercuryHeightWithInstance
|
||
|
|
case SkyVenus:
|
||
|
|
return VenusHeightWithInstance
|
||
|
|
case SkyMars:
|
||
|
|
return MarsHeightWithInstance
|
||
|
|
case SkyJupiter:
|
||
|
|
return JupiterHeightWithInstance
|
||
|
|
case SkySaturn:
|
||
|
|
return SaturnHeightWithInstance
|
||
|
|
case SkyUranus:
|
||
|
|
return UranusHeightWithInstance
|
||
|
|
}
|
||
|
|
return NeptuneHeightWithInstance
|
||
|
|
}
|
||
|
|
|
||
|
|
func altitudeCrossing(start, end, latitude, height, targetAltitude float64, options AltitudeCrossingOptions,
|
||
|
|
altitude func(jd float64) (float64, float64)) (AltitudeCrossing, error) {
|
||
|
|
if !isFiniteFloat(start) || !isFiniteFloat(end) || !(end > start) || !isFiniteFloat(latitude) || math.Abs(latitude) > 90 ||
|
||
|
|
!isFiniteFloat(height) || !isFiniteFloat(targetAltitude) || math.Abs(targetAltitude) > 90 || !options.valid() {
|
||
|
|
return AltitudeCrossing{}, ErrInvalidObservationInput
|
||
|
|
}
|
||
|
|
residual := func(jd float64) float64 {
|
||
|
|
alt, semidiameter := altitude(jd)
|
||
|
|
return alt + options.refraction(alt) + options.limbOffset(semidiameter) - targetAltitude
|
||
|
|
}
|
||
|
|
result := AltitudeCrossing{Rise: math.NaN(), Set: math.NaN(), MinResidual: math.Inf(1), MaxResidual: math.Inf(-1)}
|
||
|
|
prevT, prevR := start, residual(start)
|
||
|
|
if math.IsNaN(prevR) {
|
||
|
|
return AltitudeCrossing{}, ErrInvalidObservationInput
|
||
|
|
}
|
||
|
|
track := func(t, r float64) {
|
||
|
|
result.MinResidual = math.Min(result.MinResidual, r)
|
||
|
|
result.MaxResidual = math.Max(result.MaxResidual, r)
|
||
|
|
}
|
||
|
|
track(prevT, prevR)
|
||
|
|
record := func(root float64, rising bool) {
|
||
|
|
if rising && math.IsNaN(result.Rise) {
|
||
|
|
result.Rise = root
|
||
|
|
} else if !rising && math.IsNaN(result.Set) {
|
||
|
|
result.Set = root
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// 同号区间内朝目标一侧的极值可能越过目标高度,采样点看不到这两次穿越。
|
||
|
|
refine := func(a, b, ra float64) {
|
||
|
|
peak, rp := orbitalGoldenExtremum(residual, a, b, ra < 0)
|
||
|
|
if math.IsNaN(rp) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
track(peak, rp)
|
||
|
|
if (rp < 0) == (ra < 0) || peak <= a || peak >= b {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
record(orbitalBisect(residual, a, peak, ra), ra < 0)
|
||
|
|
record(orbitalBisect(residual, peak, b, rp), ra >= 0)
|
||
|
|
}
|
||
|
|
prev2T, prev2R, samples := prevT, prevR, 1
|
||
|
|
for t := start + altitudeScanStepDays; prevT < end; t += altitudeScanStepDays {
|
||
|
|
t = math.Min(t, end)
|
||
|
|
r := residual(t)
|
||
|
|
if math.IsNaN(r) {
|
||
|
|
return AltitudeCrossing{}, ErrInvalidObservationInput
|
||
|
|
}
|
||
|
|
track(t, r)
|
||
|
|
sameSign := (prevR < 0) == (r < 0)
|
||
|
|
switch {
|
||
|
|
case !sameSign:
|
||
|
|
record(orbitalBisect(residual, prevT, t, prevR), r >= 0)
|
||
|
|
case samples == 1:
|
||
|
|
refine(prevT, t, prevR)
|
||
|
|
case (prev2R < 0) == (prevR < 0) && altitudeTowardTarget(prev2R, prevR, r):
|
||
|
|
refine(prev2T, t, prev2R)
|
||
|
|
}
|
||
|
|
prev2T, prev2R = prevT, prevR
|
||
|
|
prevT, prevR = t, r
|
||
|
|
samples++
|
||
|
|
}
|
||
|
|
if samples > 2 && (prev2R < 0) == (prevR < 0) {
|
||
|
|
refine(prev2T, prevT, prev2R)
|
||
|
|
}
|
||
|
|
switch {
|
||
|
|
case !math.IsNaN(result.Rise) || !math.IsNaN(result.Set):
|
||
|
|
result.State = AltitudeCrosses
|
||
|
|
case math.Abs(result.MinResidual) < altitudeTangentDegrees || math.Abs(result.MaxResidual) < altitudeTangentDegrees:
|
||
|
|
result.State = AltitudeTangent
|
||
|
|
case result.MinResidual > 0:
|
||
|
|
result.State = AltitudeAlwaysAbove
|
||
|
|
default:
|
||
|
|
result.State = AltitudeAlwaysBelow
|
||
|
|
}
|
||
|
|
return result, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// altitudeTowardTarget 报告中间样本是否为朝零点一侧的局部极值:负残差取极大,正残差取极小。
|
||
|
|
func altitudeTowardTarget(left, middle, right float64) bool {
|
||
|
|
if middle < 0 {
|
||
|
|
return middle >= left && middle >= right
|
||
|
|
}
|
||
|
|
return middle <= left && middle <= right
|
||
|
|
}
|