Files
astro/basic/occultation_path.go
T
b612 16c62a97d5 feat: 完善时标与天象几何计算并扩展输出接口
- 新增时标、ΔT 模型、质心时间与 UT1 支持
- 改进日月食、月掩、行星事件及路径边界计算
- 完善恒星三维自行与动态距离传播
- 扩展 SVG、GeoJSON、KML 输出与底层距离换算工具
- 整理中英文手册、示例资源及回归测试
2026-09-23 18:55:12 +08:00

1588 lines
57 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package basic
import (
"math"
"sort"
"sync"
"time"
)
const (
// occultationPathThetaIntervalCacheMaximumEntries 限制每个 frame 记忆的中心角数量;一次求解只会用到个位数个。
// occultationPathThetaIntervalCacheMaximumEntries bounds the memory of center angles per frame; a solve uses only a handful.
occultationPathThetaIntervalCacheMaximumEntries = 8
occultationPathDefaultStepDays = 1.0 / 1440.0
occultationPathMinStepDays = 1.0 / 86400.0
occultationPathMaxSampleCount = 30000
occultationPathMaxAdaptiveDepth = 20
occultationPathBoundarySpacingKM = 500.0
occultationPathContourSpacingKM = 40.0
occultationPathBoundaryMergeKM = 1.0
occultationPathVelocityStepDays = 1.0 / 1440.0
occultationPathBoundaryScanPoints = 720
occultationPathRootToleranceDays = occultationEventSelectionToleranceDays
occultationPathRangeStepDays = 5.0 / 1440.0
occultationPathSearchSpanDays = 2.0
occultationPathWidthToleranceKM = 0.005
occultationPathMaxOutputPointCount = 2000000
occultationPathFootprintPointBudget = 2048
occultationPathBoundaryBranchJumpKM = 120.0
occultationPathBoundaryBranchSpeedKMPerSecond = 10.0
occultationPathEarthEquatorialRadiusKM = 6378.1366
occultationPathEarthPolarRatio = 0.99664719
occultationPathAstronomicalUnitKM = 149597870.7
)
// FindStarOccultationPaths 搜索单颗点源恒星月掩的全球掩带。
// 查询窗口按全球几何掩甚点选择事件,端点容差为 10 ms,与数值根精度一致。返回路径扩展到完整全球起止点;函数不会加载内嵌星表,调用者需显式提供坐标。
// FindStarOccultationPaths searches the global lunar-occultation footprint of one point-source star.
// The query window selects events by global geometric greatest, with a 10 ms endpoint tolerance matching the numerical root precision. Each returned path expands to its complete global start and end; the function does not load the embedded catalog.
func FindStarOccultationPaths(start, end time.Time, star StarCoordinate, options OccultationPathOptions) ([]StarOccultationPath, error) {
if err := validateOccultationTimeRange(start, end); err != nil {
return nil, err
}
if err := star.Validate(); err != nil {
return nil, err
}
if err := options.Validate(); err != nil {
return nil, err
}
options = normalizeOccultationPathOptions(options)
startTT := occultationTimeToTT(start)
endTT := occultationTimeToTT(end)
candidateStartTT := startTT - occultationPathSearchSpanDays
candidateEndTT := endTT + occultationPathSearchSpanDays
coarseOptions := OccultationSearchOptions{}
candidates := starOccultationGeocentricCandidateGreatestTimes(
candidateStartTT,
candidateEndTT,
starOccultationCoarseStepDays(coarseOptions),
star,
0,
)
paths := make([]StarOccultationPath, 0, len(candidates))
for _, seedTT := range candidates {
path, ok, err := starOccultationPathAtSeed(seedTT, star, options, start, end)
if err != nil {
return nil, err
}
if !ok {
continue
}
if len(paths) > 0 && math.Abs(paths[len(paths)-1].Greatest.Time.Sub(path.Greatest.Time).Seconds()) <= 60 {
continue
}
paths = append(paths, path)
}
sort.SliceStable(paths, func(i, j int) bool {
return paths[i].Greatest.Time.Before(paths[j].Greatest.Time)
})
return paths, nil
}
func normalizeOccultationPathOptions(options OccultationPathOptions) OccultationPathOptions {
if options.Algorithm == "" {
options.Algorithm = OccultationPathAlgorithmOptimized
}
if options.Step <= 0 {
options.Step = time.Duration(occultationPathDefaultStepDays * float64(24*time.Hour))
}
if float64(options.Step)/float64(24*time.Hour) < occultationPathMinStepDays {
options.Step = time.Second
}
if options.TargetSpacingKM <= 0 || math.IsNaN(options.TargetSpacingKM) || math.IsInf(options.TargetSpacingKM, 0) {
options.TargetSpacingKM = 0
}
if len(options.GreatestTimeValues) > 0 {
values := make([]float64, 0, len(options.GreatestTimeValues))
for _, value := range options.GreatestTimeValues {
if !finite(value) {
continue
}
duplicate := false
for _, existing := range values {
if math.Abs(existing-value) <= 1e-9 {
duplicate = true
break
}
}
if !duplicate {
values = append(values, value)
}
}
sort.Float64s(values)
options.GreatestTimeValues = values
}
return options
}
func starOccultationPathAtSeed(
seedTT float64,
star StarCoordinate,
options OccultationPathOptions,
selectionStart, selectionEnd time.Time,
) (StarOccultationPath, bool, error) {
location := selectionStart.Location()
cache := newStarOccultationEventCache(star)
cache.prepareLocalEphemeris(seedTT)
frameAt := cache.frameAt
candidateFrameAt := cache.candidateFrameAt
searchStart := seedTT - occultationPathSearchSpanDays
searchEnd := seedTT + occultationPathSearchSpanDays
outerStart, outerEnd, ok := starOccultationPathWindowWithCandidateFrames(
seedTT, searchStart, searchEnd, candidateFrameAt, frameAt, false,
)
if !ok {
return StarOccultationPath{}, false, nil
}
centerStart, centerEnd, hasCenter := starOccultationPathWindowWithCandidateFrames(
seedTT, searchStart, searchEnd, candidateFrameAt, frameAt, true,
)
// Global markers retain the same full-term ephemerides as event-only
// queries. The optimized path cache is selected after these markers.
greatestTT := starOccultationPathGreatestWithFrame(seedTT, outerStart, outerEnd, frameAt)
greatest, greatestOK := starOccultationPathCenterPointWithFrame(greatestTT, frameAt, location)
if !greatestOK {
if hasCenter {
greatestTT = math.Max(centerStart, math.Min(centerEnd, greatestTT))
greatest, greatestOK = starOccultationPathCenterPointWithFrame(greatestTT, frameAt, location)
}
}
if !greatestOK {
// For a non-central path the shadow axis misses the ellipsoid. Greatest
// is the nearest point on the ellipsoid to that axis, not an outer
// contact tangent; the latter is a band edge and shifts the marker.
greatest, greatestOK = occultationPathTrackPointForFrame(greatestTT, frameAt, location)
}
if !greatestOK {
return StarOccultationPath{}, false, nil
}
if !occultationTimeInSelectionWindow(greatest.Time, selectionStart, selectionEnd) {
return StarOccultationPath{}, false, nil
}
if occultationPathEstimatedPointCount(
outerStart, outerEnd, centerStart, centerEnd, hasCenter,
0, 0, false, greatestTT, options,
) > occultationPathMaxOutputPointCount {
return StarOccultationPath{}, false, ErrOccultationPathSamplingLimit
}
start := starOccultationPathBoundaryEndpointWithFrame(outerStart, frameAt, location, 1)
end := starOccultationPathBoundaryEndpointWithFrame(outerEnd, frameAt, location, -1)
if !start.valid || !end.valid {
return StarOccultationPath{}, false, nil
}
exactFrameAt := frameAt
if options.Algorithm != OccultationPathAlgorithmExact {
optimized := newStarOccultationEventCache(star)
optimized.preparePathEphemeris(seedTT, options.Algorithm)
if optimized.local.dense {
cache = optimized
frameAt = cache.frameAt
}
}
path := StarOccultationPath{
TargetID: star.ID,
Start: start.point,
Greatest: greatest,
End: end.point,
Complete: outerStart > searchStart && outerEnd < searchEnd,
Step: options.Step,
TargetSpacingKM: options.TargetSpacingKM,
}
centerLine, northern, southern, err := starOccultationPathSamplesWithFrame(
outerStart,
outerEnd,
centerStart,
centerEnd,
hasCenter,
greatestTT,
frameAt,
options,
location,
)
if err != nil {
return StarOccultationPath{}, false, err
}
if cache.local.dense {
correctOccultationCenterWidths(centerLine, exactFrameAt)
}
path.CenterLine = centerLine
path.NorthernLimit = occultationPathWithEndpoints(start.point, end.point, northern)
path.SouthernLimit = occultationPathWithEndpoints(start.point, end.point, southern)
path.NorthernLimit = occultationStationCorrectLimitSeries(
path.NorthernLimit, frameAt, cache.riseSetContextAt, false, location,
)
path.SouthernLimit = occultationStationCorrectLimitSeries(
path.SouthernLimit, frameAt, cache.riseSetContextAt, false, location,
)
path.GreatestLimitSeparationKM, _ = occultationPathLimitSeparations(
path.NorthernLimit, path.SouthernLimit, greatestTT,
)
if !options.DisableFootprints {
path.Footprints = planetOccultationFootprints(outerStart, outerEnd, greatestTT, frameAt, options, location)
}
path.RiseSetCurves = occultationRiseSetCurvesWithCache(
outerStart, outerEnd, greatestTT, options, location, cache.riseSetCache,
)
path.GreatestTimeContours = occultationGreatestTimeContours(
occultationGreatestTimeLevels(options, outerStart, outerEnd), outerStart, outerEnd, cache.riseSetCache,
[][]OccultationPathPoint{path.CenterLine, path.NorthernLimit, path.SouthernLimit},
false, location,
)
bandContourTimes := occultationRiseSetEndpointTimes(path.RiseSetCurves)
path.BandContours = occultationContactBandContoursWithAdditionalTimes(
start.point, end.point, outerStart, outerEnd, greatestTT, frameAt, options, location, bandContourTimes,
)
if options.DisableFootprints {
path.BandFootprints = planetOccultationBandFootprints(
outerStart, outerEnd, greatestTT, frameAt, location, bandContourTimes,
)
if options.IncludeFootprintTimeline {
path.Footprints = planetOccultationTimelineFootprints(
outerStart, outerEnd, greatestTT, frameAt, options, location,
)
}
}
if len(path.Footprints) > 0 {
path.Footprints = occultationStationCorrectFootprintEdges(
path.Footprints, frameAt, cache.riseSetContextAt, false, location,
)
}
if len(path.BandFootprints) > 0 {
path.BandFootprints = occultationStationCorrectFootprintEdges(
path.BandFootprints, frameAt, cache.riseSetContextAt, false, location,
)
}
path.BandContours = occultationStationCorrectContours(
path.BandContours, path.RiseSetCurves, cache.riseSetCache, location,
)
path.VisibilityContours = occultationStationVisibilityEnvelopeContours(
path.RiseSetCurves, cache.riseSetCache, location,
)
return path, true, nil
}
func occultationPathEstimatedPointCount(
outerStart, outerEnd, centerStart, centerEnd float64,
hasCenter bool,
totalStart, totalEnd float64,
hasTotal bool,
greatestTT float64,
options OccultationPathOptions,
) int {
stepDays := float64(options.Step) / float64(24*time.Hour)
if stepDays <= 0 {
stepDays = occultationPathDefaultStepDays
}
estimate := 0
add := func(value int) {
estimate = occultationPathAccumulatePointEstimate(estimate, value)
}
add(3 * len(occultationPathSampleTimes(outerStart, outerEnd, greatestTT, stepDays)))
contourStepDays := occultationPathContourStepDays(options)
add(2 * len(occultationPathSampleTimes(outerStart, outerEnd, greatestTT, contourStepDays)))
if hasCenter {
add(3 * len(occultationPathSampleTimes(centerStart, centerEnd, greatestTT, stepDays)))
}
if hasTotal {
add(3 * len(occultationPathSampleTimes(totalStart, totalEnd, greatestTT, stepDays)))
add(2 * len(occultationPathSampleTimes(totalStart, totalEnd, greatestTT, contourStepDays)))
}
footprintStep := planetOccultationFootprintSampleStepDays(options)
footprintLimit := planetOccultationFootprintMaxSamples
if options.DisableFootprints && !options.IncludeFootprintTimeline {
footprintStep = planetOccultationBandSampleStepDays()
footprintLimit = planetOccultationBandMaxSamples
}
add(len(occultationPathSampleTimesWithLimit(outerStart, outerEnd, greatestTT, footprintStep, footprintLimit)) * occultationPathFootprintPointBudget)
if options.DisableFootprints && options.IncludeFootprintTimeline {
add(len(planetOccultationBandSampleTimes(
outerStart, outerEnd, greatestTT, planetOccultationBandSampleStepDays(), planetOccultationBandMaxSamples,
)) * occultationPathFootprintPointBudget)
}
if hasTotal {
add(len(occultationPathSampleTimesWithLimit(totalStart, totalEnd, greatestTT, footprintStep, footprintLimit)) * occultationPathFootprintPointBudget)
if options.DisableFootprints && options.IncludeFootprintTimeline {
add(len(planetOccultationBandSampleTimes(
totalStart, totalEnd, greatestTT, planetOccultationBandSampleStepDays(), planetOccultationBandMaxSamples,
)) * occultationPathFootprintPointBudget)
}
}
if !options.DisableRiseSet {
riseSetStep := 5 * time.Minute
if options.RiseSetStep > 0 {
riseSetStep = options.RiseSetStep
}
riseSetCount := len(occultationPathSampleTimes(
outerStart, outerEnd, greatestTT,
float64(riseSetStep)/float64(24*time.Hour),
))
add(6 * riseSetCount)
}
return estimate
}
func occultationPathAccumulatePointEstimate(estimate, value int) int {
if value <= 0 || estimate > occultationPathMaxOutputPointCount {
return estimate
}
if value > occultationPathMaxOutputPointCount-estimate {
return occultationPathMaxOutputPointCount + 1
}
return estimate + value
}
func occultationPathWithEndpoints(start, end OccultationPathPoint, points []OccultationPathPoint) []OccultationPathPoint {
result := make([]OccultationPathPoint, 0, len(points)+2)
result = append(result, start)
for _, point := range points {
if point.Time.After(result[len(result)-1].Time) && point.Time.Before(end.Time) {
result = append(result, point)
}
}
return append(result, end)
}
// occultationPathLimitSeparations 在同一时刻的南北限采样对上填写地面间距,并返回最接近掩甚的那一对的间距。
func occultationPathLimitSeparations(
north, south []OccultationPathPoint,
greatestTT float64,
) (float64, bool) {
if len(north) == 0 || len(north) != len(south) {
return 0, false
}
separation, found := 0.0, false
nearestDelta := math.Inf(1)
for index := range north {
if !north[index].Time.Equal(south[index].Time) {
continue
}
value := occultationPathDistanceKM(north[index], south[index])
if !finite(value) || value < 0 {
continue
}
north[index].LimitSeparationKM = value
south[index].LimitSeparationKM = value
if delta := math.Abs(centerTimeTT(north[index].Time) - greatestTT); delta < nearestDelta {
nearestDelta, separation, found = delta, value, true
}
}
return separation, found
}
func occultationContactBandContours(
northern, southern []OccultationPathPoint,
) [][]OccultationPathPoint {
contours := make([][]OccultationPathPoint, 0, 4)
for _, source := range [][]OccultationPathPoint{northern, southern} {
for _, sampleRange := range occultationContinuousBoundaryRanges(source) {
if sampleRange.end-sampleRange.start < 2 {
continue
}
contour := append([]OccultationPathPoint(nil), source[sampleRange.start:sampleRange.end]...)
contours = append(contours, contour)
}
}
return contours
}
type occultationPathSampleRange struct {
start int
end int
}
func occultationContinuousBoundaryRanges(points []OccultationPathPoint) []occultationPathSampleRange {
if len(points) == 0 {
return nil
}
ranges := make([]occultationPathSampleRange, 0, 4)
start := 0
for index := 1; index < len(points); index++ {
if !occultationPathBoundaryBranchChanged(points[index-1], points[index]) {
continue
}
ranges = append(ranges, occultationPathSampleRange{start: start, end: index})
start = index
}
return append(ranges, occultationPathSampleRange{start: start, end: len(points)})
}
func occultationPathBoundaryBranchChanged(first, second OccultationPathPoint) bool {
distance := occultationPathDistanceKM(first, second)
if distance <= occultationPathBoundaryBranchJumpKM {
return false
}
duration := math.Abs(second.Time.Sub(first.Time).Seconds())
return duration == 0 || distance/duration > occultationPathBoundaryBranchSpeedKMPerSecond
}
func occultationContactBandContoursWithAdditionalTimes(
start, end OccultationPathPoint,
startTT, endTT, greatestTT float64,
frameAt occultationPathFrameFunc,
options OccultationPathOptions,
location *time.Location,
additionalTimes []float64,
) [][]OccultationPathPoint {
stepDays := occultationPathContourStepDays(options)
northern, southern := occultationPathBoundaryContourSamplesForFrame(
startTT, endTT, greatestTT, frameAt, stepDays, location, additionalTimes,
)
return occultationContactBandContours(
occultationPathWithEndpoints(start, end, northern),
occultationPathWithEndpoints(start, end, southern),
)
}
func occultationPathContourStepDays(options OccultationPathOptions) float64 {
stepDays := float64(options.Step) / float64(24*time.Hour)
if stepDays <= 0 {
stepDays = occultationPathDefaultStepDays
}
if options.DisableRiseSet {
return stepDays
}
riseSetStep := options.RiseSetStep
if riseSetStep <= 0 {
riseSetStep = 5 * time.Minute
}
riseSetStepDays := float64(riseSetStep) / float64(24*time.Hour)
if riseSetStepDays > 0 && riseSetStepDays < stepDays {
stepDays = riseSetStepDays
}
return stepDays
}
func starOccultationPathWindow(seedTT, startTT, endTT float64, star StarCoordinate, center bool) (float64, float64, bool) {
return starOccultationPathWindowWithFrame(seedTT, startTT, endTT, func(tt float64) (occultationPathFrame, bool) {
return starOccultationPathFrameAt(tt, star)
}, center)
}
func starOccultationPathWindowWithFrame(
seedTT, startTT, endTT float64,
frameAt occultationPathFrameFunc,
center bool,
) (float64, float64, bool) {
return occultationPathWindowWithCandidateFrames(
seedTT, startTT, endTT, frameAt, frameAt, center,
starOccultationPathHasBoundary,
)
}
func starOccultationPathWindowWithCandidateFrames(
seedTT, startTT, endTT float64,
candidateFrameAt, exactFrameAt occultationPathFrameFunc,
center bool,
) (float64, float64, bool) {
return occultationPathWindowWithCandidateFrames(
seedTT, startTT, endTT, candidateFrameAt, exactFrameAt, center,
starOccultationPathHasBoundary,
)
}
func occultationPathWindowWithCandidateFrames(
seedTT, startTT, endTT float64,
candidateFrameAt, exactFrameAt occultationPathFrameFunc,
center bool,
hasBoundary func(occultationPathFrame) bool,
) (float64, float64, bool) {
predicate := func(frameAt occultationPathFrameFunc, tt float64) bool {
frame, ok := frameAt(tt)
if !ok {
return false
}
if center {
_, _, ok = occultationEarthLineIntersection(frame.moon, frame.axis)
return ok
}
return hasBoundary(frame)
}
engine := occultationMovingDiskEngine()
return engine.window(
seedTT, startTT, endTT,
func(tt float64) bool { return predicate(candidateFrameAt, tt) },
func(tt float64) bool { return predicate(exactFrameAt, tt) },
)
}
func starOccultationPathGreatest(seedTT, startTT, endTT float64, star StarCoordinate) float64 {
return starOccultationPathGreatestWithFrame(seedTT, startTT, endTT, func(tt float64) (occultationPathFrame, bool) {
return starOccultationPathFrameAt(tt, star)
})
}
func starOccultationPathGreatestWithFrame(
seedTT, startTT, endTT float64,
frameAt occultationPathFrameFunc,
) float64 {
return starOccultationPathGreatestWithIterations(seedTT, startTT, endTT, frameAt, 56)
}
func starOccultationPathGreatestWithIterations(
seedTT, startTT, endTT float64,
frameAt occultationPathFrameFunc,
iterations int,
) float64 {
return occultationMovingDiskEngine().greatest(
seedTT, startTT, endTT,
func(tt float64) (float64, bool) {
frame, ok := frameAt(tt)
if !ok {
return 0, false
}
return math.Hypot(frame.moonProjectionX(), frame.moonProjectionY()), true
}, iterations,
)
}
func starOccultationPathImpactWithFrame(tt float64, frameAt occultationPathFrameFunc) float64 {
frame, ok := frameAt(tt)
if !ok {
return math.Inf(1)
}
return math.Hypot(frame.moonProjectionX(), frame.moonProjectionY())
}
func starOccultationPathSamplesWithFrame(
outerStartTT, outerEndTT float64,
centerStartTT, centerEndTT float64,
hasCenter bool,
greatestTT float64,
frameAt occultationPathFrameFunc,
options OccultationPathOptions,
location *time.Location,
) ([]OccultationPathPoint, []OccultationPathPoint, []OccultationPathPoint, error) {
var points []OccultationPathPoint
if hasCenter {
var err error
points, err = starOccultationPathCenterSamplesWithFrame(centerStartTT, centerEndTT, greatestTT, frameAt, options, location)
if err != nil {
return nil, nil, nil, err
}
}
northern, southern := occultationPathBoundarySamplesForFrame(
outerStartTT, outerEndTT, greatestTT, frameAt, occultationPathContourStepDays(options), location,
)
return points, northern, southern, nil
}
func starOccultationPathCenterSamplesWithFrame(
startTT, endTT, greatestTT float64,
frameAt occultationPathFrameFunc,
options OccultationPathOptions,
location *time.Location,
) ([]OccultationPathPoint, error) {
stepDays := float64(options.Step) / float64(24*time.Hour)
times := occultationPathSampleTimes(startTT, endTT, greatestTT, stepDays)
points := make([]OccultationPathPoint, 0, len(times))
for _, tt := range times {
point, ok := starOccultationPathCenterPointWithFrame(tt, frameAt, location)
if ok && (len(points) == 0 || point.Time.After(points[len(points)-1].Time)) {
points = append(points, point)
}
}
if options.TargetSpacingKM > 0 {
return refineOccultationPathSpacingWithFrame(points, frameAt, options.TargetSpacingKM, location)
}
return points, nil
}
func occultationPathSampleTimes(startTT, endTT, greatestTT, stepDays float64) []float64 {
return occultationPathSampleTimesWithLimit(
startTT, endTT, greatestTT, stepDays, occultationPathMaxSampleCount,
)
}
func occultationPathSampleTimesWithLimit(
startTT, endTT, greatestTT, stepDays float64,
maximumCount int,
) []float64 {
if maximumCount < 3 {
maximumCount = 3
}
engine := occultationMovingDiskEngine()
engine.maxSampleCount = maximumCount
times, _ := engine.sampleTimes(startTT, endTT, greatestTT, stepDays)
return times
}
func refineOccultationPathSpacingWithFrame(
points []OccultationPathPoint,
frameAt occultationPathFrameFunc,
targetSpacingKM float64,
location *time.Location,
) ([]OccultationPathPoint, error) {
if len(points) < 2 || targetSpacingKM <= 0 {
return points, nil
}
refined := make([]OccultationPathPoint, 0, len(points))
refined = append(refined, points[0])
widthAt := func(tt float64) (float64, bool) {
_, _, width, ok := occultationPathLimitsAndWidthForFrame(tt, frameAt)
return width, ok
}
for i := 1; i < len(points); i++ {
segmentStart := len(refined) - 1
var err error
refined, err = appendOccultationPathSegmentWithFrame(refined, points[i-1], points[i], frameAt, targetSpacingKM, location, 0)
if err != nil {
return nil, err
}
refineOccultationPathWidths(refined[segmentStart:], widthAt)
}
return refined, nil
}
func appendOccultationPathSegmentWithFrame(
points []OccultationPathPoint,
start, end OccultationPathPoint,
frameAt occultationPathFrameFunc,
targetSpacingKM float64,
location *time.Location,
depth int,
) ([]OccultationPathPoint, error) {
distance := occultationPathDistanceKM(start, end)
if distance <= targetSpacingKM {
if len(points) >= occultationPathMaxSampleCount {
return nil, ErrOccultationPathSamplingLimit
}
return append(points, end), nil
}
if depth >= occultationPathMaxAdaptiveDepth || len(points) >= occultationPathMaxSampleCount {
return nil, ErrOccultationPathSamplingLimit
}
startTT := centerTimeTT(start.Time)
endTT := centerTimeTT(end.Time)
midTT := (startTT + endTT) / 2
midTime := occultationTTToLocation(midTT, location)
if !midTime.After(start.Time) || !midTime.Before(end.Time) {
return append(points, end), nil
}
mid, ok := starOccultationPathCenterPointWithoutWidthWithFrame(midTT, frameAt, location)
if !ok {
return append(points, end), nil
}
mid.WidthKM = (start.WidthKM + end.WidthKM) / 2
var err error
points, err = appendOccultationPathSegmentWithFrame(points, start, mid, frameAt, targetSpacingKM, location, depth+1)
if err != nil {
return nil, err
}
return appendOccultationPathSegmentWithFrame(points, mid, end, frameAt, targetSpacingKM, location, depth+1)
}
func uniqueOccultationPathTimes(times []float64) []float64 {
return movingDiskUniqueTimes(times)
}
type occultationPathFrame struct {
moon occultationPathVector
axis occultationPathVector
first occultationPathVector
second occultationPathVector
moonRadius float64
targetRadius float64
// boundary 挂在被事件缓存复用的 frame 上,记忆化只依赖 frame 的边界搜索;零值 frame 为 nil 时退化为直接计算。
// boundary is attached to frames reused through the event cache and memoizes the boundary
// searches that depend only on the frame; a zero frame keeps it nil and computes directly.
boundary *occultationPathBoundaryCache
}
// occultationPathBoundaryCache 记忆化只依赖 frame 的两个边界搜索:切点与可见 θ 区间。
// 两者各自要扫描 720 个网格点,而同一 frame 会在多个调用点被反复查询。
// occultationPathBoundaryCache memoizes the two frame-only boundary searches (the tangency
// and the visible theta intervals). Each scans 720 grid points and the same frame is queried
// repeatedly from several call sites.
type occultationPathBoundaryCache struct {
// boundaryOnce 用同一份 720 点判别式网格同时恢复切点与可见 θ 区间,避免两条路径各扫一遍。
// boundaryOnce recovers both the tangency and the visible theta intervals from one 720-point
// discriminant grid instead of scanning it once per path.
boundaryOnce sync.Once
tangentPoint occultationPathVector
tangentTheta float64
tangentOK bool
intervals []occultationPathThetaInterval
// intervalMu 保护按 centerTheta 复用的 θ 区间搜索;每个 frame 只会用到个位数个中心角。
// intervalMu guards the theta-interval searches reused per center angle; a frame only ever
// uses a handful of center angles.
intervalMu sync.Mutex
intervalKeys []uint64
intervalVals []occultationPathThetaIntervalResult
}
// occultationPathFrameGeometryEqual 比较两个 frame 的几何内容,忽略只用于记忆化的 boundary 指针。
// occultationPathFrameGeometryEqual compares the geometric content of two frames and ignores
// the memo-only boundary pointer.
func occultationPathFrameGeometryEqual(first, second occultationPathFrame) bool {
return first.moon == second.moon &&
first.axis == second.axis &&
first.first == second.first &&
first.second == second.second &&
first.moonRadius == second.moonRadius &&
first.targetRadius == second.targetRadius
}
// occultationPathThetaIntervalResult 是 occultationPathBoundaryThetaInterval 的缓存结果。
// occultationPathThetaIntervalResult is the cached result of occultationPathBoundaryThetaInterval.
type occultationPathThetaIntervalResult struct {
left, right float64
ok bool
}
type starOccultationEphemerisState struct {
moonRA, moonDec float64
moonDistanceKM float64
starRA, starDec float64
starDistanceKM float64
valid bool
}
type starOccultationEventCache struct {
star StarCoordinate
states map[uint64]starOccultationEphemerisState
frames map[uint64]planetOccultationFrameCacheEntry
riseSetCache *occultationRiseSetEvaluationCache
local *starOccultationLocalEphemeris
}
func newStarOccultationEventCache(star StarCoordinate) *starOccultationEventCache {
cache := &starOccultationEventCache{
star: star,
states: make(map[uint64]starOccultationEphemerisState),
frames: make(map[uint64]planetOccultationFrameCacheEntry),
}
cache.riseSetCache = newOccultationRiseSetEvaluationCacheWithCandidate(
cache.riseSetContextAt,
cache.candidateRiseSetContextAt,
)
return cache
}
func (cache *starOccultationEventCache) stateAt(tt float64) starOccultationEphemerisState {
key := math.Float64bits(tt)
if state, ok := cache.states[key]; ok {
return state
}
if len(cache.states) >= planetOccultationEventCacheMaximumEntries {
for cachedKey := range cache.states {
delete(cache.states, cachedKey)
}
for cachedKey := range cache.frames {
delete(cache.frames, cachedKey)
}
}
var state starOccultationEphemerisState
interpolated := false
if cache.local != nil && cache.local.dense {
state, interpolated = cache.local.stateAt(tt)
}
if !interpolated {
state = starOccultationEphemerisStateAt(tt, cache.star)
}
cache.states[key] = state
return state
}
func (cache *starOccultationEventCache) prepareLocalEphemeris(center float64) {
if cache.local == nil {
cache.local = newStarOccultationLocalEphemeris(center, cache.star)
}
}
func (cache *starOccultationEventCache) candidateFrameAt(tt float64) (occultationPathFrame, bool) {
if cache.local != nil {
if state, ok := cache.local.stateAt(tt); ok {
return starOccultationPathFrameFromState(state)
}
}
return cache.frameAt(tt)
}
func (cache *starOccultationEventCache) frameAt(tt float64) (occultationPathFrame, bool) {
key := math.Float64bits(tt)
if entry, ok := cache.frames[key]; ok {
return entry.frame, entry.ok
}
frame, ok := starOccultationPathFrameFromState(cache.stateAt(tt))
if ok {
frame.boundary = &occultationPathBoundaryCache{}
}
cache.frames[key] = planetOccultationFrameCacheEntry{frame: frame, ok: ok}
return frame, ok
}
func (cache *starOccultationEventCache) riseSetContextAt(tt float64) occultationRiseSetContext {
state := cache.stateAt(tt)
return newOccultationRiseSetContext(
tt, state.moonRA, state.moonDec, state.moonDistanceKM,
state.starRA, state.starDec, state.starDistanceKM, 0,
)
}
func (cache *starOccultationEventCache) candidateRiseSetContextAt(tt float64) occultationRiseSetContext {
if cache.local != nil {
moonXYZ, targetXYZ, ok := cache.local.vectorsAt(tt)
if ok {
return newOccultationRiseSetContextFromVectors(
tt,
moonXYZ,
targetXYZ,
cache.local.starDistanceKM() > 0,
0,
)
}
}
return cache.riseSetContextAt(tt)
}
type occultationPathEndpoint struct {
point OccultationPathPoint
valid bool
}
func (f occultationPathFrame) moonProjectionX() float64 { return occultationPathDot(f.moon, f.first) }
func (f occultationPathFrame) moonProjectionY() float64 { return occultationPathDot(f.moon, f.second) }
func starOccultationPathFrameAt(tt float64, star StarCoordinate) (occultationPathFrame, bool) {
return starOccultationPathFrameFromState(starOccultationEphemerisStateAt(tt, star))
}
func starOccultationEphemerisStateAt(tt float64, star StarCoordinate) starOccultationEphemerisState {
moonRA, moonDec := HMoonGeocentricApparentRaDecN(tt, -1)
moonDistanceKM := HMoonAwayN(tt, -1)
// 方向与距离必须来自同一次三维推进,否则视差修正会退回历元距离。
starRA, starDec, starDistanceAU := starApparentRaDecDistanceGeocentric(tt, star)
starDistanceKM := starDistanceAU * occultationPathAstronomicalUnitKM
return starOccultationEphemerisState{
moonRA: moonRA, moonDec: moonDec, moonDistanceKM: moonDistanceKM,
starRA: starRA, starDec: starDec, starDistanceKM: starDistanceKM,
valid: finite(moonRA) && finite(moonDec) && finite(moonDistanceKM) && moonDistanceKM > 0 &&
finite(starRA) && finite(starDec) && finite(starDistanceKM) && starDistanceKM >= 0,
}
}
func starOccultationPathFrameFromState(state starOccultationEphemerisState) (occultationPathFrame, bool) {
if !state.valid {
return occultationPathFrame{}, false
}
moon := occultationPathRaDecVector(state.moonRA, state.moonDec, state.moonDistanceKM)
starDirection := occultationPathRaDecVector(state.starRA, state.starDec, 1)
axis := occultationPathScale(starDirection, -1)
if state.starDistanceKM > 0 {
target := occultationPathRaDecVector(state.starRA, state.starDec, state.starDistanceKM)
axis = occultationPathScale(occultationPathSub(target, moon), -1)
}
axis = occultationPathUnit(axis)
north := occultationPathVector{z: 1}
first := occultationPathCross(north, axis)
if occultationPathNorm(first) < 1e-12 {
first = occultationPathCross(occultationPathVector{x: 1}, axis)
}
first = occultationPathUnit(first)
second := occultationPathUnit(occultationPathCross(axis, first))
return occultationPathFrame{
moon: moon,
axis: axis,
first: first,
second: second,
moonRadius: math.Asin(moonEquatorialRadiusKM / state.moonDistanceKM),
}, true
}
func starOccultationPathHasBoundary(frame occultationPathFrame) bool {
_, _, ok := occultationPathBoundaryTangent(frame)
return ok
}
func starOccultationPathBoundaryEndpointWithFrame(
tt float64,
frameAt occultationPathFrameFunc,
location *time.Location,
direction int,
) occultationPathEndpoint {
if _, ok := frameAt(tt); !ok {
return occultationPathEndpoint{}
}
for offset := 0; offset <= 3; offset++ {
candidateTT := tt + float64(direction)*float64(offset)*0.5/86400.0
candidateFrame, candidateOK := frameAt(candidateTT)
if !candidateOK {
continue
}
vector, _, valid := occultationPathBoundaryTangent(candidateFrame)
if valid {
return occultationPathEndpoint{point: occultationPathPointFromVector(candidateTT, vector, 0, location), valid: true}
}
}
return occultationPathEndpoint{}
}
func starOccultationPathCenterPoint(tt float64, star StarCoordinate, location *time.Location) (OccultationPathPoint, bool) {
return starOccultationPathCenterPointWithFrame(tt, func(candidateTT float64) (occultationPathFrame, bool) {
return starOccultationPathFrameAt(candidateTT, star)
}, location)
}
func starOccultationPathCenterPointWithFrame(
tt float64,
frameAt occultationPathFrameFunc,
location *time.Location,
) (OccultationPathPoint, bool) {
frame, ok := frameAt(tt)
if !ok {
return OccultationPathPoint{}, false
}
point, _, ok := occultationEarthLineIntersection(frame.moon, frame.axis)
if !ok {
return OccultationPathPoint{}, false
}
width := 0.0
if _, _, tangentWidth, limitsOK := occultationPathLimitsAndWidthForFrame(tt, frameAt); limitsOK {
width = tangentWidth
}
return occultationPathPointFromVectorWithMoon(tt, point, width, frame.moon, location), true
}
func starOccultationPathCenterPointWithoutWidthWithFrame(
tt float64,
frameAt occultationPathFrameFunc,
location *time.Location,
) (OccultationPathPoint, bool) {
frame, ok := frameAt(tt)
if !ok {
return OccultationPathPoint{}, false
}
point, _, ok := occultationEarthLineIntersection(frame.moon, frame.axis)
if !ok {
return OccultationPathPoint{}, false
}
return occultationPathPointFromVectorWithMoon(tt, point, 0, frame.moon, location), true
}
func occultationPathScannedLimitsAtFrame(tt float64, frame occultationPathFrame) (occultationPathVector, occultationPathVector, bool) {
var northern, southern occultationPathVector
northLatitude := math.Inf(-1)
southLatitude := math.Inf(1)
consider := func(vector occultationPathVector) {
latitude := occultationPathGeodeticLatitude(vector)
if latitude > northLatitude {
northLatitude = latitude
northern = vector
}
if latitude < southLatitude {
southLatitude = latitude
southern = vector
}
}
if tangentPoint, tangentTheta, tangentOK := occultationPathBoundaryTangent(frame); tangentOK {
consider(tangentPoint)
if leftTheta, rightTheta, intervalOK := occultationPathBoundaryThetaInterval(frame, tangentTheta); intervalOK {
const intervalSamples = 128
for i := 0; i <= intervalSamples; i++ {
theta := leftTheta + (rightTheta-leftTheta)*float64(i)/intervalSamples
if vector, _, ok := occultationPathBoundaryVector(frame, theta); ok {
consider(vector)
}
}
// 同 occultationPathFiniteCrossTrackExtrema:等差末样本与 rightTheta 差 1 ULP,
// 掠射时端点会被判"无地面交点"而丢掉极值,端点原值必须补采一次。
if vector, _, ok := occultationPathBoundaryVector(frame, rightTheta); ok {
consider(vector)
}
}
}
for i := 0; i < occultationPathBoundaryScanPoints; i++ {
vector, _, ok := occultationPathBoundaryVector(frame, 2*math.Pi*float64(i)/float64(occultationPathBoundaryScanPoints))
if !ok {
continue
}
consider(vector)
}
return northern, southern, finite(northLatitude) && finite(southLatitude)
}
func occultationPathBoundaryThetaInterval(frame occultationPathFrame, centerTheta float64) (float64, float64, bool) {
if cached, hit := occultationPathCachedThetaInterval(frame, centerTheta); hit {
return cached.left, cached.right, cached.ok
}
if !occultationPathBoundaryLineIntersects(frame, centerTheta) {
return 0, 0, false
}
step := 2 * math.Pi / float64(occultationPathBoundaryScanPoints)
findEdge := func(direction float64) (float64, bool) {
inside := centerTheta
for i := 1; i <= occultationPathBoundaryScanPoints; i++ {
outside := centerTheta + direction*step*float64(i)
if occultationPathBoundaryLineIntersects(frame, outside) {
inside = outside
continue
}
for iteration := 0; iteration < 56; iteration++ {
mid := (inside + outside) / 2
if occultationPathBoundaryLineIntersects(frame, mid) {
inside = mid
} else {
outside = mid
}
}
return inside, true
}
return 0, false
}
left, leftOK := findEdge(-1)
right, rightOK := findEdge(1)
result := occultationPathThetaIntervalResult{left: left, right: right, ok: leftOK && rightOK && right > left}
occultationPathStoreThetaInterval(frame, centerTheta, result)
return result.left, result.right, result.ok
}
// occultationPathCachedThetaInterval 查询 frame 上按 centerTheta 缓存的 θ 区间。
// occultationPathCachedThetaInterval looks up a theta interval cached on the frame.
func occultationPathCachedThetaInterval(
frame occultationPathFrame,
centerTheta float64,
) (occultationPathThetaIntervalResult, bool) {
if frame.boundary == nil {
return occultationPathThetaIntervalResult{}, false
}
key := math.Float64bits(centerTheta)
frame.boundary.intervalMu.Lock()
defer frame.boundary.intervalMu.Unlock()
for index, cached := range frame.boundary.intervalKeys {
if cached == key {
return frame.boundary.intervalVals[index], true
}
}
return occultationPathThetaIntervalResult{}, false
}
// occultationPathStoreThetaInterval 记住 frame 上某个 centerTheta 的 θ 区间结果。
// occultationPathStoreThetaInterval remembers one theta interval computed on a frame.
func occultationPathStoreThetaInterval(
frame occultationPathFrame,
centerTheta float64,
result occultationPathThetaIntervalResult,
) {
if frame.boundary == nil {
return
}
key := math.Float64bits(centerTheta)
frame.boundary.intervalMu.Lock()
defer frame.boundary.intervalMu.Unlock()
if len(frame.boundary.intervalKeys) >= occultationPathThetaIntervalCacheMaximumEntries {
return
}
frame.boundary.intervalKeys = append(frame.boundary.intervalKeys, key)
frame.boundary.intervalVals = append(frame.boundary.intervalVals, result)
}
func occultationPathBoundaryLineIntersects(frame occultationPathFrame, theta float64) bool {
discriminant, b, _, ok := occultationPathBoundaryLine(frame, theta)
return ok && b < 0 && discriminant >= 0
}
func occultationPathBoundaryWidth(north, south occultationPathVector, ok bool) float64 {
if !ok {
return 0
}
return occultationPathNorm(occultationPathSub(north, south))
}
func occultationPathBoundaryVector(frame occultationPathFrame, theta float64) (occultationPathVector, float64, bool) {
origin, direction, ok := occultationPathBoundaryRay(frame, theta)
if !ok {
return occultationPathVector{}, 0, false
}
return occultationEarthLineIntersection(origin, direction)
}
// occultationPathBoundaryRay 返回月缘圆柱或两球公切锥的一个母线 /
// occultationPathBoundaryRay returns one generator of the lunar-limb cylinder or a two-sphere common-tangent cone.
// targetRadius 带符号:正值表示异侧外切,负值表示同侧内切 /
// targetRadius is signed: positive for opposite-side outer tangency and negative for same-side inner tangency.
func occultationPathBoundaryRay(frame occultationPathFrame, theta float64) (occultationPathVector, occultationPathVector, bool) {
moonDistance := occultationPathNorm(frame.moon)
if moonDistance <= 0 || !finite(moonDistance) || !finite(frame.targetRadius) {
return occultationPathVector{}, occultationPathVector{}, false
}
sineTheta, cosineTheta := math.Sincos(theta)
radial := occultationPathAdd(
occultationPathScale(frame.first, cosineTheta),
occultationPathScale(frame.second, sineTheta),
)
sine, cosine := math.Sincos(frame.targetRadius)
normal := occultationPathAdd(
occultationPathScale(radial, cosine),
occultationPathScale(frame.axis, -sine),
)
origin := occultationPathAdd(
frame.moon,
occultationPathScale(normal, moonDistance*math.Sin(frame.moonRadius)),
)
direction := occultationPathAdd(
occultationPathScale(frame.axis, cosine),
occultationPathScale(radial, sine),
)
return origin, occultationPathUnit(direction), true
}
// occultationPathBoundaryTangent 求边界锥与地球椭球的连续切点 /
// occultationPathBoundaryTangent finds the continuous tangency of a boundary cone with the Earth ellipsoid.
// 采样网格提供搜索盆地,再对线判别式做局部极大化以恢复网格点之间的切点 /
// A sample grid supplies a basin, while local maximization of the line discriminant recovers tangencies between grid points.
func occultationPathBoundaryTangent(frame occultationPathFrame) (occultationPathVector, float64, bool) {
if frame.boundary != nil {
frame.boundary.boundaryOnce.Do(func() { occultationPathBoundaryPrecompute(frame) })
return frame.boundary.tangentPoint, frame.boundary.tangentTheta, frame.boundary.tangentOK
}
return occultationPathBoundaryTangentUncached(frame)
}
// occultationPathBoundaryPrecompute 用一份网格同时算好切点与可见 θ 区间。
// occultationPathBoundaryPrecompute derives both the tangency and the visible theta intervals
// from one discriminant grid.
func occultationPathBoundaryPrecompute(frame occultationPathFrame) {
step := 2 * math.Pi / float64(occultationPathBoundaryScanPoints)
discriminants := make([]float64, occultationPathBoundaryScanPoints)
occultationPathBoundaryFillGrid(frame, discriminants)
frame.boundary.tangentPoint, frame.boundary.tangentTheta, frame.boundary.tangentOK =
occultationPathBoundaryTangentFromGrid(frame, step, discriminants)
frame.boundary.intervals = occultationPathBoundaryThetaIntervalsFromGrid(frame, step, discriminants)
}
// occultationPathBoundaryTangentUncached 是切点搜索本体;调用方通过 frame 级缓存复用结果。
// occultationPathBoundaryTangentUncached is the tangency search itself; callers reuse it through the frame cache.
func occultationPathBoundaryTangentUncached(frame occultationPathFrame) (occultationPathVector, float64, bool) {
step := 2 * math.Pi / float64(occultationPathBoundaryScanPoints)
discriminants := make([]float64, occultationPathBoundaryScanPoints)
occultationPathBoundaryFillGrid(frame, discriminants)
return occultationPathBoundaryTangentFromGrid(frame, step, discriminants)
}
// occultationPathBoundaryTangentFromGrid 在已算好的网格上恢复切点。
// occultationPathBoundaryTangentFromGrid recovers the tangency from a prepared grid.
func occultationPathBoundaryTangentFromGrid(
frame occultationPathFrame,
step float64,
discriminants []float64,
) (occultationPathVector, float64, bool) {
bestTheta := 0.0
bestDiscriminant := math.Inf(-1)
for i, discriminant := range discriminants {
if discriminant > bestDiscriminant {
bestDiscriminant = discriminant
bestTheta = step * float64(i)
}
}
if !finite(bestDiscriminant) {
return occultationPathVector{}, 0, false
}
left := bestTheta - step
right := bestTheta + step
const goldenRatio = 0.6180339887498949
x1 := right - goldenRatio*(right-left)
x2 := left + goldenRatio*(right-left)
f1, _, _, _ := occultationPathBoundaryLine(frame, x1)
f2, _, _, _ := occultationPathBoundaryLine(frame, x2)
for i := 0; i < 40; i++ {
if f1 < f2 {
left = x1
x1, f1 = x2, f2
x2 = left + goldenRatio*(right-left)
f2, _, _, _ = occultationPathBoundaryLine(frame, x2)
} else {
right = x2
x2, f2 = x1, f1
x1 = right - goldenRatio*(right-left)
f1, _, _, _ = occultationPathBoundaryLine(frame, x1)
}
}
theta := (left + right) / 2
discriminant, b, scale, ok := occultationPathBoundaryLine(frame, theta)
if !ok {
return occultationPathVector{}, 0, false
}
tolerance := 1e-12 * math.Max(scale, 1)
if discriminant < -tolerance || b >= 0 {
return occultationPathVector{}, 0, false
}
vector, _, valid := occultationPathBoundaryIntersection(frame, theta, tolerance)
return vector, theta, valid
}
func occultationPathBoundaryLine(frame occultationPathFrame, theta float64) (discriminant, b, scale float64, ok bool) {
origin, direction, rayOK := occultationPathBoundaryRay(frame, theta)
if !rayOK {
return 0, 0, 0, false
}
polarRatioSquared := occultationPathEarthPolarRatio * occultationPathEarthPolarRatio
a := direction.x*direction.x + direction.y*direction.y + direction.z*direction.z/polarRatioSquared
b = origin.x*direction.x + origin.y*direction.y + origin.z*direction.z/polarRatioSquared
c := origin.x*origin.x + origin.y*origin.y + origin.z*origin.z/polarRatioSquared - occultationPathEarthEquatorialRadiusKM*occultationPathEarthEquatorialRadiusKM
discriminant = b*b - a*c
scale = math.Max(math.Abs(b*b), math.Abs(a*c))
return discriminant, b, scale, a > 0 && finite(discriminant)
}
func occultationPathBoundaryIntersection(frame occultationPathFrame, theta, tolerance float64) (occultationPathVector, float64, bool) {
origin, direction, ok := occultationPathBoundaryRay(frame, theta)
if !ok {
return occultationPathVector{}, 0, false
}
return occultationEarthLineIntersectionWithTolerance(origin, direction, tolerance)
}
func occultationPathPointFromVector(tt float64, vector occultationPathVector, width float64, location *time.Location) OccultationPathPoint {
lon, lat := occultationPathGeodetic(tt, vector)
moonRA, moonDec := moonTopocentricApparentRaDec(tt, Observer{Longitude: lon, Latitude: lat}, -1)
return OccultationPathPoint{
Time: occultationTTToLocation(tt, location),
Longitude: lon,
Latitude: lat,
MoonAltitude: occultationAltitude(tt, Observer{Longitude: lon, Latitude: lat}, moonRA, moonDec),
WidthKM: width,
}
}
func occultationPathPointFromVectorWithMoon(
tt float64,
vector occultationPathVector,
width float64,
moon occultationPathVector,
location *time.Location,
) OccultationPathPoint {
return occultationPathPointFromVectorWithMoonSidereal(
tt, vector, width, moon, ApparentSiderealTime(TT2UT1(tt))*15, location,
)
}
func occultationPathPointFromVectorWithMoonSidereal(
tt float64,
vector occultationPathVector,
width float64,
moon occultationPathVector,
siderealDegrees float64,
location *time.Location,
) OccultationPathPoint {
lon, lat := occultationPathGeodeticWithSidereal(vector, siderealDegrees)
moonDistance := occultationPathNorm(moon)
moonRA := normalizeRA(math.Atan2(moon.y, moon.x) * 180 / math.Pi)
moonDec := math.Asin(math.Max(-1, math.Min(1, moon.z/moonDistance))) * 180 / math.Pi
moonRA, moonDec = topocentricRaDecWithSidereal(
moonRA, moonDec, lat, lon, siderealDegrees,
moonDistance/occultationPathAstronomicalUnitKM, 0,
)
return OccultationPathPoint{
Time: occultationTTToLocation(tt, location),
Longitude: lon,
Latitude: lat,
MoonAltitude: occultationAltitudeWithSidereal(
siderealDegrees, Observer{Longitude: lon, Latitude: lat}, normalizeRA(moonRA), moonDec,
),
WidthKM: width,
}
}
func occultationPathDeduplicateBoundaryPoints(
first, second []OccultationPathPoint,
) ([]OccultationPathPoint, []OccultationPathPoint) {
if len(first) != len(second) || len(first) == 0 {
return first, second
}
uniqueFirst := make([]OccultationPathPoint, 0, len(first))
uniqueSecond := make([]OccultationPathPoint, 0, len(second))
for index := range first {
if len(uniqueFirst) > 0 && !first[index].Time.After(uniqueFirst[len(uniqueFirst)-1].Time) {
continue
}
if len(uniqueFirst) > 0 && first[index].Time.Sub(uniqueFirst[len(uniqueFirst)-1].Time) < time.Second &&
occultationPathDistanceKM(first[index], uniqueFirst[len(uniqueFirst)-1]) < occultationPathBoundaryMergeKM &&
occultationPathDistanceKM(second[index], uniqueSecond[len(uniqueSecond)-1]) < occultationPathBoundaryMergeKM {
uniqueFirst[len(uniqueFirst)-1] = first[index]
uniqueSecond[len(uniqueSecond)-1] = second[index]
continue
}
uniqueFirst = append(uniqueFirst, first[index])
uniqueSecond = append(uniqueSecond, second[index])
}
return uniqueFirst, uniqueSecond
}
func centerTimeTT(value time.Time) float64 { return occultationTimeToTT(value) }
type occultationPathWidthFunc func(float64) (float64, bool)
func refineOccultationPathWidths(points []OccultationPathPoint, widthAt occultationPathWidthFunc) {
if len(points) < 3 {
return
}
cache := make(map[int]bool)
var refine func(int, int, int)
refine = func(left, right, depth int) {
if right-left <= 1 || depth >= occultationPathMaxAdaptiveDepth {
return
}
if right-left <= 4 {
for index := left + 1; index < right; index++ {
setOccultationPathExactWidth(points, index, widthAt, cache)
}
return
}
indices := uniqueOccultationPathWidthIndices(left, right)
withinTolerance := true
leftTime := points[left].Time
duration := points[right].Time.Sub(leftTime).Seconds()
for _, index := range indices {
linear := (points[left].WidthKM + points[right].WidthKM) / 2
if duration != 0 {
fraction := points[index].Time.Sub(leftTime).Seconds() / duration
linear = points[left].WidthKM + fraction*(points[right].WidthKM-points[left].WidthKM)
}
if !setOccultationPathExactWidth(points, index, widthAt, cache) ||
math.Abs(points[index].WidthKM-linear) > occultationPathWidthToleranceKM {
withinTolerance = false
}
}
anchors := append([]int{left}, indices...)
anchors = append(anchors, right)
if withinTolerance {
for index := 1; index < len(anchors); index++ {
interpolateOccultationPathWidths(points, anchors[index-1], anchors[index])
}
return
}
for index := 1; index < len(anchors); index++ {
refine(anchors[index-1], anchors[index], depth+1)
}
}
refine(0, len(points)-1, 0)
}
func uniqueOccultationPathWidthIndices(left, right int) []int {
indices := make([]int, 0, 3)
for _, numerator := range []int{1, 2, 3} {
index := left + (right-left)*numerator/4
if index <= left || index >= right || len(indices) > 0 && index == indices[len(indices)-1] {
continue
}
indices = append(indices, index)
}
return indices
}
func setOccultationPathExactWidth(
points []OccultationPathPoint,
index int,
widthAt occultationPathWidthFunc,
cache map[int]bool,
) bool {
if ok, found := cache[index]; found {
return ok
}
width, ok := widthAt(centerTimeTT(points[index].Time))
if ok && finite(width) && width >= 0 {
points[index].WidthKM = width
} else {
ok = false
}
cache[index] = ok
return ok
}
func interpolateOccultationPathWidths(points []OccultationPathPoint, left, right int) {
if right-left <= 1 {
return
}
leftTime := points[left].Time
duration := points[right].Time.Sub(leftTime).Seconds()
for index := left + 1; index < right; index++ {
fraction := float64(index-left) / float64(right-left)
if duration != 0 {
fraction = points[index].Time.Sub(leftTime).Seconds() / duration
}
points[index].WidthKM = points[left].WidthKM + fraction*(points[right].WidthKM-points[left].WidthKM)
}
}
func occultationPathDistanceKM(a, b OccultationPathPoint) float64 {
return occultationPathDistanceKMValues(a.Longitude, a.Latitude, b.Longitude, b.Latitude)
}
func occultationPathDistanceKMValues(lon1, lat1, lon2, lat2 float64) float64 {
lat1 *= math.Pi / 180
lat2 *= math.Pi / 180
dLat := lat2 - lat1
dLon := (lon2 - lon1) * math.Pi / 180
dLon = math.Mod(dLon+math.Pi, 2*math.Pi)
if dLon < 0 {
dLon += 2 * math.Pi
}
dLon -= math.Pi
h := math.Sin(dLat/2)*math.Sin(dLat/2) + math.Cos(lat1)*math.Cos(lat2)*math.Sin(dLon/2)*math.Sin(dLon/2)
return 2 * occultationPathEarthEquatorialRadiusKM * math.Asin(math.Sqrt(math.Min(1, h)))
}
type occultationPathVector struct{ x, y, z float64 }
func occultationPathRaDecVector(ra, dec, distance float64) occultationPathVector {
ra *= math.Pi / 180
dec *= math.Pi / 180
return occultationPathVector{
x: distance * math.Cos(dec) * math.Cos(ra),
y: distance * math.Cos(dec) * math.Sin(ra),
z: distance * math.Sin(dec),
}
}
func occultationPathAdd(a, b occultationPathVector) occultationPathVector {
return occultationPathVector{x: a.x + b.x, y: a.y + b.y, z: a.z + b.z}
}
func occultationPathSub(a, b occultationPathVector) occultationPathVector {
return occultationPathVector{x: a.x - b.x, y: a.y - b.y, z: a.z - b.z}
}
func occultationPathScale(a occultationPathVector, scalar float64) occultationPathVector {
return occultationPathVector{x: a.x * scalar, y: a.y * scalar, z: a.z * scalar}
}
func occultationPathDot(a, b occultationPathVector) float64 {
return a.x*b.x + a.y*b.y + a.z*b.z
}
func occultationPathCross(a, b occultationPathVector) occultationPathVector {
return occultationPathVector{
x: a.y*b.z - a.z*b.y,
y: a.z*b.x - a.x*b.z,
z: a.x*b.y - a.y*b.x,
}
}
func occultationPathNorm(value occultationPathVector) float64 {
return math.Sqrt(occultationPathDot(value, value))
}
func occultationPathUnit(value occultationPathVector) occultationPathVector {
norm := occultationPathNorm(value)
if norm <= 0 {
return occultationPathVector{}
}
return occultationPathScale(value, 1/norm)
}
func occultationEarthLineIntersection(origin, direction occultationPathVector) (occultationPathVector, float64, bool) {
return occultationEarthLineIntersectionWithTolerance(origin, direction, 0)
}
// occultationPathTrackReference 将影轴地面轨迹连续延伸到掠过阶段 /
// occultationPathTrackReference extends the shadow-axis ground track through grazing phases.
// 在地球外时,将椭球度量下最近点径向投影到表面,并在相切处与真实近侧交点连续连接 /
// Outside the Earth, the closest point under the ellipsoid metric is projected radially onto the surface and joined continuously to the real near-side intersection at tangency.
func occultationPathTrackReference(frame occultationPathFrame) (occultationPathVector, bool) {
if point, _, ok := occultationEarthLineIntersection(frame.moon, frame.axis); ok {
return point, true
}
polarRatioSquared := occultationPathEarthPolarRatio * occultationPathEarthPolarRatio
a := frame.axis.x*frame.axis.x + frame.axis.y*frame.axis.y + frame.axis.z*frame.axis.z/polarRatioSquared
b := frame.moon.x*frame.axis.x + frame.moon.y*frame.axis.y + frame.moon.z*frame.axis.z/polarRatioSquared
if a <= 0 || !finite(a) || !finite(b) {
return occultationPathVector{}, false
}
closest := occultationPathAdd(frame.moon, occultationPathScale(frame.axis, -b/a))
metricRadius := math.Sqrt(closest.x*closest.x + closest.y*closest.y + closest.z*closest.z/polarRatioSquared)
if metricRadius <= 1e-9 || !finite(metricRadius) {
return occultationPathVector{}, false
}
return occultationPathScale(closest, occultationPathEarthEquatorialRadiusKM/metricRadius), true
}
func occultationEarthLineIntersectionWithTolerance(origin, direction occultationPathVector, tolerance float64) (occultationPathVector, float64, bool) {
polarRatioSquared := occultationPathEarthPolarRatio * occultationPathEarthPolarRatio
a := direction.x*direction.x + direction.y*direction.y + direction.z*direction.z/polarRatioSquared
b := origin.x*direction.x + origin.y*direction.y + origin.z*direction.z/polarRatioSquared
c := origin.x*origin.x + origin.y*origin.y + origin.z*origin.z/polarRatioSquared - occultationPathEarthEquatorialRadiusKM*occultationPathEarthEquatorialRadiusKM
discriminant := b*b - a*c
if discriminant < -tolerance || a <= 0 {
return occultationPathVector{}, 0, false
}
if discriminant < 0 {
discriminant = 0
}
root := math.Sqrt(discriminant)
roots := [2]float64{(-b - root) / a, (-b + root) / a}
chosen := math.Inf(1)
for _, root := range roots {
if root >= 0 && root < chosen {
chosen = root
}
}
if math.IsInf(chosen, 1) {
return occultationPathVector{}, 0, false
}
return occultationPathAdd(origin, occultationPathScale(direction, chosen)), chosen, true
}
func occultationPathGeodetic(tt float64, vector occultationPathVector) (float64, float64) {
ut1 := TT2UT1(tt)
return occultationPathGeodeticWithSidereal(vector, ApparentSiderealTime(ut1)*15)
}
func occultationPathGeodeticWithSidereal(
vector occultationPathVector,
siderealDegrees float64,
) (float64, float64) {
longitude := normalizeLongitude(math.Atan2(vector.y, vector.x)*180/math.Pi - siderealDegrees)
return longitude, occultationPathGeodeticLatitude(vector)
}
func occultationPathGeodeticLatitude(vector occultationPathVector) float64 {
return math.Atan2(
vector.z,
occultationPathEarthPolarRatio*occultationPathEarthPolarRatio*math.Hypot(vector.x, vector.y),
) * 180 / math.Pi
}
func occultationPathEarthFixedVector(tt float64, vector occultationPathVector) occultationPathVector {
return occultationPathEarthFixedVectorWithRotation(vector, occultationPathEarthRotationAt(tt))
}
type occultationPathEarthRotation struct {
cosine float64
sine float64
}
func occultationPathEarthRotationAt(tt float64) occultationPathEarthRotation {
angle := ApparentSiderealTime(TT2UT1(tt)) * 15 * math.Pi / 180
return occultationPathEarthRotation{cosine: math.Cos(angle), sine: math.Sin(angle)}
}
func occultationPathEarthFixedVectorWithRotation(
vector occultationPathVector,
rotation occultationPathEarthRotation,
) occultationPathVector {
return occultationPathVector{
x: rotation.cosine*vector.x + rotation.sine*vector.y,
y: -rotation.sine*vector.x + rotation.cosine*vector.y,
z: vector.z,
}
}
func normalizeLongitude(longitude float64) float64 {
longitude = math.Mod(longitude+180, 360)
if longitude < 0 {
longitude += 360
}
return longitude - 180
}