202 lines
7.3 KiB
Go
202 lines
7.3 KiB
Go
|
|
package basic
|
|||
|
|
|
|||
|
|
import "math"
|
|||
|
|
|
|||
|
|
const solarRadiusAU = 695700.0 / astronomicalUnitKM
|
|||
|
|
|
|||
|
|
// JupiterGalileanPhenomenon 木星伽利略卫星瞬时现象 / instantaneous Galilean-satellite phenomena.
|
|||
|
|
//
|
|||
|
|
// Transit 表示卫星本体在木星盘前;Occultation 表示卫星在木星盘后被掩蔽;Eclipse 表示卫星落入木星本影;ShadowTransit 表示卫星影心落在可见木星盘面上。
|
|||
|
|
// Transit means the satellite itself is in front of Jupiter's disk; Occultation means it is hidden behind the disk; Eclipse means the satellite lies in Jupiter's umbra; ShadowTransit means the center of the satellite shadow falls on the visible Jovian disk.
|
|||
|
|
type JupiterGalileanPhenomenon struct {
|
|||
|
|
Transit bool
|
|||
|
|
Occultation bool
|
|||
|
|
Eclipse bool
|
|||
|
|
ShadowTransit bool
|
|||
|
|
|
|||
|
|
ShadowOffsetXArcsec float64
|
|||
|
|
ShadowOffsetYArcsec float64
|
|||
|
|
|
|||
|
|
ShadowOffsetXJupiterRadii float64
|
|||
|
|
ShadowOffsetYJupiterRadii float64
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// JupiterGalileanSatellitePhenomenon 单颗伽利略卫星瞬时现象 / instantaneous phenomena of one Galilean satellite.
|
|||
|
|
func JupiterGalileanSatellitePhenomenon(jd float64, satellite int) JupiterGalileanPhenomenon {
|
|||
|
|
if satellite < 1 || satellite > 4 || !isFinite(jd) {
|
|||
|
|
return invalidJupiterGalileanPhenomenon()
|
|||
|
|
}
|
|||
|
|
context := newJupiterGalileanObservationContext(jd)
|
|||
|
|
return context.phenomenonForSatellite(satellite - 1)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// JupiterGalileanSatellitePhenomena 四颗伽利略卫星瞬时现象 / instantaneous phenomena of the four Galilean satellites.
|
|||
|
|
func JupiterGalileanSatellitePhenomena(jd float64) [4]JupiterGalileanPhenomenon {
|
|||
|
|
var phenomena [4]JupiterGalileanPhenomenon
|
|||
|
|
context := newJupiterGalileanObservationContext(jd)
|
|||
|
|
for i := range phenomena {
|
|||
|
|
phenomena[i] = context.phenomenonForSatellite(i)
|
|||
|
|
}
|
|||
|
|
return phenomena
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (context jupiterGalileanObservationContext) phenomenonForSatellite(index int) JupiterGalileanPhenomenon {
|
|||
|
|
if index < 0 || index >= 4 || context.jupiterDistance == 0 {
|
|||
|
|
return invalidJupiterGalileanPhenomenon()
|
|||
|
|
}
|
|||
|
|
observation := context.observationForSatellite(index)
|
|||
|
|
stateVector := Vector3{observation.State.X, observation.State.Y, observation.State.Z}
|
|||
|
|
radiusAU := jupiterGalileanEquatorialRadiusKM / astronomicalUnitKM
|
|||
|
|
xEarth := observation.OffsetXJupiterRadii
|
|||
|
|
yEarth := observation.OffsetYJupiterRadii
|
|||
|
|
onEarthDisk := ellipseInside(xEarth, yEarth, 1, context.earthMinorRadius)
|
|||
|
|
|
|||
|
|
xSunAU := vectorDot(stateVector, context.sunEast)
|
|||
|
|
ySunAU := vectorDot(stateVector, context.sunNorth)
|
|||
|
|
zSunAU := vectorDot(stateVector, context.sunLineOfSight)
|
|||
|
|
xSun := xSunAU / radiusAU
|
|||
|
|
ySun := ySunAU / radiusAU
|
|||
|
|
umbraScale := jupiterUmbraScale(zSunAU, context.sunDistanceAU)
|
|||
|
|
eclipse := false
|
|||
|
|
if zSunAU > 0 && umbraScale > 0 {
|
|||
|
|
eclipse = ellipseInside(xSun, ySun, umbraScale, context.sunMinorRadius*umbraScale)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
shadowTransit, shadowXAU, shadowYAU := context.shadowTransitFor(stateVector)
|
|||
|
|
phenomenon := JupiterGalileanPhenomenon{
|
|||
|
|
Transit: onEarthDisk && observation.InFrontOfJupiter,
|
|||
|
|
Occultation: onEarthDisk && !observation.InFrontOfJupiter,
|
|||
|
|
Eclipse: eclipse,
|
|||
|
|
ShadowTransit: shadowTransit,
|
|||
|
|
|
|||
|
|
ShadowOffsetXArcsec: math.NaN(),
|
|||
|
|
ShadowOffsetYArcsec: math.NaN(),
|
|||
|
|
ShadowOffsetXJupiterRadii: math.NaN(),
|
|||
|
|
ShadowOffsetYJupiterRadii: math.NaN(),
|
|||
|
|
}
|
|||
|
|
if shadowTransit {
|
|||
|
|
phenomenon.ShadowOffsetXArcsec = math.Atan2(shadowXAU, context.jupiterDistance) * deg * 3600
|
|||
|
|
phenomenon.ShadowOffsetYArcsec = math.Atan2(shadowYAU, context.jupiterDistance) * deg * 3600
|
|||
|
|
phenomenon.ShadowOffsetXJupiterRadii = shadowXAU / radiusAU
|
|||
|
|
phenomenon.ShadowOffsetYJupiterRadii = shadowYAU / radiusAU
|
|||
|
|
}
|
|||
|
|
return phenomenon
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (context jupiterGalileanObservationContext) shadowTransitFor(stateVector Vector3) (bool, float64, float64) {
|
|||
|
|
radiusAU := jupiterGalileanEquatorialRadiusKM / astronomicalUnitKM
|
|||
|
|
satelliteBody := context.toBodyCoordinates(stateVector)
|
|||
|
|
satelliteBody = Vector3{satelliteBody[0] / radiusAU, satelliteBody[1] / radiusAU, satelliteBody[2] / radiusAU}
|
|||
|
|
directionBody := context.toBodyCoordinates(context.sunLineOfSight)
|
|||
|
|
intersectionBody, ok := ellipsoidRayIntersection(satelliteBody, directionBody, jupiterPolarRadiusRatio())
|
|||
|
|
if !ok {
|
|||
|
|
return false, 0, 0
|
|||
|
|
}
|
|||
|
|
normalBody := Vector3{intersectionBody[0], intersectionBody[1], intersectionBody[2] / (jupiterPolarRadiusRatio() * jupiterPolarRadiusRatio())}
|
|||
|
|
earthBody := context.toBodyCoordinates(context.earthDirection)
|
|||
|
|
if vectorDot(normalBody, earthBody) <= 0 {
|
|||
|
|
return false, 0, 0
|
|||
|
|
}
|
|||
|
|
intersection := context.fromBodyCoordinates(Vector3{
|
|||
|
|
intersectionBody[0] * radiusAU,
|
|||
|
|
intersectionBody[1] * radiusAU,
|
|||
|
|
intersectionBody[2] * radiusAU,
|
|||
|
|
})
|
|||
|
|
xAU := vectorDot(intersection, context.east)
|
|||
|
|
yAU := vectorDot(intersection, context.north)
|
|||
|
|
x := xAU / radiusAU
|
|||
|
|
y := yAU / radiusAU
|
|||
|
|
if !ellipseInside(x, y, 1, context.earthMinorRadius) {
|
|||
|
|
return false, 0, 0
|
|||
|
|
}
|
|||
|
|
return true, xAU, yAU
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (context jupiterGalileanObservationContext) toBodyCoordinates(vector Vector3) Vector3 {
|
|||
|
|
return Vector3{
|
|||
|
|
vectorDot(vector, context.bodyX),
|
|||
|
|
vectorDot(vector, context.bodyY),
|
|||
|
|
vectorDot(vector, context.bodyZ),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (context jupiterGalileanObservationContext) fromBodyCoordinates(vector Vector3) Vector3 {
|
|||
|
|
return Vector3{
|
|||
|
|
context.bodyX[0]*vector[0] + context.bodyY[0]*vector[1] + context.bodyZ[0]*vector[2],
|
|||
|
|
context.bodyX[1]*vector[0] + context.bodyY[1]*vector[1] + context.bodyZ[1]*vector[2],
|
|||
|
|
context.bodyX[2]*vector[0] + context.bodyY[2]*vector[1] + context.bodyZ[2]*vector[2],
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func jupiterProjectedMinorRadius(direction, pole Vector3) float64 {
|
|||
|
|
sinBeta := vectorDot(direction, pole)
|
|||
|
|
cos2Beta := 1 - sinBeta*sinBeta
|
|||
|
|
if cos2Beta < 0 {
|
|||
|
|
cos2Beta = 0
|
|||
|
|
}
|
|||
|
|
ratio := jupiterPolarRadiusRatio()
|
|||
|
|
return math.Sqrt(sinBeta*sinBeta + ratio*ratio*cos2Beta)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func jupiterPolarRadiusRatio() float64 {
|
|||
|
|
return jupiterPhysicalModel.polarRadius / jupiterPhysicalModel.equatorialRadius
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func ellipseInside(x, y, major, minor float64) bool {
|
|||
|
|
if major <= 0 || minor <= 0 {
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
return (x*x)/(major*major)+(y*y)/(minor*minor) <= 1+1e-12
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func ellipsoidRayIntersection(origin, direction Vector3, polarRatio float64) (Vector3, bool) {
|
|||
|
|
invPolar2 := 1 / (polarRatio * polarRatio)
|
|||
|
|
a := direction[0]*direction[0] + direction[1]*direction[1] + direction[2]*direction[2]*invPolar2
|
|||
|
|
b := 2 * (origin[0]*direction[0] + origin[1]*direction[1] + origin[2]*direction[2]*invPolar2)
|
|||
|
|
c := origin[0]*origin[0] + origin[1]*origin[1] + origin[2]*origin[2]*invPolar2 - 1
|
|||
|
|
discriminant := b*b - 4*a*c
|
|||
|
|
if discriminant < 0 {
|
|||
|
|
return Vector3{}, false
|
|||
|
|
}
|
|||
|
|
sqrtDiscriminant := math.Sqrt(discriminant)
|
|||
|
|
t1 := (-b - sqrtDiscriminant) / (2 * a)
|
|||
|
|
t2 := (-b + sqrtDiscriminant) / (2 * a)
|
|||
|
|
t := math.Inf(1)
|
|||
|
|
if t1 > 0 {
|
|||
|
|
t = t1
|
|||
|
|
}
|
|||
|
|
if t2 > 0 && t2 < t {
|
|||
|
|
t = t2
|
|||
|
|
}
|
|||
|
|
if !isFinite(t) {
|
|||
|
|
return Vector3{}, false
|
|||
|
|
}
|
|||
|
|
return Vector3{
|
|||
|
|
origin[0] + t*direction[0],
|
|||
|
|
origin[1] + t*direction[1],
|
|||
|
|
origin[2] + t*direction[2],
|
|||
|
|
}, true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func jupiterUmbraScale(distanceBehindAU, sunDistanceAU float64) float64 {
|
|||
|
|
if distanceBehindAU <= 0 || sunDistanceAU <= 0 {
|
|||
|
|
return 0
|
|||
|
|
}
|
|||
|
|
jupiterRadiusAU := jupiterGalileanEquatorialRadiusKM / astronomicalUnitKM
|
|||
|
|
umbraLength := jupiterRadiusAU * sunDistanceAU / (solarRadiusAU - jupiterRadiusAU)
|
|||
|
|
if umbraLength <= 0 {
|
|||
|
|
return 0
|
|||
|
|
}
|
|||
|
|
return 1 - distanceBehindAU/umbraLength
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func invalidJupiterGalileanPhenomenon() JupiterGalileanPhenomenon {
|
|||
|
|
nan := math.NaN()
|
|||
|
|
return JupiterGalileanPhenomenon{
|
|||
|
|
ShadowOffsetXArcsec: nan,
|
|||
|
|
ShadowOffsetYArcsec: nan,
|
|||
|
|
ShadowOffsetXJupiterRadii: nan,
|
|||
|
|
ShadowOffsetYJupiterRadii: nan,
|
|||
|
|
}
|
|||
|
|
}
|