Files
astro/basic/occultation_path.go
T

1588 lines
57 KiB
Go
Raw Normal View History

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
}