9ee2163cc7
- 新增月掩恒星和行星:支持搜索、掩甚点、全球掩带及固定地点轨迹计算 - 支持恒星星表坐标转换、有限盘面行星接触事件和月掩 SVG 输出 - 新增日月食及月掩全球投影图、时间标记和 GeoJSON 地理数据接口 - 扩展日食中心线、南北界及偏食足迹采样,支持极区投影 - 修正站心时角、月出月落、月球视半径、折射和恒星自行计算 - 优化内外行星事件搜索、边界选择、极端输入处理和计算稳定性
322 lines
14 KiB
Go
322 lines
14 KiB
Go
package basic
|
|
|
|
import (
|
|
"math"
|
|
"sort"
|
|
)
|
|
|
|
const (
|
|
planetOccultationDiagramDefaultStepDays = 2.0 / 1440.0
|
|
planetOccultationDiagramMinStepDays = 1.0 / 86400.0
|
|
planetOccultationDiagramMaxSamples = 2000
|
|
planetOccultationDiagramDuplicateDays = 1e-10
|
|
planetOccultationDiagramGeometryArcsec = 0.05
|
|
planetOccultationDiagramPositionDeg = 0.01
|
|
planetOccultationDiagramContactTimeDays = 1e-6
|
|
)
|
|
|
|
// PlanetOccultationDiagramOptions 控制本地行星月掩轨迹采样。
|
|
// PlanetOccultationDiagramOptions controls local planetary-occultation track sampling.
|
|
type PlanetOccultationDiagramOptions struct {
|
|
// StepDays 是请求的轨迹采样步长,单位为日;非正值或非有限值使用两分钟,正值小于一秒时使用一秒。长事件可能增大实际步长,使基础轨迹不超过 2000 个采样点;必要阶段帧仍会额外保留。结果会报告实际采用的值。
|
|
// StepDays is the requested track sampling step in days. Non-positive or non-finite values use two minutes, and positive values below one second use one second. Long events may increase the effective step to keep the base track within 2000 samples; required phase frames are retained in addition. The result reports the effective value.
|
|
StepDays float64
|
|
}
|
|
|
|
// PlanetOccultationDiagramFrame 描述一个时刻的站心月球与行星几何。
|
|
// PlanetOccultationDiagramFrame describes topocentric Moon-planet geometry at one instant.
|
|
type PlanetOccultationDiagramFrame struct {
|
|
// JDE 是 TT 儒略历书日。
|
|
// JDE is the TT Julian ephemeris day.
|
|
JDE float64
|
|
// PlanetXArcsec 和 PlanetYArcsec 是相对月心的切平面偏移,单位为角秒。X 向东为正,Y 向北为正。
|
|
// PlanetXArcsec and PlanetYArcsec are tangent-plane offsets from the lunar center. X is positive east and Y is positive north.
|
|
PlanetXArcsec float64
|
|
PlanetYArcsec float64
|
|
// MoonRadiusArcsec 和 PlanetRadiusArcsec 是站心视半径,单位为角秒。
|
|
// MoonRadiusArcsec and PlanetRadiusArcsec are topocentric apparent semidiameters.
|
|
MoonRadiusArcsec float64
|
|
PlanetRadiusArcsec float64
|
|
// SeparationArcsec 和 PositionAngleDeg 描述行星中心相对月心的位置。
|
|
// SeparationArcsec and PositionAngleDeg describe the planet center relative to the lunar center.
|
|
SeparationArcsec float64
|
|
PositionAngleDeg float64
|
|
// MoonAltitudeDeg 和 MoonAzimuthDeg 是站心地平坐标。
|
|
// MoonAltitudeDeg and MoonAzimuthDeg are topocentric horizontal coordinates.
|
|
MoonAltitudeDeg float64
|
|
MoonAzimuthDeg float64
|
|
// DisksOverlap 表示两个视盘面存在正面积交集。
|
|
// DisksOverlap is true while the two apparent disks have a positive-area intersection.
|
|
DisksOverlap bool
|
|
// FullyOcculted 表示行星盘面完全位于月缘内侧。
|
|
// FullyOcculted is true while the planet disk lies strictly inside the lunar limb.
|
|
FullyOcculted bool
|
|
// Label 是主阶段标识;Labels 在掠掩事件中保留重合阶段。
|
|
// Label is the primary key phase; Labels retains coincident phases for grazing events.
|
|
Label string
|
|
Labels []string
|
|
}
|
|
|
|
// PlanetOccultationDiagramResult 包含固定地点行星月掩的几何数据。
|
|
// PlanetOccultationDiagramResult contains geometry for a fixed-site planetary occultation.
|
|
type PlanetOccultationDiagramResult struct {
|
|
Occultation PlanetOccultationInfo
|
|
Frames []PlanetOccultationDiagramFrame
|
|
// StepDays 是实际采用的基础轨迹采样步长,单位为日。
|
|
// StepDays is the effective base-track sampling step in days.
|
|
StepDays float64
|
|
}
|
|
|
|
type planetOccultationDiagramTime struct {
|
|
jde float64
|
|
labels []string
|
|
}
|
|
|
|
// PlanetOccultationDiagram 为已求解的固定地点行星月掩计算以月心为原点的切平面轨迹。事件数据无效或不完整时,结果不含帧。
|
|
// PlanetOccultationDiagram computes a Moon-centered tangent-plane track for an already solved fixed-site planetary occultation. Invalid or incomplete event data produces a result without frames.
|
|
func PlanetOccultationDiagram(
|
|
info PlanetOccultationInfo,
|
|
options PlanetOccultationDiagramOptions,
|
|
) PlanetOccultationDiagramResult {
|
|
options = normalizePlanetOccultationDiagramOptions(options)
|
|
result := PlanetOccultationDiagramResult{Occultation: info, StepDays: options.StepDays}
|
|
if !planetOccultationDiagramInputValid(info) {
|
|
return result
|
|
}
|
|
|
|
config, ok := planetOccultationConfigFor(info.Planet)
|
|
if !ok {
|
|
return result
|
|
}
|
|
startTT := occultationTimeToTT(info.ExternalImmersion)
|
|
greatestTT := occultationTimeToTT(info.Greatest)
|
|
endTT := occultationTimeToTT(info.ExternalEmersion)
|
|
externalImmersionFrame, externalImmersionOK := planetOccultationDiagramFrameAt(startTT, config, info.Observer)
|
|
greatestFrame, greatestOK := planetOccultationDiagramFrameAt(greatestTT, config, info.Observer)
|
|
externalEmersionFrame, externalEmersionOK := planetOccultationDiagramFrameAt(endTT, config, info.Observer)
|
|
if !externalImmersionOK || !greatestOK || !externalEmersionOK {
|
|
return result
|
|
}
|
|
var (
|
|
internalImmersionFrame, internalEmersionFrame PlanetOccultationDiagramFrame
|
|
internalImmersionOK, internalEmersionOK bool
|
|
)
|
|
if info.HasInternalContacts {
|
|
internalImmersionFrame, internalImmersionOK = planetOccultationDiagramFrameAt(
|
|
occultationTimeToTT(info.InternalImmersion), config, info.Observer,
|
|
)
|
|
internalEmersionFrame, internalEmersionOK = planetOccultationDiagramFrameAt(
|
|
occultationTimeToTT(info.InternalEmersion), config, info.Observer,
|
|
)
|
|
}
|
|
if (info.HasInternalContacts && (!internalImmersionOK || !internalEmersionOK)) ||
|
|
!planetOccultationDiagramMatchesInfo(
|
|
info, startTT, greatestTT, endTT,
|
|
externalImmersionFrame, internalImmersionFrame, greatestFrame, internalEmersionFrame, externalEmersionFrame,
|
|
) {
|
|
return result
|
|
}
|
|
times, stepDays := planetOccultationDiagramTimes(info, startTT, greatestTT, endTT, options.StepDays)
|
|
result.StepDays = stepDays
|
|
result.Frames = make([]PlanetOccultationDiagramFrame, 0, len(times))
|
|
for _, item := range times {
|
|
frame, frameOK := planetOccultationDiagramFrameAt(item.jde, config, info.Observer)
|
|
if !frameOK {
|
|
return PlanetOccultationDiagramResult{Occultation: info, StepDays: stepDays}
|
|
}
|
|
frame.Labels = append([]string(nil), item.labels...)
|
|
frame.Label = planetOccultationDiagramPrimaryLabel(item.labels)
|
|
result.Frames = append(result.Frames, frame)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func planetOccultationDiagramMatchesInfo(
|
|
info PlanetOccultationInfo,
|
|
startTT, greatestTT, endTT float64,
|
|
externalImmersion, internalImmersion, greatest, internalEmersion, externalEmersion PlanetOccultationDiagramFrame,
|
|
) bool {
|
|
if !finite(info.MinimumSeparationArcsec) || info.MinimumSeparationArcsec < 0 ||
|
|
!finite(info.PositionAngleDeg) ||
|
|
!finite(info.MoonSemidiameterArcsec) || info.MoonSemidiameterArcsec <= 0 ||
|
|
!finite(info.PlanetSemidiameterArcsec) || info.PlanetSemidiameterArcsec <= 0 {
|
|
return false
|
|
}
|
|
if math.Abs(externalImmersion.SeparationArcsec-externalImmersion.MoonRadiusArcsec-externalImmersion.PlanetRadiusArcsec) > planetOccultationDiagramGeometryArcsec ||
|
|
math.Abs(externalEmersion.SeparationArcsec-externalEmersion.MoonRadiusArcsec-externalEmersion.PlanetRadiusArcsec) > planetOccultationDiagramGeometryArcsec {
|
|
return false
|
|
}
|
|
if math.Abs(greatest.SeparationArcsec-info.MinimumSeparationArcsec) > planetOccultationDiagramGeometryArcsec ||
|
|
math.Abs(greatest.MoonRadiusArcsec-info.MoonSemidiameterArcsec) > planetOccultationDiagramGeometryArcsec ||
|
|
math.Abs(greatest.PlanetRadiusArcsec-info.PlanetSemidiameterArcsec) > planetOccultationDiagramGeometryArcsec ||
|
|
math.Abs(signedAngleDifference(greatest.PositionAngleDeg, info.PositionAngleDeg)) > planetOccultationDiagramPositionDeg {
|
|
return false
|
|
}
|
|
|
|
externalMetric := greatest.SeparationArcsec - greatest.MoonRadiusArcsec - greatest.PlanetRadiusArcsec
|
|
internalMetric := greatest.SeparationArcsec - greatest.MoonRadiusArcsec + greatest.PlanetRadiusArcsec
|
|
switch info.Type {
|
|
case OccultationPartial:
|
|
return externalMetric < -planetOccultationGrazingToleranceArcsec &&
|
|
internalMetric >= -planetOccultationGrazingToleranceArcsec
|
|
case OccultationGrazing:
|
|
return math.Abs(externalMetric) <= planetOccultationGrazingToleranceArcsec &&
|
|
math.Abs(startTT-greatestTT) <= planetOccultationDiagramContactTimeDays &&
|
|
math.Abs(endTT-greatestTT) <= planetOccultationDiagramContactTimeDays
|
|
case OccultationTotal:
|
|
if math.Abs(internalImmersion.SeparationArcsec-internalImmersion.MoonRadiusArcsec+internalImmersion.PlanetRadiusArcsec) > planetOccultationDiagramGeometryArcsec ||
|
|
math.Abs(internalEmersion.SeparationArcsec-internalEmersion.MoonRadiusArcsec+internalEmersion.PlanetRadiusArcsec) > planetOccultationDiagramGeometryArcsec {
|
|
return false
|
|
}
|
|
return externalMetric < -planetOccultationGrazingToleranceArcsec &&
|
|
internalMetric < -planetOccultationGrazingToleranceArcsec
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func normalizePlanetOccultationDiagramOptions(options PlanetOccultationDiagramOptions) PlanetOccultationDiagramOptions {
|
|
if options.StepDays <= 0 || !finite(options.StepDays) {
|
|
options.StepDays = planetOccultationDiagramDefaultStepDays
|
|
}
|
|
if options.StepDays < planetOccultationDiagramMinStepDays {
|
|
options.StepDays = planetOccultationDiagramMinStepDays
|
|
}
|
|
return options
|
|
}
|
|
|
|
func planetOccultationDiagramInputValid(info PlanetOccultationInfo) bool {
|
|
if info.Planet.Validate() != nil || info.Observer.Validate() != nil || !info.ContactsComplete ||
|
|
info.ExternalImmersion.IsZero() || info.Greatest.IsZero() || info.ExternalEmersion.IsZero() ||
|
|
info.Greatest.Before(info.ExternalImmersion) || info.ExternalEmersion.Before(info.Greatest) {
|
|
return false
|
|
}
|
|
switch info.Type {
|
|
case OccultationTotal:
|
|
return info.HasInternalContacts && !info.InternalImmersion.IsZero() && !info.InternalEmersion.IsZero() &&
|
|
info.InternalImmersion.After(info.ExternalImmersion) && info.InternalImmersion.Before(info.Greatest) &&
|
|
info.InternalEmersion.After(info.Greatest) && info.InternalEmersion.Before(info.ExternalEmersion)
|
|
case OccultationPartial:
|
|
return !info.HasInternalContacts && info.InternalImmersion.IsZero() && info.InternalEmersion.IsZero()
|
|
case OccultationGrazing:
|
|
return !info.HasInternalContacts && info.InternalImmersion.IsZero() && info.InternalEmersion.IsZero()
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func planetOccultationDiagramTimes(
|
|
info PlanetOccultationInfo,
|
|
startTT, greatestTT, endTT, stepDays float64,
|
|
) ([]planetOccultationDiagramTime, float64) {
|
|
if !finite(startTT) || !finite(greatestTT) || !finite(endTT) || greatestTT < startTT || endTT < greatestTT {
|
|
return nil, stepDays
|
|
}
|
|
if endTT > startTT {
|
|
if sampleCount := int(math.Ceil((endTT-startTT)/stepDays)) + 1; sampleCount > planetOccultationDiagramMaxSamples {
|
|
stepDays = (endTT - startTT) / float64(planetOccultationDiagramMaxSamples-1)
|
|
}
|
|
}
|
|
times := []planetOccultationDiagramTime{
|
|
{jde: startTT, labels: []string{"C1"}},
|
|
{jde: greatestTT, labels: []string{"Greatest"}},
|
|
{jde: endTT, labels: []string{"C4"}},
|
|
}
|
|
if info.HasInternalContacts {
|
|
times = append(times,
|
|
planetOccultationDiagramTime{jde: occultationTimeToTT(info.InternalImmersion), labels: []string{"C2"}},
|
|
planetOccultationDiagramTime{jde: occultationTimeToTT(info.InternalEmersion), labels: []string{"C3"}},
|
|
)
|
|
}
|
|
for jde := startTT + stepDays; jde < endTT; jde += stepDays {
|
|
times = append(times, planetOccultationDiagramTime{jde: jde})
|
|
}
|
|
sort.SliceStable(times, func(i, j int) bool {
|
|
if times[i].jde == times[j].jde {
|
|
return planetOccultationDiagramLabelPriority(times[i].labels) < planetOccultationDiagramLabelPriority(times[j].labels)
|
|
}
|
|
return times[i].jde < times[j].jde
|
|
})
|
|
return uniquePlanetOccultationDiagramTimes(times), stepDays
|
|
}
|
|
|
|
func uniquePlanetOccultationDiagramTimes(times []planetOccultationDiagramTime) []planetOccultationDiagramTime {
|
|
unique := times[:0]
|
|
for _, item := range times {
|
|
if !finite(item.jde) {
|
|
continue
|
|
}
|
|
if len(unique) == 0 || math.Abs(item.jde-unique[len(unique)-1].jde) > planetOccultationDiagramDuplicateDays {
|
|
item.labels = append([]string(nil), item.labels...)
|
|
unique = append(unique, item)
|
|
continue
|
|
}
|
|
unique[len(unique)-1].labels = mergeStarOccultationDiagramLabels(unique[len(unique)-1].labels, item.labels)
|
|
}
|
|
return unique
|
|
}
|
|
|
|
func planetOccultationDiagramPrimaryLabel(labels []string) string {
|
|
for _, label := range labels {
|
|
if label == "Greatest" {
|
|
return label
|
|
}
|
|
}
|
|
if len(labels) == 0 {
|
|
return ""
|
|
}
|
|
return labels[0]
|
|
}
|
|
|
|
func planetOccultationDiagramLabelPriority(labels []string) int {
|
|
if len(labels) == 0 {
|
|
return 99
|
|
}
|
|
switch labels[0] {
|
|
case "C1":
|
|
return 0
|
|
case "C2":
|
|
return 1
|
|
case "Greatest":
|
|
return 2
|
|
case "C3":
|
|
return 3
|
|
case "C4":
|
|
return 4
|
|
default:
|
|
return 99
|
|
}
|
|
}
|
|
|
|
func planetOccultationDiagramFrameAt(
|
|
tt float64,
|
|
config planetOccultationConfig,
|
|
observer Observer,
|
|
) (PlanetOccultationDiagramFrame, bool) {
|
|
state := planetOccultationStateAt(tt, config, &observer, -1)
|
|
if !state.valid {
|
|
return PlanetOccultationDiagramFrame{}, false
|
|
}
|
|
positionAngle := occultationPositionAngle(
|
|
state.position.moonRA, state.position.moonDec,
|
|
state.position.planetRA, state.position.planetDec,
|
|
)
|
|
if !finite(positionAngle) {
|
|
return PlanetOccultationDiagramFrame{}, false
|
|
}
|
|
angle := positionAngle * math.Pi / 180
|
|
return PlanetOccultationDiagramFrame{
|
|
JDE: tt,
|
|
PlanetXArcsec: state.separationArcsec * math.Sin(angle),
|
|
PlanetYArcsec: state.separationArcsec * math.Cos(angle),
|
|
MoonRadiusArcsec: state.moonSemidiameter,
|
|
PlanetRadiusArcsec: state.planetSemidiameter,
|
|
SeparationArcsec: state.separationArcsec,
|
|
PositionAngleDeg: positionAngle,
|
|
MoonAltitudeDeg: occultationAltitude(tt, observer, state.position.moonRA, state.position.moonDec),
|
|
MoonAzimuthDeg: occultationAzimuth(tt, observer, state.position.moonRA, state.position.moonDec),
|
|
DisksOverlap: state.externalContactMetric < -planetOccultationGrazingToleranceArcsec,
|
|
FullyOcculted: state.internalContactMetric < -planetOccultationGrazingToleranceArcsec,
|
|
}, true
|
|
}
|