feat: 新增月掩与日月食地理绘图并提升观测计算精度
- 新增月掩恒星和行星:支持搜索、掩甚点、全球掩带及固定地点轨迹计算 - 支持恒星星表坐标转换、有限盘面行星接触事件和月掩 SVG 输出 - 新增日月食及月掩全球投影图、时间标记和 GeoJSON 地理数据接口 - 扩展日食中心线、南北界及偏食足迹采样,支持极区投影 - 修正站心时角、月出月落、月球视半径、折射和恒星自行计算 - 优化内外行星事件搜索、边界选择、极端输入处理和计算稳定性
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user