9ee2163cc7
- 新增月掩恒星和行星:支持搜索、掩甚点、全球掩带及固定地点轨迹计算 - 支持恒星星表坐标转换、有限盘面行星接触事件和月掩 SVG 输出 - 新增日月食及月掩全球投影图、时间标记和 GeoJSON 地理数据接口 - 扩展日食中心线、南北界及偏食足迹采样,支持极区投影 - 修正站心时角、月出月落、月球视半径、折射和恒星自行计算 - 优化内外行星事件搜索、边界选择、极端输入处理和计算稳定性
503 lines
20 KiB
Go
503 lines
20 KiB
Go
package basic
|
|
|
|
import (
|
|
"math"
|
|
"sort"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
planetOccultationDefaultStepDays = 0.25
|
|
planetOccultationCandidateLimitArcsec = 10 * 3600.0
|
|
planetOccultationLatitudeMarginArcsec = 3600.0
|
|
planetOccultationContactStepDays = 10.0 / 1440.0
|
|
planetOccultationContactSpanDays = 2.0
|
|
planetOccultationGrazingToleranceArcsec = 0.01
|
|
planetOccultationRootToleranceDays = occultationEventSelectionToleranceDays
|
|
planetOccultationMaxContactSteps = 10000
|
|
)
|
|
|
|
type planetOccultationConfig struct {
|
|
planet OccultationPlanet
|
|
equatorialRadiusKM float64
|
|
apparentRaDecN func(float64, int) (float64, float64)
|
|
earthDistanceN func(float64, int) float64
|
|
semidiameterN func(float64, int) float64
|
|
}
|
|
|
|
type planetMoonPosition struct {
|
|
moonRA, moonDec float64
|
|
planetRA, planetDec float64
|
|
valid bool
|
|
}
|
|
|
|
type planetOccultationState struct {
|
|
position planetMoonPosition
|
|
separationArcsec float64
|
|
moonSemidiameter float64
|
|
planetSemidiameter float64
|
|
externalContactMetric float64
|
|
internalContactMetric float64
|
|
valid bool
|
|
}
|
|
|
|
// FindPlanetOccultations 搜索固定观测点的有限盘面行星月掩。
|
|
// 经度东为正、纬度北为正,单位为度;高度为平均海平面以上米数。目标位置、视差和视半径会在每次候选、掩甚和接触计算时重新计算。
|
|
// FindPlanetOccultations searches one finite-disk planet at a fixed observing site.
|
|
// Longitude is east-positive in degrees, latitude is north-positive in degrees, and height is the observer elevation above mean sea level in meters. The target position, parallax, and semidiameter are recomputed at every candidate, greatest, and contact evaluation.
|
|
func FindPlanetOccultations(start, end time.Time, planet OccultationPlanet, longitude, latitude, height float64,
|
|
options OccultationSearchOptions) ([]PlanetOccultationInfo, error) {
|
|
if err := validateOccultationTimeRange(start, end); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := planet.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
observer := Observer{Longitude: longitude, Latitude: latitude, Height: height}
|
|
if err := observer.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := options.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
config, _ := planetOccultationConfigFor(planet)
|
|
startTT := occultationTimeToTT(start)
|
|
endTT := occultationTimeToTT(end)
|
|
resultLocation := start.Location()
|
|
candidates := planetOccultationCandidateGreatestTimes(
|
|
startTT, endTT, planetOccultationCoarseStepDays(options), config, &observer, options.SafetyMarginArcsec,
|
|
)
|
|
results := make([]PlanetOccultationInfo, 0, len(candidates))
|
|
for _, greatestTT := range candidates {
|
|
info, ok := planetOccultationInfoAtGreatest(greatestTT, config, observer, options.SafetyMarginArcsec, resultLocation)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if len(results) == 0 || math.Abs(results[len(results)-1].Greatest.Sub(info.Greatest).Seconds()) > 60 {
|
|
results = append(results, info)
|
|
if options.MaxEvents > 0 && len(results) >= options.MaxEvents {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
sort.SliceStable(results, func(i, j int) bool { return results[i].Greatest.Before(results[j].Greatest) })
|
|
return results, nil
|
|
}
|
|
|
|
// FindBestPlanetOccultations 返回窗口内每次有限盘面行星月掩的全球海平面几何掩甚点。
|
|
// 地心数据只用于搜索初值;返回位置与 FindPlanetOccultationPaths 一致,地平线可见性只报告、不参与点选择。距离查询端点 10 ms 内的掩甚时刻也会包含,与数值根精度一致。
|
|
// FindBestPlanetOccultations returns the global geometric greatest point at sea level for every finite-disk planetary occultation in the window.
|
|
// Geocentric data only seeds the search. The returned location matches FindPlanetOccultationPaths; horizon visibility is reported but does not select the point. A greatest instant within 10 ms of either query endpoint is included, matching the numerical root precision.
|
|
func FindBestPlanetOccultations(start, end time.Time, planet OccultationPlanet,
|
|
options OccultationSearchOptions) ([]PlanetOccultationInfo, error) {
|
|
if err := validateOccultationTimeRange(start, end); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := planet.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := options.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
config, _ := planetOccultationConfigFor(planet)
|
|
startTT := occultationTimeToTT(start)
|
|
endTT := occultationTimeToTT(end)
|
|
selectionStartTT := startTT - occultationEventSelectionToleranceDays
|
|
selectionEndTT := endTT + occultationEventSelectionToleranceDays
|
|
candidateStartTT := startTT - occultationPathSearchSpanDays
|
|
candidateEndTT := endTT + occultationPathSearchSpanDays
|
|
resultLocation := start.Location()
|
|
candidates := planetOccultationCandidateGreatestTimes(
|
|
candidateStartTT, candidateEndTT, planetOccultationCoarseStepDays(options), config, nil, options.SafetyMarginArcsec,
|
|
)
|
|
results := make([]PlanetOccultationInfo, 0, len(candidates))
|
|
for _, seedTT := range candidates {
|
|
greatestTT, observer, _, observerOK := planetOccultationBestObserver(seedTT, selectionStartTT, selectionEndTT, config)
|
|
if !observerOK {
|
|
continue
|
|
}
|
|
info, ok := planetOccultationInfoAtGreatest(greatestTT, config, observer, options.SafetyMarginArcsec, resultLocation)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if len(results) == 0 || math.Abs(results[len(results)-1].Greatest.Sub(info.Greatest).Seconds()) > 60 {
|
|
results = append(results, info)
|
|
if options.MaxEvents > 0 && len(results) >= options.MaxEvents {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
sort.SliceStable(results, func(i, j int) bool { return results[i].Greatest.Before(results[j].Greatest) })
|
|
return results, nil
|
|
}
|
|
|
|
func planetOccultationConfigFor(planet OccultationPlanet) (planetOccultationConfig, bool) {
|
|
config := planetOccultationConfig{planet: planet}
|
|
switch planet {
|
|
case OccultationMercury:
|
|
config.equatorialRadiusKM = mercuryEquatorialRadiusKM
|
|
config.apparentRaDecN = MercuryApparentRaDecN
|
|
config.earthDistanceN = EarthMercuryAwayN
|
|
config.semidiameterN = MercurySemidiameterN
|
|
case OccultationVenus:
|
|
config.equatorialRadiusKM = venusEquatorialRadiusKM
|
|
config.apparentRaDecN = VenusApparentRaDecN
|
|
config.earthDistanceN = EarthVenusAwayN
|
|
config.semidiameterN = VenusSemidiameterN
|
|
case OccultationMars:
|
|
config.equatorialRadiusKM = marsEquatorialRadiusKM
|
|
config.apparentRaDecN = MarsApparentRaDecN
|
|
config.earthDistanceN = EarthMarsAwayN
|
|
config.semidiameterN = MarsSemidiameterN
|
|
case OccultationJupiter:
|
|
config.equatorialRadiusKM = jupiterEquatorialRadiusKM
|
|
config.apparentRaDecN = JupiterApparentRaDecN
|
|
config.earthDistanceN = EarthJupiterAwayN
|
|
config.semidiameterN = JupiterSemidiameterN
|
|
case OccultationSaturn:
|
|
config.equatorialRadiusKM = saturnEquatorialRadiusKM
|
|
config.apparentRaDecN = SaturnApparentRaDecN
|
|
config.earthDistanceN = EarthSaturnAwayN
|
|
config.semidiameterN = SaturnSemidiameterN
|
|
case OccultationUranus:
|
|
config.equatorialRadiusKM = uranusEquatorialRadiusKM
|
|
config.apparentRaDecN = UranusApparentRaDecN
|
|
config.earthDistanceN = EarthUranusAwayN
|
|
config.semidiameterN = UranusSemidiameterN
|
|
case OccultationNeptune:
|
|
config.equatorialRadiusKM = neptuneEquatorialRadiusKM
|
|
config.apparentRaDecN = NeptuneApparentRaDecN
|
|
config.earthDistanceN = EarthNeptuneAwayN
|
|
config.semidiameterN = NeptuneSemidiameterN
|
|
default:
|
|
return planetOccultationConfig{}, false
|
|
}
|
|
return config, true
|
|
}
|
|
|
|
func planetOccultationCandidateGreatestTimes(
|
|
startTT, endTT, step float64,
|
|
config planetOccultationConfig,
|
|
observer *Observer,
|
|
safetyMarginArcsec float64,
|
|
) []float64 {
|
|
if endTT <= startTT {
|
|
return nil
|
|
}
|
|
duration := endTT - startTT
|
|
if step > duration/4 {
|
|
step = math.Max(duration/4, 0.25/86400.0)
|
|
}
|
|
step = math.Max(step, 0.25/86400.0)
|
|
scanStart := startTT - step
|
|
scanEnd := endTT + step
|
|
|
|
leftTT := scanStart
|
|
centerTT := math.Min(leftTT+step, scanEnd)
|
|
leftValue := planetOccultationExternalContactMetric(leftTT, config, observer, 8)
|
|
centerValue := planetOccultationExternalContactMetric(centerTT, config, observer, 8)
|
|
results := make([]float64, 0)
|
|
for centerTT < scanEnd {
|
|
rightTT := math.Min(centerTT+step, scanEnd)
|
|
rightValue := planetOccultationExternalContactMetric(rightTT, config, observer, 8)
|
|
candidateLimit := planetOccultationCandidateLimitArcsec + safetyMarginArcsec
|
|
if finite(leftValue) && finite(centerValue) && finite(rightValue) &&
|
|
centerValue <= leftValue && centerValue <= rightValue && centerValue <= candidateLimit {
|
|
// 最小外接触度量同时包含动态月面和行星盘面,因此定义事件是否存在以及报告的掩甚时刻。
|
|
// The minimum outer-contact metric includes both dynamic disks and therefore defines event existence and the reported greatest instant.
|
|
greatestTT := planetOccultationMinimizeExternalMetric(leftTT, rightTT, config, observer)
|
|
if greatestTT >= startTT && greatestTT <= endTT &&
|
|
planetOccultationLatitudePass(greatestTT, config, observer, safetyMarginArcsec) {
|
|
if len(results) == 0 || math.Abs(greatestTT-results[len(results)-1]) > 60.0/86400.0 {
|
|
results = append(results, greatestTT)
|
|
}
|
|
}
|
|
}
|
|
leftTT, leftValue = centerTT, centerValue
|
|
centerTT, centerValue = rightTT, rightValue
|
|
}
|
|
return results
|
|
}
|
|
|
|
func planetOccultationMinimizeExternalMetric(left, right float64, config planetOccultationConfig, observer *Observer) float64 {
|
|
if right <= left {
|
|
return left
|
|
}
|
|
const goldenRatio = 0.6180339887498949
|
|
x1 := right - goldenRatio*(right-left)
|
|
x2 := left + goldenRatio*(right-left)
|
|
f1 := planetOccultationExternalContactMetric(x1, config, observer, -1)
|
|
f2 := planetOccultationExternalContactMetric(x2, config, observer, -1)
|
|
for i := 0; i < 64 && right-left > planetOccultationRootToleranceDays; i++ {
|
|
if f1 > f2 {
|
|
left = x1
|
|
x1, f1 = x2, f2
|
|
x2 = left + goldenRatio*(right-left)
|
|
f2 = planetOccultationExternalContactMetric(x2, config, observer, -1)
|
|
} else {
|
|
right = x2
|
|
x2, f2 = x1, f1
|
|
x1 = right - goldenRatio*(right-left)
|
|
f1 = planetOccultationExternalContactMetric(x1, config, observer, -1)
|
|
}
|
|
}
|
|
return (left + right) / 2
|
|
}
|
|
|
|
func planetOccultationBestObserver(seedTT, startTT, endTT float64, config planetOccultationConfig) (float64, Observer, float64, bool) {
|
|
frameAt := func(tt float64) (occultationPathFrame, bool) {
|
|
return planetOccultationPathFrameAt(tt, config)
|
|
}
|
|
searchStart := seedTT - occultationPathSearchSpanDays
|
|
searchEnd := seedTT + occultationPathSearchSpanDays
|
|
outerStart, outerEnd, ok := occultationPathWindowForFrame(seedTT, searchStart, searchEnd, frameAt, false)
|
|
if !ok {
|
|
return 0, Observer{}, 0, false
|
|
}
|
|
greatestTT := occultationPathGreatestForFrame(seedTT, outerStart, outerEnd, frameAt)
|
|
if greatestTT < startTT || greatestTT > endTT {
|
|
return 0, Observer{}, 0, false
|
|
}
|
|
point, pointOK := occultationPathCenterPointForFrame(greatestTT, frameAt, time.UTC)
|
|
if !pointOK {
|
|
point, pointOK = occultationPathBoundaryPointForFrame(greatestTT, frameAt, time.UTC)
|
|
}
|
|
if !pointOK {
|
|
return 0, Observer{}, 0, false
|
|
}
|
|
observer := Observer{Longitude: point.Longitude, Latitude: point.Latitude}
|
|
state := planetOccultationStateAt(greatestTT, config, &observer, -1)
|
|
if !state.valid {
|
|
return 0, Observer{}, 0, false
|
|
}
|
|
return greatestTT, observer, state.externalContactMetric, true
|
|
}
|
|
|
|
func planetOccultationInfoAtGreatest(
|
|
greatestTT float64,
|
|
config planetOccultationConfig,
|
|
observer Observer,
|
|
safetyMarginArcsec float64,
|
|
location *time.Location,
|
|
) (PlanetOccultationInfo, bool) {
|
|
if !planetOccultationLatitudePass(greatestTT, config, &observer, safetyMarginArcsec) {
|
|
return PlanetOccultationInfo{}, false
|
|
}
|
|
state := planetOccultationStateAt(greatestTT, config, &observer, -1)
|
|
if !state.valid || state.externalContactMetric > 0 {
|
|
return PlanetOccultationInfo{}, false
|
|
}
|
|
info := PlanetOccultationInfo{
|
|
Planet: config.planet,
|
|
TargetID: config.planet.String(),
|
|
Observer: observer,
|
|
Type: OccultationPartial,
|
|
Greatest: occultationTTToLocation(greatestTT, location),
|
|
MinimumSeparationArcsec: state.separationArcsec,
|
|
PositionAngleDeg: occultationPositionAngle(state.position.moonRA, state.position.moonDec, state.position.planetRA, state.position.planetDec),
|
|
MoonSemidiameterArcsec: state.moonSemidiameter,
|
|
PlanetSemidiameterArcsec: state.planetSemidiameter,
|
|
MoonAltitudeAtGreatest: occultationAltitude(greatestTT, observer, state.position.moonRA, state.position.moonDec),
|
|
MoonAzimuthAtGreatest: occultationAzimuth(greatestTT, observer, state.position.moonRA, state.position.moonDec),
|
|
}
|
|
info.VisibleAtGreatest = info.MoonAltitudeAtGreatest >= 0
|
|
|
|
if math.Abs(state.externalContactMetric) <= planetOccultationGrazingToleranceArcsec {
|
|
info.Type = OccultationGrazing
|
|
info.ExternalImmersion = info.Greatest
|
|
info.ExternalEmersion = info.Greatest
|
|
info.ContactsComplete = true
|
|
return info, true
|
|
}
|
|
|
|
externalImmersionTT, externalImmersionOK := planetOccultationContact(greatestTT, -1, false, config, observer)
|
|
externalEmersionTT, externalEmersionOK := planetOccultationContact(greatestTT, 1, false, config, observer)
|
|
if !externalImmersionOK || !externalEmersionOK || externalEmersionTT <= externalImmersionTT {
|
|
return PlanetOccultationInfo{}, false
|
|
}
|
|
info.ExternalImmersion = occultationTTToLocation(externalImmersionTT, location)
|
|
info.ExternalEmersion = occultationTTToLocation(externalEmersionTT, location)
|
|
|
|
if state.internalContactMetric < -planetOccultationGrazingToleranceArcsec {
|
|
internalImmersionTT, internalImmersionOK := planetOccultationContact(greatestTT, -1, true, config, observer)
|
|
internalEmersionTT, internalEmersionOK := planetOccultationContact(greatestTT, 1, true, config, observer)
|
|
if !internalImmersionOK || !internalEmersionOK ||
|
|
internalImmersionTT <= externalImmersionTT || internalEmersionTT >= externalEmersionTT ||
|
|
internalImmersionTT >= greatestTT || internalEmersionTT <= greatestTT {
|
|
return PlanetOccultationInfo{}, false
|
|
}
|
|
info.Type = OccultationTotal
|
|
info.InternalImmersion = occultationTTToLocation(internalImmersionTT, location)
|
|
info.InternalEmersion = occultationTTToLocation(internalEmersionTT, location)
|
|
info.HasInternalContacts = true
|
|
}
|
|
info.ContactsComplete = true
|
|
return info, true
|
|
}
|
|
|
|
func planetMoonPositionAt(tt float64, config planetOccultationConfig, observer *Observer, n int) planetMoonPosition {
|
|
moonRA, moonDec := HMoonGeocentricApparentRaDecN(tt, n)
|
|
planetRA, planetDec := config.apparentRaDecN(tt, n)
|
|
if observer != nil {
|
|
ut := TD2UT(tt, false)
|
|
moonDistanceAU := HMoonAwayN(tt, n) / angularDiameterAstronomicalUnitKM
|
|
planetDistanceAU := config.earthDistanceN(tt, n)
|
|
moonRA, moonDec = TopocentricRaDec(moonRA, moonDec, observer.Latitude, observer.Longitude, ut, moonDistanceAU, observer.Height)
|
|
planetRA, planetDec = TopocentricRaDec(planetRA, planetDec, observer.Latitude, observer.Longitude, ut, planetDistanceAU, observer.Height)
|
|
moonRA = normalizeRA(moonRA)
|
|
planetRA = normalizeRA(planetRA)
|
|
}
|
|
return planetMoonPosition{
|
|
moonRA: moonRA,
|
|
moonDec: moonDec,
|
|
planetRA: planetRA,
|
|
planetDec: planetDec,
|
|
valid: finite(moonRA) && finite(moonDec) && finite(planetRA) && finite(planetDec),
|
|
}
|
|
}
|
|
|
|
func planetOccultationStateAt(tt float64, config planetOccultationConfig, observer *Observer, n int) planetOccultationState {
|
|
position := planetMoonPositionAt(tt, config, observer, n)
|
|
moonRadius := MoonSemidiameterN(tt, n)
|
|
planetRadius := config.semidiameterN(tt, n)
|
|
if observer != nil {
|
|
moonRadius = moonTopocentricSemidiameterN(tt, *observer, n)
|
|
planetRadius = planetTopocentricSemidiameterN(tt, config, *observer, n)
|
|
}
|
|
if !position.valid || !finite(moonRadius) || !finite(planetRadius) || moonRadius <= planetRadius || planetRadius <= 0 {
|
|
return planetOccultationState{}
|
|
}
|
|
separation := angularSeparationDegrees(position.moonRA, position.moonDec, position.planetRA, position.planetDec) * 3600
|
|
return planetOccultationState{
|
|
position: position,
|
|
separationArcsec: separation,
|
|
moonSemidiameter: moonRadius,
|
|
planetSemidiameter: planetRadius,
|
|
externalContactMetric: separation - (moonRadius + planetRadius),
|
|
internalContactMetric: separation - (moonRadius - planetRadius),
|
|
valid: finite(separation),
|
|
}
|
|
}
|
|
|
|
func planetTopocentricSemidiameterN(tt float64, config planetOccultationConfig, observer Observer, n int) float64 {
|
|
ra, dec := config.apparentRaDecN(tt, n)
|
|
distanceKM := config.earthDistanceN(tt, n) * angularDiameterAstronomicalUnitKM
|
|
if !finite(ra) || !finite(dec) || !finite(distanceKM) || distanceKM <= 0 {
|
|
return math.NaN()
|
|
}
|
|
distanceKM = topocentricDistanceKM(ra, dec, distanceKM, observer, TD2UT(tt, false))
|
|
if !finite(distanceKM) || distanceKM <= config.equatorialRadiusKM {
|
|
return math.NaN()
|
|
}
|
|
return angularSemidiameterArcsec(config.equatorialRadiusKM, distanceKM)
|
|
}
|
|
|
|
func planetOccultationExternalContactMetric(tt float64, config planetOccultationConfig, observer *Observer, n int) float64 {
|
|
state := planetOccultationStateAt(tt, config, observer, n)
|
|
if !state.valid {
|
|
return math.Inf(1)
|
|
}
|
|
return state.externalContactMetric
|
|
}
|
|
|
|
func planetMoonSeparationArcsec(tt float64, config planetOccultationConfig, observer *Observer, n int) float64 {
|
|
position := planetMoonPositionAt(tt, config, observer, n)
|
|
if !position.valid {
|
|
return math.Inf(1)
|
|
}
|
|
return angularSeparationDegrees(position.moonRA, position.moonDec, position.planetRA, position.planetDec) * 3600
|
|
}
|
|
|
|
func planetOccultationLatitudePass(tt float64, config planetOccultationConfig, observer *Observer, safetyMarginArcsec float64) bool {
|
|
state := planetOccultationStateAt(tt, config, observer, -1)
|
|
if !state.valid {
|
|
return false
|
|
}
|
|
_, moonLatitude := RaDecToLoBo(tt, state.position.moonRA, state.position.moonDec)
|
|
_, planetLatitude := RaDecToLoBo(tt, state.position.planetRA, state.position.planetDec)
|
|
limit := state.moonSemidiameter + state.planetSemidiameter + planetOccultationLatitudeMarginArcsec + safetyMarginArcsec
|
|
return math.Abs(moonLatitude-planetLatitude)*3600 <= limit
|
|
}
|
|
|
|
func planetOccultationContact(
|
|
greatestTT float64,
|
|
direction int,
|
|
internal bool,
|
|
config planetOccultationConfig,
|
|
observer Observer,
|
|
) (float64, bool) {
|
|
if direction != -1 && direction != 1 {
|
|
return math.NaN(), false
|
|
}
|
|
metric := func(tt float64) float64 {
|
|
state := planetOccultationStateAt(tt, config, &observer, -1)
|
|
if !state.valid {
|
|
return math.NaN()
|
|
}
|
|
if internal {
|
|
return state.internalContactMetric
|
|
}
|
|
return state.externalContactMetric
|
|
}
|
|
nearTT := greatestTT
|
|
nearValue := metric(nearTT)
|
|
if !finite(nearValue) || nearValue > 0 {
|
|
return math.NaN(), false
|
|
}
|
|
maxSteps := int(math.Ceil(planetOccultationContactSpanDays / planetOccultationContactStepDays))
|
|
if maxSteps > planetOccultationMaxContactSteps {
|
|
maxSteps = planetOccultationMaxContactSteps
|
|
}
|
|
for i := 1; i <= maxSteps; i++ {
|
|
farTT := greatestTT + float64(direction*i)*planetOccultationContactStepDays
|
|
farValue := metric(farTT)
|
|
if !finite(farValue) {
|
|
continue
|
|
}
|
|
if farValue >= 0 {
|
|
return planetOccultationRoot(nearTT, farTT, nearValue, farValue, metric)
|
|
}
|
|
nearTT, nearValue = farTT, farValue
|
|
}
|
|
return math.NaN(), false
|
|
}
|
|
|
|
func planetOccultationRoot(
|
|
leftTT, rightTT, leftValue, rightValue float64,
|
|
metric func(float64) float64,
|
|
) (float64, bool) {
|
|
if leftTT > rightTT {
|
|
leftTT, rightTT = rightTT, leftTT
|
|
leftValue, rightValue = rightValue, leftValue
|
|
}
|
|
if !finite(leftValue) || !finite(rightValue) || leftValue*rightValue > 0 {
|
|
return math.NaN(), false
|
|
}
|
|
for i := 0; i < 64 && math.Abs(rightTT-leftTT) > planetOccultationRootToleranceDays; i++ {
|
|
midTT := (leftTT + rightTT) / 2
|
|
midValue := metric(midTT)
|
|
if !finite(midValue) {
|
|
return math.NaN(), false
|
|
}
|
|
if leftValue*midValue <= 0 {
|
|
rightTT, rightValue = midTT, midValue
|
|
} else {
|
|
leftTT, leftValue = midTT, midValue
|
|
}
|
|
}
|
|
return (leftTT + rightTT) / 2, true
|
|
}
|
|
|
|
func planetOccultationCoarseStepDays(options OccultationSearchOptions) float64 {
|
|
step := planetOccultationDefaultStepDays
|
|
if options.MaxStep > 0 {
|
|
requested := options.MaxStep.Hours() / 24
|
|
if requested > 0 && requested < step {
|
|
step = requested
|
|
}
|
|
}
|
|
return math.Max(step, occultationSearchMinimumStep.Hours()/24)
|
|
}
|