828 lines
30 KiB
Go
828 lines
30 KiB
Go
|
|
package basic
|
|||
|
|
|
|||
|
|
import "math"
|
|||
|
|
|
|||
|
|
const (
|
|||
|
|
jupiterGalileanContactBracketStepDays = 2.0 / 1440.0
|
|||
|
|
jupiterGalileanContactBracketSpanDays = 10.0 / 24.0
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// JupiterGalileanPhenomenonContactPhase 接触阶段 / contact phase.
|
|||
|
|
type JupiterGalileanPhenomenonContactPhase string
|
|||
|
|
|
|||
|
|
const (
|
|||
|
|
// JupiterGalileanDisappearanceContact 初亏/初入接触阶段 / disappearance ingress contact.
|
|||
|
|
JupiterGalileanDisappearanceContact JupiterGalileanPhenomenonContactPhase = "disappearance"
|
|||
|
|
// JupiterGalileanReappearanceContact 复圆/复出接触阶段 / reappearance egress contact.
|
|||
|
|
JupiterGalileanReappearanceContact JupiterGalileanPhenomenonContactPhase = "reappearance"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// JupiterGalileanPhenomenonContact 伽利略卫星接触窗口 / 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 JupiterGalileanPhenomenonContact struct {
|
|||
|
|
Valid bool
|
|||
|
|
Phase JupiterGalileanPhenomenonContactPhase
|
|||
|
|
|
|||
|
|
Start float64
|
|||
|
|
ModelCrossing float64
|
|||
|
|
End float64
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// JupiterGalileanPhenomenonContactEvent IMCCE 风格的 D/F 接触事件 / IMCCE-style D/F contact event.
|
|||
|
|
//
|
|||
|
|
// 与 `JupiterGalileanPhenomenonEvent` 不同,这里返回的是有限圆盘/有限影斑的初亏与复圆接触窗口;
|
|||
|
|
// 现有整场事件 API 返回的则是零半径几何模型处于 active 状态的整段区间。
|
|||
|
|
// 对 `shadow_transit`,这里按 IMCCE 的影凌语义处理:先用半影/本影边界求出部分相持续时间,
|
|||
|
|
// 再把这段持续时间中心放在旧 `shadow_transit` API 的影轴过盘时刻上。
|
|||
|
|
// Unlike `JupiterGalileanPhenomenonEvent`, this returns the finite-disk / finite-shadow D/F contact windows.
|
|||
|
|
// The existing full-event API returns the whole active interval of the zero-radius geometric model.
|
|||
|
|
// 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 JupiterGalileanPhenomenonContactEvent struct {
|
|||
|
|
Valid bool
|
|||
|
|
Satellite int
|
|||
|
|
Type JupiterGalileanPhenomenonType
|
|||
|
|
|
|||
|
|
Disappearance JupiterGalileanPhenomenonContact
|
|||
|
|
Greatest float64
|
|||
|
|
Reappearance JupiterGalileanPhenomenonContact
|
|||
|
|
|
|||
|
|
GreatestPhenomenon JupiterGalileanPhenomenon
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type jupiterGalileanContactGeometry struct {
|
|||
|
|
signedDistance float64
|
|||
|
|
effectiveRadius float64
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// LastJupiterGalileanPhenomenonContactEvent 上一次 IMCCE 风格接触事件 / previous IMCCE-style contact event.
|
|||
|
|
func LastJupiterGalileanPhenomenonContactEvent(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType) JupiterGalileanPhenomenonContactEvent {
|
|||
|
|
return jupiterGalileanPhenomenonContactEventFromEvent(
|
|||
|
|
LastJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType),
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NextJupiterGalileanPhenomenonContactEvent 下一次 IMCCE 风格接触事件 / next IMCCE-style contact event.
|
|||
|
|
func NextJupiterGalileanPhenomenonContactEvent(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType) JupiterGalileanPhenomenonContactEvent {
|
|||
|
|
return jupiterGalileanPhenomenonContactEventFromEvent(
|
|||
|
|
NextJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType),
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ClosestJupiterGalileanPhenomenonContactEvent 最近一次 IMCCE 风格接触事件 / closest IMCCE-style contact event.
|
|||
|
|
func ClosestJupiterGalileanPhenomenonContactEvent(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType) JupiterGalileanPhenomenonContactEvent {
|
|||
|
|
return jupiterGalileanPhenomenonContactEventFromEvent(
|
|||
|
|
ClosestJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType),
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func jupiterGalileanPhenomenonContactEventFromEvent(event JupiterGalileanPhenomenonEvent) JupiterGalileanPhenomenonContactEvent {
|
|||
|
|
if !event.Valid {
|
|||
|
|
return invalidJupiterGalileanPhenomenonContactEvent()
|
|||
|
|
}
|
|||
|
|
var (
|
|||
|
|
disappearance JupiterGalileanPhenomenonContact
|
|||
|
|
reappearance JupiterGalileanPhenomenonContact
|
|||
|
|
ok bool
|
|||
|
|
)
|
|||
|
|
if event.Type == JupiterGalileanShadowTransit {
|
|||
|
|
disappearance, reappearance, ok = refineJupiterGalileanShadowContactPair(event)
|
|||
|
|
} else if event.Type == JupiterGalileanEclipse {
|
|||
|
|
disappearance, reappearance, ok = refineJupiterGalileanEclipseContactPair(event)
|
|||
|
|
} else {
|
|||
|
|
disappearance, reappearance, ok = refineJupiterGalileanContactPair(event.Greatest, event.Satellite, event.Type)
|
|||
|
|
}
|
|||
|
|
if !ok {
|
|||
|
|
return invalidJupiterGalileanPhenomenonContactEvent()
|
|||
|
|
}
|
|||
|
|
return JupiterGalileanPhenomenonContactEvent{
|
|||
|
|
Valid: true,
|
|||
|
|
Satellite: event.Satellite,
|
|||
|
|
Type: event.Type,
|
|||
|
|
Disappearance: disappearance,
|
|||
|
|
Greatest: event.Greatest,
|
|||
|
|
Reappearance: reappearance,
|
|||
|
|
GreatestPhenomenon: event.GreatestPhenomenon,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func refineJupiterGalileanContactPair(
|
|||
|
|
greatestJD float64,
|
|||
|
|
satellite int,
|
|||
|
|
phenomenonType JupiterGalileanPhenomenonType,
|
|||
|
|
) (JupiterGalileanPhenomenonContact, JupiterGalileanPhenomenonContact, bool) {
|
|||
|
|
signedDistance := func(jd float64) float64 {
|
|||
|
|
geometry, ok := jupiterGalileanContactGeometryAt(jd, satellite, phenomenonType)
|
|||
|
|
if !ok {
|
|||
|
|
return math.NaN()
|
|||
|
|
}
|
|||
|
|
return geometry.signedDistance
|
|||
|
|
}
|
|||
|
|
disappearanceStartTarget := func(jd float64) float64 {
|
|||
|
|
geometry, ok := jupiterGalileanContactGeometryAt(jd, satellite, phenomenonType)
|
|||
|
|
if !ok {
|
|||
|
|
return math.NaN()
|
|||
|
|
}
|
|||
|
|
return geometry.signedDistance - geometry.effectiveRadius
|
|||
|
|
}
|
|||
|
|
insideTarget := func(jd float64) float64 {
|
|||
|
|
geometry, ok := jupiterGalileanContactGeometryAt(jd, satellite, phenomenonType)
|
|||
|
|
if !ok {
|
|||
|
|
return math.NaN()
|
|||
|
|
}
|
|||
|
|
return geometry.signedDistance + geometry.effectiveRadius
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
disappearanceModel, ok := refineJupiterGalileanContactRoot(greatestJD, -1, signedDistance)
|
|||
|
|
if !ok {
|
|||
|
|
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
|
|||
|
|
}
|
|||
|
|
reappearanceModel, ok := refineJupiterGalileanContactRoot(greatestJD, 1, signedDistance)
|
|||
|
|
if !ok || reappearanceModel <= disappearanceModel {
|
|||
|
|
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
disappearanceStart, ok := refineJupiterGalileanContactRoot(disappearanceModel, -1, disappearanceStartTarget)
|
|||
|
|
if !ok {
|
|||
|
|
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
|
|||
|
|
}
|
|||
|
|
disappearanceEnd, ok := refineJupiterGalileanContactRoot(disappearanceModel, 1, insideTarget)
|
|||
|
|
if !ok || disappearanceEnd <= disappearanceStart {
|
|||
|
|
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
|
|||
|
|
}
|
|||
|
|
reappearanceStart, ok := refineJupiterGalileanContactRoot(reappearanceModel, -1, insideTarget)
|
|||
|
|
if !ok {
|
|||
|
|
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
|
|||
|
|
}
|
|||
|
|
reappearanceEnd, ok := refineJupiterGalileanContactRoot(reappearanceModel, 1, disappearanceStartTarget)
|
|||
|
|
if !ok || reappearanceEnd <= reappearanceStart {
|
|||
|
|
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return JupiterGalileanPhenomenonContact{
|
|||
|
|
Valid: true,
|
|||
|
|
Phase: JupiterGalileanDisappearanceContact,
|
|||
|
|
Start: disappearanceStart,
|
|||
|
|
ModelCrossing: disappearanceModel,
|
|||
|
|
End: disappearanceEnd,
|
|||
|
|
}, JupiterGalileanPhenomenonContact{
|
|||
|
|
Valid: true,
|
|||
|
|
Phase: JupiterGalileanReappearanceContact,
|
|||
|
|
Start: reappearanceStart,
|
|||
|
|
ModelCrossing: reappearanceModel,
|
|||
|
|
End: reappearanceEnd,
|
|||
|
|
}, true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func refineJupiterGalileanEclipseContactPair(
|
|||
|
|
event JupiterGalileanPhenomenonEvent,
|
|||
|
|
) (JupiterGalileanPhenomenonContact, JupiterGalileanPhenomenonContact, bool) {
|
|||
|
|
satelliteRadius := jupiterGalileanSatelliteRadiusJupiterRadii(event.Satellite)
|
|||
|
|
if !isFinite(satelliteRadius) || satelliteRadius <= 0 {
|
|||
|
|
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
|
|||
|
|
}
|
|||
|
|
// IMCCE 的 EC.D/EC.F 更接近“目视消失/重现”而不是纯几何接触:
|
|||
|
|
// D 相用半影入段近似,F 相用本影/半影出段的中点近似。
|
|||
|
|
penumbraOuterTarget := func(jd float64) float64 {
|
|||
|
|
signedDistance, ok := jupiterGalileanEclipseSignedDistanceAt(jd, event.Satellite, true)
|
|||
|
|
if !ok {
|
|||
|
|
return math.NaN()
|
|||
|
|
}
|
|||
|
|
return signedDistance - satelliteRadius
|
|||
|
|
}
|
|||
|
|
penumbraInnerTarget := func(jd float64) float64 {
|
|||
|
|
signedDistance, ok := jupiterGalileanEclipseSignedDistanceAt(jd, event.Satellite, true)
|
|||
|
|
if !ok {
|
|||
|
|
return math.NaN()
|
|||
|
|
}
|
|||
|
|
return signedDistance + satelliteRadius
|
|||
|
|
}
|
|||
|
|
umbraInnerTarget := func(jd float64) float64 {
|
|||
|
|
signedDistance, ok := jupiterGalileanEclipseSignedDistanceAt(jd, event.Satellite, false)
|
|||
|
|
if !ok {
|
|||
|
|
return math.NaN()
|
|||
|
|
}
|
|||
|
|
return signedDistance + satelliteRadius
|
|||
|
|
}
|
|||
|
|
umbraOuterTarget := func(jd float64) float64 {
|
|||
|
|
signedDistance, ok := jupiterGalileanEclipseSignedDistanceAt(jd, event.Satellite, false)
|
|||
|
|
if !ok {
|
|||
|
|
return math.NaN()
|
|||
|
|
}
|
|||
|
|
return signedDistance - satelliteRadius
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
disappearanceStart, ok := refineJupiterGalileanContactRoot(event.Start, -1, penumbraOuterTarget)
|
|||
|
|
if !ok {
|
|||
|
|
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
|
|||
|
|
}
|
|||
|
|
disappearanceEnd, ok := refineJupiterGalileanContactRoot(event.Start, 1, penumbraInnerTarget)
|
|||
|
|
if !ok || disappearanceEnd <= disappearanceStart {
|
|||
|
|
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
|
|||
|
|
}
|
|||
|
|
reappearanceUmbraStart, ok := refineJupiterGalileanContactRoot(event.End, -1, umbraInnerTarget)
|
|||
|
|
if !ok {
|
|||
|
|
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
|
|||
|
|
}
|
|||
|
|
reappearancePenumbraStart, ok := refineJupiterGalileanContactRoot(event.End, -1, penumbraInnerTarget)
|
|||
|
|
if !ok {
|
|||
|
|
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
|
|||
|
|
}
|
|||
|
|
reappearanceStart := (reappearanceUmbraStart + reappearancePenumbraStart) / 2
|
|||
|
|
reappearanceUmbraEnd, ok := refineJupiterGalileanContactRoot(event.End, 1, umbraOuterTarget)
|
|||
|
|
if !ok {
|
|||
|
|
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
|
|||
|
|
}
|
|||
|
|
reappearancePenumbraEnd, ok := refineJupiterGalileanContactRoot(event.End, 1, penumbraOuterTarget)
|
|||
|
|
if !ok {
|
|||
|
|
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
|
|||
|
|
}
|
|||
|
|
reappearanceEnd := (reappearanceUmbraEnd + reappearancePenumbraEnd) / 2
|
|||
|
|
if !ok || reappearanceEnd <= reappearanceStart {
|
|||
|
|
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return JupiterGalileanPhenomenonContact{
|
|||
|
|
Valid: true,
|
|||
|
|
Phase: JupiterGalileanDisappearanceContact,
|
|||
|
|
Start: disappearanceStart,
|
|||
|
|
ModelCrossing: event.Start,
|
|||
|
|
End: disappearanceEnd,
|
|||
|
|
}, JupiterGalileanPhenomenonContact{
|
|||
|
|
Valid: true,
|
|||
|
|
Phase: JupiterGalileanReappearanceContact,
|
|||
|
|
Start: reappearanceStart,
|
|||
|
|
ModelCrossing: event.End,
|
|||
|
|
End: reappearanceEnd,
|
|||
|
|
}, true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func refineJupiterGalileanShadowContactPair(
|
|||
|
|
event JupiterGalileanPhenomenonEvent,
|
|||
|
|
) (JupiterGalileanPhenomenonContact, JupiterGalileanPhenomenonContact, bool) {
|
|||
|
|
penumbraMetric := func(jd float64) float64 {
|
|||
|
|
value, ok := jupiterGalileanShadowLimbMetricAt(jd, event.Satellite, true)
|
|||
|
|
if !ok {
|
|||
|
|
return math.NaN()
|
|||
|
|
}
|
|||
|
|
return value
|
|||
|
|
}
|
|||
|
|
umbraMetric := func(jd float64) float64 {
|
|||
|
|
value, ok := jupiterGalileanShadowLimbMetricAt(jd, event.Satellite, false)
|
|||
|
|
if !ok {
|
|||
|
|
return math.NaN()
|
|||
|
|
}
|
|||
|
|
return value
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
penumbraSeedStartJD, penumbraSeedStartValue, ok := findJupiterGalileanNegativeMetricSeed(event.Start, penumbraMetric)
|
|||
|
|
if !ok {
|
|||
|
|
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
|
|||
|
|
}
|
|||
|
|
disappearanceStart, ok := refineJupiterGalileanNegativeWindowRoot(penumbraSeedStartJD, penumbraSeedStartValue, -1, penumbraMetric)
|
|||
|
|
if !ok {
|
|||
|
|
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
|
|||
|
|
}
|
|||
|
|
umbraSeedStartJD, umbraSeedStartValue, ok := findJupiterGalileanNegativeMetricSeed(event.Start, umbraMetric)
|
|||
|
|
if !ok {
|
|||
|
|
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
|
|||
|
|
}
|
|||
|
|
disappearanceUmbraIn, ok := refineJupiterGalileanNegativeWindowRoot(umbraSeedStartJD, umbraSeedStartValue, 1, umbraMetric)
|
|||
|
|
if !ok || disappearanceUmbraIn <= disappearanceStart {
|
|||
|
|
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
umbraSeedEndJD, umbraSeedEndValue, ok := findJupiterGalileanNegativeMetricSeed(event.End, umbraMetric)
|
|||
|
|
if !ok {
|
|||
|
|
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
|
|||
|
|
}
|
|||
|
|
reappearanceUmbraOut, ok := refineJupiterGalileanNegativeWindowRoot(umbraSeedEndJD, umbraSeedEndValue, -1, umbraMetric)
|
|||
|
|
if !ok {
|
|||
|
|
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
|
|||
|
|
}
|
|||
|
|
penumbraSeedEndJD, penumbraSeedEndValue, ok := findJupiterGalileanNegativeMetricSeed(event.End, penumbraMetric)
|
|||
|
|
if !ok {
|
|||
|
|
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
|
|||
|
|
}
|
|||
|
|
reappearancePenumbraOut, ok := refineJupiterGalileanNegativeWindowRoot(penumbraSeedEndJD, penumbraSeedEndValue, 1, penumbraMetric)
|
|||
|
|
if !ok || reappearancePenumbraOut <= reappearanceUmbraOut {
|
|||
|
|
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
|
|||
|
|
}
|
|||
|
|
disappearanceDuration := disappearanceUmbraIn - disappearanceStart
|
|||
|
|
reappearanceDuration := reappearancePenumbraOut - reappearanceUmbraOut
|
|||
|
|
if disappearanceDuration <= 0 || reappearanceDuration <= 0 {
|
|||
|
|
return JupiterGalileanPhenomenonContact{}, JupiterGalileanPhenomenonContact{}, false
|
|||
|
|
}
|
|||
|
|
disappearanceCenteredStart := event.Start - disappearanceDuration/2
|
|||
|
|
disappearanceCenteredEnd := event.Start + disappearanceDuration/2
|
|||
|
|
reappearanceCenteredStart := event.End - reappearanceDuration/2
|
|||
|
|
reappearanceCenteredEnd := event.End + reappearanceDuration/2
|
|||
|
|
return JupiterGalileanPhenomenonContact{
|
|||
|
|
Valid: true,
|
|||
|
|
Phase: JupiterGalileanDisappearanceContact,
|
|||
|
|
Start: disappearanceCenteredStart,
|
|||
|
|
ModelCrossing: event.Start,
|
|||
|
|
End: disappearanceCenteredEnd,
|
|||
|
|
}, JupiterGalileanPhenomenonContact{
|
|||
|
|
Valid: true,
|
|||
|
|
Phase: JupiterGalileanReappearanceContact,
|
|||
|
|
Start: reappearanceCenteredStart,
|
|||
|
|
ModelCrossing: event.End,
|
|||
|
|
End: reappearanceCenteredEnd,
|
|||
|
|
}, true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func refineJupiterGalileanNegativeMetricWindow(
|
|||
|
|
seedJD float64,
|
|||
|
|
metric func(jd float64) float64,
|
|||
|
|
) (float64, float64, bool) {
|
|||
|
|
activeJD, activeValue, ok := findJupiterGalileanNegativeMetricSeed(seedJD, metric)
|
|||
|
|
if !ok {
|
|||
|
|
return math.NaN(), math.NaN(), false
|
|||
|
|
}
|
|||
|
|
start, ok := refineJupiterGalileanNegativeWindowRoot(activeJD, activeValue, -1, metric)
|
|||
|
|
if !ok {
|
|||
|
|
return math.NaN(), math.NaN(), false
|
|||
|
|
}
|
|||
|
|
end, ok := refineJupiterGalileanNegativeWindowRoot(activeJD, activeValue, 1, metric)
|
|||
|
|
if !ok {
|
|||
|
|
return math.NaN(), math.NaN(), false
|
|||
|
|
}
|
|||
|
|
return start, end, true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func findJupiterGalileanNegativeMetricSeed(
|
|||
|
|
seedJD float64,
|
|||
|
|
metric func(jd float64) float64,
|
|||
|
|
) (float64, float64, bool) {
|
|||
|
|
value := metric(seedJD)
|
|||
|
|
if isFinite(value) && value < 0 {
|
|||
|
|
return seedJD, value, true
|
|||
|
|
}
|
|||
|
|
step := jupiterGalileanContactBracketStepDays
|
|||
|
|
maxSteps := int(math.Ceil(jupiterGalileanContactBracketSpanDays / step))
|
|||
|
|
for i := 1; i <= maxSteps; i++ {
|
|||
|
|
for _, direction := range []float64{-1, 1} {
|
|||
|
|
candidateJD := seedJD + direction*step*float64(i)
|
|||
|
|
candidateValue := metric(candidateJD)
|
|||
|
|
if isFinite(candidateValue) && candidateValue < 0 {
|
|||
|
|
return candidateJD, candidateValue, true
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return math.NaN(), math.NaN(), false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func refineJupiterGalileanNegativeWindowRoot(
|
|||
|
|
activeJD, activeValue float64,
|
|||
|
|
direction int,
|
|||
|
|
metric func(jd float64) float64,
|
|||
|
|
) (float64, bool) {
|
|||
|
|
currentJD := activeJD
|
|||
|
|
currentValue := activeValue
|
|||
|
|
step := jupiterGalileanContactBracketStepDays
|
|||
|
|
maxSteps := int(math.Ceil(jupiterGalileanContactBracketSpanDays / step))
|
|||
|
|
for i := 1; i <= maxSteps; i++ {
|
|||
|
|
candidateJD := currentJD + float64(direction)*step
|
|||
|
|
candidateValue := metric(candidateJD)
|
|||
|
|
if !isFinite(candidateValue) {
|
|||
|
|
currentJD = candidateJD
|
|||
|
|
currentValue = math.Inf(1)
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
if candidateValue >= 0 {
|
|||
|
|
return bisectJupiterGalileanContactRoot(currentJD, currentValue, candidateJD, candidateValue, metric)
|
|||
|
|
}
|
|||
|
|
currentJD = candidateJD
|
|||
|
|
currentValue = candidateValue
|
|||
|
|
}
|
|||
|
|
return math.NaN(), false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func refineJupiterGalileanContactRoot(
|
|||
|
|
modelCrossingJD float64,
|
|||
|
|
direction int,
|
|||
|
|
target func(jd float64) float64,
|
|||
|
|
) (float64, bool) {
|
|||
|
|
if direction != -1 && direction != 1 {
|
|||
|
|
return math.NaN(), false
|
|||
|
|
}
|
|||
|
|
modelValue := target(modelCrossingJD)
|
|||
|
|
if !isFinite(modelValue) {
|
|||
|
|
return math.NaN(), false
|
|||
|
|
}
|
|||
|
|
step := jupiterGalileanContactBracketStepDays
|
|||
|
|
maxSteps := int(math.Ceil(jupiterGalileanContactBracketSpanDays / step))
|
|||
|
|
nearJD := modelCrossingJD
|
|||
|
|
nearValue := modelValue
|
|||
|
|
for i := 1; i <= maxSteps; i++ {
|
|||
|
|
farJD := modelCrossingJD + float64(direction)*step*float64(i)
|
|||
|
|
farValue := target(farJD)
|
|||
|
|
if !isFinite(farValue) {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
if nearValue == 0 {
|
|||
|
|
return nearJD, true
|
|||
|
|
}
|
|||
|
|
if farValue == 0 {
|
|||
|
|
return farJD, true
|
|||
|
|
}
|
|||
|
|
if nearValue*farValue < 0 {
|
|||
|
|
return bisectJupiterGalileanContactRoot(nearJD, nearValue, farJD, farValue, target)
|
|||
|
|
}
|
|||
|
|
nearJD = farJD
|
|||
|
|
nearValue = farValue
|
|||
|
|
}
|
|||
|
|
return math.NaN(), false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func bisectJupiterGalileanContactRoot(
|
|||
|
|
jd1, value1, jd2, value2 float64,
|
|||
|
|
target func(jd float64) float64,
|
|||
|
|
) (float64, bool) {
|
|||
|
|
leftJD := jd1
|
|||
|
|
rightJD := jd2
|
|||
|
|
leftValue := value1
|
|||
|
|
rightValue := value2
|
|||
|
|
if rightJD < leftJD {
|
|||
|
|
leftJD, rightJD = rightJD, leftJD
|
|||
|
|
leftValue, rightValue = rightValue, leftValue
|
|||
|
|
}
|
|||
|
|
if !isFinite(leftValue) || !isFinite(rightValue) || leftValue*rightValue > 0 {
|
|||
|
|
return math.NaN(), false
|
|||
|
|
}
|
|||
|
|
for i := 0; i < 80 && rightJD-leftJD > jupiterGalileanEventEpsilonDays; i++ {
|
|||
|
|
midJD := (leftJD + rightJD) / 2
|
|||
|
|
midValue := target(midJD)
|
|||
|
|
if !isFinite(midValue) {
|
|||
|
|
return math.NaN(), false
|
|||
|
|
}
|
|||
|
|
if midValue == 0 {
|
|||
|
|
return midJD, true
|
|||
|
|
}
|
|||
|
|
if leftValue*midValue <= 0 {
|
|||
|
|
rightJD = midJD
|
|||
|
|
rightValue = midValue
|
|||
|
|
} else {
|
|||
|
|
leftJD = midJD
|
|||
|
|
leftValue = midValue
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return (leftJD + rightJD) / 2, true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func jupiterGalileanContactGeometryAt(
|
|||
|
|
jd float64,
|
|||
|
|
satellite int,
|
|||
|
|
phenomenonType JupiterGalileanPhenomenonType,
|
|||
|
|
) (jupiterGalileanContactGeometry, bool) {
|
|||
|
|
if !isFinite(jd) || satellite < 1 || satellite > 4 || !isValidJupiterGalileanPhenomenonType(phenomenonType) {
|
|||
|
|
return jupiterGalileanContactGeometry{}, false
|
|||
|
|
}
|
|||
|
|
evaluationJD := TD2UT(jd, true)
|
|||
|
|
context := newJupiterGalileanObservationContext(evaluationJD)
|
|||
|
|
if context.jupiterDistance == 0 {
|
|||
|
|
return jupiterGalileanContactGeometry{}, false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
index := satellite - 1
|
|||
|
|
observation := context.observationForSatellite(index)
|
|||
|
|
stateVector := Vector3{observation.State.X, observation.State.Y, observation.State.Z}
|
|||
|
|
satelliteRadius := jupiterGalileanSatelliteRadiusJupiterRadii(satellite)
|
|||
|
|
if !isFinite(satelliteRadius) || satelliteRadius <= 0 {
|
|||
|
|
return jupiterGalileanContactGeometry{}, false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
switch phenomenonType {
|
|||
|
|
case JupiterGalileanTransit, JupiterGalileanOccultation:
|
|||
|
|
return jupiterGalileanContactGeometry{
|
|||
|
|
signedDistance: ellipseSignedDistance(observation.OffsetXJupiterRadii, observation.OffsetYJupiterRadii, 1, context.earthMinorRadius),
|
|||
|
|
effectiveRadius: satelliteRadius,
|
|||
|
|
}, true
|
|||
|
|
case JupiterGalileanEclipse:
|
|||
|
|
xSunAU := vectorDot(stateVector, context.sunEast)
|
|||
|
|
ySunAU := vectorDot(stateVector, context.sunNorth)
|
|||
|
|
zSunAU := vectorDot(stateVector, context.sunLineOfSight)
|
|||
|
|
umbraScale := jupiterUmbraScale(zSunAU, context.sunDistanceAU)
|
|||
|
|
if zSunAU <= 0 || umbraScale <= 0 {
|
|||
|
|
return jupiterGalileanContactGeometry{}, false
|
|||
|
|
}
|
|||
|
|
radiusAU := jupiterGalileanEquatorialRadiusKM / astronomicalUnitKM
|
|||
|
|
return jupiterGalileanContactGeometry{
|
|||
|
|
signedDistance: ellipseSignedDistance(xSunAU/radiusAU, ySunAU/radiusAU, umbraScale, context.sunMinorRadius*umbraScale),
|
|||
|
|
effectiveRadius: satelliteRadius,
|
|||
|
|
}, true
|
|||
|
|
case JupiterGalileanShadowTransit:
|
|||
|
|
radiusAU := jupiterGalileanEquatorialRadiusKM / astronomicalUnitKM
|
|||
|
|
axisDenominator := vectorDot(context.sunLineOfSight, context.lineOfSight)
|
|||
|
|
if math.Abs(axisDenominator) < 1e-12 {
|
|||
|
|
return jupiterGalileanContactGeometry{}, false
|
|||
|
|
}
|
|||
|
|
axisDistanceAU := -vectorDot(stateVector, context.lineOfSight) / axisDenominator
|
|||
|
|
if axisDistanceAU <= 0 {
|
|||
|
|
return jupiterGalileanContactGeometry{}, false
|
|||
|
|
}
|
|||
|
|
axisPoint := Vector3{
|
|||
|
|
stateVector[0] + axisDistanceAU*context.sunLineOfSight[0],
|
|||
|
|
stateVector[1] + axisDistanceAU*context.sunLineOfSight[1],
|
|||
|
|
stateVector[2] + axisDistanceAU*context.sunLineOfSight[2],
|
|||
|
|
}
|
|||
|
|
xAU := vectorDot(axisPoint, context.east)
|
|||
|
|
yAU := vectorDot(axisPoint, context.north)
|
|||
|
|
return jupiterGalileanContactGeometry{
|
|||
|
|
signedDistance: ellipseSignedDistance(xAU/radiusAU, yAU/radiusAU, 1, context.earthMinorRadius),
|
|||
|
|
effectiveRadius: jupiterGalileanPenumbraRadiusJupiterRadii(satellite, axisDistanceAU, context.sunDistanceAU),
|
|||
|
|
}, true
|
|||
|
|
default:
|
|||
|
|
return jupiterGalileanContactGeometry{}, false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func jupiterGalileanEclipseSignedDistanceAt(jd float64, satellite int, penumbra bool) (float64, bool) {
|
|||
|
|
if !isFinite(jd) || satellite < 1 || satellite > 4 {
|
|||
|
|
return math.NaN(), false
|
|||
|
|
}
|
|||
|
|
evaluationJD := TD2UT(jd, true)
|
|||
|
|
context := newJupiterGalileanObservationContext(evaluationJD)
|
|||
|
|
if context.jupiterDistance == 0 {
|
|||
|
|
return math.NaN(), false
|
|||
|
|
}
|
|||
|
|
state := context.observationForSatellite(satellite - 1).State
|
|||
|
|
stateVector := Vector3{state.X, state.Y, state.Z}
|
|||
|
|
xSunAU := vectorDot(stateVector, context.sunEast)
|
|||
|
|
ySunAU := vectorDot(stateVector, context.sunNorth)
|
|||
|
|
zSunAU := vectorDot(stateVector, context.sunLineOfSight)
|
|||
|
|
if zSunAU <= 0 {
|
|||
|
|
return math.NaN(), false
|
|||
|
|
}
|
|||
|
|
scale := jupiterUmbraScale(zSunAU, context.sunDistanceAU)
|
|||
|
|
if penumbra {
|
|||
|
|
scale = jupiterPenumbraScale(zSunAU, context.sunDistanceAU)
|
|||
|
|
}
|
|||
|
|
if scale <= 0 {
|
|||
|
|
return math.NaN(), false
|
|||
|
|
}
|
|||
|
|
radiusAU := jupiterGalileanEquatorialRadiusKM / astronomicalUnitKM
|
|||
|
|
return ellipseSignedDistance(xSunAU/radiusAU, ySunAU/radiusAU, scale, context.sunMinorRadius*scale), true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func jupiterGalileanSatelliteRadiusJupiterRadii(satellite int) float64 {
|
|||
|
|
switch satellite {
|
|||
|
|
case 1:
|
|||
|
|
return 1821.6 / jupiterGalileanEquatorialRadiusKM
|
|||
|
|
case 2:
|
|||
|
|
return 1560.8 / jupiterGalileanEquatorialRadiusKM
|
|||
|
|
case 3:
|
|||
|
|
return 2634.1 / jupiterGalileanEquatorialRadiusKM
|
|||
|
|
case 4:
|
|||
|
|
return 2410.3 / jupiterGalileanEquatorialRadiusKM
|
|||
|
|
default:
|
|||
|
|
return math.NaN()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func jupiterPenumbraScale(distanceBehindAU, sunDistanceAU float64) float64 {
|
|||
|
|
if distanceBehindAU <= 0 || sunDistanceAU <= 0 {
|
|||
|
|
return 0
|
|||
|
|
}
|
|||
|
|
jupiterRadiusAU := jupiterGalileanEquatorialRadiusKM / astronomicalUnitKM
|
|||
|
|
return 1 + distanceBehindAU*(solarRadiusAU+jupiterRadiusAU)/(sunDistanceAU*jupiterRadiusAU)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func jupiterGalileanUmbraRadiusJupiterRadii(satellite int, pathLengthAU, sunDistanceAU float64) float64 {
|
|||
|
|
if pathLengthAU <= 0 || sunDistanceAU <= 0 {
|
|||
|
|
return math.NaN()
|
|||
|
|
}
|
|||
|
|
satelliteRadiusAU := jupiterGalileanSatelliteRadiusJupiterRadii(satellite) * jupiterGalileanEquatorialRadiusKM / astronomicalUnitKM
|
|||
|
|
umbraRadiusAU := satelliteRadiusAU - pathLengthAU*(solarRadiusAU-satelliteRadiusAU)/sunDistanceAU
|
|||
|
|
if umbraRadiusAU <= 0 {
|
|||
|
|
return math.NaN()
|
|||
|
|
}
|
|||
|
|
return umbraRadiusAU / (jupiterGalileanEquatorialRadiusKM / astronomicalUnitKM)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func jupiterGalileanPenumbraRadiusJupiterRadii(satellite int, pathLengthAU, sunDistanceAU float64) float64 {
|
|||
|
|
if pathLengthAU <= 0 || sunDistanceAU <= 0 {
|
|||
|
|
return math.NaN()
|
|||
|
|
}
|
|||
|
|
satelliteRadiusAU := jupiterGalileanSatelliteRadiusJupiterRadii(satellite) * jupiterGalileanEquatorialRadiusKM / astronomicalUnitKM
|
|||
|
|
penumbraRadiusAU := satelliteRadiusAU + pathLengthAU*(solarRadiusAU+satelliteRadiusAU)/sunDistanceAU
|
|||
|
|
if penumbraRadiusAU <= 0 {
|
|||
|
|
return math.NaN()
|
|||
|
|
}
|
|||
|
|
return penumbraRadiusAU / (jupiterGalileanEquatorialRadiusKM / astronomicalUnitKM)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func jupiterGalileanShadowLimbMetricAt(jd float64, satellite int, penumbra bool) (float64, bool) {
|
|||
|
|
if !isFinite(jd) || satellite < 1 || satellite > 4 {
|
|||
|
|
return math.NaN(), false
|
|||
|
|
}
|
|||
|
|
evaluationJD := TD2UT(jd, true)
|
|||
|
|
context := newJupiterGalileanObservationContext(evaluationJD)
|
|||
|
|
if context.jupiterDistance == 0 {
|
|||
|
|
return math.NaN(), false
|
|||
|
|
}
|
|||
|
|
state := context.observationForSatellite(satellite - 1).State
|
|||
|
|
stateVector := Vector3{state.X, state.Y, state.Z}
|
|||
|
|
satelliteBody := context.toBodyCoordinates(stateVector)
|
|||
|
|
axisBody := normalizeVector(context.toBodyCoordinates(context.sunLineOfSight))
|
|||
|
|
if vectorMagnitude(axisBody) == 0 {
|
|||
|
|
return math.NaN(), false
|
|||
|
|
}
|
|||
|
|
radiusAU := jupiterGalileanEquatorialRadiusKM / astronomicalUnitKM
|
|||
|
|
satelliteRadiusAU := jupiterGalileanSatelliteRadiusJupiterRadii(satellite) * radiusAU
|
|||
|
|
limbU, limbV, ok := jupiterGalileanVisibleLimbBasis(context)
|
|||
|
|
if !ok {
|
|||
|
|
return math.NaN(), false
|
|||
|
|
}
|
|||
|
|
metricAtAngle := func(angle float64) float64 {
|
|||
|
|
limbPointBody := jupiterGalileanVisibleLimbPoint(angle, limbU, limbV, jupiterPolarRadiusRatio())
|
|||
|
|
limbPointAU := Vector3{
|
|||
|
|
limbPointBody[0] * radiusAU,
|
|||
|
|
limbPointBody[1] * radiusAU,
|
|||
|
|
limbPointBody[2] * radiusAU,
|
|||
|
|
}
|
|||
|
|
return jupiterGalileanShadowConeMetricForPoint(limbPointAU, satelliteBody, axisBody, satelliteRadiusAU, context.sunDistanceAU, penumbra)
|
|||
|
|
}
|
|||
|
|
return minimizeJupiterGalileanPeriodicMetric(metricAtAngle)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func jupiterGalileanVisibleLimbBasis(context jupiterGalileanObservationContext) (Vector3, Vector3, bool) {
|
|||
|
|
polar := jupiterPolarRadiusRatio()
|
|||
|
|
earthBody := context.toBodyCoordinates(context.earthDirection)
|
|||
|
|
planeNormal := Vector3{earthBody[0], earthBody[1], earthBody[2] / polar}
|
|||
|
|
planeNormal = normalizeVector(planeNormal)
|
|||
|
|
if vectorMagnitude(planeNormal) == 0 {
|
|||
|
|
return Vector3{}, Vector3{}, false
|
|||
|
|
}
|
|||
|
|
reference := Vector3{0, 0, 1}
|
|||
|
|
if math.Abs(vectorDot(reference, planeNormal)) > 0.9 {
|
|||
|
|
reference = Vector3{1, 0, 0}
|
|||
|
|
}
|
|||
|
|
u := normalizeVector(pxp(planeNormal, reference))
|
|||
|
|
if vectorMagnitude(u) == 0 {
|
|||
|
|
reference = Vector3{0, 1, 0}
|
|||
|
|
u = normalizeVector(pxp(planeNormal, reference))
|
|||
|
|
if vectorMagnitude(u) == 0 {
|
|||
|
|
return Vector3{}, Vector3{}, false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
v := normalizeVector(pxp(planeNormal, u))
|
|||
|
|
if vectorMagnitude(v) == 0 {
|
|||
|
|
return Vector3{}, Vector3{}, false
|
|||
|
|
}
|
|||
|
|
return u, v, true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func jupiterGalileanVisibleLimbPoint(angle float64, u, v Vector3, polar float64) Vector3 {
|
|||
|
|
sinAngle := math.Sin(angle)
|
|||
|
|
cosAngle := math.Cos(angle)
|
|||
|
|
q := Vector3{
|
|||
|
|
u[0]*cosAngle + v[0]*sinAngle,
|
|||
|
|
u[1]*cosAngle + v[1]*sinAngle,
|
|||
|
|
u[2]*cosAngle + v[2]*sinAngle,
|
|||
|
|
}
|
|||
|
|
return Vector3{q[0], q[1], polar * q[2]}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func jupiterGalileanShadowConeMetricForPoint(
|
|||
|
|
pointAU, satelliteAU, axisUnit Vector3,
|
|||
|
|
satelliteRadiusAU, sunDistanceAU float64,
|
|||
|
|
penumbra bool,
|
|||
|
|
) float64 {
|
|||
|
|
vector := Vector3{
|
|||
|
|
pointAU[0] - satelliteAU[0],
|
|||
|
|
pointAU[1] - satelliteAU[1],
|
|||
|
|
pointAU[2] - satelliteAU[2],
|
|||
|
|
}
|
|||
|
|
axisDistanceAU := vectorDot(vector, axisUnit)
|
|||
|
|
if axisDistanceAU <= 0 {
|
|||
|
|
return math.Inf(1)
|
|||
|
|
}
|
|||
|
|
perpendicular := Vector3{
|
|||
|
|
vector[0] - axisDistanceAU*axisUnit[0],
|
|||
|
|
vector[1] - axisDistanceAU*axisUnit[1],
|
|||
|
|
vector[2] - axisDistanceAU*axisUnit[2],
|
|||
|
|
}
|
|||
|
|
perpendicularDistanceAU := vectorMagnitude(perpendicular)
|
|||
|
|
if penumbra {
|
|||
|
|
penumbraRadiusAU := satelliteRadiusAU + axisDistanceAU*(solarRadiusAU+satelliteRadiusAU)/sunDistanceAU
|
|||
|
|
return perpendicularDistanceAU - penumbraRadiusAU
|
|||
|
|
}
|
|||
|
|
umbraRadiusAU := satelliteRadiusAU - axisDistanceAU*(solarRadiusAU-satelliteRadiusAU)/sunDistanceAU
|
|||
|
|
return perpendicularDistanceAU - umbraRadiusAU
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func minimizeJupiterGalileanPeriodicMetric(metric func(angle float64) float64) (float64, bool) {
|
|||
|
|
const (
|
|||
|
|
samples = 144
|
|||
|
|
phi = 0.6180339887498948482
|
|||
|
|
)
|
|||
|
|
step := 2 * math.Pi / float64(samples)
|
|||
|
|
bestAngle := 0.0
|
|||
|
|
bestValue := math.Inf(1)
|
|||
|
|
for i := 0; i < samples; i++ {
|
|||
|
|
angle := float64(i) * step
|
|||
|
|
value := metric(angle)
|
|||
|
|
if value < bestValue {
|
|||
|
|
bestValue = value
|
|||
|
|
bestAngle = angle
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if !isFinite(bestValue) {
|
|||
|
|
return math.NaN(), false
|
|||
|
|
}
|
|||
|
|
left := bestAngle - step
|
|||
|
|
right := bestAngle + step
|
|||
|
|
x1 := right - phi*(right-left)
|
|||
|
|
x2 := left + phi*(right-left)
|
|||
|
|
f1 := metric(x1)
|
|||
|
|
f2 := metric(x2)
|
|||
|
|
for i := 0; i < 80; i++ {
|
|||
|
|
if f1 <= f2 {
|
|||
|
|
right = x2
|
|||
|
|
x2 = x1
|
|||
|
|
f2 = f1
|
|||
|
|
x1 = right - phi*(right-left)
|
|||
|
|
f1 = metric(x1)
|
|||
|
|
} else {
|
|||
|
|
left = x1
|
|||
|
|
x1 = x2
|
|||
|
|
f1 = f2
|
|||
|
|
x2 = left + phi*(right-left)
|
|||
|
|
f2 = metric(x2)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return math.Min(bestValue, metric((left+right)/2)), true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func ellipseSignedDistance(x, y, major, minor float64) float64 {
|
|||
|
|
if major <= 0 || minor <= 0 {
|
|||
|
|
return math.NaN()
|
|||
|
|
}
|
|||
|
|
targetX := math.Abs(x)
|
|||
|
|
targetY := math.Abs(y)
|
|||
|
|
if targetX == 0 && targetY == 0 {
|
|||
|
|
if minor < major {
|
|||
|
|
return -minor
|
|||
|
|
}
|
|||
|
|
return -major
|
|||
|
|
}
|
|||
|
|
left := 0.0
|
|||
|
|
right := math.Pi / 2
|
|||
|
|
const phi = 0.6180339887498948482
|
|||
|
|
x1 := right - phi*(right-left)
|
|||
|
|
x2 := left + phi*(right-left)
|
|||
|
|
f1 := ellipseDistanceSquaredAtAngle(targetX, targetY, major, minor, x1)
|
|||
|
|
f2 := ellipseDistanceSquaredAtAngle(targetX, targetY, major, minor, x2)
|
|||
|
|
for i := 0; i < 80; i++ {
|
|||
|
|
if f1 <= f2 {
|
|||
|
|
right = x2
|
|||
|
|
x2 = x1
|
|||
|
|
f2 = f1
|
|||
|
|
x1 = right - phi*(right-left)
|
|||
|
|
f1 = ellipseDistanceSquaredAtAngle(targetX, targetY, major, minor, x1)
|
|||
|
|
} else {
|
|||
|
|
left = x1
|
|||
|
|
x1 = x2
|
|||
|
|
f1 = f2
|
|||
|
|
x2 = left + phi*(right-left)
|
|||
|
|
f2 = ellipseDistanceSquaredAtAngle(targetX, targetY, major, minor, x2)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
distance := math.Sqrt(ellipseDistanceSquaredAtAngle(targetX, targetY, major, minor, (left+right)/2))
|
|||
|
|
if ellipseInside(x, y, major, minor) {
|
|||
|
|
return -distance
|
|||
|
|
}
|
|||
|
|
return distance
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func ellipseDistanceSquaredAtAngle(x, y, major, minor, angle float64) float64 {
|
|||
|
|
ellipseX := major * math.Cos(angle)
|
|||
|
|
ellipseY := minor * math.Sin(angle)
|
|||
|
|
dx := ellipseX - x
|
|||
|
|
dy := ellipseY - y
|
|||
|
|
return dx*dx + dy*dy
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func invalidJupiterGalileanPhenomenonContactEvent() JupiterGalileanPhenomenonContactEvent {
|
|||
|
|
return JupiterGalileanPhenomenonContactEvent{
|
|||
|
|
Disappearance: JupiterGalileanPhenomenonContact{
|
|||
|
|
Start: math.NaN(),
|
|||
|
|
ModelCrossing: math.NaN(),
|
|||
|
|
End: math.NaN(),
|
|||
|
|
},
|
|||
|
|
Greatest: math.NaN(),
|
|||
|
|
Reappearance: JupiterGalileanPhenomenonContact{
|
|||
|
|
Start: math.NaN(),
|
|||
|
|
ModelCrossing: math.NaN(),
|
|||
|
|
End: math.NaN(),
|
|||
|
|
},
|
|||
|
|
GreatestPhenomenon: invalidJupiterGalileanPhenomenon(),
|
|||
|
|
}
|
|||
|
|
}
|