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 }