feat: 新增月掩与日月食地理绘图并提升观测计算精度
- 新增月掩恒星和行星:支持搜索、掩甚点、全球掩带及固定地点轨迹计算 - 支持恒星星表坐标转换、有限盘面行星接触事件和月掩 SVG 输出 - 新增日月食及月掩全球投影图、时间标记和 GeoJSON 地理数据接口 - 扩展日食中心线、南北界及偏食足迹采样,支持极区投影 - 修正站心时角、月出月落、月球视半径、折射和恒星自行计算 - 优化内外行星事件搜索、边界选择、极端输入处理和计算稳定性
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
package basic
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sort"
|
||||
)
|
||||
|
||||
const (
|
||||
starOccultationDiagramDefaultStepDays = 2.0 / 1440.0
|
||||
starOccultationDiagramMinStepDays = 1.0 / 86400.0
|
||||
starOccultationDiagramMaxSamples = 2000
|
||||
starOccultationDiagramDuplicateDays = 1e-10
|
||||
starOccultationDiagramGeometryArcsec = 0.05
|
||||
starOccultationDiagramPositionDeg = 0.01
|
||||
)
|
||||
|
||||
// StarOccultationDiagramOptions 控制本地恒星月掩图的轨迹采样。
|
||||
// StarOccultationDiagramOptions controls local stellar-occultation diagram sampling.
|
||||
type StarOccultationDiagramOptions 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
|
||||
}
|
||||
|
||||
// StarOccultationDiagramFrame 描述一个时刻的站心月球与恒星几何。
|
||||
// StarOccultationDiagramFrame describes topocentric Moon-star geometry at one instant.
|
||||
type StarOccultationDiagramFrame struct {
|
||||
// JDE 是 TT 儒略历书日。
|
||||
// JDE is the TT Julian ephemeris day.
|
||||
JDE float64
|
||||
// StarXArcsec 和 StarYArcsec 是相对月心的切平面偏移,单位为角秒。X 向东为正,Y 向北为正。
|
||||
// StarXArcsec and StarYArcsec are tangent-plane offsets from the lunar center. X is positive east and Y is positive north.
|
||||
StarXArcsec float64
|
||||
StarYArcsec float64
|
||||
// MoonRadiusArcsec 是站心月球视半径,单位为角秒。
|
||||
// MoonRadiusArcsec is the topocentric apparent lunar semidiameter.
|
||||
MoonRadiusArcsec float64
|
||||
// SeparationArcsec 和 PositionAngleDeg 描述恒星相对月心的位置。
|
||||
// SeparationArcsec and PositionAngleDeg describe the star relative to the lunar center.
|
||||
SeparationArcsec float64
|
||||
PositionAngleDeg float64
|
||||
// MoonAltitudeDeg 和 MoonAzimuthDeg 是站心地平坐标。
|
||||
// MoonAltitudeDeg and MoonAzimuthDeg are topocentric horizontal coordinates.
|
||||
MoonAltitudeDeg float64
|
||||
MoonAzimuthDeg float64
|
||||
// BehindMoon 表示点光源恒星位于月缘内侧。
|
||||
// BehindMoon is true while the point-source star lies strictly inside the lunar limb.
|
||||
BehindMoon bool
|
||||
// Label 是主阶段标识;Labels 在掠掩事件中保留重合阶段。
|
||||
// Label is the primary key phase; Labels retains coincident phases for grazing events.
|
||||
Label string
|
||||
Labels []string
|
||||
}
|
||||
|
||||
// StarOccultationDiagramResult 包含固定地点恒星月掩的几何数据。
|
||||
// StarOccultationDiagramResult contains geometry for a fixed-site stellar occultation.
|
||||
type StarOccultationDiagramResult struct {
|
||||
Occultation StarOccultationInfo
|
||||
Frames []StarOccultationDiagramFrame
|
||||
// StepDays 是实际采用的基础轨迹采样步长,单位为日。
|
||||
// StepDays is the effective base-track sampling step in days.
|
||||
StepDays float64
|
||||
}
|
||||
|
||||
type starOccultationDiagramTime struct {
|
||||
jde float64
|
||||
labels []string
|
||||
}
|
||||
|
||||
// StarOccultationDiagram 为已求解的固定地点恒星月掩计算以月心为原点的切平面轨迹。事件数据无效或不完整时,结果不含帧。
|
||||
// StarOccultationDiagram computes a Moon-centered tangent-plane track for an already solved fixed-site stellar occultation. Invalid or incomplete event data produces a result without frames.
|
||||
func StarOccultationDiagram(
|
||||
info StarOccultationInfo,
|
||||
star StarCoordinate,
|
||||
options StarOccultationDiagramOptions,
|
||||
) StarOccultationDiagramResult {
|
||||
options = normalizeStarOccultationDiagramOptions(options)
|
||||
result := StarOccultationDiagramResult{Occultation: info, StepDays: options.StepDays}
|
||||
if star.Validate() != nil || info.Observer.Validate() != nil ||
|
||||
!info.ContactsComplete || info.Immersion.IsZero() || info.Greatest.IsZero() || info.Emersion.IsZero() ||
|
||||
info.Greatest.Before(info.Immersion) || info.Emersion.Before(info.Greatest) ||
|
||||
(info.Type != OccultationTotal && info.Type != OccultationGrazing) {
|
||||
return result
|
||||
}
|
||||
|
||||
startTT := occultationTimeToTT(info.Immersion)
|
||||
greatestTT := occultationTimeToTT(info.Greatest)
|
||||
endTT := occultationTimeToTT(info.Emersion)
|
||||
immersionFrame, immersionOK := starOccultationDiagramFrameAt(startTT, star, info.Observer)
|
||||
greatestFrame, greatestOK := starOccultationDiagramFrameAt(greatestTT, star, info.Observer)
|
||||
emersionFrame, emersionOK := starOccultationDiagramFrameAt(endTT, star, info.Observer)
|
||||
if !immersionOK || !greatestOK || !emersionOK ||
|
||||
!starOccultationDiagramMatchesInfo(info, immersionFrame, greatestFrame, emersionFrame) {
|
||||
return result
|
||||
}
|
||||
times, stepDays := starOccultationDiagramTimes(startTT, greatestTT, endTT, options.StepDays)
|
||||
result.StepDays = stepDays
|
||||
result.Frames = make([]StarOccultationDiagramFrame, 0, len(times))
|
||||
for _, item := range times {
|
||||
frame, ok := starOccultationDiagramFrameAt(item.jde, star, info.Observer)
|
||||
if !ok {
|
||||
return StarOccultationDiagramResult{Occultation: info, StepDays: stepDays}
|
||||
}
|
||||
frame.Labels = append([]string(nil), item.labels...)
|
||||
frame.Label = starOccultationDiagramPrimaryLabel(item.labels)
|
||||
result.Frames = append(result.Frames, frame)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func starOccultationDiagramMatchesInfo(
|
||||
info StarOccultationInfo,
|
||||
immersion, greatest, emersion StarOccultationDiagramFrame,
|
||||
) bool {
|
||||
if !finite(info.MinimumSeparationArcsec) || !finite(info.MoonSemidiameterArcsec) ||
|
||||
!finite(info.PositionAngleDeg) {
|
||||
return false
|
||||
}
|
||||
if math.Abs(immersion.SeparationArcsec-immersion.MoonRadiusArcsec) > starOccultationDiagramGeometryArcsec ||
|
||||
math.Abs(emersion.SeparationArcsec-emersion.MoonRadiusArcsec) > starOccultationDiagramGeometryArcsec {
|
||||
return false
|
||||
}
|
||||
return math.Abs(greatest.SeparationArcsec-info.MinimumSeparationArcsec) <= starOccultationDiagramGeometryArcsec &&
|
||||
math.Abs(greatest.MoonRadiusArcsec-info.MoonSemidiameterArcsec) <= starOccultationDiagramGeometryArcsec &&
|
||||
math.Abs(signedAngleDifference(greatest.PositionAngleDeg, info.PositionAngleDeg)) <= starOccultationDiagramPositionDeg
|
||||
}
|
||||
|
||||
func normalizeStarOccultationDiagramOptions(options StarOccultationDiagramOptions) StarOccultationDiagramOptions {
|
||||
if options.StepDays <= 0 || !finite(options.StepDays) {
|
||||
options.StepDays = starOccultationDiagramDefaultStepDays
|
||||
}
|
||||
if options.StepDays < starOccultationDiagramMinStepDays {
|
||||
options.StepDays = starOccultationDiagramMinStepDays
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
func starOccultationDiagramTimes(startTT, greatestTT, endTT, stepDays float64) ([]starOccultationDiagramTime, 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 > starOccultationDiagramMaxSamples {
|
||||
stepDays = (endTT - startTT) / float64(starOccultationDiagramMaxSamples-1)
|
||||
}
|
||||
}
|
||||
times := []starOccultationDiagramTime{
|
||||
{jde: startTT, labels: []string{"Immersion"}},
|
||||
{jde: greatestTT, labels: []string{"Greatest"}},
|
||||
{jde: endTT, labels: []string{"Emersion"}},
|
||||
}
|
||||
for jde := startTT + stepDays; jde < endTT; jde += stepDays {
|
||||
times = append(times, starOccultationDiagramTime{jde: jde})
|
||||
}
|
||||
sort.SliceStable(times, func(i, j int) bool {
|
||||
if times[i].jde == times[j].jde {
|
||||
return starOccultationDiagramLabelPriority(times[i].labels) < starOccultationDiagramLabelPriority(times[j].labels)
|
||||
}
|
||||
return times[i].jde < times[j].jde
|
||||
})
|
||||
return uniqueStarOccultationDiagramTimes(times), stepDays
|
||||
}
|
||||
|
||||
func uniqueStarOccultationDiagramTimes(times []starOccultationDiagramTime) []starOccultationDiagramTime {
|
||||
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) > starOccultationDiagramDuplicateDays {
|
||||
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 mergeStarOccultationDiagramLabels(existing, incoming []string) []string {
|
||||
for _, label := range incoming {
|
||||
found := false
|
||||
for _, current := range existing {
|
||||
if current == label {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
existing = append(existing, label)
|
||||
}
|
||||
}
|
||||
return existing
|
||||
}
|
||||
|
||||
func starOccultationDiagramPrimaryLabel(labels []string) string {
|
||||
for _, label := range labels {
|
||||
if label == "Greatest" {
|
||||
return label
|
||||
}
|
||||
}
|
||||
if len(labels) == 0 {
|
||||
return ""
|
||||
}
|
||||
return labels[0]
|
||||
}
|
||||
|
||||
func starOccultationDiagramLabelPriority(labels []string) int {
|
||||
if len(labels) == 0 {
|
||||
return 99
|
||||
}
|
||||
switch labels[0] {
|
||||
case "Immersion":
|
||||
return 0
|
||||
case "Greatest":
|
||||
return 1
|
||||
case "Emersion":
|
||||
return 2
|
||||
default:
|
||||
return 99
|
||||
}
|
||||
}
|
||||
|
||||
func starOccultationDiagramFrameAt(tt float64, star StarCoordinate, observer Observer) (StarOccultationDiagramFrame, bool) {
|
||||
position := starMoonPositionAt(tt, star, observer)
|
||||
moonRadius := moonTopocentricSemidiameterN(tt, observer, -1)
|
||||
if !position.valid || !finite(moonRadius) || moonRadius <= 0 {
|
||||
return StarOccultationDiagramFrame{}, false
|
||||
}
|
||||
separation := angularSeparationDegrees(position.moonRA, position.moonDec, position.starRA, position.starDec) * 3600
|
||||
positionAngle := occultationPositionAngle(position.moonRA, position.moonDec, position.starRA, position.starDec)
|
||||
if !finite(separation) || !finite(positionAngle) {
|
||||
return StarOccultationDiagramFrame{}, false
|
||||
}
|
||||
angle := positionAngle * math.Pi / 180
|
||||
return StarOccultationDiagramFrame{
|
||||
JDE: tt,
|
||||
StarXArcsec: separation * math.Sin(angle),
|
||||
StarYArcsec: separation * math.Cos(angle),
|
||||
MoonRadiusArcsec: moonRadius,
|
||||
SeparationArcsec: separation,
|
||||
PositionAngleDeg: positionAngle,
|
||||
MoonAltitudeDeg: occultationAltitude(tt, observer, position.moonRA, position.moonDec),
|
||||
MoonAzimuthDeg: occultationAzimuth(tt, observer, position.moonRA, position.moonDec),
|
||||
BehindMoon: separation < moonRadius-starOccultationGrazingTolerance,
|
||||
}, true
|
||||
}
|
||||
Reference in New Issue
Block a user